Zurück zu allen Beiträgen
Veröffentlicht am · von Renaud Deraison

Sie zogen zuerst die sicheren Versionen zurück

Am 20. August veröffentlichte jemand ein vergiftetes arrayref auf crates.io und zog sechzehn Sekunden später die sicheren Versionen dahinter zurück. Cargos eigene Warnung zeigte danach auf die einzige moderne Version, die noch stand. Die Nutzlast steckte in einem Build-Skript, ein Type-Check des Projekts genügte also, um sie auszuführen. Bromure Agentic Coding macht aus der ganzen Operation ein Stoppuhr-Problem, das sie verliert: die vergifteten Crates lebten 86 Minuten, und die voreingestellte Altersschwelle liegt bei zwei Tagen.

Der Angreifer musste niemanden dazu überreden, Schadsoftware zu installieren. Er ließ die sauberen Versionen verschwinden und überließ den Rest dem Update-Rat der Registry selbst.

Um 07:15:00 UTC am 20. August erschien [email protected] auf crates.io unter dem Konto seines echten Maintainers. Vierundzwanzig Sekunden später begann der Angreifer, die Versionen 0.3.5 bis 0.3.9 zurückzuziehen: sechzehn Sekunden geskriptete Arbeit, um das Regal dahinter zu leeren.

Ein Yank ist der Rückruf-Mechanismus von crates.io, der Hebel, den ein Maintainer zieht, wenn ein Release sich als kaputt herausstellt. Cargo reagiert darauf genau so, wie man es sich wünscht. Es warnt, dass eine Version in Ihrer Lockfile zurückgezogen wurde, und es weigert sich, bei einer frischen Auflösung eine zurückgezogene Version zu wählen. Jeder Ratschlag, der auf so eine Warnung folgt, sagt Ihnen: aktualisieren.

Also zog der Angreifer die sicheren Versionen zurück, ließ eine moderne Version stehen und ließ das Richtige zu tun Sie geradewegs hineinlaufen.

Neunzig Minuten, von Anfang bis Ende

Das Advisory der Rust Security Response WG und die von StepSecurity rekonstruierte Zeitleiste beschreiben eine Operation, die der Angreifer zwei Tage lang vorbereitet hatte.

Am 18. August um 01:25:58 registrierte er auf crates.io ein Konto namens dtolney, einen Buchstaben neben David Tolnay, dessen Name auf den meisten Proc-Macro-Crates des Ökosystems steht. Eine halbe Stunde später veröffentlichte er ein sauberes [email protected], einen Typosquat von proc-macro2, das überhaupt nichts tat. Dieses Release existierte nur, um dem Konto eine Historie zu geben.

Das echte kam fünf Stunden später. [email protected] ging am 20. August um 07:11:15 online, mit einer bösartigen build.rs an Bord. Vier Minuten danach ging [email protected] vom Konto des rechtmäßigen Eigentümers hinaus, mit genau einer Änderung darin: einer neuen Abhängigkeitszeile, die auf proc-macro1 ^1.0.107 zeigte. Die Security Response WG geht behutsam mit dem Maintainer um, und das zu Recht. Sie „glauben nicht, dass der Autor von arrayref böswillig handelt“, und merken an, dass „sein Rechner oder seine Zugangsdaten wahrscheinlich kompromittiert sind“. [email protected] folgte um 07:34:07, [email protected] um 07:37:49.

Dann war es fast so schnell vorbei, wie es begonnen hatte. Das Forschungsteam von Nextron Systems fand die bösartige Crate und meldete sie. crates.io zog arrayref nach 86 Minuten, internment nach 90 und append-only-vec nach 107 zurück, löschte sechs Crates des Angreifers und sperrte das kompromittierte Konto. RustSec verzeichnete keinen Hinweis darauf, dass jemand gegen eine vergiftete Version gebaut hat.

Neunzig Minuten sind eine schnelle Reaktion. Für alle, deren Build innerhalb des Fensters lief, kam sie trotzdem danach.

Zwei Tage Vorbereitung, neunzig Minuten Exposition18. AUG · 01:25Konto „dtolney“ angelegteinen Buchstaben neben einemNamen, dem jeder Rustler traut01:55 · sauberer Köder 1.0.10620. AUG · 07:11:15[email protected]Typosquat von proc-macro2mit einer bösartigen build.rsein Name, den niemand tippt20. AUG · 07:15:00[email protected]echtes Konto, gestohlene LoginsBibliothekscode unveränderteine neue Abhängigkeitszeile20. AUG · 07:15:24 → 07:15:40 — sechzehn Sekundenarrayref 0.3.5, 0.3.6, 0.3.7, 0.3.8, 0.3.9 — alle zurückgezogender Rückruf der Registry, rückwärts gefahren: Cargo warnt, dass Ihre gelockte Version zurückgezogen wurde, und lehnt sie neu abEine einzige moderne installierbare Version bleibt. Sie führen cargo update aus, um die Warnung loszuwerden.build.rs läuft bei cargo build, cargo check und cargo test — keine Funktion der Bibliothek wird je aufgerufen
Der Auslieferungsweg berührte das Urteilsvermögen eines Entwicklers nie. Eine typosquattete Build-Abhängigkeit wurde vier Minuten vor der Crate veröffentlicht, die sie hereinzog, jede sichere Version hinter dieser Crate wurde in einem sechzehn Sekunden langen Stoß zurückgezogen, und Cargos eigene Warnung über den Rückzug zeigte auf das einzige moderne Release, das übrig blieb.

Nichts in der Bibliothek musste aufgerufen werden

Die Nutzlast lebt in einem Build-Skript, und Build-Skripte laufen zur Compile-Zeit mit Ihren Rechten. The Hacker News berichtet, dass die Nutzlast bei cargo build, cargo check und cargo test feuerte. Das mittlere ist die vorsichtige Option: das, was Ihr Editor beim Speichern ausführt, das, was Sie tippen, wenn Sie die Meinung des Compilers wollen, ohne etwas laufen zu lassen. Hier genügte es.

Das Skript selbst ist klein und alles andere als subtil, sobald man es vor sich hat. Es setzt https://23.254.165[.]112:9089/ aus Base64-Fragmenten wieder zusammen, schaltet die TLS-Zertifikatsprüfung ab, lädt eine zu Ihrem Betriebssystem und Ihrer Architektur passende Nutzlast herunter und startet sie abgekoppelt. Der nette Kniff ist std::mem::forget(child), das den Handle fallen lässt, damit der Prozess Cargos Job Object entkommt. Das Implantat läuft weiter, und der Compiler beendet sich mit einem grünen Build.

Was als Nächstes herunterkommt, ist ein Infostealer. Wiz' Analyse beschreibt, wie er per HTTPS POST an /49890878 funkt, Hostname, Benutzername, Betriebssystem und installierte Anwendungen sammelt und die SQLite-Login-Datenbanken von Chrome, Brave und Edge nach gespeicherten Zugangsdaten abfragt. Er persistiert als Registry-Run-Schlüssel unter Windows, als LaunchAgent unter macOS und als systemd-Benutzerdienst unter Linux, und wenn sein C2 verstummt, fällt er auf einen Domain-Generation-Algorithmus zurück, der alle fünf Tage zehn .com-Domains erzeugt. Wiz verbindet die Infrastruktur mit DPRK-Aktivität: derselbe /49890878-Endpunkt wie bei der Mastra-npm-Kampagne, ein gemeinsamer Zertifikatsaussteller und der wiederholte Einsatz eines Hostwinds-Adressbereichs.

Die ausgelieferten Builds decken x86_64-Linux, Windows und macOS ab, dazu aarch64-macOS. Letzteres ist ein Apple-Silicon-Laptop. Jemand hat darüber nachgedacht, wer Rust kompiliert.

Die Abhängigkeit, die Sie nie gewählt haben

arrayref hat 245 Millionen Downloads über die Zeit, und fast niemand hat es in eine Cargo.toml geschrieben. StepSecurity zählte 406 abhängige Crate-Versionen, wobei arrayref unter winit sitzt und damit unter egui und iced, unter blake3 und blake2b_simd und unter einem guten Teil des Ethereum- und Solana-Werkzeugkastens. internment und append-only-vec bringen zusammen weitere 19 Millionen Downloads mit.

Der Rat, Abhängigkeiten vor dem Hinzufügen zu prüfen, greift hier also nicht. Niemand hat sie hinzugefügt. Sie kam vier Ebenen tiefer an, unter einer Crate, die Sie tatsächlich hinzugefügt haben, in einem Lockfile-Diff, das vorbeiscrollt. Den Bibliothekscode zu lesen hätte ebenfalls nichts geholfen. Niemand hat den Quellcode der drei vergifteten Crates angefasst; die gesamte Änderung war eine Abhängigkeitszeile, die eine zwei Tage alte Crate nannte, veröffentlicht von einem Konto, dessen Name richtig aussah.

Warum ein Blog über agentisches Coden das angeht

Sehen Sie sich den Auslöser noch einmal an. Eine Yank-Warnung erscheint in der Build-Ausgabe. Etwas beseitigt sie, indem es cargo update ausführt.

Dieses Etwas ist oft kein Mensch mehr. Eine Warnung wegzuräumen ist der Haushalt, den ein Agent auf dem Weg zu dem erledigt, worum Sie ihn gebeten haben: Er sieht eine zurückgezogene Version, er kennt die Behebung, er wendet die Behebung an. Er arbeitet, während Sie schlafen, in einer Schleife, über einen ganzen Rückstand hinweg. Er frischt Lockfiles auf, weil ein Build fehlgeschlagen ist, und fügt Crates hinzu, weil ein Feature eine brauchte.

Ein unbeaufsichtigt arbeitender Agent ist der Akteur, der am wahrscheinlichsten in einem 86-Minuten-Fenster war, und am unwahrscheinlichsten wegen einer unbekannten transitiven Abhängigkeit innehält, die von einem Namen veröffentlicht wurde, den er wiedererkennt. Geben Sie ihm ein kompromittiertes Maintainer-Konto, einen Typosquat einen Buchstaben neben David Tolnay und eine Registry-Warnung, die das Upgrade empfiehlt, und in der Kette bleibt nichts mehr übrig, was Urteilsvermögen abfangen könnte.

Das können Sie überleben, denn Urteilsvermögen ist ohnehin ein schlechter Ort, um das zu entscheiden.

Bromure macht daraus ein Stoppuhr-Problem

Jedes Bromure-Agentic-Coding-Profil kommt mit eingeschalteter Altersschwelle, gesetzt auf zwei Tage. Der MITM-Proxy auf dem Host sieht jeden Paketabruf, den die VM macht, über npm, PyPI, Cargo, RubyGems, Maven, NuGet, Go-Module und Packagist hinweg, und Versionen, die jünger als der Grenzwert sind, kommen nicht zurück.

Cargo bekommt die volle Behandlung. Bromure erkennt index.crates.io, crates.io und static.crates.io und schreibt dann die Crate-Metadaten im Durchlauf um: Es entfernt zu frische Versionen aus der Versionsliste und setzt max_version, max_stable_version und newest_version auf das neueste Release, das den Filter überlebt. Cargos Resolver erfährt nie, dass eine neuere Version existiert.

Jetzt legen Sie die Zahlen nebeneinander.

Wie alt jede Version je wurde, gegen eine Zwei-Tage-Grenze[email protected]4 Minuten alt, als arrayref sie einband[email protected]86 Minuten in der Registry[email protected]90 Minuten[email protected]107 MinutenBromure-Altersschwelle2 Tage — 2.880 Minuten — standardmäßig an, 0 bis 90 einstellbarAm Host-Proxy gefiltert: zu frische Versionen fallen aus den Crate-Metadaten, die Felder für die „neueste Version“ werden vor Cargo umgeschrieben.
Jede Version in diesem Angriff wurde geboren und starb innerhalb eines Fensters, das die Standardrichtlinie gar nicht öffnet. Die Altersschwelle ist kein Urteil über das Paket; sie ist eine Uhr, und die Operation hatte keine Möglichkeit, sie auszusitzen, ohne gefunden zu werden.

Nichts in dieser Operation wurde zwei Tage alt. [email protected] war vier Minuten alt, als arrayref es als Abhängigkeit nannte. Keine der beiden Crates ist für einen Resolver hinter der Schwelle sichtbar, also findet cargo update nichts Neues.

Sie werden das auf Ihrem Bildschirm sehen. Mit zurückgezogenen 0.3.5 bis 0.3.9 und herausgefiltertem 0.3.10 kann eine frische Auflösung von arrayref mit der Unfähigkeit zurückkommen, eine Version zu wählen — was ein Fehler ist statt eines stillen Erfolgs. Eine bestehende Cargo.lock, die auf 0.3.9 festgenagelt ist, baut weiter, denn ein Yank blockiert nur die neue Auflösung. So oder so verbringen Sie diesen Donnerstagmorgen damit, eine Resolver-Meldung zu lesen, statt jede Zugangsberechtigung zu rotieren, die Sie besitzen.

Bromure prüft ein zweites Mal, für den Fall, dass der Resolver seine Antwort schon hatte. Es gleicht jeden Artefaktabruf, die .crate-Datei selbst, mit dem erfassten Veröffentlichungszeitpunkt ab und liefert ein 451, das das Alter benennt: vor 41 Minuten veröffentlicht, die Richtlinie verlangt mindestens 2 Tage. Ein warmer Metadaten-Cache bringt dem Angreifer nichts.

Der erste Sprung des Droppers führt ins Leere

Nehmen wir an, Sie hätten die Uhr abgeschaltet. Jemand nimmt ein Paket aus, oder senkt den Grenzwert für einen Nachmittag. Das Build-Skript muss immer noch 23.254.165.112 auf Port 9089 erreichen.

Jedes Bromure-Profil trägt seine eigene Egress-Richtlinie, geschrieben als kurzes Regelwerk im pf-Stil:

allow web crates.io
allow web static.crates.io
allow web index.crates.io
allow tcp api.anthropic.com:443
default deny

Ihr Mac setzt sie auf zwei Ebenen durch, die sich ein Regelwerk teilen: der virtuelle Switch, der über alle Protokolle hinweg auf Ziel-IP und per DNS mitgelesenen Hostnamen prüft, und der Proxy, der auf TLS-SNI und HTTP-Methode prüft. Eine nackte IP auf Port 9089 passt auf keine Regel in dieser Datei, fällt also auf default deny und die Verbindung stirbt auf der Host-Seite der Grenze. Der Stage-2-DGA, zehn frische .com-Domains alle fünf Tage, trifft auf dieselbe Wand, denn ein generierter Name ist immer noch ein Name, den niemand auf die Liste gesetzt hat. Jede Ablehnung landet als egress.firewall-Zeile in Window → Security Timeline, mit Host, IP, Port und Verdikt, geschrieben dorthin, wo Code innerhalb der VM nicht hinreicht.

Da drin gibt es nichts zu stehlen

Lesen Sie die Wiederherstellungs-Checkliste, die mit diesem Vorfall kam: SSH-Schlüssel, Cloud-Token, API-Token und Signaturschlüssel rotieren. Den Registry-Cache leeren. CI-Cache-Layer löschen. Vendor-Verzeichnisse neu bauen. Auf jeder Maschine, die während des Fensters gebaut hat.

Ein Bromure-Workspace beantwortet den größten Teil dieser Liste mit einem leeren Raum.

Die echten Zugangsdaten betreten die VM nie. Der ANTHROPIC_API_KEY in diesem Gast ist ein brm_…-Platzhalter. Die kubeconfig enthält Wegwerf-Client-Zertifikate. Die Auth für die Container-Registry ist ein abgeleiteter Basic-String. Das GitHub-Token ist eine Fälschung, und der SSH-Schlüssel ist ein pro Profil erzeugter ed25519-Schlüssel für diesen Workspace, nicht der auf Ihrem Laptop. Der MITM-Proxy auf dem Host setzt den echten Wert in die ausgehende Anfrage ein, und bei AWS geht er noch einen Schritt weiter: Der Host signiert die SigV4-Anfrage mit dem echten Material neu, sodass alles, was am Proxy vorbeiläuft, von Amazon eine InvalidSignatureException zurückbekommt statt eines funktionierenden Aufrufs.

Ein Infostealer, der diesen Gast nach Zugangsdaten durchkämmt, findet Zugangsdaten. Sie lassen sich parsen, sie sehen richtig aus, und außerhalb der Maschine sind sie nichts wert.

Der Rest seines Einkaufszettels liegt auf der falschen Seite des Hypervisors. Chrome, Brave und Edge halten ihre Login-Datenbanken auf Ihrem Mac, in einem Benutzerprofil, zu dem der Gast keinen Pfad hat. Der macOS-LaunchAgent, den er schreiben will — der Grund, warum er einen aarch64-macOS-Build mitbrachte —, hat kein macOS, in das er schreiben könnte. Er kann einen systemd-Benutzerdienst im Home des Gasts installieren, und diese Schicht hat einen Knopf: Erase home… setzt das /home/ubuntu des Profils auf den Zustand direkt nach dem Klonen zurück, und Reset to base… klont die System-Disk des Workspace erneut aus dem schreibgeschützten Basis-Image.

Und die Inventur haben Sie schon

Die letzte Zeile jedes Remediation-Leitfadens zu diesem Vorfall ist ein Grep: jede Cargo.lock auf jeder Maschine nach sechs Crate-Namen und drei Versionsstrings durchsuchen und dann ~/.cargo/registry/cache nach den Tarballs. Das ist eine gute Anweisung und ein elender Nachmittag, und sie funktioniert nur auf den Maschinen, die Sie noch haben.

Bromure hält die Antwort fest, während sie entsteht. Jeder Paketabruf durch den Proxy schreibt eine supply_chain.fetch-Zeile in Window → Security Timeline mit Ökosystem, Paket, Version, Ergebnis und dem Grund hinter dem Ergebnis, host-seitig, neben jedem Zugangsdaten-Tausch und jedem Firewall-Verdikt. Wenn das nächste Advisory eine Version und ein Zwei-Stunden-Fenster benennt, ist die Frage „hat hier irgendetwas das abgerufen?“ eine Suche statt einer Expedition.

Bromure Enterprise Manager rollt denselben Strom über eine ganze Flotte auf, und genau in dieser Form kommt die Frage an: hat eine der neunzig Maschinen, auf denen über Nacht Agenten liefen, das gebaut.

Der Teil, den man behalten sollte

crates.io hat sich hier gut geschlagen. Neunzig Minuten von der Meldung bis zur Entfernung, sechs Angreifer-Crates weg, das kompromittierte Konto gesperrt, ein klares Advisory und kein Hinweis darauf, dass jemand getroffen wurde. Das Rust-Team hat sich außerdem Mühe gegeben, einem Maintainer, dessen Zugangsdaten gestohlen wurden, keine Schuld zu geben, was der richtige Instinkt ist und nicht immer der verbreitete.

Jemand wird den Mechanismus dennoch wiederverwenden, weil er billig ist und weil er clever ist. Ein Yank ist ein öffentlicher, sofortiger Ein-Zeilen-Hebel, der eine Registry dazu bringt, ein Upgrade zu empfehlen, und jedes Ökosystem hat einen. Was auch immer Ihre Builds ausführt, wird diese Empfehlung sehen und danach handeln, und dem, was heute danach handelt, fehlt oft ein Donnerstagmorgen an Kontext darüber, welche Crate welche ist.

Legen Sie die Entscheidung also irgendwohin, wo sie keinen Kontext braucht. Zwei Tage Tageslicht zwischen der Veröffentlichung einer Version und Ihrem Build sind kein Urteil über ein Paket, einen Maintainer oder einen Kontonamen, der einen Buchstaben danebenliegt. Sie sind eine Uhr, und diese Operation hatte neunzig Minuten.


Quellen: Rust Security Response WG, „Supply chain attack on arrayref“ (20. August 2026) · Wiz, „Rust Supply Chain Attack on arrayref: Significant Overlap with DPRK Campaigns“ (20. August 2026) · StepSecurity, „arrayref, internment, and append-only-vec Poisoned by the proc-macro1 Build-Time Dropper“ (20. August 2026) · The Hacker News, „Rust Supply Chain Attack Puts Build-Time Malware in Crates with 245 Million Downloads“ (20. August 2026) · BleepingComputer, „Hackers poison arrayref Rust crate to push infostealer malware“ (20. August 2026) · Semgrep, „Rust crates arrayref & append-only-vec compromised“ (20. August 2026)