The following post by 0xb0b is licensed under CC BY 4.0
Recon
We start with a Nmap scan and find two open ports. We have SSH on port 22 and a web server on port 80.
We visit the page and see that we are logged in as a guest. There is talk of an SSO cookie that is supposed to be secured by military grade encryption.
We find the cookie as follows:
In the source, we find a comment that the .bak files should still be removed. Furthermore crypt is written in bold.
We intercept a request to the index page using burp and check whether index.php can also be called. And we get the index page.
If we now request index.php.bak, we find the source of the index page and thus also the generation of the cookie. The config.php is unfortunately not available as a backup, with that we might have had access to the key.
HTTP/1.1 200 OK
Date: Fri, 28 Feb 2025 21:18:26 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: /');
}
?>
Source Code Analysis
If a cookie secure_cookie is not set, it gets generated by generate_cookie(). The structure of the cookie is user:UserAgent:ENC_SECRET_KEY. So we are at least in control of the User-Agent parameter, but more on that later. The generate_cookie function first calls generatesalt(), to generates a random 2-byte salt from an alphanumeric character set. This salt and the cookie string is then passed to the function make_secure_cookie.
The make_secure_cookie() function essentially applies crypt() in chunks of 8 characters of the passed cookie string including the salt.
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);
}
The verification process extracts the salt from the first two characters of the stored cookie (substr($_COOKIE['secure_cookie'],0,2)). Which is then used to generate a secure cookie to compare it with the one passed.
Once a valid cookie is verified, the user is either given access or denied based on their username. The username is set by the user cookie. If the user is admin a flag will be shown.
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";
}
Admin Access
Since we are able to extract the salt of a hash, and we know that every chunks of 8 characters of the cookie string will be hashed with the salt, it is easy to impersonate the admin. All we have to do is to replace the first hash with the one representing the admin user and setting the user cookie to admin.
We run our script to generate a new secure_cookie.
Next, we replace the secure_cookie with ours and set the user cookie to admin and reload the page. We are now logged in as admin and get the first flag.
Obtaining The Secret: ENC_SECRET_KEY
The next step is to extract the secret key. Since we know the salt, we can now bruteforce any 8 chunk block. With a character set of 65 characters, that would be 65^8 permutations that we would have to try for each block. That would not be feasible.
Instead, we could reduce it to one byte. Because we have control over the User-Agent and could create a block of which we already know 7 bytes and would only have to bruteforce one byte.
We hash the block with the 7 known bytes and the unknown character and when this resulting hash reappears in the cookie, we know that the unknown byte must be part of the key.
So we could brute-force the second chunk with the following string, generated by the user agent = AAAAAAAAguest:AAAAAAAA:_ and thus determine the first character of the key.
Chunk A: guest:AA
Chunk B: AAAAAA:_
So, we use the User-Agent as padding, that allows us to brute-force a single character at a time.
We chose a padding of 256 since the flag is really long.
We then just need to reduce the padding and update our know string $test_string with the character sequence found until we finally uncovered the entire key.
We illustrate this in the following graphic. The test string is outlined. If we get a hit with a hash, we reduce the user-agent by one A and adjust the test string by removing the first character and appending the next known one.
extreact_key.php
<?php
// Target URL
$target_url = "http://cryptofailures.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", 256); // Adjusted length
$known_prefix = "guest:" . $user_agent . ":"; // 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 breaks off after a while.
But since we know how long the already leaked string is, and we have the last 8 characters, we update our script to make it continue at that point.