We start with a Nmap scan that reveals two open ports: port 22 (SSH)and port 80 (a web server).
┌──(kali㉿kali)-[~]
└─$ nmap -p- crypto.thm -T4
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-03-03 11:15 IST
Stats: 0:01:54 elapsed; 0 hosts completed (1 up), 1 undergoing Connect Scan
Nmap scan report for crypto.thm (10.10.56.57)
Host is up (0.15s latency).
Not shown: 65529 closed tcp ports (conn-refused)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
Nmap done: 1 IP address (1 host up) scanned in 897.81 seconds
┌──(kali㉿kali)-[~]
└─$ nmap -sC -sV -sT -p 22,80 crypto.thm -T4
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-03-03 11:34 IST
Stats: 0:00:06 elapsed; 0 hosts completed (1 up), 1 undergoing Service Scan
Nmap scan report for crypto.thm (10.10.56.57)
Host is up (0.15s latency).
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-title: Did not follow redirect to /
|_http-server-header: Apache/2.4.59 (Debian)
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 11.90 seconds
Upon visiting the page, we find that we are logged in as a guest. The site mentions an SSO cookie, which is claimed to be secured using military-grade encryption.
We find the cookie as follows:
In the source code, we discover a comment stating that .bak files still need to be removed. Additionally, the word crypt is emphasized in bold.
We intercept a request to the index page using Burp and test if index.php can be accessed directly. The request is successful, and we receive the index page.
By requesting index.php.bak, we retrieve the source code of the index page, including the logic for generating the SSO cookie. Unfortunately, config.php is not available as a backup; otherwise, we might have gained access to the encryption key.
HTTP/1.1 200 OK
Date: Mon, 03 Mar 2025 06:11:27 GMT
Server: Apache/2.4.59 (Debian)
Last-Modified: Tue, 18 Jun 2024 06:23:20 GMT
ETag: "7bb-61b241e413aab"
Accept-Ranges: bytes
Content-Length: 1979
Connection: close
Content-Type: application/x-trash
<?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: /');
}
?>
If the secure_cookie is not set, it is generated by generate_cookie(). The cookie follows the structure:
user:UserAgent:ENC_SECRET_KEY
This means we have control over the User-Agent parameter, but we'll explore that later.
The generate_cookie() function first calls generatesalt(), which creates a random 2-byte salt using an alphanumeric character set. This salt, along with the cookie string, is then passed to the make_secure_cookie function.
Once a valid cookie is verified, access is granted or denied based on the username. The username is determined by the user cookie. If the username is set to admin, a flag is displayed.
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";
}
Initial Access
Since we can extract the salt from the hash and know that the crypt() function operates on 8-character chunks of the cookie string, impersonating the admin user becomes straightforward.
To do this:
Extract the salt from the existing secure_cookie.
Compute the hash of admin: using the same salt.
Replace the first hashed chunk of the cookie with this new hash.
Set the user cookie to admin.
This allows us to bypass authentication and access the flag.
Next, we replace the secure_cookie with our forged one and set the user cookie to admin. After reloading the page, we successfully log in as admin and retrieve the first flag.
Obtaining The Encryption Key
To extract the secret key, we take advantage of our control over the User-Agent and the fact that the hash operates in 8-character chunks.
Problem with Bruteforcing the Entire Key
The full brute-force approach would require 65⁸ permutations per chunk, which is infeasible.
Instead, we reduce the unknown portion to a single byte at a time.
Exploiting Controlled Input
Since we control User-Agent, we craft a cookie string where we already know 7 out of 8 bytes in a chunk.
We then brute-force the remaining single byte until we find a match in the generated secure_cookie.
When the resulting hash appears in the secure_cookie, we know that the unknown byte is part of the secret key.
Step-by-Step Process
Craft the attack string using User-Agent padding:
Example: guest:AAAAAAAAAA:_
Chunk A:guest:AA (first 8-character block)
Chunk B:AAAAAA:_ (7 known bytes + 1 unknown character from the key)
Brute-force the unknown character:
Generate hashes for all possible last bytes (_ in Chunk B).
When a hash matches the corresponding chunk in the cookie, we recover that byte of the key.
Iterate and reduce padding:
Use the previously found characters to refine the attack.
Slowly reveal the entire key by repeating the process with adjusted padding.
By continuously reducing the padding and updating our test string, we fully recover the secret key.
extract_key.php
<?php
// Target URL
$target_url = "http://crypto.thm/index.php";
// Possible characters
$charset = implode('', array_merge(
range('a', 'z'), // Lowercase letters
range('A', 'Z'), // Uppercase letters
range('0', '9'), // Digits
str_split("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~") // Special symbols
));
// Initial User-Agent
$user_agent = str_repeat("A", 137); // Adjusted length
$known_prefix = "guest:" . $user_agent . ":ac65ed4a"; // Base structure
$test_string = substr($known_prefix, -8);
// Function to fetch secure_cookie with a specific User-Agent
function get_secure_cookie($user_agent) {
global $target_url;
$context = stream_context_create([
"http" => [
"method" => "GET",
"header" =>
"Host: cryptofailures.thm\r\n" .
"User-Agent: $user_agent\r\n" .
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8\r\n" .
"Accept-Language: en-US,en;q=0.5\r\n" .
"Accept-Encoding: gzip, deflate, br\r\n" .
"Connection: close\r\n" .
"Upgrade-Insecure-Requests: 1\r\n"
]
]);
// Fetch response
$response = file_get_contents($target_url, false, $context);
// Extract "secure_cookie" from headers
foreach ($http_response_header as $header) {
if (stripos($header, "Set-Cookie: secure_cookie=") !== false) {
preg_match('/secure_cookie=([^;]+)/', $header, $matches);
return $matches[1] ?? null;
}
}
return null;
}
// Start brute-force process
$found_text = "";
while (true) {
echo "\nCurrent known part: {$found_text}\n";
// Get new secure_cookie for the current prefix
$secure_cookie = get_secure_cookie($user_agent);
$user_agent = substr($user_agent, 1);
if (!$secure_cookie) {
die("❌ Failed to retrieve secure_cookie!\n");
}
echo "✅ Retrieved (Decoded) secure_cookie: $secure_cookie\n";
// Extract salt
$salt = substr($secure_cookie, 0, 2);
// Brute-force the next character
$found_char = null;
foreach (str_split($charset) as $char) {
$test_string_temp = substr($test_string, 1) . $char;
print($test_string_temp . "\n");
$hashed_test = crypt($test_string_temp, $salt);
if (str_contains($secure_cookie, $hashed_test)) {
echo "✅ Found character: $char\n";
$found_text .= $char;
$test_string = $test_string_temp;
}
}
}
?>
Unfortunately, the script crashes after running for some time.
Since we know the length of the leaked string and have the last 8 characters, we modify our script to resume from that point.