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

Der Code lief vor dem Modell

Pluto Security hat 31 Hugging-Face-Repositories gefunden, die sich als winzige Qwen2-Test-Checkpoints ausgeben. Keines enthielt ein Modell. Jedes enthielt eine Python-Datei, die in dem Moment läuft, in dem das Modell mit trust_remote_code geladen wird, und einen versteckten Miner, Startup-Hooks und in manchen Builds eine Reverse Shell installiert. Das Flaggschiff kam durch die Scanner von Hugging Face und erreichte 28.412 Downloads, und eine fast identische Kopie blieb nach der Entfernung online. Coding Agents greifen beim Schreiben von Tests zu winzigen Testmodellen. In Bromure Agentic Coding läuft dieses Laden in einer VM, aus der der Miner seinen Pool nicht erreicht, die Security Timeline ordnet jede Verbindung dem Tool-Aufruf zu, der sie geöffnet hat, und ein Zurückspulen entfernt, was die Payload hinterlassen hat.

Die Gewichtsdatei war 42 Bytes groß und enthielt keine Tensoren. Die Arbeit erledigte eine Python-Datei, die lief, sobald das Modell geladen wurde, bevor irgendein Modellcode etwas tat.

Am 29. Oktober hat Pluto Security Load/Mine veröffentlicht, eine Analyse einer Kampagne, die seit September auf Hugging Face und auf Chinas ModelScope läuft. Pluto fand 31 bösartige Repositories in sieben Konten. Sie sind als kleine Qwen2-Test-Checkpoints getarnt, mit Namen wie qwen2-tiny, und kamen zusammen auf mehr als 32.000 erfasste Downloads. BleepingComputer berichtete am selben Tag.

Pluto fasst die Kampagne in einem Satz zusammen: „The code runs before the model does“ (der Code läuft, bevor das Modell läuft).

Wie aus dem Laden eines Modells ein Miner wird

Der Angriff nutzt keine vergifteten Gewichte und keinen Pickle-Exploit. Er nutzt ein Feature. Ein Hugging-Face-Modell kann eigenen Python-Code mitbringen, und die Bibliothek Transformers führt diesen Code aus, wenn der Aufrufer trust_remote_code=True übergibt. Die config.json des Repositorys nennt in einem auto_map-Eintrag ein Modul und eine Klasse, etwa modeling_evil2.EvilModel. Transformers lädt diese .py-Datei herunter und importiert sie. Alles auf oberster Ebene der Datei läuft während des Imports, noch bevor die Klasse zurückgegeben wird.

In diesen Repositories ist model.safetensors 42 Bytes groß. Darin steckt kein Modell. Die Python-Datei erledigt die ganze Arbeit:

  1. Sie nistet sich ein. Sie schreibt nach /tmp/.cch/svc.py einen Watchdog, der den Miner alle zwei Minuten neu startet, und hängt einen Startup-Hook an ~/.bashrc, /root/.bashrc und /root/.profile an. Builds der ersten Welle legen außerdem einen SSH-Schlüssel namens qx-5090-op in authorized_keys und einen Cron-Job unter /etc/cron.d/.sync-helper ab.
  2. Sie holt den Miner und versteckt ihn. Der Downloader wird mit einem leeren Proxy-Handler gebaut und ignoriert so die Einstellungen HTTP_PROXY und HTTPS_PROXY, auf die sich ein Unternehmen vielleicht verlässt. Bei der 40,6 MB großen Binärdatei sind die UPX-Marker so verändert, dass upx -d sie ablehnt. Sie läuft aus einer anonymen Datei im Speicher, unter dem Prozessnamen [kworker/u20:1], und schürft Pearl über Kryptex auf Port 8048.
  3. Sie öffnet eine Tür. Drei Builds fügen eine Reverse Shell zu 212.32.253.3 auf Port 443 oder 9443 hinzu, die sich nach jedem Abbruch neu verbindet.
  4. Sie nimmt den Host ins Visier. Ein verwandtes Skript der zweiten Welle hängt aus einem privilegierten Container die Festplatten des Hosts ein und verschafft sich Root auf dem Server darunter.
Ein Modell geladen, drei PayloadsDas Ladenfrom_pretrained("…/qwen2-tiny", trust_remote_code=True)config.json · auto_mapmodeling_evil2.EvilModelmodel.safetensors: 42 Bytes, 0 TensorenDer Import führt die Datei ausCode auf oberster Ebene läuft,bevor die Klasse zurückkommtPersistenzWatchdog in /tmp/.cch/svc.pyHooks in ~/.bashrc und /root/.profileCron-Job, SSH-Schlüssel qx-5090-opVersteckter MinerDownloader ignoriert HTTP_PROXYläuft im Speicher als [kworker/u20:1]schürft zu prl-us.kryptex.network:8048Reverse Shellzu 212.32.253.3 auf 443 oder 9443,verbindet sich neu nach Abbruch(drei Builds)
Was das Laden eines der Kampagnenmodelle auslöst, nach der Analyse von Pluto Security. config.json verweist Transformers auf eine Python-Datei, die Datei läuft beim Import, und die 42 Byte große Gewichtsdatei wird nie benutzt. Die Payload richtet Persistenz ein, startet einen getarnten Miner, der die Proxy-Einstellungen ignoriert, und öffnet in manchen Builds eine Reverse Shell.

Sauber gescannt, und wieder da

Hugging Face prüft Uploads mit einem Virenscanner und mit Partnertools von Protect AI und JFrog. Laut Pluto suchen diese Tools nach bekannter Malware und unsicheren Modellformaten. Was die Python-Datei tut, prüfen sie nicht. Das Flaggschiff der zweiten Welle, MENXIANGYU/qwen2-tiny, „scanned clean while it collected 28,412 downloads“ (blieb im Scan unauffällig, während es 28.412 Downloads sammelte). Das Urteil eines Partnerscanners über die Payload-Datei lautete „not a machine-learning model“ (kein Machine-Learning-Modell).

Pluto meldete die erste Welle an Hugging Face und ModelScope, und beide entfernten die Konten. Innerhalb weniger Tage tauchten 19 neue Repositories unter neuen Konten auf. Stunden nach Plutos Bericht veröffentlichte der Betreiber ein Container-Escape-Skript. Hugging Face deaktivierte das Flaggschiff, aber BleepingComputer bestätigte, dass eine fast identische Kopie, htt0118/qwen2-tiny, mit demselben Miner und derselben Reverse Shell weiter online war.

Ein winziges Testmodell ist genau das, wonach Ihr Agent sucht

Die Kampagne zielte auf GPU-Server mit Inferenzplattformen wie Xinference und GPUStack. Der Köder wirkt aber auch auf dem Rechner eines Entwicklers, denn ein Coding Agent sucht einen winzigen Qwen2-Checkpoint, wenn er Tests für Code schreibt, der ein Modell aufruft. Bitten Sie einen Agenten, einen Test für Ihre Embedding-Pipeline oder Ihr Fine-Tuning-Skript zu schreiben, und er sucht ein Modell, das klein genug ist, um in Sekunden zu laden. Die Namen dieser Kampagne, qwen2-tiny und seine Varianten, sind genau auf diese Suche zugeschnitten.

Der Agent muss nicht nachlässig sein, um das Flag zu setzen. Wenn ein Repository eigenen Code mitbringt, bricht Transformers ab und fordert den Aufrufer auf, trust_remote_code=True zu übergeben. Für einen Agenten, der einen fehlschlagenden Test repariert, ist das ein Fehler, dessen Lösung gleich in der Meldung steht. Er ergänzt das Argument und startet den Test neu. Ob der Test dann besteht, spielt keine Rolle mehr: Der Miner ist während des Imports gestartet.

Auf einem Laptop läuft diese Datei als Sie. Der Startup-Hook in ~/.bashrc läuft in jedem Terminal, das Sie danach öffnen. Die Reverse Shell gibt dem Betreiber einen Prompt mit Ihrem ~/.ssh, Ihren Cloud-Zugangsdaten, Ihrem gh-Login und Ihrem HF_TOKEN. Plutos Rat an alle, die diese Modelle geladen haben: den Rechner als kompromittiert behandeln und jede Zugangsdaten rotieren, die der Prozess erreichen konnte.

In Plutos Härtungsempfehlungen beschreibt eine Zeile das Setup, das Sie wollen: nicht vertrauenswürdigen Modellcode „with limited privileges, restricted egress and no production secrets“ ausführen (mit eingeschränkten Rechten, beschränktem ausgehendem Verkehr und ohne Produktionsgeheimnisse).

Derselbe Test in Bromure Agentic Coding

Bromure Agentic Coding führt Claude Code, Codex, Grok, Kimi und omp in Ubuntu-VMs auf Ihrem Mac aus. Jede Maschine hat ihre eigene Festplatte, ihr eigenes Home und ihre eigene Netzwerkrichtlinie, durchgesetzt auf dem Mac, wo der Agent nicht hinkommt. Hier ist derselbe Test auf einer Bromure-Maschine, eine Stufe der Payload nach der anderen.

Der Miner erreicht seinen Pool nie

Jede Verbindung, die die VM öffnet, läuft über Bromures virtuellen Switch und Proxy auf dem Mac. Die Ports 80 und 443 werden am Switch in den Proxy umgeleitet, also wird auch ein Downloader mit leerem Proxy-Handler abgefangen. Der Trick, mit dem die Kampagne an Proxy-Einstellungen von Unternehmen vorbeikommt, hat hier nichts zu umgehen, denn das Abfangen hängt von keiner Einstellung in der VM ab.

Stellen Sie Unmatched traffic in Sandbox Policy auf Deny, oder stellen Sie die Maschine auf die Engine OpenShell + Sentry um, bei der alles verweigert wird, was keine Regel erlaubt. Die Startrichtlinie erlaubt GitHub, npm und PyPI nur lesend sowie die Ubuntu-Mirrors, und Sie ergänzen huggingface.co für die Downloads. Dann passt die Verbindung des Miners zu prl-us.kryptex.network:8048 auf keine Regel und wird abgelehnt. Ebenso die Reverse Shell zu 212.32.253.3, auf beiden Ports. Mit Tell the agent when policy blocks it, bei OpenShell-Maschinen standardmäßig aktiv, liest der Agent außerdem in seiner Unterhaltung, was blockiert wurde und dass er anhalten und Sie fragen soll, statt die nächsten zehn Minuten nach einem Umweg zu suchen.

Auf einer OpenShell-Maschine mit Enforce binaries wird jede Verbindung zudem dem Programm zugeordnet, das sie geöffnet hat, und der Hash jedes Programms wird beim ersten Start festgeschrieben. Ein Miner, der aus einer anonymen Speicherdatei startet, ist ein Programm, das keine Regel nennt. Ihn in [kworker/u20:1] umzubenennen, ändert daran nichts, denn der Attestor identifiziert die Binärdatei, nicht den Namen, den sie sich gibt.

Sie sehen, welcher Tool-Aufruf es war

Der Betreiber hat den Namen [kworker/u20:1] gewählt, um jemanden zu täuschen, der auf einem ausgelasteten Server ps oder top liest. Auf einer Bromure-Maschine mit dem Kernel-Sentry meldet der Kernel der VM jeden Programmstart und jede Verbindung an den Mac, über einen privaten Kanal, den der Agent nicht beenden kann. Jede blockierte Verbindung landet in der Security Timeline mit der Kette dahinter, etwa so: Tool pytest tests/test_embed.py: claude → bash → python → TCP 212.32.253.3:443.

Ein Doppelklick auf die Zeile, und network lineage zeigt die ganze Geschichte: den Prompt, den Sie gegeben haben, was der Agent vor dem Aufruf geschrieben hat, den genauen Befehl, jeden Prozess vom Agenten bis zu dem, der sich verbunden hat, und welche Schicht abgelehnt hat. Wenn ein Testlauf eine nackte IP-Adresse anwählt, bekommen Sie eine rote Zeile mit dem Befehl, der das Modell geladen hat.

Der Agent-Watchdog, in Guardrails auf Quarantine gestellt, reagiert auf dieselben Signale, ohne auf Sie zu warten. Eine Häufung von Blockaden, oder neue Ziele, nachdem sich die Sitzung eingependelt hat, über seiner Schwelle innerhalb einer Minute, und Bromure trennt das Netzwerk der VM und pausiert sie binnen Millisekunden. Die Warnung listet auf, was er gesehen hat. Keep quarantined setzt die VM fort, mit weiterhin getrenntem Netzwerk, damit Sie hineinschauen können.

Laptop: Der Test läuft als SieAgent lädt qwen2-tiny für einen Testsetzt trust_remote_code=True gegen den FehlerPayload läuft unter Ihrem KontoMiner erreicht den Kryptex-Pool,Reverse Shell erreicht 212.32.253.3Sie bleibt, und der Betreiber hat einen PromptHook in ~/.bashrc läuft in jedem neuen Terminal;~/.ssh, Cloud-Keys, gh-Login, HF_TOKEN erreichbarBromure: Der Test läuft in einer VMGleiches Laden, in der Ubuntu-VM der MaschineHF_TOKEN, gh und Cloud-Keys sind PlatzhalterSwitch und Proxy auf dem Mac: DenyPool :8048, Shell :443/:9443: keine Regel;Sentry nennt den Tool-Aufruf; Watchdog: QuarantäneReset disk, Rewind homefrische Systemplatte aus dem Basis-Image;Home auf den Start vor dem Test zurückgesetzt
Dasselbe Laden eines Modells auf einem Laptop und auf einer Bromure-Maschine. Auf dem Laptop läuft die Payload als Sie, erreicht ihren Pool und ihre Shell und bleibt in Ihren Startdateien. Auf der Bromure-Maschine lehnen Switch und Proxy auf dem Mac beide Verbindungen ab, der Kernel-Sentry führt sie auf den Tool-Aufruf zurück, der Watchdog kann die VM unter Quarantäne stellen, und Reset disk plus Rewind home entfernen, was abgelegt wurde.

Die Schlüssel in der VM sind Platzhalter

Angenommen, ein Build der Payload kommt weiter, auf einer Maschine, die Sie auf der Standard-Firewall gelassen haben. Der Betreiber hat eine Shell in einer Ubuntu-VM, und alle Zugangsdaten darin sind Platzhalter. Der GH_TOKEN in der Umgebung und das Token in ~/.git-credentials sind Fälschungen, die der Proxy auf Ihrem Mac erst auf dem Weg zu GitHub gegen das echte tauscht. Tragen Sie Ihr Hugging-Face-Token unter Other API keys mit dem Variablennamen HF_TOKEN und dem Host-Filter huggingface.co ein, und die VM bekommt einen brm_…-Platzhalter, der nur dort funktioniert. SSH-Schlüssel kommen gar nicht erst in die VM: Der Agent kann eine Signatur anfordern, und die Schlüsselbytes bleiben auf dem Mac.

Ein Platzhalter ist zugleich ein Stolperdraht. Schickt die Payload den GitHub-Platzhalter irgendwohin außer per HTTPS zu GitHub, blockiert der Proxy die Anfrage mit HTTP 451, pausiert die VM und öffnet eine Warnung, die die Zugangsdaten und den Zielhost nennt.

Das Aufräumen sind zwei Klicks

Plutos Aufräumliste für einen infizierten Host ist lang: den Watchdog finden, die Shell-Hooks entfernen, den Cron-Job löschen, den SSH-Schlüssel aus sechs authorized_keys-Pfaden streichen, nach fremden Registrierungen suchen und im Zweifel aus einem vertrauenswürdigen Image neu aufsetzen. Eine Bromure-Maschine hat dieses vertrauenswürdige Image schon. Reset disk klont die Systemfestplatte neu aus dem Basis-Image und nimmt dabei /etc/cron.d/.sync-helper und alles andere mit, was die Payload außerhalb des Home geschrieben hat. Rewind home… setzt /home/ubuntu auf einen Checkpoint eines früheren Starts zurück. Bromure legt bei jedem Start der Maschine einen an, also ist die ~/.bashrc von vor dem Test darunter. Ihre mit dem Host geteilten Ordner liegen auf dem Mac und bleiben unberührt.

Auf einer OpenShell-Maschine mit Enforce binaries wird ein Großteil dieser Liste gar nicht erst geschrieben. Der Agent läuft ohne Root, also werden die Hooks in /root/.bashrc und /root/.profile, der Cron-Job und die Schreibzugriffe auf die authorized_keys von Root abgelehnt, und jede Ablehnung erscheint als Guest sandbox-Zeile in der Timeline.

Ausgehender Verkehr, entschieden auf dem Mac

Die Ports 80 und 443 gehen an den Proxy, egal was die Einstellungen der VM sagen. Wird nicht zugeordneter Verkehr verweigert, werden Pool und Shell abgelehnt.

Jede Verbindung hat einen Namen

Der Kernel-Sentry ordnet jede Verbindung dem Tool-Aufruf und der Prozesskette dahinter zu. Ein getarnter Miner erscheint trotzdem als das Laden des Modells.

Platzhalter statt Schlüssel

GitHub-, Cloud- und Hugging-Face-Tokens in der VM sind Fälschungen, die nur auf ihren eigenen Hosts funktionieren. Anderswo lösen sie eine Warnung aus.

Eine saubere Maschine auf Abruf

Reset disk und Rewind home versetzen die VM in den Zustand vor dem Test, Persistenz eingeschlossen.

Behandeln Sie ein Modell-Repository wie ein Paket

Pluto listet seine Indikatoren im Bericht auf. Wenn Sie eines dieser Modelle mit aktiviertem eigenem Code geladen haben, suchen Sie nach /tmp/.cch/, dem Prozess [kworker/u20:1] mit Mining-Argumenten und den Startup-Hooks, und rotieren Sie dann alle Zugangsdaten, die der Rechner erreichen konnte. Bevor Sie trust_remote_code für ein Repository einschalten, lesen Sie dessen modeling_*.py und pinnen Sie den Commit, den Sie gelesen haben.

Plutos Bericht zeigt ein Modell-Repository, das als Code-Repository arbeitet, während die Scanner es als Dateiformat prüften. Wenn Ihr Agent Modelle für seine Tests aussucht, wird er irgendwann eine dieser Dateien laden. Auf einer Bromure-Maschine läuft diese Datei in einer VM mit Platzhalter-Schlüsseln und einer auf dem Mac durchgesetzten Egress-Richtlinie, und danach können Sie die Maschine auf einen sauberen Checkpoint zurücksetzen.

Für einen früheren Fall, in dem die Prüfung von trust_remote_code selbst versagt hat, lesen Sie Das Flag sagte Nein. Wie Bromure die Pakete prüft, die ein Agent installiert, lesen Sie in Das Release war zwei Stunden alt.