Node

Reconnaissance:

NMAP:

┌──(kali💀kali)-[~]
└─$ sudo nmap -sC -sV -O 10.10.10.58 

22/tcp   open  ssh                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)
|   256 6c:8e:5e:5f:4f:d5:41:7d:18:95:d1:dc:2e:3f:e5:9c (ECDSA)
|_  256 d8:78:b8:5d:85:ff:ad:7b:e6:e2:b5:da:1e:52:62:36 (ED25519)

3000/tcp open  hadoop-tasktracker Apache Hadoop
| hadoop-datanode-info: 
|_  Logs: /login
| hadoop-tasktracker-info: 
|_  Logs: /login
|_http-title: MyPlace

Device type: general purpose|specialized|phone|storage-misc
Running (JUST GUESSING): Linux 3.X|4.X (90%), Crestron 2-Series (86%), Google Android 4.X (86%), HP embedded (85%)
OS CPE: cpe:/o:linux:linux_kernel:3 cpe:/o:linux:linux_kernel:4 cpe:/o:crestron:2_series cpe:/o:google:android:4.0 cpe:/h:hp:p2000_g3
Aggressive OS guesses: Linux 3.10 - 4.11 (90%), Linux 3.12 (90%), Linux 3.13 (90%), Linux 3.13 or 4.2 (90%), Linux 3.16 - 4.6 (90%), Linux 3.2 - 4.9 (90%), Linux 3.8 - 3.11 (90%), Linux 4.2 (90%), Linux 4.4 (90%), Linux 3.16 (88%)

The OpenSSH version that is running on port 22 is not associated with any critical vulnerabilities, so it’s unlikely that we gain initial access through this port, unless we find credentials.

Ports 3000 is running a web server, so we’ll perform our standard enumeration techniques on it.

Enumeration: TCP Port 3000

I always start off with enumerating HTTP first.

http://10.10.10.58:3000/

View page source to to see if there are any left over comments, extra information, version number, etc.

view-source:http://10.10.10.58:3000/

http://10.10.10.58:3000/assets/js/app/controllers/home.js

There’s a link to a list of users. Let’s see if that link is restricted.

We get back the above results giving us what seems to be usernames and hashed passwords. As stated with the “is-admin” flag, none of them have admin functionality.

Similarly, the /admin.js script contains the following code.

http://10.10.10.58:3000/assets/js/app/controllers/admin.js

http://10.10.10.58:3000/api/admin/backup

When you visit the /api/admin/backup link, you get an “authenticated: false” error. This link is restricted but at least we know that the admin account has a backup file in it.

The /profile.js script contains the following code.

http://10.10.10.58:3000/assets/js/app/controllers/profile.js

http://10.10.10.58:3000/api/users/

When you visit the /api/users/ link, we get a full list of hashed user credentials, including the admin account!

Copy the credentials and save them in a file.

Use a password cracking tool in order to crack as many passwords as possible. For this blog, I used an online tool since it’s faster than my local machine.

We get back the following result showing that it cracked 3/4 passwords.

One thing to note here is none of the passwords are salted. This can be verified using the following command.

This obviously considerably decreased the amount of time it would have taken the tool to crack all the passwords. Let’s login with the admin’s account myP14ceAdm1nAcc0uNT/manchester.

http://10.10.10.58:3000/admin myP14ceAdm1nAcc0uNT manchester

Click on the Download Backup button to download the file. Run the following command to determine the file type.

It contains ASCII text. Let’s view the first few characters of the file.

This looks like base64 encoding. Let’s try and decode the file.

Now view the file type.

It’s a zip file! Let’s try and decompress it.

It requires a password. Run a password cracker on the file.

-u: try to decompress the first file by calling unzip with the guessed password -D: select dictionary mode -p: password file

Unzip the file using the above password.

Now it’s a matter of going through the files to see if there are hard coded credentials, exploitable vulnerabilities, use of vulnerable dependencies, etc. While reviewing the files, you’ll see hard coded mongodb credentials in the app.js file.

We found a username ‘mark’ and a password ‘5AYRft73VtFpc84k’ to connect to mongodb locally. We also see a backup_key which we’re not sure where it’s used, but we’ll make note of it.

Shell as mark

Most user’s reuse passwords, so let’s use the password we found to SSH into mark’s account.

SSH:

It worked! Let’s locate the user.txt flag and view it’s contents.

We need to either escalate our privileges to tom or root in order to view the flag. Let’s transfer the linpeas script from our attack machine to the target machine. In the attack machine, start up a server in the same directory that the script resides in.

In the target machine, move to the /tmp directory where we have write privileges and download the linpeas script.

Shell as tom

Below are the important snippets of the script output that will allow us to escalate privileges to tom.

The networking section tells us that mongodb is listening locally on port 27017. We can connect to it because we found hardcoded credentials in the app.js file. The services section tells us that there is a process compiling the app.js file that is being run by Tom. Since we are trying to escalate our privileges to Toms’, let’s investigate this file.

We only have permissions to read the file, so we can’t simply include a reverse shell in there. Let’s view the file, maybe we can exploit it in another way.

If you’re like me and you’re not too familiar with the mongodb structure, this might help

We login using mark’s credentials and access the scheduler database. The set interval function seems to be checking for documents (equivalent to rows) in the tasks collection (equivalent to tables). For each document it executes the cmd field. Since we do have access to the database, we can add a document that contains a reverse shell as the cmd value to escalate privileges. Let’s connect to the database.

-u: username -p: password host:port/db: connection string

Let’s run a few commands to learn more about the database.

The tasks collection does not contain any documents. Let’s add one that sends a reverse shell back to our attack machine.

Set up a listener to receive the reverse shell.

Wait for the scheduled task to run.

We get a shell! Let’s upgrade it to a better shell.

Grab the user.txt flag.

Shell as root

When gaining access to a second user in a CTF machine, it’s always useful to think about what files can be accesses/run now that couldn’t before. One way to approach that is to look at the groups associated with the new user:

sudo is the first to jump out, but trying to run sudo prompts for tom’s password, which I don’t have:

adm means that I can access all the logs, and that’s worth checking out, but admin is more interesting. It’s group id (gid) is above 1000, which means it’s a group created by an admin instead of by the OS, which means it’s custom. Looking for files with this group, there’s only one:

It’s also a SUID binary owned by root, which means it runs as root. Interestingly, this binary is called from /var/www/myplace/app.js

It calls backup -q backup_key __dirname, where __dirname is the current directory. The binary is a 32-bit ELF:

Dynamic Analysis:

Before pulling this binary back and opening in in Ghidra, I’ll try running it on Node. It returns without any output:

I tried giving it arguments to see if there was a check at the front looking for a certain number, and on three, it output something:

This makes sense with how this binary is called from app.js above. It’s complaining about needing a magic word.

Token Check:

I’ll run that again with ltrace, and change the three args so that they are different (to better track which is which), so ltrace a b c. I’ll walk through the output in chunks. First it checks the effective user id, and then sets the uid to 0, root. Then it does a string comparison between “a” (first arg input) and “-q”:

/etc/myplace/keys shows the three 64-characters hashes and a blank line just as observed with ltrace:

If I put one of those hashes into the second argument, it runs past the access token check:

Interestingly, it will also work with an empty string as the token arg (because there’s an empty line in the keys file):

Path:

With a valid token, it says it’s “archiving c”, and then complains that the path doesn’t exist. I’ll try replacing “c” with a path. I’ll create a single file in /dev/shm, and then pass that path to backup:

If I change “a” to “-q”, it will just print the base64:

Just like before, the base64 decodes to a zip file, which contains the directory:

It unzips with the same password as before (“magicword”):

Troll:

The obvious next step is to backup /root. Right at the start I can tell something is different because there’s a message that prints, even in -q mode:

The string does decode to a .zip archive, but it’s a different kind of archive, as it doesn’t decompress with unzip:

I’ll bring that base64 string back to my vm and uze 7z to decompress. The file is an ASCII art troll:

Abusing the Program Dependencies:

One thing that stuck out at me is the program runs zip directly on the user-passed string without any sanitization or validation.

I should be able to pass a string into the application and it will be appended to the zip command:

Let's give it a shot with something like the example give in GTFOBins.

Using user.txt as it is a small file and won't cause the program to hang

This causes the program to run:

The only problem with this shell is that I don't have any output. But, I can confirm I have a shell by creating a file:

Last updated