DOJO Challenge #29 - SantaLock

January 3, 2024

The DOJO CHALLENGE, #29 SantaLock was a special Christmas challenge where the goal was to find the secret key to Santa's vault and decrypt the secret message hidden inside!

WINNERS!

We are happy to announce the list of winners of the #29 DOJO Challenge. The best write-ups reports were submitted by:

πŸ₯‡ jtof_fap - Nintendo Switch + Exclusive YesWeHack Swag pack

πŸ₯ˆ Tihmz - Logitech G pro X Gaming Headset + Exclusive YesWeHack Swag pack

πŸ₯‰ fravoi - Exclusive YesWeHack Swag pack

For this Christmas challenge, we also decided to randomly select 10 reports that will receive some swags, keep an eye on your mailbox to check if you've won! 🎁

We want to thank everyone who participated and reported to the DOJO challenge.
Subscribe to our Twitter and/or Linkedin feeds to be notified of the upcoming challenges.

Read on to find out how one of the winners managed to solve the challenge.

The challenge

DOJO challenge SantaLock

We asked you to produce a qualified write-up report explaining the logic allowing such exploitation. This write-up serves two purposes:

  • Ensure no copy-paste would occur.
  • Determine the contestant ability to properly describe a vulnerability and its vectors inside a professionally redacted report. This capacity gives us invaluable hints on your own, unique, talent as a bug hunter.

jtof_fap's Write-Up

————– START OF jtof_fap's REPORT β€”β€”β€”β€”β€”β€”

Introduction

In order to save Christmas, the aim of the challenge is to help Santa find the key to the digital vault containing the Christmas gifts. The challenge covers a wide range of concepts relating to encryption, hash functions and password security.

We're going to help Santa find the contents of his vault, but also advise him on how to use these concepts to strengthen the security of his digital vault.

Challenge

Part 1 - Find the key corresponding to the MD5 hash in the source code

Description

The initial source code of the challenge presents a Vault class, used for symmetrically ciphering/decrypting data using the AES algorithm, as well as a form for entering a key to decrypt the contents of the vault using this class.

Santa has lost the key, but has left the MD5 hash of this key in the code:

// Create a new vault (backup vault) to store our data in.
// When we have our vault, set a secret key that will lock the vault (I have to use the MD5 hash of the key because we forgot the actual key value...)
var vault = new Vault()
vault.setKey("7f16e4fc3d6c9bd92de59e9369891dba")
vault.lock()
// We only got the MD5 hash value of the key.
// I'm going to move this AES encrypted data from my other vault that uses the same key, so I'll add it as its AES encrypted state...
vault.setData("U2FsdGVkX1/SpGvO7gL9H5GgEjLvEUhKcB9yK4sEC/JTVqUwyPZJrD9g84JkXuJurQY6naZ9KOFKXsll1jqWO10i3CVyZOcyDCi+3anUuqawcsErq+k5SC5ExWd7S59ANl/kcMDQf6cHI7s+hG3Z6als7+whFD+2fHHo7FJOjSu7KCTfBZlOhfT2nd1kjV/gIEN4EFFOAU2BuRwEOYbjSQ7G0kFX3Bws4oupyFLZiiTE5y2YFVgt8KExnSk2hQOCZbNRmOQsvlY+Z9Jbtp2XtduIyRbJRPhDrG9Bb8UFwdAmroaa0aaLfobHbRrvo2fd3b2VA+AGIdyBYPYgpCL0DL8b/kbL9Usleseb+A2rFr0lvFEisfHRvme4N4T3zwg+QhDAOrCBxhoOO2yeL8FaEGxT2DGJnyDB63Dl6zDiFzydTI4jxZ8E7rOA3iwXsccCI5lZE6LJid+bPCvoTva2SGexkm8Yn1E6LTEG87zqb+xTawhotU+YzmUOHvnEg9YrRg0VVtx8GX0EI+lzSV04ughDa+lqiCNcdkePOlMsM9ZqnXvwjxOOUTrJsQ+00DXRZuX8FK/SaeTJSJNRAYDnrpSscMxUZ5S3gg0SoMN0+Ngb3yMxI+AOFOU/+kJ2egI9hbJnh7aDvX8RXd/0DDcj5YbQgolfPRNK7vR0/gLHw728uJfy3dsUhiDYNemui5BP/9Tl3uG5xTWP7ncPaPnymGYGUiW61yilIR4iPFmqN2feSFpsoJNFQClu/EJU+bPvuehy1onA8UR+zW9idOJCd5VzSY4e7PjsNduWWMVI45SGrJzxKZj7avedoAOFKwcdZqQ4BNUBRmW0UzEjhBiXx3lZZ7OjzXjp5dp13pDrn5MRVA+bLD95EK0JH8wJD32E22KNjXw4SfWx8+0RF5UKNXqTJAQTe67bVCJS11Z3fbc=")
// Let's take a guess!
const key = "$KEY"
// Check if we were able to unlock the vault with our guessed key:
error = vault.unlock(key)
  • Bad practice #1: Usage of weak and vulnerable hash function to store the vault key

First observation on the use of the MD5 hash function. Santa thought it was doing the right thing by not directly storing the key in clear text, but rather hashing it into MD5 format.

The condensate (or hash) is the result of a hash function. One of the properties of this type of function is that it is not reversible. So, even if an attacker manages to obtain the vault key hash, he can't in theory simply deduce it. Despite this interesting property, hash functions are not a good choice for storing passwords:

  • Most commonly used hash functions (MD5, SHA-256, etc.) have the property of being fast to compute. They are therefore vulnerable to dictionary and/or bruteforce attacks. That's all we want to avoid here;
  • Some functions, such as MD5 and SHA-1, are known to have cryptographic weaknesses (collisions) and should therefore be avoided;
  • In the absence of an external element (called salt) added to the password before the condensate is calculated, the condensate will always be the same. This means that if an attacker succeeds in recovering hashes, he will instantly be able to identify all users with the same password. This also means that the condensate of a very large list of passwords (or even entire charsets) can be "pre-calculated" in advance in Rainbow Tables to make it easier to find them.

It is advisable to use key derivation functions designed for this purpose. Please refer to the recommendation section for further details on this point.

  • Bad practice #2: Password reuse

A second bad practice on key reuse can also be seen here: I'm going to move this AES encrypted data from my other vault that uses the same key. The compromise of one vault using a symmetric key will result in the compromise of all vaults using that key.

Key recovery

As we don't have much time for this mission, the aim is to find the key associated with this MD5 condensate as fast as possible. From the fastest to the slowest, the strategy is as follows:

  • Paste the hash directly into the google search engine form. In our case, this didn't work;
  • Use online services (free or paid) to search for the password associated with condensates. Most of these services have rainbow tables as well as tables generated from lists of leaked passwords;
  • Run a bruteforce by dictionary (then complete) using tools such as JohnTheRipper or Hashcat.

On this challenge, we were able to use the second solution, using the https://crackstation.net/ service:

  • Bad practice #3: Perfectible password policy

Despite the key's length (13 characters), the password policy applied to this key (or pseudo passphrase) by Santa is too weak, hence the fact that it is easily found on this kind of service:

  • Use of only one character class (lowercase) out of 4 (numbers, lowercase, uppercase, special characters);
  • Use of a password based on dictionary words.

This key can be used on the challenge form to retrieve the contents of the digital vault:

Part 2 - Find the pin code and the challenge flag

Description

Now that the key to Santa has been found, the challenge is not over and proposes the following " ciphering/decrypting " function, based on XOR:

function chiperXOR(v, pincode) {
    if ( pincode.match(/^[0-9]{4}$/) === null ) {
        return Error("The PIN code must be exactly 4 digits long and may only contain digits from 0-9.");
    }
    /* Perform an XOR operation from they provided key for each character in the given input (value) */
    v = v.split("");
    for (let i = 0; i < v.length; i++) {
        v[i] = (String.fromCharCode((v[i].charCodeAt(0)) ^ pincode.charCodeAt(0)));
    }
    /* Combine all the characters to a string and base64 encode the new encrypted value. Then return the value: */
    return btoa( v.join("") )
}
/* Decrypt me (format: "FLAG{...}") => dX9ydEhnWwBsfQBEbHBBSkNHA2xeBxdHAEFO */

We understand that we need to discover the 4-digit pincode in order to "decrypt" the challenge flag.

  • Bad practice #4: Bad pincode initialisation

The first thing to note when reading this code is that only the first character of the pin code pincode.charCodeAt(0) is actually used in this function:

for (let i = 0; i < v.length; i++) {
        v[i] = (String.fromCharCode((v[i].charCodeAt(0)) ^ pincode.charCodeAt(0)));
}

Only the first digit of the pin code needs to be found to "decrypt" the value dX9ydEhnWwBsfQBEbHBBSkNHA2xeBxdHAEFO. The next three digits can be any number, but will not be taken into consideration.

Pincode recovery

Reading the code, the first natural approach to retrieving the pin code is to brute force it, no matter if there's only one digit, or 4 (if you didn't notice the previous point) by calling the function in a loop. We've gone from 10000 tries to 10 (pincode.charCodeAt(0)) but there's even more efficient.

The XOR (or exclusive) operation is a binary operation that takes two bits and returns 1 if exactly one of the bits is 1, otherwise it returns 0. This binary operation has some interesting properties:

  • Commutativity: The order of the operands does not affect the result. If A and B are two bits, then A XOR B is equal to B XOR A;
  • Associativity: The operand grouping does not affect the result. That is, (A XOR B) XOR C is equal to A XOR (B XOR C);
  • Reversibility: The XOR operation is reversible. If you XOR a bit A to a bit B to get a bit C, you can XOR a bit B to C to get A, and vice versa. The function given in the challenge can be used to both cipher and decrypt data.

It is therefore possible here to use these properties to retrieve the pincode by performing an XOR between the piece of plaintext FLAG{ and the ciphertext, using the following code:

function getPincode(clear_text, cipher_text) {
    cipher_text = cipher_text.split("");
    clear_text = clear_text.split("");

    /* Perform XOR operation between clear text and cipher text to get the pincode */
    let pincode = clear_text.map((char, i) => String.fromCharCode(char.charCodeAt(0) ^ cipher_text[i].charCodeAt(0))).join("");

    return pincode;
}

clear_text = "FLAG{"
cipher_text_b64 = 'dX9ydEhnWwBsfQBEbHBBSkNHA2xeBxdHAEFO';
cipher_text = atob(cipher_text_b64);

// Get pincode
pincode = getPincode(clear_text.substring(0,4), cipher_text.substring(0,4));
console.log("Pincode = " + pincode);

Note: Here we've taken 4 characters from the ciphertext to obtain a 4-digit pincode, but only the first digit really counts here. Any pincode between 3000 and 3999 will work in this challenge.

All that remains is to use the discovered pincode to retrieve the challenge flag:

  • Bad practice #5: Unsuitable XOR ciphering to secure sensitive data

Even if XOR-based ciphering can be useful in certain specific contexts (limited amount of data to be transmitted, checksum calculation, etc.), it is not sufficiently robust to offer an acceptable symmetrical encryption solution for sensitive data:

  • Like hash functions, XOR is fast to compute, making it a poor candidate to resist bruteforce attacks;
  • The XOR ciphering is vulnerable to several types of attack, especially repetition attacks. If the same key is used to cipher several messages, an attacker can compare the ciphered texts to obtain information about the key;
  • If an attacker has access to both plaintext and ciphertext, he can easily find the key;
  • Key size: for XOR ciphering to be secure, the key must be at least as long as the message to be encrypted. This can be impractical for ciphering large volumes of data.
  • Etc.

Challenge solution

Source code

Here is the source code to retrieve the challenge flag, which can be pasted directly into the browser JavaScript console:

function chiperXOR(v, pincode) {
    if ( pincode.match(/^[0-9]{4}$/) === null ) {
        return Error("The PIN code must be exactly 4 digits long and may only contain digits from 0-9.");
    }
    /* Perform an XOR operation from they provided key for each character in the given input (value) */
    v = v.split("");
    for (let i = 0; i < v.length; i++) {
        v[i] = (String.fromCharCode((v[i].charCodeAt(0)) ^ pincode.charCodeAt(0)));
    }
    /* Combine all the characters to a string and base64 encode the new encrypted value. Then return the value: */
    return btoa( v.join("") )
}

function getPincode(clear_text, cipher_text) {
    cipher_text = cipher_text.split("");
    clear_text = clear_text.split("");

    /* Perform XOR operation between clear text and cipher text to get the pincode */
    let pincode = clear_text.map((char, i) => String.fromCharCode(char.charCodeAt(0) ^ cipher_text[i].charCodeAt(0))).join("");

    return pincode;
}

clear_text = "FLAG{"
cipher_text_b64 = 'dX9ydEhnWwBsfQBEbHBBSkNHA2xeBxdHAEFO';
cipher_text = atob(cipher_text_b64);

// Get pincode
pincode = getPincode(clear_text.substring(0,4), cipher_text.substring(0,4));
console.log("Pincode = " + pincode);

// Use pincode to get flag 
flag = atob(chiperXOR(cipher_text, pincode));
console.log("DOJO #29 flag = " + flag);

Flag

The DOJO #29 challenge flag is: FLAG{Th3_N3w_Crypt0_m4$t3r}.

Recommendations

To conclude this challenge, here are a few recommendations that will help Santa improve the security of its digital vault for next year.

Bad practice #1: Usage of weak and vulnerable hash function to store the vault key

It's recommends storing vault passphrase securely to protect them against offline attacks in the event of a leakage.

To achieve this, we need to replace hash functions with key derivation functions (e.g. Scrypt, Bcrypt, and more recently Argon2id), which have been specially designed for this purpose. Their main features are:

  • High computation time (configurable by varying a given number of iterations);
  • Include a salt to ensure that the fingerprint of the same password is different, making it unnecessary to generate a rainbow table for this hash type.

These functions are now natively integrated into most application languages and frameworks. If the function is well configured (random salt and large iteration number) and a complex password policy is implemented, a bruteforce attack on this hash type may become unachievable in a reasonable time, even on a powerful machine.

Important note: Even if it's a widely observed practice, simply adding a salt to the password before hashing it with a hash function (e.g. in SHA-512) won't change the underlying problem. In any case, since the salt can be recovered at the same time as the hash, a "salted" hash will not take longer to bruteforce.

Bad practice #2: Password reuse

We recommend using a different key for each digital vault to be protected.

We also recommend the use of a password vault, either local or using a centralized solution. This will prevent Santa from losing the key to the digital vault containing the Christmas presents.

Bad practice #3: Perfectible password policy

For the digital vault key, we recommend the use of a robust passphrase that will make bruteforce impossible if the hash associated with it is discovered. To achieve this, the passphrase must be :

  • Sufficiently long (>= 100 Bits for ANSSI, >= 7 words for CNIL);
  • Complex, using the 4 available character classes (digits, lower case, upper case and special characters);
  • Not based on dictionary or predictable elements.

Bad practice #4: Bad pincode initialisation

Although it's not advisable to keep XOR for ciphering sensitive data, here's the corrected code that would have taken each of the pincode digits into consideration:

function chiperXOR(v, pincode) {
    if ( pincode.match(/^[0-9]{4}$/) === null ) {
        return Error("The PIN code must be exactly 4 digits long and may only contain digits from 0-9.");
    }
    /* Perform an XOR operation from the provided key for each character in the given input (value) */
    v = v.split("");
    for (let i = 0; i < v.length; i++) {
        /* Use each digit of the pincode for the XOR operation */
        let pinIndex = i % pincode.length;
        v[i] = (String.fromCharCode((v[i].charCodeAt(0)) ^ pincode.charCodeAt(pinIndex)));
    }
    /* Combine all the characters to a string and base64 encode the new encrypted value. Then return the value: */
    return btoa( v.join("") )
}

Regarding the key length, a pin code is not enough. For XOR to be considered a minimum safe, the key length must be at least equal to the size of the text to be ciphered.

Bad practice #5: Unsuitable XOR ciphering to secure sensitive data

Finally, it is recommended not to rely on XOR to cipher sensitive data, preferring a much more robust algorithm such as AES, designed for this purpose.

————– END OF jtof_fap's REPORT β€”β€”β€”β€”β€”β€”