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>
var Cipher = {
LstAllow: ' !"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~',
Value: '',
};
function reverseStr(s) {
return s.split('').reverse().join('');
}
inputValue = "$input"
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))
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)
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])
index = (symbolPosition * 1337) % (Cipher.LstAllow).length
Cipher.Value += Cipher.LstAllow[index]
Cipher.Value = reverseStr(btoa(Cipher.Value))
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));
let plain = "";
for (let i in right_order) {
symbolPosition = Cipher.LstAllow.indexOf(right_order[i]);
index = (symbolPosition * 68) % (Cipher.LstAllow).length;
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",
'#': ".",
'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));
let plaintext = "";
for (let j in right_order) {
let c = right_order[j];
plaintext += lookup_table[c];
}
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!🎯