Dope Beats

JS SAFE 2.0 [121]

You stumbled upon someone's "JS Safe" on the web. It's a simple HTML file that can store secrets in the browser's localStorage. This means that you won't be able to extract any secret from it (the secrets are on the computer of the owner), but it looks like it was hand-crafted to work only with the password of the owner...

This challenge gives us a single HTML file which shows an input box representing a key and an ominous spinning cube (presumably the safe it opens).

Safe Screenshot

To get the flag, we need to reverse engineer the code to figure out what passphrase will unlock the safe. When we try to enter some arbitrary text, the page will give us a big red Access Denied error.

Safe Screenshot Locked

Initial Analyses

Before we can properly work on solving the challenge, we need to do a bit of intel gathering. How is the locking code implemented? What protections has the developer put in place to stop us from simply reading the password?

First off it runs in a browser so it's probably JavaScript, but CSS 3 is Turing Complete so you never know. Lets dive into the code...

At the top of the page we get a nice comment telling us pretty much everything we need to know. The safe is implemented in JavaScript and the developer has done their best to prevent it from being debugged. Anti debugging techniques are quite common among proprietary JavaScript libraries, because in this situation you are essentially selling your source code, and want to prevent one of your consumers from simply looking at your code, learning how you solved a certain problem, and then implementing it themselves and selling their version for a lower price.

<title>JS safe v2.0 - the leading localStorage based safe solution with military grade JS anti-debug technology</title>
Looking for a hand-crafted, browser based virtual safe to store your most
interesting secrets? Look no further, you have found it. You can order your own
by sending a mail to When ordering, please specify the
password you'd like to use to open and close the safe. We'll hand craft a
unique safe just for you, that only works with your password of choice.

Then we get all the CSS styling and animations for the rotating cube, and at the very bottom we get the JavaScript for unlocking the safe. Some of the code is minified, so let's use the Atom beautifier to make it more readable.

    function x(х) {
        ord =''.charCodeAt);
        chr = String.fromCharCode;
        str = String;

        function h(s) {
            for (i = 0; i != s.length; i++) {
                /* --snip-- */
            return chr(b >> 8) + chr(b & 0xFF) + chr(a >> 8) + chr(a & 0xFF)

        function c(a, b, c) {
            for (i = 0; i != a.length; i++) c = (c || '') + chr(ord(str(a[i])) ^ ord(str(b[i % b.length])));
            return c
        for (a = 0; a != 1000; a++) debugger;
        x = h(str(x));
        source = /Ӈ#7ùª9¨M¤ŸÀ.áÔ¥6¦¨¹.ÿÓÂ.։£JºÓ¹WþʖmãÖÚG¤…¢dÈ9&òªћ#³­1᧨/;
        source.toString = function() {
            return c(source, x)
        try {
            console.log('debug', source);
            with(source) return eval('eval(c(source,x))')
        } catch (e) {}
    function open_safe() {
        keyhole.disabled = true;
        password = /^CTF{([0-9a-zA-Z_@!?-]+)}$/.exec(keyhole.value);
        if (!password || !x(password[1])) return document.body.className = 'denied';
        document.body.className = 'granted';
        /* --snip-- */

    function save() {
        /* --snip-- */

The code doesn't look too bad, it's pretty short, there are only a few functions, and the developer was nice enough to use ord, chr, and str to make it easier for us to understand what's going on.

When we enter some input into the safe it gets checked against a regex to make sure it's in the form CTF{some ascii here}, and then the part between the curly braces gets passed to the x() function. If the return value of x() evaluates to true, then we know we have the right password.

Inside of x(), there are a few helper functions. First we have h() which transforms our input into a 4 character string. The exactly implementation doesn't really matter, we just treat it as a hash function which generates 4 character hashes. We also have c() which appears to be doing a repeating key xor.

There is also an interesting variable called source which is assigned to a regex literal containing a bunch of unicode characters. This is passed to the xor function in a few places, so it looks like there are some goodies hidden inside which the developer is trying to hide from us.



  • open_safe - Opens the safe
  • save - Saves data to localStorage if the safe is open
  • x - Takes a password and tells us if it's correct
  • h - Generates a 4 character hash
  • c - Computes a repeating key xor

Other objects of interest:

  • source - Some bytes probably containing the key, but "encrypted" by c()

Anti Anti Debug

One of the ways we could try to solve this challenge is to add a bunch of console.log() printouts throughout the code to gain some insight into intermediate values. For example, if we can print out the the xor key used to decrypt the bytes of source or better yet, print the result of that xor, we would be a lot closer to getting the flag.

However, if we try this, the first thing we'll notice is that when the developer tools are open, JavaScript execution will pause whenever it reaches a debugger statement. And unfortunately the code will do this 1000 times during x().

for (a = 0; a != 1000; a++) debugger;

Well that's easy enough to deal with. We can just delete (or comment out) the loop and refresh the page. Now we can try to print out the value of the xor key after it is calculated.

x = h(str(x));
console.log("xor key is:", x)

We see some unicode characters printed out, but after a few seconds our computer fans start to spin up and we get an alert that the tab is not responding. We also notice that the debug output which was present in the original, never happens.

Script Hanging

If we look closer at the code we will notice that this is happening because of the modified toString function which is applied to source. It appears that when source is printed, it should decrypt itself first, however, the xor function (c()) is expecting the input to have a length attribute, which is not defined for regex literals. As a result the for loop's condition (i != a.length) is always true and it gets stuck in an infinite loop.

When we investigate what attributes actually exists for regex literals, we see that they have an attribute called source which is a string representation of the regex.

Regex Literal

So we can fix this little issue by using source.source instead. And now when we run the page again we see the decrypted value printed out as we would expect.

source.toString = function() {
    return c(source.source, x)

Printed Source

Hmmm... That still looks like garbage. Somehow we must not be using the correct xor key. Wait a second, the xor key that is being printed out now is different than it was before! We used the same test input, so what changed? Is the input being modified somewhere else that we haven't noticed? Lets do a search for all occurrences of x in the file.

Searching For X

And now we notice something very strange. The function appears to take an argument which is also named x, however it isn't highlighted when we searched for 'x' in the file.

When we copy and paste the parameter х into DuckDuckGo it brings up the Wikipedia page for the Cyrillic letter Kha Wow! The parameter is actually just a unicode character which looks exactly the same as the ascii character 'x' but is a completely different symbol in JavaScript. The xor key is not being generated from our input, but instead is the result of hashing the string representation of the whole function!

That's why it changed when we modified the toString function. We also realize now, that beautifying the code in the first place would break the functionality of the safe. The good news is that all we have to do is calculate the hash of the original string representation of the minified function.

We can grab a fresh copy of the file, open up the browser console on it, and call the hash function manually.

Now, there are a few catches here. The first is that we need to paste in h() because it's only defined within the scope of x(). The second is much more profound and also a giant headache to figure out. Do you remember the debugger loop from earlier?

for (a = 0; a != 1000; a++) debugger;

Notice that the variable it's using isn't i like the rest of the loops. It's actually a, which appears again inside the hash function. So in order to get the correct output, we need to set a to 1000 at the start of h().

Calling H

Now that we have the key, we can finally figure out what secrets are hidden in the mess of the unicode regex. We can paste in the xor function and source regex and manually invoke the decryption.

Decrypting Source

Great! We are almost there. It looks like source actually contains more valid JavaScript code which checks that the unicode х parameter from before is equal to the flag. Unfortunately the flag isn't in plaintext. Once again we have some encrypted unicode string which we need to decrypt first. Luckily it's just a multibyte xor, and we even know the key length.


Because we know that the key length is 4, we know that every 4th character has been xored with the same character from the key. For example if the key were abcd and the text were 01234567, then 0 and 4 would get xored with a, 1 and 5 would get xored with b and so on. Therefore if we split the ciphertext into 4 parts, where we know that each part has been xored with the same character, we can solve each part separately by trying to xor it with every possible character, and evaluating whether the result looks like valid plaintext. The caveat is that we need to know something about the original plaintext, in order to evaluate if our candidates look valid. In our case we have the regex from open_safe() which we can test our candidates against.

First we create a JavaScript file which we will run using NodeJS. We copy in the ciphertext and create our 4 single byte xor strings.

cipher = '¢×&\u0081Ê´cʯ¬$¶³´}ÍÈ´T\u0097©Ð8ͳÍ|Ô\u009c÷aÈÐÝ&\u009b¨þJ'
transposed = ['', '', '', '']

for (let i = 0; i < cipher.length; i++) {
    transposed[i % transposed.length] += cipher[i]

Output: [ '¢Ê¯³È©³Ð¨', '×´¬´´ÐÍ÷Ýþ', '&c$}T8|a&J', 'ʶÍÍÔÈ ]

Then we solve each of the strings as a single byte xor against the regex from open_safe() and print any possible candidates.

for (let i = 0; i < transposed.length; i++) {
    for (let j = 0; j < 1000; j++) {
        xored = c(transposed[i], chr(j));
        if (/^[0-9a-zA-Z_@!?-]+$/.test(xored)) {
            console.log(i, xored)


0 '_7RN5TNa-U'
1 'B!9!!EXbHk'
1 'N-5--ITnDg'
2 '3v1hA-it3_'
3 'x3O4n4-1b'

This looks very promising! Only the second string had more than one possible solution, so now we just need to pick one (or try both), and put the pieces back together. Since we know that the flag will probably be English text written in leetspeak, we guess that the second candidate is the correct one.

chunks = ['_7RN5TNa-U', 'N-5--ITnDg', '3v1hA-it3_', 'x3O4n4-1b']
flag = ''
for (let i = 0; i < cipher.length; i++) {
    flag += chunks[i % chunks.length][Math.floor(i / chunks.length)]
console.log("CTF{" + flag + "}")

Output: CTF{_N3x7-v3R51ON-h45-AnTI-4NTi-ant1-D3bUg_}