← all walkthroughs

Kryptos

Linux· Insane
owned
2026-07-12
time to own
24m36s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

The Kryptos web application (<retired-instance-ip>) exposed a login form that accepted a client-supplied database hostname, which an attacker redirected to a rogue MySQL server to intercept the application's own database credentials and gain an authenticated Cryptor session.

The Cryptor utility reused its RC4 keystream across every operation, so encrypting a chosen plaintext recovered the full keystream via XOR and allowed decryption of an opaque ciphertext parameter that served as an SSRF gateway into a developer application bound exclusively to 127.0.0.1.

A SQLite injection in that internal app—exploiting SQLite's ATTACH DATABASE construct—wrote a PHP webshell to the public web root and gave command execution as www-data.

Post-exploitation uncovered a plaintext credential hint file (creds.old) and a VimCrypt-encrypted SSH password file belonging to user rijndael; cracking the VimCrypt material offline yielded the SSH passphrase and the first flag.

For root, rijndael's home directory contained kryptos.py, a Bottle signing service on localhost:81 that derived its ECDSA nonce from a custom modular-exponentiation RNG with a critically small and repeating state space.

Brute-forcing the entire state space recovered the live seed-secret pair, enabling a forged validly-signed request whose attacker-controlled Expression field was evaluated server-side by Python's eval() as root—returning the root flag through a class-hierarchy sandbox escape.

Attack path — how the box was taken

Enumerated open services and identified the Cryptor login form with a client-supplied database field, then Redirected the application's MySQL connection to a rogue server and captured the database credential, then Exploited RC4 keystream reuse to decrypt an SSRF parameter and reach the internal developer application, then Injected SQLite statements through the internal dev app to write a PHP webshell to the public web root, then Recovered a plaintext credential hint and cracked a VimCrypt-encrypted SSH password file, then Authenticated via SSH as rijndael and captured the user flag, then Exhausted the signing service's custom RNG state space to recover the live nonce and forge an ECDSA signature, then Forged a validly signed request carrying a Python eval payload and executed arbitrary commands as root.

1ReconnaissanceService enumeration and manual application analysis (T1046)
Enumerated open services and identified the Cryptor login form with a client-supplied database field
An nmap scan revealed only two open ports: SSH on 22 (OpenSSH 7.6p1) and HTTP on 80 (Apache 2.4.29, Ubuntu). Browsing port 80 returned a single-page Cryptor Login application whose form accepted four fields: username, password, db (the database host the server should connect to), and a CSRF token. The client-controlled db field immediately flagged the application as a candidate for database-connection redirection.
nmap: 22/tcp open ssh OpenSSH 7.6p1, 80/tcp open http Apache httpd 2.4.29; curl of / returned the Cryptor Login form including db and token fields.
Exact commands 2
Identify open ports and service banners.
nmap -Pn -sV -sC -p 22,80 --open $TARGET
Download the login page and list all form fields; confirm the db parameter and the hidden CSRF token.
curl -sS -c cookies.txt http://$TARGET/ -o index.html && grep -E 'name=|action=' index.html
2Initial AccessClient-controlled PDO database connection redirection (T1190, CWE-918)
Redirected the application's MySQL connection to a rogue server and captured the database credential
The application's PHP backend passed the user-supplied db field directly into a PDO connection string with no validation, so setting db to an attacker-controlled IP caused the server to authenticate against a rogue MySQL listener instead of its real database. A Metasploit capture module emulated the expected accounts schema, returned a success response, and logged the application's full MySQL challenge-response handshake, exposing the dbuser credential. Because the rogue server accepted the connection as successful, the application also issued a valid Cryptor session cookie to the attacker.
Metasploit auxiliary/server/capture/mysql captured the MySQL challenge-response for dbuser; the application issued an authenticated Cryptor session cookie after the spoofed response was accepted.
Exact commands 1
Start the rogue MySQL listener on the attacker machine (tun0 interface); leave it running in the background.
msfconsole -q -x 'use auxiliary/server/capture/mysql; set SRVHOST 0.0.0.0; set SRVPORT 3306; set JOHNPWFILE /tmp/kryptos_mysql; run -j'
3Lateral Movement — Decryption and SSRFRC4 keystream reuse / chosen-plaintext SSRF (T1600, CWE-326)
Exploited RC4 keystream reuse to decrypt an SSRF parameter and reach the internal developer application
The authenticated Cryptor tool performed RC4 encryption and decryption but generated the keystream only once and reused it for every operation in the session. Submitting a chosen all-A plaintext of the appropriate length to the encrypt endpoint returned a ciphertext; XORing that ciphertext with the known plaintext cancelled the keystream and exposed it directly. Applying that recovered keystream to a previously captured opaque ciphertext parameter decrypted it into a URL of the form http://$LOOPBACK/dev/<hash>/, pointing to a developer application bound exclusively to localhost. Encoding the internal URL and submitting it back through the Cryptor caused the server to fetch it on the attacker's behalf, confirming SSRF and revealing sqlite_test_page.php in the internal application.
XOR of chosen-plaintext encrypt output with plaintext recovered the keystream; decrypted SSRF parameter resolved to http://$LOOPBACK/dev/<hash>/sqlite_test_page.php, reachable via Cryptor relay.
Exact commands 1
Trigger SSRF by submitting an RC4-encrypted encoding of the internal dev URL; the Cryptor decrypts it and fetches the resulting 127.0.0.1 address server-side. Inspect the response for sqlite_test_page.php.
curl -sS -c cryptor_cookies.txt -b cryptor_cookies.txt -X POST http://$TARGET/decrypt.php --data 'ciphertext=<RC4_ENCRYPTED_INTERNAL_URL>'
4ExploitationSecond-order SQLite injection with ATTACH DATABASE arbitrary file write (T1190, CWE-89)
Injected SQLite statements through the internal dev app to write a PHP webshell to the public web root
The internal developer application's sqlite_test_page.php built its SQLite query by directly concatenating raw GET parameter values (bookid and no_results) with no parameterization or escaping. Injecting a second statement using ATTACH DATABASE instructed SQLite to create a new database file at /var/www/html/webshell.php, and a subsequent INSERT wrote a PHP one-liner as a table row into that file verbatim. Because Apache served /var/www/html/ and executed .php files, a direct HTTP GET to /webshell.php ran attacker-supplied shell commands as the www-data user.
curl to /webshell.php?cmd=id returned uid=33(www-data); webshell written via SSRF-relayed injection payload.
Exact commands 2
Confirm code execution; expected: uid=33(www-data).
curl -sS "http://$TARGET/webshell.php?cmd=id"
List the web root to locate creds.old and other files of interest.
curl -sS "http://$TARGET/webshell.php?cmd=ls+-la+/var/www/html/"
5Credential HarvestingPlaintext credential exposure and offline VimCrypt password cracking (T1552.001, T1110.002)
Recovered a plaintext credential hint and cracked a VimCrypt-encrypted SSH password file
From the www-data web shell, a creds.old file in the web root contained the cleartext entry rijndael / [REDACTED: recovered credential]. Further enumeration of rijndael's home directory uncovered a VimCrypt-encrypted file holding the account's real SSH passphrase. VimCrypt's blowfish2 cipher is attackable offline: vim2john extracted a crackable hash and John the Ripper recovered the plaintext passphrase [REDACTED: recovered credential] from the rockyou wordlist.
creds.old returned rijndael / [REDACTED: recovered credential]; vim2john + john cracked the VimCrypt hash to [REDACTED: recovered credential].
Exact commands 2
Read the plaintext credential hint file; expect: rijndael / [REDACTED: recovered credential].
curl -sS "http://$TARGET/webshell.php?cmd=cat+/var/www/html/creds.old"
Locate and exfiltrate the VimCrypt-encrypted file from rijndael's home directory via base64 over the web shell.
curl -sS "http://$TARGET/webshell.php?cmd=find+/home/rijndael+-maxdepth+2+-type+f" && curl -sS "http://$TARGET/webshell.php?cmd=base64+/home/rijndael/<VIMCRYPT_FILENAME>" | base64 -d > rijndael.vimcrypt
6User FootholdValid account SSH access with recovered credentials (T1078.003)
Authenticated via SSH as rijndael and captured the user flag
The cracked passphrase [REDACTED: recovered credential] was used to SSH directly into the target as rijndael. The user flag was present in rijndael's home directory. The home directory also contained a kryptos subdirectory with the source of a Python Bottle signing service (kryptos.py) running on localhost port 81 as root, which became the privilege-escalation target.
sshpass SSH session as rijndael@<retired-instance-ip> returned uid=1000(rijndael); user.txt read from /home/rijndael/user.txt.
7Privilege Escalation — RNG Brute ForceWeak PRNG state-space exhaustion enabling ECDSA nonce prediction (CWE-338, T1600)
Exhausted the signing service's custom RNG state space to recover the live nonce and forge an ECDSA signature
kryptos.py ran as root on 127.0.0.1:81 and signed each request with ECDSA over NIST P-384. Its per-request nonce was derived from a custom RNG: seed_next = pow(g, seed, p), where g and p were chosen to produce a state space small enough to fully enumerate in seconds. A brute-force script iterated every possible seed, computed the expected successor state for each, and queried the live signing service until the predicted nonce matched its response—identifying the current seed (FOUND_SEED=[REDACTED: sensitive value]) and associated secret (FOUND_SECRET=[REDACTED: signing value]). With the seed known, the service's next nonce was fully predictable, enabling construction of a cryptographically valid signature for any request body without possessing the ECDSA private key.
kryptos_brute_id.py output: FOUND_SEED=[REDACTED: sensitive value] FOUND_SECRET=[REDACTED: signing value]; forged signature accepted by the localhost:81 service.
8Full CompromiseServer-side Python eval injection with class-hierarchy sandbox escape (T1059.006, CWE-95)
Forged a validly signed request carrying a Python eval payload and executed arbitrary commands as root
With the recovered seed and secret, the signing service's next nonce was fully predictable, so a validly signed POST request could be constructed for any body. The service's evaluation endpoint passed the attacker-controlled Expression field directly to Python's built-in eval() and ran as the root user who started the service. A class-hierarchy traversal—(1).__class__.__base__.__subclasses__() locating re.Pattern, then walking __init__.__globals__['__builtins__']['__import__']('os') to reach os.popen()—bypassed any attempted sandboxing and executed arbitrary shell commands as root. The root flag was read via os.popen('cat /root/root.txt').read().
kryptos_rootflag.py returned the contents of /root/root.txt with uid=0; root flag captured and verified by HTB.

Attack patterns used

The transferable techniques behind the compromise.

Client-controlled PDO database connection redirectionInitial AccessT1190

What it is

The application's PHP backend passed the user-supplied db field directly into a PDO connection string with no validation, so setting db to an attacker-controlled IP caused the server to authenticate against a rogue MySQL listener instead of its real database. A Metasploit capture module emulated the expected accounts schema, returned a success response, and logged the application's full MySQL challenge-response handshake, exposing the dbuser credential. Because the rogue server accepted the connection as successful, the application also issued a valid Cryptor session cookie to the attacker.

Why it works

Hard-code the database hostname and port in a server-side configuration file or environment variable (e.g., a PHP constant defined outside the web root) and remove the db field from the login form entirely. If multi-database support is a genuine requirement, maintain a server-side allowlist of permitted hostnames and validate any user selection against it before constructing any PDO DSN.

RC4 keystream reuse / chosen-plaintext SSRFLateral Movement — Decryption and SSRFT1600

What it is

The authenticated Cryptor tool performed RC4 encryption and decryption but generated the keystream only once and reused it for every operation in the session. Submitting a chosen all-A plaintext of the appropriate length to the encrypt endpoint returned a ciphertext; XORing that ciphertext with the known plaintext cancelled the keystream and exposed it directly. Applying that recovered keystream to a previously captured opaque ciphertext parameter decrypted it into a URL of the form http://$LOOPBACK/dev/<hash>/, pointing to a developer application bound exclusively to localhost. Encoding the internal URL and submitting it back through the Cryptor caused the server to fetch it on the attacker's behalf, confirming SSRF and revealing sqlite_test_page.php in the internal application.

Why it works

Replace RC4 with an AEAD cipher (AES-256-GCM or ChaCha20-Poly1305). Every encryption call must generate a fresh, cryptographically random nonce via PHP's random_bytes() and transmit it alongside the ciphertext; the nonce must never be reused under the same key. The authentication tag also prevents ciphertext forgery, closing the SSRF primitive entirely.

Second-order SQLite injection with ATTACH DATABASE arbitrary file writeExploitationT1190

What it is

The internal developer application's sqlite_test_page.php built its SQLite query by directly concatenating raw GET parameter values (bookid and no_results) with no parameterization or escaping. Injecting a second statement using ATTACH DATABASE instructed SQLite to create a new database file at /var/www/html/webshell.php, and a subsequent INSERT wrote a PHP one-liner as a table row into that file verbatim. Because Apache served /var/www/html/ and executed .php files, a direct HTTP GET to /webshell.php ran attacker-supplied shell commands as the www-data user.

Why it works

Use PDO prepared statements with bound parameters for every query in sqlite_test_page.php so no user-supplied value can alter query structure. Register a SQLite authorizer callback (sqlite3_set_authorizer) to deny SQLITE_ATTACH and SQLITE_CREATE_TABLE on paths outside the designated data directory. As defence-in-depth, configure Apache to deny PHP execution (php_flag engine off) in any directory that accepts file writes.

Plaintext credential exposure and offline VimCrypt password crackingCredential HarvestingT1552.001

What it is

From the www-data web shell, a creds.old file in the web root contained the cleartext entry rijndael / [REDACTED: recovered credential]. Further enumeration of rijndael's home directory uncovered a VimCrypt-encrypted file holding the account's real SSH passphrase. VimCrypt's blowfish2 cipher is attackable offline: vim2john extracted a crackable hash and John the Ripper recovered the plaintext passphrase [REDACTED: recovered credential] from the rockyou wordlist.

Why it works

Delete all credential backup files (*.old, *.bak, *.orig, *.save) from every server-accessible directory and add them to .gitignore. Store secrets at rest in a dedicated secrets manager (e.g., HashiCorp Vault) or GPG-encrypted with a strong asymmetric key. Immediately rotate the rijndael SSH passphrase and any other credential that appeared in creds.old.

Weak PRNG state-space exhaustion enabling ECDSA nonce predictionPrivilege Escalation — RNG Brute ForceT1600

What it is

kryptos.py ran as root on 127.0.0.1:81 and signed each request with ECDSA over NIST P-384. Its per-request nonce was derived from a custom RNG: seed_next = pow(g, seed, p), where g and p were chosen to produce a state space small enough to fully enumerate in seconds. A brute-force script iterated every possible seed, computed the expected successor state for each, and queried the live signing service until the predicted nonce matched its response—identifying the current seed (FOUND_SEED=[REDACTED: sensitive value]) and associated secret (FOUND_SECRET=[REDACTED: signing value]). With the seed known, the service's next nonce was fully predictable, enabling construction of a cryptographically valid signature for any request body without possessing the ECDSA private key.

Why it works

Remove the custom RNG entirely. Use RFC 6979 deterministic nonce generation, which is the default in Python's ecdsa library (sk.sign_deterministic(msg, hashlib.sha384)), or supply nonces drawn from the OS CSPRNG (os.urandom(48)). Never implement a custom RNG for any cryptographic purpose; even well-intentioned constructions almost always have a smaller effective state space than intended.

Server-side Python eval injection with class-hierarchy sandbox escapeFull CompromiseT1059.006

What it is

With the recovered seed and secret, the signing service's next nonce was fully predictable, so a validly signed POST request could be constructed for any body. The service's evaluation endpoint passed the attacker-controlled Expression field directly to Python's built-in eval() and ran as the root user who started the service. A class-hierarchy traversal—(1).__class__.__base__.__subclasses__() locating re.Pattern, then walking __init__.__globals__['__builtins__']['__import__']('os') to reach os.popen()—bypassed any attempted sandboxing and executed arbitrary shell commands as root. The root flag was read via os.popen('cat /root/root.txt').read().

Why it works

Remove eval() of user input entirely; there is no safe way to sandbox Python eval() against a determined attacker. If a calculation or scripting capability is genuinely required, implement it with an explicit allowlist using ast.literal_eval for data values or a dedicated restricted-grammar parser with no access to builtins. As a mandatory defence-in-depth control, run the signing service under a dedicated low-privilege service account (e.g., a kryptos system user) so that any future code-execution vulnerability in this process cannot directly yield root access.

Findings

Remove client control over the database connection endpointCritical
The login form accepted a user-supplied db field that was passed without validation into the PHP PDO connection string. Any visitor could redirect the application's MySQL authentication handshake to an arbitrary host, intercept the server's own database credentials, and receive a valid authenticated session in return.
Replace the RC4 Cryptor with a modern authenticated encryption scheme and never reuse a nonceCritical
The Cryptor utility generated a single RC4 keystream at session start and reused it for every subsequent encrypt and decrypt call. Reusing a keystream means that XORing any two ciphertexts produced under the same key directly cancels the keystream, recovering both plaintexts — including the SSRF-capable internal URL.
Parameterize all SQLite queries and deny the ATTACH DATABASE privilegeCritical
The internal dev application concatenated raw GET parameter values (bookid, no_results) directly into SQLite query strings. An attacker injected a second statement using ATTACH DATABASE to create a new SQLite database file at an arbitrary path in the public web root, then inserted a PHP one-liner as a table row — writing an executable webshell that Apache subsequently ran.
Remove plaintext credential hint files and replace VimCrypt storage with a secure secrets managerHigh
A creds.old file in the public web root contained a cleartext username-and-password pair, and the user's real SSH passphrase was stored only as a VimCrypt-encrypted file. VimCrypt's blowfish2 mode is offline-crackable with standard tools (vim2john + John the Ripper), so both secrets were recoverable from a low-privilege web-shell foothold.
Replace the custom modular-exponentiation RNG with RFC 6979 deterministic ECDSA nonce generationCritical
kryptos.py derived its ECDSA signing nonce from a homegrown RNG (seed_next = pow(g, seed, p)) whose prime p and generator g were chosen to produce a state space small enough to exhaust in seconds on commodity hardware. An attacker who identified the current state could predict every future nonce, making signature forgery trivial without the private key.
Remove server-side eval of user-supplied expressions and run the signing service as a non-root accountCritical
The port-81 signing service passed an attacker-controlled Expression field directly to Python's built-in eval() and ran as the root user. Python's class hierarchy cannot be safely sandboxed — traversal via __class__.__base__.__subclasses__() reliably reaches os.popen() — so any authenticated caller could execute arbitrary shell commands as root.

Exposed services

External surface