
PoC et write-up pour l'élévation de privilèges HackTheBox « Nexus » : service de synchronisation de templates Gitea exécuté en root, vulnérable à la traversée de chemin via des objets Git falsifiés. Usage éducatif uniquement.
Traversée de chemin via la synchronisation de modèles Gitea avec des objets Git forgés → root
Un exploit de preuve de concept pour l'élévation de privilèges sur la machine HackTheBox Nexus.
Un service systemd appartenant à root synchronise les dépôts de modèles Gitea sur le disque
sans assainir les noms d'entrées, permettant à un utilisateur authentifié à faibles privilèges
d'écrire des fichiers arbitraires en tant que root via une traversée de répertoire — utilisé ici pour implanter
une clé SSH dans /root/.ssh/authorized_keys.
Ce code est fourni à des fins éducatives et de tests de sécurité autorisés uniquement — c'est-à-dire des environnements de laboratoire tels que HackTheBox, ou des systèmes que vous possédez ou pour lesquels vous disposez d'une autorisation écrite explicite de tester. L'utilisation non autorisée contre des systèmes que vous ne contrôlez pas est illégale. L'auteur décline toute responsabilité en cas de mauvaise utilisation.
Si la machine Nexus est encore active sur HackTheBox, ne publiez pas et ne partagez pas cet exploit — les règles de HTB interdisent la distribution de solutions pour des machines actives.
Le script de synchronisation lit les fichiers des dépôts marqués comme modèles dans Gitea et les écrit sur le système de fichiers local. Le chemin de destination est construit approximativement ainsi :
dest = os.path.join(base_dir, name) # name comes straight from the Git tree
open(dest, "w").write(content)
os.path.join() ne neutralise pas les séquences ... Une entrée d'arbre dont le nom
est un chemin de traversée (par ex. ../../../../../root/.ssh/authorized_keys) fait donc
que le processus privilégié écrit en dehors du répertoire prévu.
Git stocke le contenu et les noms séparément :
nom → objet (c'est là que réside le nom de fichier) ;Le nom de fichier dans une entrée d'arbre n'est que du texte, il peut donc être .. — mais le client
Git standard refuse de créer une telle entrée, précisément pour empêcher cette attaque.
Ce PoC contourne cette vérification côté client en écrivant des objets Git bruts directement
dans .git/objects/ (en calculant les identifiants SHA-1 à la main) et en fabriquant refs/heads/main
manuellement. Le dépôt poussé contient alors une entrée d'arbre nommée .., que le
processus de synchronisation root résout et suit.
www-data..env de production, réutilisés comme
mot de passe système → SSH en tant qu'utilisateur à faibles privilèges.root.Le thème récurrent à travers ces trois étapes : un processus privilégié faisant confiance à des données contrôlées par l'attaquant sans validation (un type MIME usurpé, un mot de passe réutilisé, un nom de fichier non assaini).
http://localhost:3000.git, python3, curl et ssh disponibles (par défaut sur la machine).chmod +x nexus_privesc.sh
./nexus_privesc.sh -t <TARGET_IP> -u <GITEA_USER> -p <GITEA_PASS> [-d DEPTH]
Exemple :
./nexus_privesc.sh -t 10.129.21.192 -u jones -p 'REDACTED' -d 5
/tmp/.exploit_key)... qui pointe vers
/root/.ssh/authorized_keys, avec la clé publique de l'attaquant comme contenu.root avec la clé implantée.Surveiller la synchronisation sur la cible avec :
tail -f /var/log/template-sync.log
Une exécution réussie journalise une ligne comme :
synced: ../../../../../root/.ssh/authorized_keys
DEPTHDEPTH est le nombre de niveaux .. nécessaires pour remonter depuis le répertoire de
synchronisation/staging jusqu'à la racine du système de fichiers (/). Si la synchronisation réussit dans le journal mais que le
SSH root échoue, la traversée a atterri au mauvais niveau — réessayez avec -d 4 ou
-d 6. La valeur exacte peut être confirmée en lisant le chemin de staging dans
/etc/gitea/template-sync.py.
Ne jamais construire de chemins de système de fichiers à partir de noms non fiables avec os.path.join() seul.
Normalisez et confinez les chemins, par ex. :
base = os.path.realpath(base_dir)
dest = os.path.realpath(os.path.join(base, name))
if not dest.startswith(base + os.sep):
raise ValueError("path traversal blocked")
Rejeter les noms d'entrées d'arbre contenant .., des chemins absolus ou des séparateurs de chemin.
Exécuter le service de synchronisation sous un compte dédié à faibles privilèges, et non root.
Activer receive.fsckObjects = true dans Gitea pour rejeter les objets poussés malformés.
<your name / handle> — for educational use on HackTheBox.
| Classe | Traversée de chemin (CWE-22) → Élévation de privilèges locale |
| Composant | gitea-template-sync.service (personnalisé /etc/gitea/template-sync.py) |
| Exécuté en tant que | root, déclenché toutes les ~60s par gitea-template-sync.timer |
| Cause racine | os.path.join() sur des noms non assainis issus d'objets d'arbre Git |
| Impact | Utilisateur authentifié à faibles privilèges → root |
| Option | Description | Défaut |
|---|
-t | IP cible (pour le SSH root final) | — (requis) |
-u | Nom d'utilisateur Gitea | — (requis) |
-p | Mot de passe Gitea | — (requis) |
-d | Nombre de niveaux .. pour atteindre / depuis le répertoire de synchronisation | 5 |