TryHackMe Crypto Failures—Writeup

Welcome back to my Writeup!! Today, I Will show the step-by-step on how I solved Crypto Failures from TryHackMe!! We'll dive deep into the challenge, explore the vulnerabilities, and go over the thought process and tools I used to crack them. Whether you're new to cryptography or sharpening your skills, there's something here for you to learn. Let’s get started and unravel the flaws together!

Reconnaissance

To begin, I performed a scan on the target using Nmap to identify any open ports and discover potential endpoints.

nmap -sV -sC -v <target_ip>

22 and 80 are open

I then navigated to the web interface to begin exploring the application's behavior.

Just plain interface with Crypt highlighting.

As I check the source code of the page, this is what I've found.

TODO remember to remove .bak files

Web Flag

While brute-forcing subdirectories, I discovered that the site was using index.php. This led me to suspect there might be a backup version of the file, so I tried accessing index.php.bak.

This is the code inside

<?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’s logic is relatively straightforward. It begins by including the config.php file. Although the variable ENC_SECRET_KEY is used in the main script (index.php), it's not defined there, implying that its value must come from config.php.

The next part of the script checks if the secure_cookie and user cookies are set. If they aren't, the application invokes the generate_cookie function using the username guest and the ENC_SECRET_KEY. If both cookies are present, it instead calls the verify_cookie function, passing in the same key.

When verify_cookie returns true, the script checks the value of the user cookie. If it's set to "admin", the flag is revealed. Otherwise, the application simply displays a message indicating the currently logged-in user.

Let’s break down the behavior when no cookies are initially set. The generate_cookie function is first triggered. It calls generatesalt(2), which produces a random two-byte salt using alphanumeric characters. It then constructs a string composed of the user, User-Agent, and ENC_SECRET_KEY, separated by colons. This string is passed to the make_secure_cookie function, along with the generated salt. The result becomes the secure_cookie value, while the user is saved in another cookie.

Digging into make_secure_cookie, we see that it splits the input string into 8-byte chunks. Each chunk is processed through the cryptstring function using the provided salt. The resulting encrypted chunks are concatenated to form the final cookie value.

The cryptstring function itself is straightforward—it uses PHP’s built-in crypt() function to hash each chunk with the given salt.

Now, when cookies are present, the verify_cookie function handles validation. It retrieves both cookies and reconstructs the original string using the user, User-Agent, and ENC_SECRET_KEY. Since all chunks share the same salt, and the first two bytes of each hash contain this salt, it extracts the salt from the first portion of the secure_cookie. It then calls make_secure_cookie again using the reconstructed string and the extracted salt. Finally, it compares the newly generated cookie value with the existing secure_cookie and returns the result of that comparison.

To give you an idea, here's an example

In this scenario, the username is set to guest and the User-Agent string is Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0

Here’s how the server puts together the string before hashing:

guest:Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0:SECRET_KEY

And the server returns the secure_cookie as a hash in 8-byte blocks.

Example

"guest:Mo" = k0asAVEwgsdsw
"zilla/5." = k0fdieAfwerds
.....

We can further verify that these hashes correspond to the blocks by manually computing the hashes:

I blocked the flags since I already solved this challenge before I made this writeup hehe

Now, how do we manipulate this? Pretty simple, just change the guest:Mo to admin:Mo and hash it with the same salt(ka).

After this, I've changed the beginning of the cookie with this new hash, send the request and here's the result

Web Flag!

Final Flag

The next step is to extract the secret key. Since we know the salt, we can now brute-force any 8-byte block. However, using a character set of 65 characters, each 8-byte block would result in 65⁸ permutations—an enormous number of combinations, making a full brute-force attack impractical.

Fortunately, we can take advantage of how the application handles hashing to streamline the process. Since the input string is divided into 8-byte blocks before being hashed, and the user-controlled input comes at the beginning of the string, we can manipulate the length of the User-Agent header to isolate and target specific bytes.

The idea is to align the data in such a way that a single 8-byte block starts with known characters, leaving only one unknown byte to brute-force. For instance, sending an empty User-Agent would cause the string to be hashed as guest::SECRET_KEY. The first 8-byte block becomes guest::X, where X is the first character of the secret key. By iterating over possible characters for X, hashing each attempt, and comparing it to the first segment of the secure_cookie returned by the server, we can determine the correct value.

Repeating this process—adjusting the padding to progressively reveal each character—we can reconstruct the entire SECRET_KEY one byte at a time, drastically reducing the brute-force complexity.

When we modify the User-Agent to be AAAAAAA, the complete string that the server prepares for hashing becomes: guest:AAAAAAA:<SECRET_KEY>

This string is then broken into 8-byte blocks by the server, following its hashing logic. As a result:

  • Block 1 contains: guest:AA – This block isn’t relevant to our brute-force attempt.

  • Block 2 includes the segment: AAAAA:<first two bytes of SECRET_KEY>

Let’s say we’ve already identified the first character of the secret key — for instance, the letter T. That makes Block 2 appear as: AAAAA:T<second character of SECRET_KEY>

Now, by iterating through all possible characters to fill in the unknown position after T, and appending each one to the known prefix AAAAA:T, we can hash each guess and compare the result to the second chunk of the secure_cookie. Since our target block has shifted to the second 8-byte chunk, it makes sense to compare against the second hash.

This technique lets us reveal the key one character at a time, by controlling the alignment of the data in the hashed string, leveraging both padding and partial knowledge.

Following the same approach as earlier, we test each possible character and discover that appending H produces a matching hash. This confirms that the second character of the SECRET_KEY is H.

It matched right? Take a look at the burpsuite response.

From here, we can keep going and brute-force the third character the same way—by making sure the server hashes a block where only one byte is unknown, and the rest are already known. We can keep tweaking the User-Agent to make this happen each time. But instead of repeating this manually for every single character in the secret key, we can save ourselves the effort and write a Python script to automate the whole process.

#!/usr/bin/env python3
# Import necessary libraries
import crypt
import requests
import urllib.parse 
import string  
import time                 

# Base URL of the target application
BASE_ENDPOINT = "http://10.10.38.32/"

# Static part of the string to be hashed (usually the username)
ACCESS_ID = "guest:"

# Separator used in the string formatting
DELIMITER = ":"

# The set of characters we'll test during brute-force (printable ASCII characters)
CHARACTER_SET = string.printable

def retrieve_protected_token(user_identifier: str) -> str:
    """
    Sends a request with the given User-Agent and retrieves the decoded secure_cookie value.
    """
    print(f"\nInitiating connection to {BASE_ENDPOINT} with User-Agent: {user_identifier}")
    
    # Create a new session for consistent cookie handling
    session = requests.Session()

    # Send GET request with custom User-Agent
    response = session.get(BASE_ENDPOINT, headers={"User-Agent": user_identifier})
    print(f"Received response with status code: {response.status_code}")

    # Extract and decode the secure_cookie from the response
    token = session.cookies.get("secure_cookie")
    decoded_token = urllib.parse.unquote(token)
    print(f"Successfully retrieved and decoded secure token: {decoded_token[:10]}... (partial)")

    return decoded_token
    
def execute():
    """
    Attempts to brute-force and recover the secret key from the secure_cookie by aligning
    one unknown character per 8-byte chunk using padding and hash comparisons.
    """
    print(f"Starting decryption process with access ID: {ACCESS_ID}")
    print(f"Using delimiter: {DELIMITER}")
    print(f"Character set for testing includes: {CHARACTER_SET[:20]}... (partial)")

    uncovered = ""      # This will store the recovered secret key
    iteration = 0       # To track how many characters we have processed

    # Continue brute-forcing until no new character is found
    while True:
        iteration += 1
        print(f"\nStarting iteration {iteration}...")
        print(f"Current progress: {uncovered}")

        # Calculate how much padding is needed to align the target character into its own 8-byte block
        padding_size = (7 - len(ACCESS_ID + DELIMITER + uncovered)) % 8
        print(f"Calculating padding size: {padding_size} characters needed")

        # Build the User-Agent with padding (e.g., "A" * padding_size)
        user_identifier = "A" * padding_size
        print(f"Constructing user identifier with padding: {user_identifier}")

        # Build the full prefix: guest + padding + : + currently discovered key portion
        prefix = ACCESS_ID + user_identifier + DELIMITER + uncovered
        print(f"Building prefix for this round: {prefix}")

        # Determine which 8-byte block of the secure_cookie we're targeting
        block_position = len(prefix) // 8
        print(f"Targeting block at position: {block_position}")

        # Retrieve the secure_cookie value using the current User-Agent
        protected_token = retrieve_protected_token(user_identifier)

        # Extract the 13-character crypt hash from the correct block
        target_segment = protected_token[block_position * 13:(block_position + 1) * 13]
        print(f"Extracted target segment from token: {target_segment}")

        # The first two characters of a crypt hash represent the salt (a.k.a. "seasoning")
        seasoning = target_segment[:2]
        print(f"Using seasoning for hash: {seasoning}")

        character_found = False  # Flag to indicate if we successfully matched a character
        print(f"Beginning character testing with {len(CHARACTER_SET)} possible characters...")

        # Brute-force one character at a time by appending to the current prefix
        for symbol in CHARACTER_SET:
            print(f"Testing character: {symbol}", end=" ")

            # Only take the last 8 characters (aligns with how the server hashes blocks)
            candidate = (prefix + symbol)[-8:]
            candidate_hash = crypt.crypt(candidate, seasoning)

            # If hashes match, we found the correct next character
            if candidate_hash == target_segment:
                uncovered += symbol
                print(f"\nSuccess! Matched hash with character: {symbol}")
                print(f"Current decryption state: {uncovered}")
                character_found = True
                time.sleep(0.5)
                break
            else:
                print(".", end="", flush=True)

        # If no match is found, we've likely reached the end of the key
        if not character_found:
            print(f"\nNo matching character found. Decryption process completed.")
            break

    # Final output of the uncovered secret key
    print(f"\nFinal decrypted result: {uncovered}")
    print("Decryption process has successfully concluded!")

# Entry point of the script
if __name__ == "__main__":
    execute()

Run the script and it will print each character of the secret key (the flag) until it finds the entire string.

DONE!!

We've successfully completed the Crypto Failures Challenge!!!

Conclusion

Throughout this process, we successfully navigated the cryptographic challenge step by step, starting with identifying the structure of the hashing mechanism and leveraging the predictable behavior of the system. Initially, we analyzed how the secure token was generated, understood the structure of the hashed blocks, and identified key patterns in the encryption process. From there, we devised an efficient brute-force approach by exploiting the fact that the system uses 8-byte blocks and a known salt.

Rather than brute-forcing the entire secret key at once, we carefully padded the User-Agent to isolate and target individual 8-byte blocks. By systematically testing possible characters for each block, starting with the first character and moving through the key one step at a time, we narrowed down the solution without needing to test every possible combination. This iterative process, combined with a bit of automation through a Python script, allowed us to uncover the key more efficiently than manual methods would have.

The challenge also highlighted the importance of understanding how cryptographic systems generate and hash tokens, and how such knowledge can be applied to bypass security mechanisms. By successfully retrieving the secret key, we demonstrated a clear example of how methodical brute-forcing, coupled with an understanding of system mechanics, can lead to the decryption of protected data.

Ultimately, the entire process reinforced key principles in cryptography and cybersecurity: the significance of hashing functions, the impact of predictable patterns in encryption, and the power of careful planning and automation in breaking cryptographic challenges. With the secret key recovered, the next phase of the challenge was made possible, proving once again that patience, persistence, and smart strategy are vital in overcoming cybersecurity obstacles.

Last updated