Analysis

For this challenge we get a password manager.

Entering a password and clicking add returns a error message.

Taking a look at the source we can see that there’s two scripts loaded.

"manager.js"
"sweetalert.min.js"

Opening manager.js we see a large obfuscated script.

Deobfuscation

Looking through the source we find a lot of values like _0x5195[0x3 * -0x32d + -0x16f6 + -0x681 * -0x5]. Taking a look at the assignment of the variable _0x5195 we can see that the elements assigned are mostly generated by calling a function.

Using the browser console we can take a look at the array to find out what it contains.

Great we got the strings, lets write a quick python script to replace all obfuscated strings with the deobfuscated strings.

Copying the array by entering copy(_0x5195) in the browser console we get the whole array in the clip-board. Fortunately we can use this data as-is to assign the array in python.

_0x5195 = [
    "use strict",
    "length",
    "test",
    ...
    "classList",
    "I need a parameter.. maybe look at the sourcecode?"
]

Now we have the string array created, the next step is to replace the obfuscated strings in the original javascript.

First we need to download the manager.js file so we can open it with our python script. When we have the script we can add the following to our script.

with open('manager.js', 'r') as f:
    data = f.read()

Now we need to replace the obfuscated strings.

First we can use regex to find all the obfuscated strings. After we got all the strings we can use eval on each of them to get the corresponding deobfuscated value. When we got both values all we need to do is to replace the obfuscated value with the deobfuscated value.

matches = re.findall(r'_0x5195\[[0-9a-f x\-+*]*\]', data)
for match in matches:
    try:
        value = eval(match)
        data = data.replace(match, f'\'{value}\'')
    except:
        continue

All we have to do now is save the data to a file for further analysis.

with open('deobfuscated.js', 'w') as f:
    f.write(data)

Now we got a script to deobfuscate the strings that looks like the following.

#!/usr/bin/env python

import re

_0x5195 = [
    "use strict",
    "length",
    "test",
    ...
    "classList",
    "I need a parameter.. maybe look at the sourcecode?"
]

with open('manager.js', 'r') as f:
    data = f.read()

matches = re.findall(r'_0x5195\[[0-9a-f x\-+*]*\]', data)
for match in matches:
    try:
        value = eval(match)
        data = data.replace(match, f'\'{value}\'')
    except:
        continue

with open('deobfuscated.js', 'w') as f:
    f.write(data)

Reviewing the source code

When we open the deobfuscated code, we find the code for the button click event at line 1409-1452.

document['querySelector']('#add')['onclick'] = function() {
        function g3(c, d) {
            return fp(d, c - -0x13);
        }
        if (document['querySelector']('#new-password input')['value']['length'] == 0) {
            var k = {};
            k[g3(0x79, 0x82)] = 'Uh, oh!';
            k[g3(0xb7, 0x198)] = 'Please, provide a password';
            k[g3(-0x101, -0x97)] = 'error';
            k[g3(-0x125, 0x73)] = 'Keep hunting';
            swal(k);
        } else {
            if (window['location']['href']['indexOf']('?password=') > 0) {
                var l = i('password')['replaceAll'](' ', '+');
                if (e(l) === !![]) {
                    var m = atob(l);
                } else {
                    var m = 'amsterdam_coffeeshops';
                    console['log']('try harder');
                };
                document['querySelector']('#passwords')['innerHTML'] += '' + '<div class="password"><span id="passwordsaved">' + AntIH4Ck3RC0D3zzzzzzzzz[g3(-0x1db, -0x1bf)](m) + '</span><button class="delete"><i class="fa fa-trash-o"></i></button></div>';
                var n = document['querySelectorAll']('.delete');
                for (var o = 0; o < n['length']; o++) {
                    n[o]['onclick'] = function() {
                        this['parentNode']['remove']();
                    };
                };
                var p = document['querySelectorAll']('#passwordsaved');
                for (var o = 0; o < p['length']; o++) {
                    p[o]['onclick'] = function() {
                        this['classList']['toggle']('completed');
                    };
                };
                document['querySelector']('#new-password input')['value'] = '';
            } else {
                var q = {};
                q[g3(0x79, 0x1f2)] = 'Uh, oh!';
                q[g3(0xb7, 0x203)] = 'I need a parameter.. maybe look at the sourcecode?';
                q[g3(-0x101, -0x29b)] = 'error';
                q[g3(-0x125, 0x56)] = 'Keep hunting';
                swal(q);
            }
        }
    };

The first check after we press the button is to see if there’s anything entered in the input field. If there is anything entered we continue to a check to see if the parameter password is supplied.

If the password parameter is supplied the line var l = i('password')['replaceAll'](' ', '+'); gets the query parameter value and replaces all spaces with plus signs.

The next check, e(l) === !![], simply validates that the data in the password parameter is valid base64, if it is the decoded value is assigned to m, if not a constant is assigned to m and the message “Try harder” is logged to the console.

After all the checks there’s an assignment to document.querySelector('#passwords').innerHTML with some constant elements and the value of m after it has been passed to the function AntIH4Ck3RC0D3zzzzzzzzz[g3(-0x1db, -0x1bf)].

Lets try to inject something. Base64 encoding <h1>test</h1> and using the encoded value for the password parameter we get manager.html?password=PGgxPnRlc3Q8L2gx. Using this and adding a password we can see our injected HTML in the password list.

Great, but when we try to inject <script>alert(document.domain)</script> nothing is rendered, trying <img src=x onerror=alert(document.domain) /> we can see that the onerror attribute is stripped. So AntIH4Ck3RC0D3zzzzzzzzz has to be some kind of sanitizer.

Lets find out a bit more about AntIH4Ck3RC0D3zzzzzzzzz. Using the browser console to take a look at the properties we get some more info about the object.

A google search for clearConfig isSupported isValidAttribute reveals that AntIH4Ck3RC0D3zzzzzzzzz is DOMPurify.

Printing the version property reveals that the version used is 2.0.8.

Searching for DOMPurify 2.0.8 bypass we find the following article Mutation XSS via namespace confusion – DOMPurify < 2.0.17 bypass.

Modifying the alert in the payload from the article to alert(document.domain), base64 encoding it and using it as the password parameter we get our final payload.

manager.html?password=PGZvcm0+CjxtYXRoPjxtdGV4dD4KPC9mb3JtPjxmb3JtPgo8bWdseXBoPgo8c3R5bGU+PC9tYXRoPjxpbWcgc3JjIG9uZXJyb3I9YWxlcnQoZG9jdW1lbnQuZG9tYWluKT4=

Now when we add a password an alert is triggered!