Down to the Wire

Google CTF 2018: Injection Modèle

Zachary Wade

Google CTF has come to a close, and with a very narrow victory on our part. As we decompress and mull over a set of excellent problems, we will post a few writeups of the ones we solved. Here is a writeup for the web problem “Translate.”

An Odd Sort of Translation

Upon visiting http://translate.ctfcompetition.com:1337/, the homepage of the Translate service, we are greeted with a service that claims to be a “Translation utility for technical terms in French and English.” Although the front page is rather bare-bones, it immediately offers a way for us to engage with it, in the form of an input field. “What French word,” it invites, “do you want to translate into English?” Although it might be tempting to make our first query " or 1=1; --, or perhaps "><script>alert(1)</script><, a better use of our time might simply be to ask for one of the suggested prompts.

Submitting téléverser to the form causes it to disappear and be replaced with a helpful translation of the form “In french, téléverser is spelled upload.” While I have some suspicions about the grammatical accuracy of that statement, it is helpful to see how the service acts when its working. Now that we know that, its time to poke at our target.

Poke and Prod

First, let’s look at the capabilities of this site. As listed in the menu bar at the bottom, we may

  • Translate from French to English
  • Translate from English to French
  • Add words to our dictionary
  • Debug our translations
  • Reset the problem

Thus, we don’t have much to start with, although we now have a few places to look.

In the problem description, we’re given the hint “Client-side rendering, but not in a browser.” Although you might have your suspicions going in, a quick glance at the clients code confirms them. The total number of source lines (including scripts and styles) is only 31. That, in combination with tags of the form <div ng-if="!userQuery"> suggest that this problem involves server-side AngularJS rendering.

Now, any time you have a templating engine of any form running server-side, the first thing to look for is always a template injection. Whether its liquid templates, handlebars, or now Angular, these set-ups are often pointing in the same direction. Sure enough, upon clicking the “debug translations” link, we come across two suspicious looking “dictionaries”. While each of these maps do act as a literal dictionary, they also have keys that appear to be part of the UI. In fact each of them has the same (modulo language), key value pair:

JavaScript content_copy
{ "in_lang_query_is_spelled":"In french, {{userQuery}} is spelled ." }

If this sounds familiar, that’s because this is the prefix we saw when we queried téléverser earlier. It seems that this is being used as a template for our requests. Indeed, for those familiar with Angular’s templating engine, the substring "{{userQuery}}" is an example of its embeded markup. Specifically, at run time that string will be replaced by a scoped variable called userQuery. Thus, we have found evidence of templating.

While this is a step in the right direction, it is far from sufficient. All that we have so far is a static string and no injection point ourselves. Recall, now, the options we found initially. Since one of the options was to add new words to our dictionary, and this template string seems to, oddly enough, be in our dictionary, perhaps it is possible that we can overwrite their template string with one of our own.

Malicious Assistance

Now that we have an idea of how the site works, let’s try adding our own words to the dictionary. However, instead of helpfully adding translations, we are going to say that the “Original Word” in_lang_query_is_spelled is translated as "I can do math: 1 + 1 = {{1+1}}". If our suspicions are correct, we upon querying téléverser again we should see a different response.

"Dictionnary"

Sure enough, if we query again, we see the exciting output: "I can do math: 1 + 1 = 2". With this, we have template injections. At this point, we should be basically done.

Cold Hard Truth

In reality, a good CTF problem is never going to be that simple. Now that we have a template, we need to figure out exactly what we can do with it. This first thing that I tried was attempting to inspect the process. This is a variable in all Node applications that contains information about the execution process and a few methods for interacting with the outside world. However, initial attempts at doing this proved fruitless. Using {{ process }} as the query string returns nothing, and no obvious scopes were available that could access it (i.e. {{ angular.process }} or the like). Even more perplexing, logging {{ this }} gave us the single element $SCOPE, but logging that likewise returned the empty string. Finally, attempting to write statements instead of expressions gave ugly errors. Fo instance, using the query string {{ x = 1; for (i = 0; i < 10; i++) { x += i } }} caused an exception whose body had the word "Error" in it a whopping 5 times! At this point, it seems that we need to break out of our current context.

While I do not know much about Angular’s templating language, it seems to be a parser that matches a subset of valid JavaScript and then evaluates it in an isolated context. In order to do anything useful, we need to be able to escape this limited scope. Through trial and error we see that it allows strings and expressions, so if we can get an eval call into the template, then we might have a fighting chance. Unfortunately (once again), attempting to find one through either {{ eval }} or {{ Function }} return nothing but the empty string.

Hopefully at this point, you have an inkling of what’s coming next. Think once again about what we just established — it allows strings and expressions. If this is true, then we should be able to access the string constructor through {{ 'a'.constructor }}, and then the string constructor’s constructor (i.e. the function constructor) through {{ 'a'.constructor.constructor }}. Fingers crossed, we submit this and see that instead of the empty string, we’re presented with a glorious function Function() { [native code] }; a reference to the Function constructor which is eval in everything but name.

Don’t be Eval

Now that we have our eval, we can try again. Indeed, running {{ 'a'.constructor.constructor('return process') }} yields us

JavaScript content_copy
{
  "argv": [
    
  ],
  "title": "node",
  "version": "v8.11.3",
  "versions": {
    "http_parser": "2.8.0",
    ...
    "tz": "2017c"
  },
  "arch": "x64",
  "platform": "linux",
  "env": {
    
  },
  "pid": 174,
  "features": {
    "debug": false,
    ...
    "tls": true
  }
}

This means that we’ve successfully escaped our scope and have access to the underlying node VM.

Yet, our victory is short-lived. We notice quickly that we still do not have access to some of the things we would like, such as require or the angular object. Furthermore, the this object appears to only have the above process and an empty object called console. To figure out why this is, we can construct a more powerful query

JavaScript content_copy
'a'.constructor.constructor("let o = ''; for (x in this) o+=x+' '; return o")()

This prints out all of the keys in this, even the ones that JSON-ification leaves out. Much to our dismay, we see the following list: x VMError Buffer setTimeout setInterval setImmediate clearTimeout clearInterval clearImmediate process console. This list is small, certainly, but what is most discouraging is the presence of VMError. For those who are unfamiliar with it, this is an exception added by the vm2, a tightly locked sandbox for running untrusted JS. Although I spent a while looking, I could not find any recent bugs in it.

Something Seems Off

At this point, we’re still stymied. We have arbitrary JS access, but within a sandbox. Although we know we just need to read flag.txt from a file, we have no way of doing it. Clearly, we are still missing something and a brief reflection causes us to realize that when we logged {{ this }} earlier, we saw a reference to $SCOPE that was not present on the this we logged more recently. This indicates that there might be more properties available on the outside this that we can use. To find out what these are, we can augment the script from above by passing in an argument:

JavaScript content_copy
'a'.constructor.constructor("args", "let o = ''; for (x in args) o+=x+' '; return o")(this)

Using this, we can see all of the keys on the parent scope. Much to our joy, this this is the good this, with the properties: $$childTail $$childHead $$nextSibling $$watchers $$listeners $$listenerCount $$watchersCount $id $$ChildScope $parent $$prevSibling $$transcluded window i18n userQuery $$phase $root $$destroyed $$isolateBindings $$asyncQueue $$postDigestQueue $$applyAsyncQueue constructor $new $watch $watchGroup $watchCollection $digest $destroy $eval $evalAsync $$postDigest $apply $applyAsync $on $emit $broadcast.

Although I would love to talk about each and every one of these in an organized manner, at this point I’ve surely bored you to tears, and frankly I don’t know about any of them. Upon seeing the reference to i18n, I used the above trick to enumerate all of its properties. Unlike its parent, all that resided on it were template and word. While there are approaches you can take to figure out more about what a black-box function does, we can opt for the simple one and try executing it on an input string.

For my part, that’s exactly what I did, executing the template {{ i18n.template("foo") }}. In what was a huge surprise to me, as it may be for you, I was presented with the lovely error message: Couldn't load template: Error: ENOENT: no such file or directory, open './foo'. This means that it attempted to open foo, but wasn’t able to because it didn’t exist. However, we want to read flag.txt which surely does.

With a tremor in our hand and a flutter in our hearts, we upload the template {{ i18n.template("flag.txt") }}. Lo-and-behold, out pops the string: CTF{Televersez_vos_exploits_dans_mon_nuagiciel}.

No source was needed, not even a sandbox could stop us. Congratulations, you hacked Google Translate!