Google CTF Quals 2018: translate (246pt)

Client-side rendering, but not in a browser! Get the flag in ./flag.txt, and seeing the source will likely help.

About the service

The service is a website to translate French and English words. When entering informatique en nuage, we get cloud computing as the matching translation.

At the footer, the link add words allows to edit and add new words. So we can change the translation for informatique en nuage to 1337.

The feature debug translations dumps the content of both dictionaries:

In those dictionaries, the value for the key in_lang_query_is_spelled contains the substring {{ userQuery }}. Double curly brackets are typical for templating languages, meaning the templating engine might execute the code in between the brackets. The name userQuery seems to stand for the looked up word. Thinking back to our first queried word, we are already familiar with this sentence: We already saw In french, {{userQuery}} is spelled . as In french, informatique en nuage is spelled cloud computing.

A quick method to check for a template injection is the tag {{ 1+1 }}. If the output contains 2, we know that the server executes the injected code.

After setting in_lang_query_is_spelled to {{ 1+1 }}, the result of each translation is 2, confirming our idea.

Exploiting the vulnerability

Background information

To exploit the template injection, we first have to find the expected language for the templates. As the HTTP response headers include X-Powered-By: Express, we predict the template language to be NodeJS.

Exploring the scope

For template injection, it is helpful to know which methods and objects are accessible in the scope of the injected code. Usually, this works in javascript using {{ ""+Object.keys(this) }}. The code works as follows: In javascript this is usually an object containing most of the variables and methods in the current scope. Object.keys(this) returns a list with the names of each variable and ""+ is used to convert it to a string. First, this payload did not work because Object is undefined in the current scope. To fix this, I replaced Object with {}.__proto__.constructor. This works because {} creates a new Object and {}.__proto__.constructor resolves to the constructor, which is again Object.

But as the payload {{ ""+{}.__proto__.constructor.keys(this) }} returns $$childTail,$$childHead,$$nextSibling,$$watchers,$$listeners,$$listenerCount,$$watchersCount,$id,$$ChildScope,$parent,$$prevSibling, $transcluded instead of useful variables and functions, we have to find another way.

More background information

Looking at the HTML source, we find many ng-* attributes in various HTML tags. A quick google search reveals that the server side probably runs AngularJS, which is also the reason why dumping this does not work. In the result page of a query, we also find again the variable userQuery in the attribute ng-if="userQuery". The attribute defines that the div is shown if the variable userQuery is defined. Inside the div, there is another div with the attribute ng-if="i18n.word(userQuery)", which means that the div appears only if the function call to i18n.word returns something != false. As userQuery is available in the template, the object i18n and the function i18n.word might be available too.

Exploring i18n

To check if i18n is accessible, we use the payload {{ ""+(i18n === undefined) }}. As the result is false, we know that i18n exists in the scope and we use {{ ""+{}.__proto__.constructor.keys(i18n) }} to dump the keys of the object. The result is template,word. Leaking both fields using {{ ""+i18n.template }} ### {{ ""+i18n.word }} reveals that i18.template and i18n.word are just wrappers for myI18n.forTemplate and myI18n.forSingleWord. Sadly myI18n is undefined, so we cannot leak the source code of the functions.

Assuming that myI18n.forTemplate allowes template injection in another scope, we passed our usual template injection check to the template function: {{ i18n.template('{'+'{ 1+1 }'+'}') }}. But instead of 2 the result was Couldn't load template: ReferenceError: file is not defined. This leads to the assumtion that i18n.template loads a template from a given file, and therfore the payload {{ i18n.template('./flag.txt') }} opens the flag file and uses it as a template.

As expected the result is the flag: CTF{Televersez_vos_exploits_dans_mon_nuagiciel}