Welcome to another Hack the Box walkthrough. In this blog post, I have demonstrated how I owned the Previous machine on Hack the Box. Hack The Box is a cybersecurity platform that helps you bridge knowledge gaps and prepares you for cyber security jobs.
About the Machine
Previous is a medium Linux machine on Hack the Box built around a Next.js web app and a misconfigured Terraform sudo rule. The machine featured a web enumeration (virtual hosts + JS inspection), an exploitation of a Next.js middleware bypass (CVE-2025-29927), arbitrary file read in the app/container, discovery of a hard-coded credential in a compiled NextAuth route, and a clean privilege escalation via terraform -chdir=/opt/examples apply allowed by sudo and abused by supplying a malicious local provider. The vulnerability can be exploited by adding X-Middleware-Subrequest: middleware:middleware:middleware:middleware:middleware to the HTTP header.
Remediation & defensive notes
- Patch Next.js / middleware — apply vendor patches and specifically address CVE-2025-29927 by ensuring middleware correctly differentiates internal subrequests from external requests and does not trust unvalidated
x-middleware-subrequeststyle headers. - Do not accept untrusted internal headers — do not base authentication decisions on headers that clients can supply; validate the origin and canonicalize request classification.
- Avoid leaking secrets in deployed JS bundles or server build artifacts. Never embed fallback secrets in compiled code. Use environment secrets and vaults, and rotate secrets.
- Harden file-serving endpoints — validate and canonicalize path parameters, enforce allow-lists, and avoid exposing arbitrary file reads.
- Scope sudo rules narrowly — never allow
terraform applyor similar infrastructure tooling to run as root in directories writable by unprivileged users. If such tooling must be used, wrap it in a safe, audited wrapper that validates inputs and prevents arbitrary provider execution. - Protect Terraform CLI config & provider install paths — ensure unprivileged users cannot control
TF_CLI_CONFIG_FILEor provider installation directories used by privileged runs.
The first step in owning the Previous machine like I have always done in my previous writeups is to connect my Kali Linux terminal with Hack the Box server. To establish this connection, I ran the following command in the terminal:
Once the connection between my Kali Linux terminal and Hack the Box server has been established, I started the Previous machine and I was assigned an IP address (10.10.11.83)
After been assigned 10.10.11.83, I decided to map the target machine IP address to the domain name. This way, I could access the service by name instead of by IP address by running:
I added 10.10.11.83 previous.htb to the /etc/hosts file and performed reconnaissance using Nmap to find all the open port and services associated with the target machine. Using the following command, I found all the services and port running at 10.10.11.83:
Nmap came back fast: the host previous.htb (10.10.11.83) is up and only two TCP ports were open.
- Port 22 - OpenSSH 8.9p1 (Ubuntu): The machine is running OpenSSH. The scan revealed host key fingerprints (ECDSA and ED25519). That’s useful for later verification if we come back via SSH or capture keys. An open SSH service is a potential foothold (credential reuse, weak passwords, private keys, misconfigured accounts), so I noted this for credential-based checks later.
- Port 80 - nginx 1.18.0 (Ubuntu): The webserver is nginx and the HTTP title advertises PreviousJS. Because the box uses a hostname (
previous.htb) and I already added it to/etc/hosts, the service is likely serving virtual‑host specific content - a perfect place to look for web logic, JavaScript, and hidden endpoints. nginx 1.18 is a common Ubuntu package; nothing immediately screams a critical remote exploit, but web apps often leak sensitive files, credentials in JS, or admin interfaces.
Why this matters for the walkthrough
Two easy attack surfaces: a web app (HTTP) and a remote admin interface (SSH). My priority is the web app because it often yields low‑privilege file reads, credentials in JavaScript, or unauthenticated endpoints that let me pivot to SSH access later.
Web Enumeration — Directory & File Discovery
After confirming that previous.htb hosts a web application, I ran a directory brute-force using dirsearch to uncover hidden endpoints and resources:
This scan probed common directories and file types (php, aspx, jsp, html, js) with 25 concurrent threads. The goal was to identify any accessible or overlooked paths that might expose sensitive information, documentation, or potential attack surfaces.
Key Findings
The scan returned a large number of redirects (307 and 308) pointing mostly to an authentication page:
- API endpoints: Almost every API-related path such as
/api,/api-docs,/api/v1/swagger.json,/api/2/issue/createmetaredirected to/api/auth/signin. This indicated that the API is protected behind authentication, but it also confirmed that the application exposes a RESTful interface with structured endpoints that could be explored further once credentials or tokens are discovered. - Documentation pages: Paths like
/docs/html/admin/index.html,/docs/html/developer/ch02.html,/docs/CHANGELOG.html, and/docs/swagger.jsonwere all present but redirected to authentication. These pages suggest the presence of internal documentation and API references — potential goldmines for discovering endpoint structures, version info, or hidden functionality. - Other resources: A few static files such as
/engine/classes/swfupload/swfupload.swfand/extjs/resources/charts.swfwere directly accessible (308redirects normalized the paths). While not immediately exploitable, these indicate legacy client-side technologies that sometimes leak clues or credentials. - Login page: The only fully reachable page was
/signin(HTTP200), which appears to be the main authentication portal. This became the natural pivot point for further enumeration, such as testing for exposed credentials, login bypasses, or client-side JavaScript logic.
Recon — landing on PreviousJS
I pointed my browser at the machine (HTTP 10.10.11.83) and it served a site that redirected me to https://previous.htb.
The homepage (PreviousJS) looks like a marketing splash for an old/nostalgic JS framework - the banner even says “the technology of yesterday.” with two buttons visible: Get Started and Docs
Clicking Get Started sent me to a login page - the same page I found by visiting http://previous.htb/signin. The login form looks standard: username, password, and a Sign in button.
Hunting for an authentication bypass — an interesting lead
The signin page had no registration flow, so I switched gears from credential hunting to vulnerability research. While searching for exploits related to PreviousJS and common vulnerabilities exploit for JavaScript, I came across CVE‑2025‑29927, an authentication bypass affecting Next.js middleware which can allow a remote attacker to bypass security checks.
You can read more about the vulnerability here.
What I discovered
The CVE-2025-29927 described a weakness in how Next.js middleware classifies requests. By manipulating a special request header used for internal/middleware subrequests, an attacker can trick the middleware into treating an external request as an internal one, effectively skipping the normal authentication checks by adding x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware to the HTTP header.
Testing the middleware bypass — dirsearch with the exploit header
With the CVE‑2025‑29927 lead in hand I re‑ran directory discovery against the /api surface while injecting the suspicious middleware header to see if protected endpoints would behave differently:
This injection changed the app’s responses in interesting ways - it didn’t give me an immediate “open sesame,” but the probe revealed that the backend was now returning real responses (and different status codes) for locations that previously redirected to the login. That made this a high‑value investigative path.
What the results showed (and why it matters)
- Many auth‑related endpoints returned
400instead of a redirect: Paths such as/api/auth/login,/api/auth/admin,/api/auth/logonreturned400 Bad Request. Previously these redirected to/api/auth/signin//signin. A400suggests the server is now processing the request internally (and rejecting it as malformed) rather than immediately redirecting to the global auth flow. That’s a strong sign the middleware behavior changed under the spoofed header — it’s processing subrequest logic rather than performing the normal redirect. /api/auth/signinreturned302to a localhost callback: The/api/auth/signinprobe produced:
That’s interesting: the application attempted to include a callbackUrl pointing to http://localhost:3000. Callback parameters like this are often used by auth frameworks and, if not validated, can be abused for open redirect or SSRF-style flows. It also demonstrates the app is constructing auth redirects differently when it treats the request as internal/subrequest.
- Some paths were normalized (
308) rather than fully blocked: dirsearch showed normalized redirects for odd paths (e.g./api/%2e%2e//google.com -> /api/%2E%2E/google.com) and a number of legacy resource paths (Axis, Citrix, swfupload, extjs swfs) under/api/…. Those resources being reachable (or at least not simply redirected to login) means the header may be allowing access to static assets and legacy endpoints that were previously gated. - A
/api/downloadprobe returned400:The400here again suggests the endpoint is being reached and parsed — it just refuses the current request. That implies there may be ways to craft a valid request (or parameter) to retrieve files if the bypass can be refined.
Fuzzing the file-download parameter — ffuf against /api/download
I fuzzed the download endpoint (which previously returned 400 when hit normally) to discover valid query parameters that might trigger different behavior or return files. Because the middleware bypass header looked promising, I included it in every request:
The scan returned example. This indicates the example parameter produced a 404 Not Found response with a tiny body. Because I used -fw 2 (filter responses with 2 words i.e., ignore trivial responses), ffuf reported only responses whose bodies had more than 2 words; the result shows example had 3 words so it passed the filter.
Verifying the /api/download probe
I reproduced the request that ffuf flagged (the example parameter) with a verbose curl so we could see the full HTTP exchange:
What happened
- The client resolved
previous.htbto10.10.11.83and successfully opened a TCP connection to port 80. - The request included the special
X-Middleware-Subrequestheader (the CVE trigger) so the server treated it differently than normal external requests. - The server returned an HTTP
404 Not Foundwith a JSON body:{"error":"File not found"}.
Retrieving /etc/passwd — confirmed directory-traversal via the download endpoint
I tested the download endpoint with the same middleware-spoof header and a classic path-traversal payload:
Instead of a JSON error, the server returned the contents of /etc/passwd. The download handler is vulnerable to path traversal and will return arbitrary file contents when supplied the right parameter + filename under the middleware bypass.
By sending the x-middleware-subrequest header (CVE-2025-29927 trigger) and exploiting a path-traversal vector in /api/download, I was able to read arbitrary files, found two users (node and nextjs), demonstrated by successfully retrieving /etc/passwd. This confirms an authenticated-gate bypass combined with an arbitrary file read primitive.
Reading the process environment — /proc/self/environ shows runtime context
I used the same path-traversal trick against the /api/download handler (with the middleware bypass header) to read the process environment:
The server returned the target process’s environment variables:
Why this matters
Reading /proc/self/environ proves two things at once:
- The file-read primitive is working for process internals, and
- It reveals runtime configuration that can point straight to useful next steps (process user, working directory, app port, runtime version, and environment mode).
Mapping the app — what the routes-manifest.json leak tells us
Pulling /app/.next/routes-manifest.json was a great payoff: it confirmed this is a Next.js app and gave me a clear map of the server’s routing surface, basically a shopping list of endpoints to target next by running:
Dumping /app/.next/routes-manifest.json confirmed this is a Next.js application and gave me the app’s route map. Notably, there’s a NextAuth catch-all handler at /api/auth/[...nextauth], a set of docs routes (/docs, /docs/content/getting-started, etc.), and explicit static routes including /signin. With the middleware exploit already allowing internal handlers to be reached, the manifest turned the engagement from “guesswork” into a concrete plan: fetch the docs for API shapes, probe the NextAuth endpoints for callback/open-redirect or provider misconfigurations, and pull build artifacts (.next chunks and config files) to hunt for secrets or credentials. This manifest was the roadmap that guided my next focused, read-only probes.
Finding the server-side auth logic — smoking gun in the Next.js build
I pulled the compiled Next.js API route for api/auth/[...nextauth] from the server build and it revealed the app’s actual authentication configuration, not just hints anymore but the real server-side logic by running:
I downloaded the compiled NextAuth route from the server build and found the credentials provider’s authorize function in cleartext. It authenticates only the user jeremy with hard-coded password MyNameIsJeremyAndILovePancakes.
Gaining a shell — SSH to jeremy@10.10.11.83
With the credentials discovered earlier I attempted an SSH login:
After supplying the password MyNameIsJeremyAndILovePancakes, the session opened and I landed at an interactive shell as jeremy. A quick enumeration in the home directory showed two entries and the user flag:
Hurray!!! I got the user flag.
Local user map — reading /etc/passwd
I dumped /etc/passwd from the shell to get an overview of system users and their shells:
I listed /etc/passwd to map system users. jeremy is UID 1000 with a bash shell (the account I am currently on). Notably, the host /etc/passwd does not contain the nextjs/node entries we saw earlier via the web file-read, indicating the app’s environment (the one exposed through the vulnerable /api/download) is separate from the host (likely a container or build environment). This discrepancy guided me to pursue both vectors: follow the app-level clues (files and credentials found via the web endpoint) and perform host-level enumeration (sudo rights, docker/lxd sockets, SUID binaries, cron jobs) to find a path from jeremy to root.
Network layout — reading ip a to understand the host & container networking
I ran ip a to map the machine’s network interfaces and got a clear picture of both the host-facing interface and Docker/container networks:
ip a revealed the host’s network (eth0: 10.10.11.83/23) and multiple container bridge interfaces (docker0: 172.17.0.1/16 and br-ba37c692e454: 172.18.0.1/16). A veth interface attached to the br-* bridge confirmed at least one live container on the host. This matched earlier clues (the web file-read appeared to expose a different /etc/passwd), indicating the app likely runs inside a container. From here I inspected the Docker socket, listed containers (if accessible), and probed services on the bridge subnets - standard, non-destructive steps to discover containerized services and potential escalation paths.
Privilege escalation — sudo -l reveals a terraform-as-root vector
I checked sudo privileges to see whether jeremy had any delegated admin powers:
sudo -l showed that jeremy can run /usr/bin/terraform -chdir=/opt/examples apply as root. This is a privileged but narrowly-scoped sudo rule — the trick is that Terraform executes arbitrary provisioner commands during apply. If /opt/examples is writable, we can drop a malicious main.tf (for example a null_resource using a local-exec provisioner) and then run the allowed sudo command to execute that provisioner as root. I first checked /opt/examples permissions and contents (to confirm writability and whether terraform configs already exist). If writable, creating a small Terraform file and running sudo /usr/bin/terraform -chdir=/opt/examples apply (then approving the apply) gives a straightforward path to escalate to root.
Reading the Terraform config — what main.tf in /opt/examples means
Dropping into /opt/examples I found main.tf (and a terraform.tfstate) - this is the exact directory jeremy is allowed to run terraform apply in as root, so this file is the heart of our escalation vector.
The Terraform configuration in /opt/examples/main.tf uses a custom provider previous.htb/terraform/examples and exposes a variable source_path that is constrained to paths under /root/examples/. Because jeremy can run terraform -chdir=/opt/examples apply as root, this directory is a privileged execution point: modifying the Terraform configuration (or supplying a malicious provider) lets us run arbitrary commands as root during apply. In practice I verified the directory contents, and then replaced the config with a minimal null_resource that runs a local-exec provisioner; running sudo /usr/bin/terraform -chdir=/opt/examples apply executes that provisioner as root and gives a straightforward path to full system compromise.
Poisoning the provider chain — using TF_CLI_CONFIG_FILE + a fake provider to get root
With terraform apply permitted as root in /opt/examples, I looked for ways to force Terraform to run code I control when it resolves providers. The Terraform docs point out the TF_CLI_CONFIG_FILE environment variable (and the CLI config it points to) can change how Terraform finds and installs providers.
For example, you can configure a local filesystem mirror or a bespoke installation method that causes Terraform to execute a provider binary from a location you control.
So I followed that path:
-
I checked the Terraform docs for environment variables and the CLI config options (the
TF_CLI_CONFIG_FILEsetting can tell Terraform to use a specific CLI config file). That config can be used to instruct Terraform to load providers from a local directory or otherwise alter provider discovery. In short: it’s a way to make Terraform load a provider binary from a path I control instead of pulling an official provider from the registry. -
I created a little directory under my user to host a fake provider binary:
Inside that directory I created a file named exactly like Terraform expects provider binaries to be named for a provider with source previous.htb/terraform/examples and version v0.1:
-
The file I pasted was a tiny shell script:
That script, when executed as root, sets the SUID bit on /bin/bash. Once /bin/bash is SUID-root, running /bin/bash -p (or executing the new suid shell) yields a root shell. In CTF writeups this is a common, quick proof-of-success for a local privilege escalation — it’s a blunt way to demonstrate code execution as root.
-
I also saved a CLI config file in the same dir (
dev.tfrc) — the intent being to useTF_CLI_CONFIG_FILEto point Terraform at that config so Terraform will look in/home/jeremy/boltechfor the provider binary instead of fetching it from the network.
I inspected the fake provider file I created and then made it runnable:
Afterwards, I created a new file dev.tfrc in /home/jeremy/boltech directory and pasted:
I attempted to force Terraform to use my local provider via TF_CLI_CONFIG_FILE, but my initial dev_overrides key ("previous.htb/examples") didn’t match the provider source declared in main.tf (previous.htb/terraform/examples) and used a non-absolute path. Terraform issued a CLI config error but still proceeded, using the existing provider and reporting “No changes.”
How I corrected it
To make the dev override work (so Terraform will load my local provider binary), the dev_overrides key must exactly match the provider source string in main.tf, and the path must point to the real directory containing the executable. For this box the fixed config should look like:
Checking for a root shell — what actually happened
I tried to confirm the provider payload had given me a root shell by checking /bin/bash permissions and launching a preserved shell:
The listing shows no SUID bit on /bin/bash (-rwxr-xr-x — there’s no s in the owner execute field). That means bash is not setuid-root, so there’s nothing there to give me an immediate root shell.
I then ran bash -p:
bash -p starts a shell that preserves privileges if the process already has elevated privileges. It does not elevate your privileges by itself. Because /bin/bash did not have the SUID bit set and my process was not running with effective UID 0, bash -p ran as my normal jeremy user and thus could not read /root/root.txt.
Getting a root shell — /bin/bash -p paid off
After fixing the Terraform override and triggering the malicious provider, I re-ran a preserved shell and it came up privileged:
This is the escalation moment: by abusing the allowed sudo terraform -chdir=/opt/examples apply and controlling the provider installation via TF_CLI_CONFIG_FILE, I forced Terraform to execute a provider binary I controlled as root. That provider performed a simple, reliable payload (set SUID on a shell or similar), and now I have an interactive root shell to finish the box.
I re-verified the escalation artifacts and used the privileged shell to access the final proof:
ls -al /bin/bash shows the owner execute bit set to s:
Hurray!!! I got the root flag. With that the machine was officially pwned.
If you enjoy reading my walkthrough, do not forget to like, comment, and subscribe to my YouTube channel and also connect with me on LinkedIn. Also, don't forget to turn on post notification on my YouTube channel and Medium to get notification as soon as I write.
Subscribe to my YouTube channel and Follow me on: LinkedIn | Medium | Twitter | Boltech Twitter | Buy Me a Coffee
Keywords:
Previous Hack the Box Writeup
Previous Hack the Box Machine Writeup
Previous Hack the Box Walkthrough
Previous Hack the Box Machine Walkthrough
Previous HTB Writeup
Previous HTB Walkthrough
Previous HTB Machine Walkthrough
Previous HTB Machine Writeup
Previous Hack the Box HTB Machine Walkthrough Writeup
Previous Hack the Box Solution
Previous Machine Walkthrough
HTB Previous
Previous Machine Write-up
HackTheBox Previous Writeup
previous htb writeup
previous htb walkthrough
previous hack the box writeup
previous hack the box writeups
previous htb walkthroughs
previous htb solution
previous htb writeups
previous htb complete writeup
previous htb complete walkthrough

































0 Comments