Kryptos
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.
Exact commands 2
nmap -Pn -sV -sC -p 22,80 --open $TARGETcurl -sS -c cookies.txt http://$TARGET/ -o index.html && grep -E 'name=|action=' index.htmlExact commands 1
msfconsole -q -x 'use auxiliary/server/capture/mysql; set SRVHOST 0.0.0.0; set SRVPORT 3306; set JOHNPWFILE /tmp/kryptos_mysql; run -j'Exact commands 1
curl -sS -c cryptor_cookies.txt -b cryptor_cookies.txt -X POST http://$TARGET/decrypt.php --data 'ciphertext=<RC4_ENCRYPTED_INTERNAL_URL>'Exact commands 2
curl -sS "http://$TARGET/webshell.php?cmd=id"curl -sS "http://$TARGET/webshell.php?cmd=ls+-la+/var/www/html/"Exact commands 2
curl -sS "http://$TARGET/webshell.php?cmd=cat+/var/www/html/creds.old"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.vimcryptAttack 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
Exposed services
| External surface | 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. |