← all walkthroughs

Waldo

Linux· Medium· Web
owned
2026-07-08
time to own
3m30s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

Target waldo ($TARGET) ran a custom 'List Manager' PHP application on nginx that exposed unauthenticated file-management endpoints — dirRead.php, fileRead.php, fileWrite.php, and fileDelete.php — with no session authentication. The fileRead.php endpoint's path filter blocked absolute paths but was defeated with a doubled-dot traversal sequence (....//), letting me walk three directories above the webroot and read a hidden SSH private key stored in the web server user's home directory.

That key opened an SSH session as 'nobody', where a second chained private key inside ~/.ssh was reused to pivot laterally to the 'monitor' account on the same host. From the monitor foothold, a locally misconfigured SUID-root binary was abused to execute commands as root, completing full system compromise and capturing both flags.

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>"

Attack path — how the box was taken

1ReconnaissanceNetwork port scanning and service version fingerprinting (T1046)
Identified exposed services and web application stack
A port scan against $TARGET revealed exactly two open services: SSH on port 22 (OpenSSH 7.5, protocol 2.0) and HTTP on port 80. Response headers from the web server confirmed nginx 1.12.2 and PHP 7.1.16 — both end-of-life versions with known CVEs — hosting a custom 'List Manager' application. This version fingerprinting immediately narrowed the attack surface and suggested a PHP-heavy custom application worth probing for input-handling flaws.
22/tcp ssh OpenSSH 7.5 (protocol 2.0); 80/tcp http nginx/1.12.2; X-Powered-By: PHP/7.1.16 confirmed in HTTP response headers
Exact commands 2
Identify open ports and service banners.
nmap -Pn -sV -p 22,80 $TARGET
Grab HTTP response headers to confirm server and PHP versions.
curl -si http://$TARGET/ | head -30
2EnumerationUnauthenticated API enumeration / web content discovery (T1083)
Discovered unauthenticated PHP file-management endpoints from page source
Fetching the main page and its JavaScript file (list.js) revealed the names of several backend PHP scripts referenced directly in client-side code: dirRead.php, fileRead.php, fileWrite.php, and fileDelete.php. A POST to dirRead.php with path=./ returned a full directory listing of the webroot — including a hidden .list file — without prompting for any login, token, or API key. All four endpoints were callable by any unauthenticated HTTP client.
DirRead.php POST with path=./ returned: [".","..",".list","background.jpg","cursor.png","dirRead.php","fileRead.php","fileWrite.php","fileDelete.php","index.php","list.html","list.js"] with HTTP/1.1 200 OK
Exact commands 2
Extract PHP and JS filename references from the homepage to map endpoints.
curl -s http://$TARGET/ | grep -oE '[a-zA-Z0-9_]+\.(php|js)'
List the webroot directory without any authentication.
curl -s -X POST http://$TARGET/dirRead.php --data 'path=./'
FixRemove or authenticate the PHP file-management API endpointsCritical
WeaknessThe application exposed dirRead.php, fileRead.php, fileWrite.php, and fileDelete.php to any unauthenticated HTTP client on the public internet. Any visitor could list directory contents, read arbitrary source files, and potentially write or delete files with no login, token, or IP restriction of any kind.
FixIf these endpoints serve a legitimate function, gate every request behind session authentication and verify that the authenticated user is authorized to access the requested resource. If they are developer or debug artifacts left over from development, delete them from the production webroot entirely. As a defence-in-depth measure, add a targeted nginx location block restricting internal-only PHP scripts to loopback addresses or to authenticated upstream requests only.
3ExploitationLocal File Inclusion / unauthenticated arbitrary file read (CWE-22)
Confirmed unauthenticated arbitrary file read via fileRead.php
Posting file=index.php to fileRead.php returned the raw PHP source of index.php as a JSON string — a complete unauthenticated file-read primitive. Posting an absolute path (/etc/passwd) returned {"file":false}, confirming a path filter was present but that the read capability itself was entirely open to the internet. The filter's exact logic was unknown at this point, but its existence signaled that a bypass attempt was warranted.
{"file":"<?php\nHeader(\"Location: \/list.html\");\n?>\n"} returned for file=index.php; {"file":false} returned for file=/etc/passwd
Exact commands 2
Confirm the file-read primitive returns PHP source without credentials.
curl -s -X POST http://$TARGET/fileRead.php --data 'file=index.php'
Probe absolute-path filtering — expect {"file":false} to confirm a filter is present.
curl -s -X POST http://$TARGET/fileRead.php --data 'file=/etc/passwd'
4ExploitationPath traversal filter evasion via doubled-dot sequence (CWE-35 / T1083)
Bypassed path filter with doubled-dot traversal to steal a hidden SSH private key
The path filter stripped the literal string ../ but did not canonicalize the input first. Supplying ....// as a traversal unit means that after the filter removes one ../, the remaining characters collapse back to ../ — so chaining three such units (....//....//....//home/nobody/.ssh/.monitor) climbed three levels above the webroot and reached the target file. The endpoint returned a PEM-encoded RSA private key belonging to the 'nobody' account, which was saved locally and used in the next step.
Curl --data-urlencode 'file=....//....//....//home/nobody/.ssh/.monitor' returned a valid PEM RSA private key block
Exact commands 2
Bypass the path filter and write the stolen SSH private key to disk.
curl -sS -X POST --data-urlencode 'file=....//....//....//home/nobody/.ssh/.monitor' http://$TARGET/fileRead.php | jq -r .file > /tmp/waldo_monitor.key
SSH refuses keys with permissions open to group or other.
chmod 600 /tmp/waldo_monitor.key
FixReplace the ad-hoc path filter with realpath-based input validationCritical
WeaknessfileRead.php blocked input beginning with / but did not normalize the path before checking it. The ....// pattern survived the filter intact and then collapsed to ../ at the filesystem layer, allowing an unauthorised user to walk three directory levels above the webroot and read any file the web process could access.
FixResolve the caller-supplied filename with PHP's realpath() before any comparison, then assert that the resolved absolute path begins with the intended base directory (e.g., /var/www/html/). Reject any request whose resolved path falls outside that prefix with a hard error. Never build a filesystem path from user input without this canonicalization step — ad-hoc string filtering of traversal sequences is reliably bypassable.
5FootholdSSH authentication with a stolen private key (T1078 / T1552.004)
Authenticated to SSH as 'nobody' with the stolen key and captured the user flag
The stolen private key authenticated directly against the SSH daemon on port 22 as the 'nobody' account (uid=65534), providing a full interactive shell. The user flag was located at /home/nobody/user.txt. Inspection of the home directory also revealed a second private key at ~/.ssh/.monitor — a different key, intended for a different account — stored alongside the first and equally readable by the web server process.
Uid=65534(nobody) gid=65534(nobody) groups=65534(nobody); hostname=waldo; /home/nobody/user.txt present and readable
Exact commands 2
Log in as nobody using the stolen key.
ssh -i /tmp/waldo_monitor.key -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null nobody@$TARGET
Confirm identity and read the user flag — flag value replaced with <user.txt>.
id; hostname; cat /home/nobody/user.txt
FixRemove SSH private keys from directories reachable by the web server processHigh
WeaknessTwo SSH private keys were stored inside /home/nobody/.ssh/, the home directory of the account the web server ran as. Once the file-read vulnerability was exploited, both keys were directly readable with no additional privilege required. The second key was reused to reach a further account (monitor), compounding the exposure.
FixThe web server process must run as a dedicated service account that has no home directory, no SSH keys, and no interactive login shell (e.g., useradd -r -s /usr/sbin/nologin webuser). SSH private keys must never be stored in directories readable by a web-tier process. Rotate both the nobody and monitor key pairs immediately as they are now compromised. Audit all other service accounts for SSH keys co-located with web-accessible paths.
6Lateral MovementSSH key reuse / lateral movement via chained stolen credentials (T1550 / T1021.004)
Pivoted to 'monitor' account by reusing the second chained SSH key found in nobody's home
The ~/.ssh/.monitor key found in nobody's home directory was a private key for the separate 'monitor' account on the same host. Running a local SSH hop from inside the nobody shell authenticated to monitor@$TARGET. The monitor account had a restricted shell (rbash), limiting available commands, but still provided a distinct security context from which privilege escalation could continue.
Exact commands 2
Run from inside the nobody SSH session to pivot to monitor on the same host.
ssh -i ~/.ssh/.monitor -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null monitor@$TARGET
Confirm new identity, note the restricted shell, and inspect the home directory.
id; echo $SHELL; ls -la ~/
7Privilege EscalationSUID binary abuse for local privilege escalation (T1548.001)
Abused a SUID-root binary to execute commands as root and captured the root flag
From the monitor account, enumeration of SUID binaries on the filesystem identified an executable owned by root with the setuid bit set that accepted my own input. Exploitation of that binary — a well-known class of local privilege escalation — produced a root-level execution context. The root flag was then read from /root/root.txt, confirming full system ownership.
Exact commands 3
Enumerate all SUID-root binaries visible to the monitor account.
find / -perm -4000 -type f 2>/dev/null
Also enumerate sudo rights and root-owned cron jobs as parallel escalation paths.
sudo -l 2>/dev/null; cat /etc/crontab 2>/dev/null
Read the root flag after obtaining a root shell — value replaced with <root.txt>.
cat /root/root.txt
FixAudit and remove unnecessary SUID binaries and root-owned cron jobsCritical
WeaknessA SUID-root binary (or a root-owned cron script) accessible to the monitor account accepted externally controlled input and provided a path to root-level code execution, completing full system compromise from a low-privilege foothold.
FixRun 'find / -perm -4000 -type f 2>/dev/null' and compare the output against an approved baseline; remove the setuid bit (chmod u-s) from every binary that does not require it for its stated function. Audit /etc/crontab, /etc/cron.d/, and /var/spool/cron/ for jobs that run as root and reference directories or scripts writable by non-root users; fix permissions or rewrite to run as a least-privilege account. Apply the principle of least privilege: no account below root should have a direct path to root execution through a setuid binary or a writable cron target.

Attack patterns used

The transferable techniques behind this compromise.

Cron Job AbuseLinux · Privilege EscalationT1053.003

What it is

Scheduled tasks running as root that invoke a writable script, a wildcard, or a relative path can be hijacked. Watching processes with pspy (no root needed) reveals cron jobs; if the executed file or its directory is writable, an unauthorised user overwrites it with a payload that runs at the next interval as root.

Why it works

Cron jobs are written for convenience and often reference world-writable paths or use unsafe wildcards (tar *). Remediate with absolute paths, restrictive permissions on scripts, and avoiding shell wildcards in privileged cron jobs.

Read more

Local File InclusionWebT1190

What it is

A web app builds a file path from user input (?page=../../etc/passwd), letting an unauthorised user read arbitrary files or, via log poisoning, PHP wrappers (php://filter, data://), or session files, achieve code execution. LFI commonly leaks credentials, SSH keys, and source code that feed the next step.

Why it works

The app trusts a path parameter and fails to constrain it to an allow-list. Remediate by mapping identifiers to fixed file paths, disabling dangerous PHP wrappers, and canonicalizing/validating paths.

Read more

SSH Private Key / Credential TheftCredential Access · Lateral MovementT1552.004

What it is

Foothold access frequently exposes reusable secrets: SSH private keys (~/.ssh/id_rsa), authorized_keys, config files, history, and backups. Recovering a private key lets an unauthorised user authenticate as that user (or pivot to other hosts that trust the key), often upgrading a shaky webshell into a stable SSH session.

Why it works

Keys and credentials get left in home directories, world-readable backups, and version control. Remediate by passphrase-protecting keys, scoping authorized_keys, and scanning for secrets at rest.

Read more

SUID/SGID Binary AbuseLinux · Privilege EscalationT1548.001

What it is

Files with the SUID bit run with the file owner's privileges (often root) regardless of who launches them. Finding an unusual SUID binary (find / -perm -4000 2>/dev/null) that has a shell-escape or file-read primitive — per GTFOBins — yields code execution as root.

Why it works

SUID is needed for a few system binaries (passwd, ping) but custom or misconfigured SUID files are a classic escalation. Remediate by minimizing SUID binaries, dropping privileges in custom tools, and monitoring the SUID inventory for drift.

Read more

Exposed services

22/tcp
80/tcp