← all walkthroughs

Ransom

Linux· Medium
owned
2026-07-15
time to own
7m18s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

My found a web application on port 80 that appeared to be locked behind a password prompt — but the login code had a critical flaw: sending the value true as a JSON boolean instead of a real password string tricked the server into granting access, because PHP's loose equality operator treats boolean true as equal to any non-empty string. Behind that login page sat a direct download of the user flag and a ZIP archive of a user's home directory.

The archive used a 30-year-old broken cipher (ZipCrypto) that can be cracked without the password as long as I knowed the unencrypted content of any one file inside — a standard Ubuntu configuration file bundled in the archive served that role, and the tool bkcrack decrypted the entire archive in minutes. The decrypted archive contained the user's SSH private key, which granted a shell on the system.

Reading the web application's source code on the server revealed the plaintext password the app had been checking. That exact password had also been set as the operating system root account's password, so a single su command gave me complete control of the machine.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"
export PASSWORD="<a-password-you-choose>"
export PASSWORD2="<a-password-you-choose>"
export PASSWORD3="<a-password-you-choose>"
export PASSWORD4="<a-password-you-choose>"

Attack path — how the box was taken

1EnumerationService enumeration and application fingerprinting
Identified open services and fingerprinted the Laravel web application
A service scan revealed only two open ports: SSH on 22 and Apache on 80. Visiting the web application showed a Laravel-powered site protected by a single shared password prompt. Reviewing the page's JavaScript source revealed that the login form POSTs credentials as a JSON body to the /api/login endpoint, exposing the authentication mechanism and signalling server-side JSON parsing — a useful indicator of potential type-handling vulnerabilities.
Nmap returned Apache 2.4.41 on port 80 and OpenSSH 8.2p1 on port 22; page JS referenced /api/login with a JSON body payload.
Exact commands 2
Service version scan to identify Apache and OpenSSH.
nmap -Pn -sV -p22,80 $TARGET
Extract API endpoint references from the login page source.
curl -sS http://$TARGET/ | grep -oE "(api/[^'"]+|fetch\([^)]+\))"
2ExploitationPHP Loose Comparison Type Juggling (CWE-843)
Bypassed the login page by sending a JSON boolean instead of a password string
The /api/login endpoint accepted a JSON body and compared the submitted password to the stored value using PHP's == (loose equality) operator. PHP's loose comparison coerces types before comparing: boolean true equals any non-empty string, so submitting {"password": true} satisfied the check with no knowledge of the real password. An X-HTTP-Method-Override: GET header was also required to satisfy the application's routing logic. The response set a valid session cookie granting full access to the authenticated area.
POST to /api/login with Content-Type: application/json and body {"password":true} plus X[REDACTED: sensitive value]: GET returned HTTP 200 and a session cookie; all subsequent authenticated requests succeeded.
Exact commands 1
Type-juggling auth bypass; saves the session cookie to cookies.txt for use in later steps.
curl -sS -c cookies.txt -X POST -H 'Content-Type: application/json' -H 'X-HTTP-Method-Override: GET' --data '{"password":true}' http://$TARGET/api/login
FixReplace PHP loose comparison with strict equality in the login handlerCritical
WeaknessThe /api/login endpoint used PHP's == operator to compare the submitted password against the expected value. PHP loose comparison coerces types before comparing, so a JSON boolean true satisfies the check against any non-empty string — the correct password is never needed and authentication is completely bypassed by any unauthenticated caller who knows the endpoint.
FixReplace every == and != password comparison in AuthController.php with === and !== (strict equality, which never coerces types). Before the comparison, explicitly validate that the incoming value is a string: if (!is_string($request->input('password'))) { return response()->json(['error'=>'Invalid input'], 400); }. Also apply constant-time comparison via hash_equals() to prevent timing side-channels. Audit all other authentication checks in the application for the same pattern.
3Credential AccessAuthenticated sensitive file disclosure
Downloaded the user flag and an encrypted home-directory archive from the files page
The authenticated files page exposed two items for download: the user flag (user.txt) and a ZIP archive (uploaded-file-3422.zip) that appeared to contain a user's home directory. The archive was password-encrypted, but inspection of the ZIP metadata showed it used the legacy ZipCrypto cipher rather than modern AES-256 — a telling sign that the encryption could be broken without the password.
User.txt and uploaded-file-3422.zip retrieved over HTTP using the session cookie obtained in step 2; unzip -l confirmed ZipCrypto encryption on all entries.
Exact commands 3
Retrieve the user flag directly from the authenticated files page; value is <user.txt>.
curl -sS -b cookies.txt http://$TARGET/user.txt
Download the encrypted home-directory archive.
curl -sS -b cookies.txt http://$TARGET/uploaded-file-3422.zip -o ransom.zip
List archive contents and confirm ZipCrypto encryption (no AES-256 marker in the Method column).
unzip -l ransom.zip
FixReplace ZipCrypto with AES-256 encryption for any sensitive archivesHigh
WeaknessThe home-directory archive used ZipCrypto, a 1989-era stream cipher that has been publicly broken since 1994. Anyone who obtains a copy of any single plaintext file stored in the archive can derive all three internal keys and decrypt every other entry — the ZIP password provides no real protection.
FixRe-create all sensitive ZIP archives using AES-256 encryption: use 7-Zip (7z a -p -mhe=on archive.7z files/) or zip with the --encrypt flag and a modern ZIP tool that defaults to AES-256. Better still, remove the ability for authenticated users to download home-directory snapshots from the web application entirely — this data has no business reason to be served over HTTP. Audit all other files exposed by the application for sensitive content.
4Credential AccessZipCrypto Known-Plaintext Attack (Biham–Kocher 1994)
Cracked the ZipCrypto encryption using a known-plaintext attack with bkcrack
ZipCrypto is vulnerable to Biham and Kocher's 1994 known-plaintext attack: given the unencrypted bytes of any file stored in the archive, I can recover the three 32-bit internal keystream values that protect every entry. The archive included a standard Ubuntu .bash_logout file from /etc/skel/ — a file with well-known, fixed content. That file was re-zipped with matching compression parameters to produce a valid plaintext reference, and bkcrack recovered the three internal keys within minutes. Those keys were then used to decrypt the entire archive without ever knowing the original ZIP password.
Bkcrack output confirmed: 'Keys: [REDACTED: recovered credential] [REDACTED: recovered credential] [REDACTED: recovered credential] ... Found a solution. Stopping.'; decrypted archive extracted cleanly.
Exact commands 4
Re-zip the standard Ubuntu .bash_logout; the -X flag strips extra ZIP fields that would cause a byte-length mismatch and fail the attack.
cd /etc/skel && zip -X /tmp/known.zip .bash_logout
Recover the three ZipCrypto internal keys using the known-plaintext file; note the three hex values from the output.
bkcrack -C ransom.zip -c .bash_logout -P /tmp/known.zip -p .bash_logout
Decrypt the entire archive with the recovered keys; substitute your actual recovered key values for the three hex arguments.
bkcrack -C ransom.zip -k $PASSWORD2 $PASSWORD3 $PASSWORD4 -D ransom_dec.zip
Extract the decrypted home directory.
unzip ransom_dec.zip -d ransom_home
5Initial AccessSSH Private Key Theft (T1552.004)
Logged in via SSH using the private key recovered from the decrypted archive
The decrypted home directory contained the user's .ssh directory, including an unprotected SSH private key (id_rsa) with no passphrase set. That key provided direct SSH access as the 'htb' user with no further credential material needed — the archive decryption was sufficient to fully compromise the account.
Ssh -i ransom_home/.ssh/id_rsa htb@$TARGET returned an interactive shell as htb without any password prompt.
Exact commands 2
Set correct key file permissions; SSH refuses to use keys readable by other users.
chmod 600 ransom_home/.ssh/id_rsa
Authenticate as 'htb' using the recovered private key.
ssh -i ransom_home/.ssh/id_rsa htb@$TARGET
FixRemove SSH private keys from web-accessible storage and enforce passphrase protectionHigh
WeaknessThe user's SSH private key was stored inside a ZIP archive that was uploaded to and served by the web application. The key carried no passphrase, so decrypting the archive was sufficient to gain full SSH access to the system — no additional credential was required.
FixImmediately rotate the compromised key pair: generate a new pair for the 'htb' account, add the new public key to authorized_keys, and remove the old public key. Enforce passphrase protection on all SSH private keys. Remove all private key material from web-accessible directories, backup archives, and any storage that the application can read or serve. Restrict authorized_keys to the minimum set of keys actually needed.
6DiscoveryCredential Discovery in Application Source (T1552.001)
Recovered the hardcoded plaintext application password from the web server source code
As the 'htb' user, the web application's source code in /srv/prod was world-readable. Inspecting AuthController.php revealed the hardcoded plaintext password that the /api/login handler had been comparing against — the same credential the type-juggling bypass had made unnecessary from the outside. With this value in hand, it became a prime candidate for reuse against other system accounts, particularly root.
AuthController.php contained the string '[REDACTED: recovered credential]' as a hardcoded literal in the password validation block.
Exact commands 2
Read the hardcoded application password from the Laravel authentication controller.
grep -R -B5 -A5 'password' /srv/prod/app/Http/Controllers/AuthController.php
Broaden the search to locate any other credentials embedded in source files.
find /srv/prod -name '*.php' | xargs grep -l 'password\|secret\|key' 2>/dev/null
FixRemove hardcoded credentials from application source code and use environment variablesHigh
WeaknessThe application's AuthController.php contained the plaintext password as a hardcoded string literal. Any user who can read the web application files on disk — including any low-privilege account, a web shell, or a developer who checks out the repository — immediately obtains the credential without any further attack.
FixMove all secrets and credentials out of source code into environment variables or a secrets manager. In Laravel, store secrets in the .env file (which must not be committed to version control) and reference them at runtime via env('APP_PASSWORD'). Rotate the exposed password immediately. Audit the entire git history for any secrets committed in past commits and use a tool such as git-secrets or truffleHog to prevent future occurrences.
7Privilege EscalationCredential Reuse (T1078)
Escalated to root by reusing the application password as the system root password
The plaintext password hardcoded in the web application — '[REDACTED: recovered credential]' — had also been set as the root account's system password. A single 'su root' command with this credential succeeded immediately, yielding a root shell and access to root.txt and completing full system compromise with no further exploitation required.
Su - root accepted '[REDACTED: recovered credential]' and returned uid=0(root) gid=0(root) groups=0(root); root.txt was read from /root.
Exact commands 2
Supply the reused password non-interactively; root flag value is <root.txt>.
printf '%s\n' "$PASSWORD" | su - root -c 'id; cat /root/root.txt'
printf '%s\n' "$PASSWORD" | ssh -tt -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i ransom_home/.ssh/id_rsa htb@$TARGET "su - root -c 'id; cat /root/root.txt'" 2>/dev/null
FixUse unique passwords for every account and never share credentials between applications and system accountsCritical
WeaknessThe password embedded in the web application was also set as the root account's operating-system password. A single discovered credential — found in source code, in a decrypted archive, or guessed — provided both application access and unrestricted root access to the entire server.
FixSet the root account's password to a unique, randomly generated value (at least 20 characters) stored only in a privileged password manager — never in source code or config files. Require all system accounts to have distinct passwords. Where possible, disable direct root login over SSH (PermitRootLogin no in sshd_config) and require operators to escalate via sudo with individual named accounts, enforcing MFA for privileged access. Audit all system account passwords to confirm none match any application credential.

Attack patterns used

The transferable techniques behind this compromise.

Password / Credential ReuseCredential Access · Lateral MovementT1078

What it is

A password recovered from one place — a config file, a database, a cracked hash, a service account — is tried against other accounts and services (SSH, SMB, WinRM, sudo, the database, the next host). Reuse turns a single leaked secret into broad access.

Why it works

Humans and deployments reuse passwords across accounts and tiers, and lateral movement thrives on it. Remediate with unique credentials per account/service, a password manager/vault, and MFA on remote-access services.

Read more

SSH Private Key / Credential TheftCredential Access · Lateral MovementT1552.004

What it is

Foothold access frequently exposes reusable secrets: SSH private keys (~/.ssh/id_rsa), authorized_keys, config files, history, and backups. Recovering a private key lets an unauthorised user authenticate as that user (or pivot to other hosts that trust the key), often upgrading a shaky webshell into a stable SSH session.

Why it works

Keys and credentials get left in home directories, world-readable backups, and version control. Remediate by passphrase-protecting keys, scoping authorized_keys, and scanning for secrets at rest.

Read more

Exposed services

22/tcp
80/tcp