TryHackMe: Crypto Failures
Crypto Failures began by discovering the source code of the web application and examining it to understand the authentication functionality, which we then used to log in as the admin user. Afterward, we leveraged the same authentication functionality to brute-force a secret key used within it to complete the room.
Initial Enumeration
Nmap Scan
We start with an nmap
scan.
1
2
3
4
5
6
7
8
9
10
11
12
13
$ nmap -T4 -n -sC -sV -Pn -p- 10.10.46.169
Nmap scan report for 10.10.46.169
Host is up (0.094s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.7 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 57:2c:43:78:0c:d3:13:5b:8d:83:df:63:cf:53:61:91 (ECDSA)
|_ 256 45:e1:3c:eb:a6:2d:d7:c6:bb:43:24:7e:02:e9:11:39 (ED25519)
80/tcp open http Apache httpd 2.4.59 ((Debian))
|_http-server-header: Apache/2.4.59 (Debian)
|_http-title: Did not follow redirect to /
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
There are two open ports:
- 22 (
SSH
) - 80 (
HTTP
)
Web 80
Visiting http://10.10.46.169/
, we are simply greeted with a “logged in” message.
Checking the requests in Burp, we see that in our first request, the server sets the secure_cookie
and user
cookies, then redirects us back to the index using the Location
header.
The second request is more interesting. Besides the “logged in” message, there is also a comment in the response:
<!-- TODO: remember to remove .bak files -->
Examining the Source Code
The comment we discovered suggests that some .bak
files were left on the web server. We can fuzz for them and discover the index.php.bak
file.
1
2
3
4
5
$ ffuf -u 'http://10.10.46.169/FUZZ' -w /usr/share/seclists/Discovery/Web-Content/directory-list-2.3-small.txt -e .php,.php.bak -t 100 -mc all -ic -fc 404
...
index.php [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 554ms]
index.php.bak [Status: 200, Size: 1979, Words: 282, Lines: 96, Duration: 3569ms]
config.php [Status: 200, Size: 0, Words: 1, Lines: 1, Duration: 121ms]
We can download the index.php.bak
file using wget
:
1
$ wget http://10.10.46.169/index.php.bak
Examining index.php.bak
, it appears to contain the source code of the current web application:
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
<?php
include "config.php";
function generate_cookie($user, $ENC_SECRET_KEY)
{
$SALT = generatesalt(2);
$secure_cookie_string = $user . ":" . $_SERVER["HTTP_USER_AGENT"] . ":" . $ENC_SECRET_KEY;
$secure_cookie = make_secure_cookie($secure_cookie_string, $SALT);
setcookie("secure_cookie", $secure_cookie, time() + 3600, "/", "", false);
setcookie("user", "$user", time() + 3600, "/", "", false);
}
function cryptstring($what, $SALT)
{
return crypt($what, $SALT);
}
function make_secure_cookie($text, $SALT)
{
$secure_cookie = "";
foreach (str_split($text, 8) as $el) {
$secure_cookie .= cryptstring($el, $SALT);
}
return $secure_cookie;
}
function generatesalt($n)
{
$randomString = "";
$characters = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
for ($i = 0; $i < $n; $i++) {
$index = rand(0, strlen($characters) - 1);
$randomString .= $characters[$index];
}
return $randomString;
}
function verify_cookie($ENC_SECRET_KEY)
{
$crypted_cookie = $_COOKIE["secure_cookie"];
$user = $_COOKIE["user"];
$string = $user . ":" . $_SERVER["HTTP_USER_AGENT"] . ":" . $ENC_SECRET_KEY;
$salt = substr($_COOKIE["secure_cookie"], 0, 2);
if (make_secure_cookie($string, $salt) === $crypted_cookie) {
return true;
} else {
return false;
}
}
if (isset($_COOKIE["secure_cookie"]) && isset($_COOKIE["user"])) {
$user = $_COOKIE["user"];
if (verify_cookie($ENC_SECRET_KEY)) {
if ($user === "admin") {
echo "congrats: ******flag here******. Now I want the key.";
} else {
$length = strlen($_SERVER["HTTP_USER_AGENT"]);
print "<p>You are logged in as " . $user . ":" . str_repeat("*", $length) . "\n";
print "<p>SSO cookie is protected with traditional military grade en<b>crypt</b>ion\n";
}
} else {
print "<p>You are not logged in\n";
}
} else {
generate_cookie("guest", $ENC_SECRET_KEY);
header("Location: /");
}
?>
- The application is fairly simple. It starts by including the
config.php
file. Since the application uses theENC_SECRET_KEY
variable, but it is not defined inindex.php
, we can infer thatconfig.php
contains its value.
1
include "config.php";
- Next, it checks whether the
secure_cookie
anduser
cookies are set. If not, it callsgenerate_cookie
with"guest"
andENC_SECRET_KEY
. If they are set, it callsverify_cookie
withENC_SECRET_KEY
. If this function returnstrue
, it checks the value of theuser
cookie. If it is"admin"
, the flag is printed. Otherwise, a logged-in message with the current user is displayed.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (isset($_COOKIE["secure_cookie"]) && isset($_COOKIE["user"])) {
$user = $_COOKIE["user"];
if (verify_cookie($ENC_SECRET_KEY)) {
if ($user === "admin") {
echo "congrats: ******flag here******. Now I want the key.";
} else {
$length = strlen($_SERVER["HTTP_USER_AGENT"]);
print "<p>You are logged in as " . $user . ":" . str_repeat("*", $length) . "\n";
print "<p>SSO cookie is protected with traditional military grade en<b>crypt</b>ion\n";
}
} else {
print "<p>You are not logged in\n";
}
} else {
generate_cookie("guest", $ENC_SECRET_KEY);
header("Location: /");
}
- Let’s analyze the case where the cookies are not set and examine the
generate_cookie
function. First, it callsgeneratesalt(2)
, which generates a random 2-byte salt from an alphanumeric character set. After that, it creates a string using the provideduser
,User-Agent
, andENC_SECRET_KEY
, separated by:
. This string is then passed tomake_secure_cookie
along with the generated salt. Finally, it sets the returned value as thesecure_cookie
cookie and theuser
in another cookie.
1
2
3
4
5
6
7
8
9
10
11
function generate_cookie($user, $ENC_SECRET_KEY)
{
$SALT = generatesalt(2);
$secure_cookie_string = $user . ":" . $_SERVER["HTTP_USER_AGENT"] . ":" . $ENC_SECRET_KEY;
$secure_cookie = make_secure_cookie($secure_cookie_string, $SALT);
setcookie("secure_cookie", $secure_cookie, time() + 3600, "/", "", false);
setcookie("user", "$user", time() + 3600, "/", "", false);
}
- Checking
make_secure_cookie
, we see that it splits the input string into 8-byte chunks and callscryptstring
for each chunk, along with the provided salt. It then concatenates the return values and returns the final string.
1
2
3
4
5
6
7
8
9
10
function make_secure_cookie($text, $SALT)
{
$secure_cookie = "";
foreach (str_split($text, 8) as $el) {
$secure_cookie .= cryptstring($el, $SALT);
}
return $secure_cookie;
}
- Examining
cryptstring
, we see that it simply calls PHP’scrypt()
function to hash the string passed with the given salt.
1
2
3
4
function cryptstring($what, $SALT)
{
return crypt($what, $SALT);
}
- Now, let’s also analyze what happens when the
secure_cookie
anduser
cookies are set by looking at theverify_cookie
function. First, it retrieves the values of these cookies. It then reconstructs the original string usinguser
,User-Agent
, andENC_SECRET_KEY
. Since all hashes use the same salt and the first two bytes of each hash represent the salt, it extracts the salt from the first hash insecure_cookie
. Finally, it callsmake_secure_cookie
with the reconstructed string and extracted salt, then compares the result with the value stored insecure_cookie
and returns the result of the comparison.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function verify_cookie($ENC_SECRET_KEY)
{
$crypted_cookie = $_COOKIE["secure_cookie"];
$user = $_COOKIE["user"];
$string = $user . ":" . $_SERVER["HTTP_USER_AGENT"] . ":" . $ENC_SECRET_KEY;
$salt = substr($_COOKIE["secure_cookie"], 0, 2);
if (make_secure_cookie($string, $salt) === $crypted_cookie) {
return true;
} else {
return false;
}
}
For example, in our case, user
is guest
, and the User-Agent
is Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
.
So, the server constructs the string to hash as:
1
guest:Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0:<ENC_SECRET_KEY>
And the server returns secure_cookie
as:
1
AdSleedEWCRK2Adyb8twq9SeTwAd1ewQQZ2hHRcAdxNnQGj8bSPMAdOz9pHkM6hjwAd1xuiZYw7z0wAdf7clVSfkhFQAdjjS2u2vogycAdBP8msSwLPn6AdcrIsmiZ7xcwAd4R54HxWvY/sAd507x3ic0BCwAdoWO7KprUNEIAdsF0KAF1noBkAdfGw5AZYSiawAdiESYxpdMbMwAdQOL4Bzw3FI6Add
Since the server hashes the string in 8-byte blocks, we can see the hashes for each block:
1
2
3
"guest:Mo" "zilla/5." "0 (X11; " ...:<ENC_SECRET_KEY>
V V V
AdSleedEWCRK2 Adyb8twq9SeTw Ad1ewQQZ2hHRc ...
We can also confirm that these hashes match the blocks by hashing them manually:
1
2
3
4
5
6
7
$ php -a
php > echo crypt("guest:Mo", "Ad");
AdSleedEWCRK2
php > echo crypt("zilla/5.", "Ad");
Adyb8twq9SeTw
php > echo crypt("0 (X11; ", "Ad");
Ad1ewQQZ2hHRc
Logging in as Admin
Examining the source code, we notice that even though we don’t know ENC_SECRET_KEY
, the first 8-byte block to be hashed consists only of the user
and the User-Agent
. After hashing, it is directly compared to the first hash in the secure_cookie
cookie. This means we can control both the plaintext (since user
is read from the user
cookie and User-Agent
is set via the User-Agent
header) and the hash it is compared to (by modifying secure_cookie
), allowing us to log in as any user we want.
Since our goal is to capture the flag by logging in as admin
, we can simply set the user
cookie to "admin"
. This changes the first block to be hashed from "guest:Mo"
to "admin:Mo"
.
Then, by replacing the first hash in secure_cookie
with AdBOdWNO.9Zps
(crypt("admin:Mo", "Ad")
) instead of AdSleedEWCRK2
(crypt("guest:Mo", "Ad")
), we can also pass the check in verify_cookie
and log in successfully.
1
2
3
4
php > echo crypt("guest:Mo", "Ad");
AdSleedEWCRK2
php > echo crypt("admin:Mo", "Ad");
AdBOdWNO.9Zps
Modifying the secure_cookie
and user
cookies as mentioned, we can see that we are able to log in successfully and capture the first flag.
Discovering the Key
We were able to log in as the admin
user and obtain the first flag, but now it seems we need to find the ENC_SECRET_KEY
for the next flag. One way to do this would be to simply brute-force each hash in the secure_cookie
, but this would require brute-forcing 8 bytes for every hash, which would take a really long time.
Instead, we can leverage how the string is being hashed to make the process more efficient. Since the string to be hashed starts with our input and is hashed in 8-byte blocks, we can utilize this to make brute-forcing easier by using the User-Agent
as padding and changing its length to always have a single 8-byte block where we know the first 7 bytes, allowing us to only brute-force the last character of that block.
For example, if we send an empty User-Agent
to the server, the string to be hashed would be: guest::<ENC_SECRET_KEY>
. Since it is hashed in 8-byte blocks, the first block that is hashed would end up as: guest::<First character of ENC_SECRET_KEY>
. We can then simply iterate over all the characters and append them to the guest::
string, hash it, and check the resulting hash against the first hash in the secure_cookie
returned from the server. If they match, we can identify the first character of the ENC_SECRET_KEY
. By repeating this process for the remaining characters, we can discover the key by brute-forcing one character at a time.
As we can see, making such a request to the server with an empty User-Agent
, the first hash returned from the server is: 2c2QeitMw0e1g
.
Now, we can simply try appending every character to guest::
and hash it, then compare it to the first hash from the secure_cookie
. When we append T
to guest::
, we see that the hashes match, and thus we have found the first character of the ENC_SECRET_KEY
.
1
2
3
4
5
6
7
8
...
php > echo crypt("guest::S", "2c");
2cri//aAPLqkY
php > echo crypt("guest::T", "2c");
2c2QeitMw0e1g
php > echo crypt("guest::U", "2c");
2cYsF2IWJsvrE
...
Next, we can move on to the second character. This time, if we set the User-Agent
as AAAAAAA
, the string to be hashed would be: guest:AAAAAAA:<ENC_SECRET_KEY>
. If we split it into 8-byte chunks, just like the server does, we can see that the first chunk would be guest:AA
, which we don’t care about. However, the second chunk would be: AAAAA:<First two characters of ENC_SECRET_KEY>
. Since we already discovered the first character of the ENC_SECRET_KEY
, the second block would be AAAAA:T<Second character of ENC_SECRET_KEY>
. We can then once again append every character to AAAAA:T
and compare it to the second hash from the secure_cookie
. (We use the second hash since our block with 7 known bytes and 1 unknown byte is the second one now.)
If we make a request to the server with the User-Agent
set to AAAAAAA
as mentioned, we can see that the hash for the second block is 0gy7IR0MNsLPo
.
Then, using the same method as before, we can try every character and see that when we append H
, the hashes match. Therefore, the second character of the ENC_SECRET_KEY
is H
.
1
2
3
4
5
6
7
8
...
php > echo crypt("AAAAA:TG", "0g");
0gjGaQws2eVs6
php > echo crypt("AAAAA:TH", "0g");
0gy7IR0MNsLPo
php > echo crypt("AAAAA:TI", "0g");
0gaQtrM9hmGDc
...
Next, we can modify the User-Agent
to AAAAAA
, so the string becomes: guest:AAAAAA:<ENC_SECRET_KEY>
, with the second block to be hashed being: AAAA:TH<Third character of ENC_SECRET_KEY>
. We can then brute-force the third character as before and continue discovering the secret key by using the User-Agent
to always have a hashed block with 7 known bytes and 1 unknown byte to brute-force.
But instead of doing this manually for each character of the key, we can automate the process by writing a Python
script, as shown below:
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
#!/usr/bin/env python3
import crypt
import requests
import urllib.parse
import string
BASE_URL = "http://10.10.46.169/"
USERNAME = "guest:"
SEPARATOR = ":"
CHARSET = string.printable
def get_secure_cookie(user_agent: str) -> str:
session = requests.Session()
response = session.get(BASE_URL, headers={"User-Agent": user_agent})
cookie = session.cookies.get("secure_cookie")
return urllib.parse.unquote(cookie)
def main():
discovered = ""
while True:
ua_padding_length = (7 - len(USERNAME + SEPARATOR + discovered)) % 8
user_agent = "A" * ua_padding_length
prefix = USERNAME + user_agent + SEPARATOR + discovered
block_index = len(prefix) // 8
secure_cookie = get_secure_cookie(user_agent)
target_block = secure_cookie[block_index * 13:(block_index + 1) * 13]
salt = target_block[:2]
found_char = False
for char in CHARSET:
candidate = (prefix + char)[-8:]
candidate_hash = crypt.crypt(candidate, salt)
if candidate_hash == target_block:
discovered += char
print(char, end="", flush=True)
found_char = True
break
if not found_char:
break
print()
if __name__ == "__main__":
main()
Running the script, we successfully discover the ENC_SECRET_KEY
, which is the second flag and complete the room.
1
2
$ python3 solve.py
THM{Tr[REDACTED]b9}