DOJO Challenge #24 Winners!

June 19, 2023

Cipher challenge

The #24th DOJO CHALLENGE, Cipher aims to decrypt a given cipher that is encrypted by client side JavaScript. To solve the challenge, the participants had to understand how the JavaScript code encrypts a string in order to decrypt the secret cipher!

💡 You want to create your own DOJO and publish it? Send us a message on Twitter!

WINNERS!

We are glad to announce the #24 DOJO Challenge winners list.

3 BEST WRITE-UP REPORTS

  • The best write-ups reports were submitted by: fravoi, stamet and Ruulian! Congrats 🥳

The swag is on its way! 🎁

Subscribe to our Twitter and/or Linkedin feeds to be notified of the upcoming challenges.

Read on to find the best write-up as well as the challenge author’s recommendations.

The challenge

Chiper

See the challenge page >

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.

BEST WRITE-UP REPORT

We would like to thank everyone who participated. The #24 DOJO challenge provided us with a large amount of high quality reports and everyone did a great job!

We had to make a selection of the best ones. These challenges allow to see that there are almost as many different solutions… as long as there is creativity! 😉

Thanks again for all your submissions and thanks for playing with us!

Ruulian‘s Write-Up

————– START OF Ruulian‘s REPORT ——————

Description

The purpose of this challenge is to retrieve the flag which is encrypted by a given Javascript algorithm.

<h2 id="out"></h2>

<script>
    //Cipher object:
    var Cipher = {
        LstAllow: ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~',
        Value: '',
    };

    function reverseStr(s) {
        return s.split('').reverse().join('');
    }

    // DECODE : x1VXnQiR1hTPX1lR98WXhYkU51EJX1VOxUVXdN2cJdDb
    inputValue = "$input"

    //Check all characters in the user input and encod each character:
    for (let i in inputValue ) {
        let v = inputValue.charCodeAt(i)

        symbolPosition = Cipher.LstAllow.indexOf(inputValue[i])
        index = (symbolPosition * 1337 ) % (Cipher.LstAllow).length

        Cipher.Value += Cipher.LstAllow[index]
    };

    Cipher.Value = reverseStr(btoa(Cipher.Value))

    //Output the final cipher value:
    document.getElementById('out').innerText = Cipher.Value

</script>

Source code review

Useful declarations

First we have an object Cipher:

var Cipher = {
  LstAllow: ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~',
  Value: '',
};

The attribute LstAllow is a constant which is useful for the encryption process and the Value is going to receive our cipher.

Then, we have a short function which just reverses a string:

function reverseStr(s) {
  return s.split('').reverse().join('');
}

Encryption process

inputValue = "$input"

for (let i in inputValue) {
  let v = inputValue.charCodeAt(i) // ADDITIONAL COMMENT: this line is not useful because "v" not used

  symbolPosition = Cipher.LstAllow.indexOf(inputValue[i])
  index = (symbolPosition * 1337) % (Cipher.LstAllow).length

  Cipher.Value += Cipher.LstAllow[index]
};

Cipher.Value = reverseStr(btoa(Cipher.Value))

For each character of our input:

  • It gets the index of our character in the constant LstAllow
  • It computes a new index by multiplying our index by 1337 modulo the length of LstAllow which is 95
  • It takes the corresponding new index char in LstAllow and adds it to the cipher value

To finish the encryption process, the code is just encoding our cipher in base64 and then reverse the base64 encoded cipher using the function we described in the encryption process part.

Char encryption example

Let us encode a char to make sure that the encryption process is fully understood.

If we want to encode “A” using the algorithm:

symbolPosition = Cipher.LstAllow.indexOf(inputValue[i]) // => symbolPosition = 33
index = (symbolPosition * 1337) % (Cipher.LstAllow).length // => index = (33 * 1337) % 95 = 41
Cipher.Value += Cipher.LstAllow[index] // => Cipher.Value = "I"

Cipher.Value = reverseStr(btoa(Cipher.Value)) // => Cipher.Value = reverseStr("SQ==) = "==QS"

Exploitation

The purpose of this challenge is to decode the cipher x1VXnQiR1hTPX1lR98WXhYkU51EJX1VOxUVXdN2cJdDb, so let us find a way to retrieve our plaintext.

I found 2 ways to retrieve any plain from a ciphertext:

  • A cryptographic way
  • A naive way

Cryptographic way

Our encryption function is just a modular product, thus if the factor (1337) and the modulus (95) are co-prime, we can find the modular inverse of 1337 modulo 95 and we can retrieve our plain.

>>> from Crypto.Util.number import GCD
>>> assert GCD(1337, 95) == 1
>>>

Thus, we can compute the modular inverse of 1337:

>>> pow(1337, -1, 95)
68
>>>

So we can just re-write the encryption process but with 68 as factor:

function decode(s) {
  let right_order = atob(reverseStr(s)); // We have to reverse the string and decode the base64 with "atob"

  let plain = "";
  for (let i in right_order) {
    symbolPosition = Cipher.LstAllow.indexOf(right_order[i]);
    index = (symbolPosition * 68) % (Cipher.LstAllow).length; // We replaced the 1337 by its inverse modulo 95
    plain += Cipher.LstAllow[index];
  }
  return plain;
}

And now we can retrieve our plaintext:

>> decode("x1VXnQiR1hTPX1lR98WXhYkU51EJX1VOxUVXdN2cJdDb")
"FLAG{__y0u_Cr4ck3d_Th3_Ch1p3r!__}"

Naive way decoding

If you want to avoid some maths and cryptography, you can basically generate a lookup table for all characters of the LstAllow since the characters of our input are encoded one by one.

let lookup_table = {};
for (let i in Cipher.LstAllow) {
  let c = Cipher.LstAllow[i];

  symbolPosition = Cipher.LstAllow.indexOf(c)
  index = (symbolPosition * 1337) % (Cipher.LstAllow).length

  lookup_table[Cipher.LstAllow[index]] = c;
}

It basically stores in lookup_table the encrypted chars and their corresponding plaintext.

>> lookup_table
{
  ' ': " ",
  '!': "d",
  '"': "I",
  '#': ".",
  // ... 86 lines
  'z': "H",
  '{': "-",
  '|': "q",
  '}': "V",
  '~': ";"
}

Thus now, we just have to traverse our cipher (after reversing and base64 decoding it) and get the corresponding plain in the lookup table:

function decode(s) {
  let lookup_table = {};
  for (let i in Cipher.LstAllow) {
    let c = Cipher.LstAllow[i];

    symbolPosition = Cipher.LstAllow.indexOf(c)
    index = (symbolPosition * 1337) % (Cipher.LstAllow).length

    lookup_table[Cipher.LstAllow[index]] = c;
  }

  let right_order = atob(reverseStr(s)); // We have to reverse the string and decode the base64 with "atob"

  let plaintext = "";
  for (let j in right_order) {
    let c = right_order[j];
    plaintext += lookup_table[c]; // Retrieve our string in the lookup table
  }

  return plaintext;
}

And now we can retrieve our plaintext (again):

>> decode("x1VXnQiR1hTPX1lR98WXhYkU51EJX1VOxUVXdN2cJdDb")
"FLAG{__y0u_Cr4ck3d_Th3_Ch1p3r!__}"

Conclusion

It was a cool challenge with a bit of cryptography and Javascript to understand, I really appreciate the fact that we can solve this challenge by using different methods according to our favorite way.
Thanks to the author for this awesome challenge!

————– END OF Ruulian‘s REPORT ——————

START HUNTING!🎯