Executive Summary
On July 14, 2026, Ossprey Security detected five malicious packages published under the @asyncapi npm namespace: @asyncapi/generator-helpers@1.1.1, @asyncapi/generator-components@0.7.1, @asyncapi/generator@3.3.1, @asyncapi/specs@6.11.2, and @asyncapi/specs@6.11.2-alpha.1.
The attacker exploited a pull_request_target misconfiguration in the AsyncAPI generator GitHub Actions workflow to steal the asyncapi-bot service account Personal Access Token, then used it to push malicious commits directly to release branches, causing the official CI/CD pipeline to sign and publish the trojanized packages.
Each package contains a double-obfuscated JavaScript payload hidden behind whitespace in legitimate source files that executes on module import, downloads an 8.25 MB encrypted second stage from IPFS, and connects to command-and-control infrastructure at 85.137.53[.]71 via HTTP, Nostr relays, and an Ethereum smart contract.
Decrypted configuration recovered from the IPFS payload self-identifies the campaign as miasma-train-p1; the Stage 3 runtime labels itself M-RED-TEAM v6.4. The tooling is a variant of the Miasma malware framework, but the operator is distinct from both the actor behind the original Miasma release and the earlier Shai-Hulud 2.0 compromise of @asyncapi.
Combined, the affected packages exceed 2 million downloads per week, putting a large number of developer workstations and CI/CD environments at risk. Ossprey users can confirm whether any affected version reached their projects from the dashboard.
Who is Behind This?
The operator self-identifies as M-RED-TEAM v6.4, and the campaign labels itself miasma-train-p1 internally.
The tooling is clearly Miasma-derived: the obfuscation scheme and configuration structure match the Miasma framework, the giteaPackagesOrg field in the decrypted config is set to miasma-test-org, the Linux persistence unit is named miasma-monitor.service, and the Nostr channel uses Miasma-branded event tags.
We analyzed the Miasma framework itself in a previous post. This campaign is most likely a separate operator rather than Miasma's original author: the framework was released publicly, so anyone can pick it up.
It is also separate from the previous compromise of the AsyncAPI namespace: @asyncapi/specs was one of the most heavily infected packages in the Shai-Hulud 2.0 worm back in November 2025, an unrelated actor whose payload spread through an npm preinstall hook rather than an import-time IPFS loader (covered by BleepingComputer). That makes this the namespace's second compromise in eight months.
A Rentry dead-drop slug used during initial access matches naming patterns from the prt-scan campaign, but prt-scan has not been linked to Miasma. With the framework circulating publicly, more operators are likely to reuse it; book a demo to see how Ossprey can help protect your dependencies.
Technical Breakdown
Stage 0: GitHub Actions Pwn-Request
We traced the infection chain to its starting point outside the npm registry entirely. The asyncapi/generator repository contained a GitHub Actions workflow that used pull_request_target to trigger on pull requests while also checking out the pull request's code.
Because pull_request_target runs in the context of the base repository with full access to secrets, this created a critical escalation path. The attacker opened 37 pull requests to generate noise, then submitted one containing obfuscated JavaScript hidden after approximately 1,000 bytes of whitespace in a markdown file.
The workflow executed the attacker-controlled code with base-repository secret access at 05:16 UTC, exfiltrating the asyncapi-bot Personal Access Token to a Rentry dead-drop. That token had push access across the AsyncAPI organization.
The attacker used it to push malicious commits directly to the next branch of asyncapi/generator at 06:58 UTC and to asyncapi/spec-json-schemas across 11 commits between 07:51 and 08:28 UTC.
Both repositories had CI/CD workflows configured to publish to npm on direct pushes, so the packages were distributed automatically without any additional review.
Stage 1: First-Stage Loader in Source Files
We found the injected payload occupying line 77 of utils.js in @asyncapi/generator-helpers@1.1.1, preceded by legitimate helper functions and separated from them by an 880-byte run of whitespace that pushes the obfuscated code far off-screen in most editors. The injection in @asyncapi/specs index.js is structurally different but functionally equivalent: TypeScript-style ES module import declarations for fs, path, https, child_process, and os are prepended to the file, followed by an async main() function that is called immediately, before the legitimate module.exports block. In both cases, the payload uses a two-level obfuscation scheme. The outer layer employs javascript-obfuscator with a custom base64 alphabet and an integer-shuffle anti-tamper loop. When the obfuscated string lookup function resolves, it yields a second obfuscated JavaScript program passed directly to node -e. The critical call in the generator-helpers variant is spawn('node', ['-e', decodedPayload], {detached: true, stdio: 'ignore', windowsHide: true}).unref(), which creates a process completely detached from the parent, invisible to most process-tree monitors.
Stage 2: IPFS Downloader (Inner Payload)
The string passed to node -e is a second obfuscated JavaScript program. Once we decoded its string table, the behaviour became clear.
It implements a getTargetDirectory() function that selects a platform-specific drop path: %LOCALAPPDATA%\NodeJS\ on Windows, ~/Library/Application Support/NodeJS/ on macOS, and ~/.local/share/NodeJS/ on Linux. It then creates that directory if absent and calls downloadFile() with the IPFS URL and a target filename of sync.js.
The download uses Node's built-in https.get(), pipes the response to a file write stream, and on success spawns node sync.js with the same detached+ignore pattern. Error handling removes the partially written file before propagating.
The generator-helpers variant uses IPFS CID QmQobZSp1wRPrpSEQ56qnyq7ecZh5Bg5k1fnjt4SUwwHb9; the specs variant uses Qmet4fhsAaWMBUxNDfREHwgiyDeSWy4YSYs9wiKUW5jGyf. Both resolve to payloads with the same campaign configuration.
Stage 3: AES-256-GCM Config Decryption
The sync.js payload begins with a short bootstrap routine that decrypts an embedded campaign configuration.
It derives a 32-byte AES key using HKDF-SHA256 with the hardcoded master key material rt-vault-master-key-32b-aaaaaaaa and the context label rt-baked-key, then uses that key to decrypt a base64-encoded AES-256-GCM ciphertext.
We decrypted this blob and recovered the full campaign configuration.
The config identifies the operation as target.name: miasma-train-p1 on the npm ecosystem. It hardcodes c2 details as primary exfiltration endpoints.
Server: http://85.137.53[.]71:8080
uploadServer: http://85.137.53[.]71:8081
c2ProxyMgmt: http://85.137.53[.]71:8091
Fallback channels include Nostr relays (wss://relay.damus.io and wss://relay.nostr.com/) and an Ethereum smart contract at 0x12c37A86a0Ed0beBe5d1d6a43E42f07860eAc710.
The same configuration, including an identical attacker EC public key, was recovered from both IPFS CIDs, confirming that these packages belong to a single actor.
Stage 4: Decrypted Campaign Configuration
Decrypting the AES-256-GCM blob reveals the full operational configuration stored in globalThis.RT_BAKED.
Both IPFS CIDs yield identical campaign parameters with the same attacker EC public key (04-prefixed uncompressed secp256k1 point), confirming shared infrastructure. The config sets safeMode: true and actualPersist: false, but both flags are misleading: safeMode is never actually enforced in the runtime, and the operative switch is toggles.persist, which is set to true.
The miasma-train-p1 name and the disabled recon and propagation toggles point to a staging or training deployment, but the persistence path still runs. Propagation to npm, PyPI, Ruby, and Cargo is disabled (all false), and recon and evasion toggles are off.
The persist toggle is set to true, however, meaning the Stage 3 framework does install the systemd service. The maxGen: 4 field indicates the worm is capable of up to four generations of self-propagation when enabled.
The blockchain stanza references an Ethereum mainnet contract and lists two Nostr relays for decentralized, censorship-resistant C2 communication. The batch strategy is set to CANARY with a 5% canary rate, a waveSize of 100, and a stopOnFailurePercent of 50, describing a staged rollout capability intended to limit detection.
The config's own target.downloadCount is set to 1, consistent with a staging seed rather than a live-fire deployment.
Stage 5: Runtime Capabilities and C2 Communication
The bulk of the sync.js payload is a 92,000-line malware framework whose main functionality is encrypted within the file and decrypted at runtime using the recovered campaign keys.
The live capability in this build is remote command execution: the framework exposes a command interface (Dirlist, GetFile, PutFile) and a ShellExec command that passes attacker-supplied strings to child_process.exec() with only killall on its blacklist.
A modular credential harvester targeting browser saved passwords and cookies (Chrome, Brave, Firefox, Edge), SSH private keys under ~/.ssh/, npm tokens from ~/.npmrc, AWS keys from ~/.aws/credentials, the macOS Keychain, and cryptocurrency wallets is present in the code but disabled in this build: the recon toggle is set to false, so the harvester exits before collecting anything.
Persistence, by contrast, does run. On Linux the framework writes a systemd user unit named miasma-monitor.service; on macOS it appends a nohup block to ~/.zshrc, ~/.bashrc, or ~/.bash_profile; on Windows it writes an HKCU Run value named miasma-monitor.
C2 is over HTTP to 85.137.53[.]71, with Nostr WebSocket relays using Miasma-branded event tags and the Ethereum smart contract at 0x12c37A86a0Ed0beBe5d1d6a43E42f07860eAc710 as fallback beacon channels, alongside a libp2p/DHT mesh bootstrapped from public BitTorrent DHT nodes.
An encrypted operational log is kept at ~/.cache/.sys_cache/.diag.enc in a 200 MB FIFO ring buffer.
Response
Immediately remove @asyncapi/generator-helpers@1.1.1, @asyncapi/generator-components@0.7.1, @asyncapi/generator@3.3.1, @asyncapi/specs@6.11.2, and @asyncapi/specs@6.11.2-alpha.1 from all environments and roll back to the last known-good versions.
Rotate all secrets accessible from any machine where these packages were installed: GitHub PATs, SSH private keys, npm authentication tokens, AWS access keys, CI/CD pipeline secrets, and any credentials stored in browser profiles.
Search affected systems for the Stage 2 payload and its encrypted log: ~/.local/share/NodeJS/sync.js (Linux), ~/Library/Application Support/NodeJS/sync.js (macOS), %LOCALAPPDATA%\NodeJS\sync.js (Windows), and ~/.cache/.sys_cache/.diag.enc.
Check all three persistence mechanisms and remove any present: the miasma-monitor.service systemd user unit on Linux (systemctl --user status miasma-monitor.service), a nohup block appended to ~/.zshrc, ~/.bashrc, or ~/.bash_profile on macOS, and an HKCU Run value named miasma-monitor on Windows.
Audit all GitHub Actions workflows in your organization for pull_request_target triggers that check out pull-request code; split such workflows into two jobs that isolate secret access from untrusted code execution.
Review GitHub audit logs for July 14, 2026 for any direct pushes to protected branches by service accounts, unexpected npm publish events, or anomalous PAT usage patterns.
Implement dependency allowlisting or lockfile integrity checks to ensure CI/CD pipelines cannot silently upgrade to unexpected package versions, even within trusted namespaces.
Block or alert on outbound HTTP connections from build agents to unknown IPs on non-standard ports; the C2 at 85.137.53[.]71:8080/8081/8091 and Nostr WebSocket connections to relay.damus.io should be treated as indicators of active compromise.
Note that the @asyncapi/specs@6.11.2-alpha.1 tarball is still downloadable from its direct npm URL even though it has been pulled from registry metadata; purge it from internal mirrors, caches, and CDN backing storage. Hunt for systems that imported (not just installed) any affected version, since the payload runs on require().
Indicators of Compromise
Network
http://85.137.53[.]71:8080 (C2 server)http://85.137.53[.]71:8081 (upload/exfiltration server)http://85.137.53[.]71:8091 (C2 proxy management)https://ipfs.io/ipfs/QmQobZSp1wRPrpSEQ56qnyq7ecZh5Bg5k1fnjt4SUwwHb9 (Stage 2 payload - generator-helpers variant)https://ipfs.io/ipfs/Qmet4fhsAaWMBUxNDfREHwgiyDeSWy4YSYs9wiKUW5jGyf (Stage 2 payload - specs variant)wss://relay.damus.io (Nostr C2 relay)wss://relay.nostr.com/ (Nostr C2 relay)https://ethereum-rpc.publicnode.com (Ethereum RPC for smart contract C2)rentry.co (initial PAT exfiltration dead-drop)
Filesystem
~/.local/share/NodeJS/sync.js (Stage 2 persisted payload, Linux)~/Library/Application Support/NodeJS/sync.js (Stage 2 persisted payload, macOS)%LOCALAPPDATA%\NodeJS\sync.js (Stage 2 persisted payload, Windows)~/.config/systemd/user/miasma-monitor.service (Linux systemd persistence unit)~/.cache/.sys_cache/.diag.enc (encrypted runtime log, 200 MB FIFO buffer)~/.zshrc, ~/.bashrc, ~/.bash_profile (macOS persistence: appended nohup block)HKCU\Software\Microsoft\Windows\CurrentVersion\Run value "miasma-monitor" (Windows persistence)~/.config/.miasma/run/node.lock (single-instance lock file)~/.cache/mesa_shader_cache/gl_cache.bin (Linux masquerade/identity file)~/Library/Application Support/com.apple.spotlight/index-v2.cache (macOS masquerade/identity file)%APPDATA%\Microsoft\CryptnetUrlCache\Content\msrt.dat (Windows masquerade/identity file)
Credentials
~/.aws/credentials (AWS access keys)~/.npmrc (npm authentication token)~/.ssh/ (SSH private keys)Browser saved passwords and cookies (Chrome, Brave, Firefox, Edge)macOS Keychain (system and login keychains)Cryptocurrency wallet filesGitHub personal access tokens and session cookiesCI/CD environment variables and repository secrets
Embedded keys
Attacker EC public key: 0432fa4ba871877d94081fe83323fa24dfa1491e9de8725cbab7b734de9e9be3b233ef6742fd6264437c9532223d687b05fa540b70af6a516b8539af84d0eeb48eEthereum C2 contract: 0x12c37A86a0Ed0beBe5d1d6a43E42f07860eAc710Ethereum backup contract: 0x1969ab05d67b67fdcaa26240f738ccb077e1cd84Ethereum deployer wallet: 0x92d4C5413e4F7B258a114964101F9e1C6d64C6BaHKDF master key material label: rt-vault-master-key-32b-aaaaaaaaBaked config label: rt-baked-key
Hashes
SHA1:a7e18d96efd3cdb127ef4cdcad9e3ad26c482bf2 (generator-helpers utils.js)SHA1:c70e105e212ff3c1daa04bb2a62507717f296b0b (specs index.js)SHA1:c8cb3f6d5b90c46686d2bf531dc1a5786e27edc5 (sync.js, generator-family build)SHA256:6e78713b75bd34828d49896176627f7face7aa9036cd874f2e02d9f23a9a9c71 (generator-helpers utils.js)SHA256:8351d251cf0b5a0bd82242deaa0a14e3e1394418d55c0f4259dac4303b79fc0c (specs index.js, both versions)SHA1:22bf76fe317ea6769bd38619bd440e42d119bd6b (generator validator.js)SHA1:9890950adcbc2478e7a080234f053214adbad44e (generator-components ErrorHandling.js)SHA256:34014776d3d3ff11bc4439b02fd7ac0f02a887eb3a052eeafff236e2f6db8ad1 (@asyncapi/generator-helpers@1.1.1 tarball)SHA256:9b2e65db653ca8575c9b10eefb9a80c6006404812c2ec212bf5675e3c690233b (@asyncapi/specs@6.11.2 tarball)SHA256:d425e4583cc6185d41e95c45eda00550045a5d1919b9a012236a4520d009dbd7 (@asyncapi/specs@6.11.2-alpha.1 tarball)SHA256:bfaeb987faa6de2b5a5eb63b1233d055215b09b0349a9394f2175fd7cdf385e4 (@asyncapi/generator@3.3.1 tarball)SHA256:082d733db0687dcd768104972b065d4b58cb1e6043688c6c20fa3702337f36ab (@asyncapi/generator-components@0.7.1 tarball)SHA1:2f226e51b7536db6fbc633af430ec02b9c9ed8a4 (sync.js, specs build)SHA256:24b9ee242f21a73b55f7bb3297eafb33c60840907386b542ed79fc6b72365168 (sync.js, generator-family build)SHA256:e9544a648d8fbaccd01b8477cf68471d48b89bf93eeacbdf5bba20fd296ff7b5 (sync.js, specs build)
Affected Versions
@asyncapi/generator-helpers@1.1.1@asyncapi/generator-components@0.7.1@asyncapi/generator@3.3.1@asyncapi/specs@6.11.2@asyncapi/specs@6.11.2-alpha.1
MITRE ATT&CK
ID | Technique | Why it applies |
|---|---|---|
| Supply Chain Compromise: Compromise Software Dependencies and Development Tools | A stolen asyncapi-bot PAT allowed the attacker to publish trojanized packages through the official AsyncAPI npm release pipeline. |
| Command and Scripting Interpreter: JavaScript | The entire multi-stage chain executes as Node.js JavaScript, from the inline loader to the 92,000-line Stage 3 framework, which also exposes a ShellExec command that runs attacker-supplied strings via |
| Obfuscated Files or Information: Software Packing | The payload uses javascript-obfuscator with a custom base64 alphabet plus whitespace padding to defeat visual and static analysis. |
| Ingress Tool Transfer | Stage 1 downloads the 8.25 MB sync.js from two IPFS CIDs using HTTPS, writing it to a platform-specific directory before execution. |
| Create or Modify System Process: Systemd Service | The decrypted campaign config enables persist: true and references miasma-monitor.service for Linux user-level systemd persistence. |
| Event Triggered Execution: Unix Shell Configuration Modification | On macOS the framework appends a nohup launch block to ~/.zshrc, ~/.bashrc, or ~/.bash_profile. |
| Boot or Logon Autostart Execution: Registry Run Keys / Startup Folder | On Windows the framework writes an HKCU Run value named miasma-monitor. |
| Unsecured Credentials: Credentials In Files | The Stage 3 framework contains a harvester for ~/.aws/credentials, ~/.npmrc, ~/.ssh/ keys, and browser credential stores, but it is disabled in this build (recon toggle set to false). |
| Application Layer Protocol: Web Protocols | Primary C2 uses HTTP to 85.137.53[.]71:8080 and 8081; fallback channels include Nostr WebSocket relays and an Ethereum smart contract beacon. |

Real World Attacks

How Ossprey uncovered a 300+ package DPRK Contagious-Interview campaign

Valentino Duval
2 Jul 2026
Real World Attacks

Miasma Shai Hulud targets leoplatform on NPM

Ossprey Research Team
25 Jun 2026
Real World Attacks

DPRK Contagious Interview: NPMJS chalk-ultra and vitest-cli

Ossprey Research Team
23 Jun 2026
