Volver a todas las publicaciones
Publicado el · por Renaud Deraison

El gusano se volvió código abierto

En algún momento de la semana del 11 de mayo de 2026, las personas detrás de Shai-Hulud — el gusano autorreplicante de la cadena de suministro de npm que viene devorando cuentas de mantenedores desde septiembre de 2025 — filtraron su propio código fuente. Para el fin de semana, OX Security había encontrado cuatro paquetes typosquatted de npm publicados desde una sola cuenta, uno de los cuales es una copia casi literal del gusano filtrado, otro es un bot DDoS en Golang, y los otros dos son simples infostealers que envían claves SSH y billeteras cripto a C2 de saldo. El piso para crear forks de ataques a la cadena de suministro acaba de bajar muchísimo, y las personas con más probabilidad de instalar uno de estos paquetes ya no son humanas.

En algún momento de la semana pasada, las personas detrás de Shai-Hulud — el gusano de npm que viene masticando cuentas de mantenedores desde septiembre de 2025 — filtraron su propio código fuente. Para el fin de semana, una cuenta de npm llamada deadcode09284814 había publicado cuatro typosquats reutilizando ese código. Uno era el gusano, casi literal. Uno era un bot DDoS en Golang. Dos eran simples infostealers que envían por POST tus claves SSH, tu ~/.aws/credentials y tu bóveda de MetaMask a una IP alquilada. Dos mil seiscientas setenta y ocho instalaciones después, la pregunta ya no es "¿alguien va a convertir la filtración en arma?". La pregunta es cuál de los agentes que están escribiendo npm install en tu terminal esta tarde se topa con uno de ellos.

Hay algo que pasa, periódicamente, en el software ofensivo, donde una herramienta cerrada termina volcada en un foro y la población de personas capaces de ejecutarla pasa de "la única banda que la escribió" a "cualquier adolescente con un VPS". Mimikatz. EternalBlue. El código fuente de Conti. Cada vez, la capacidad no cambió — simplemente dejó de ser escasa. El titular de la semana pasada, reportado por BleepingComputer sobre la base de la investigación de OX Security, es que Shai-Hulud — el gusano sobre el que escribimos cuando se comió cuarenta y dos paquetes @tanstack en seis minutos — se sumó a esa lista.

El fork no es teórico. Para el domingo, OX había documentado cuatro paquetes maliciosos publicados desde una sola cuenta de npm llamada deadcode09284814:

  • chalk-tempalte — un typosquat de chalk-template, que lleva una copia casi literal del código fuente filtrado de Shai-Hulud, incluyendo el comportamiento característico de Shai-Hulud de subir credenciales robadas como repositorios de GitHub públicos autogenerados. La lectura de OX: "el código del malware Shai-Hulud es una copia casi exacta del código fuente filtrado, sin técnicas de ofuscación", lo que sugiere un nuevo actor de amenazas que no se molestó siquiera en limar los números de serie.
  • axois-utils — un typosquat de axios-utils, que envía un payload en Golang al que OX llama Phantom Bot: floods HTTP, TCP, UDP y de reset, persistencia a través de la carpeta de Inicio de Windows y tareas programadas, y un C2 en b94b6bcfa27554.lhr.life. Tu máquina de desarrollo, reclutada.
  • @deadcode09284814/axios-util — un typosquat distinto, un payload distinto: claves SSH, variables de entorno, credenciales de AWS/GCP/Azure, enviadas a 80.200.28.28:2222. "Bastante directo", en palabras de OX.
  • color-style-utils — un infostealer simple que captura tu IP, geolocalización y datos de billeteras cripto y los envía por POST a edcf8b03c84634.lhr.life.

Descargas semanales combinadas al momento del informe de OX: 2.678.

Hay dos historias enredadas aquí que merecen ser desenredadas. La aburrida es que npm tiene typosquats. Los tiene; siempre los ha tenido; siempre los tendrá, por la misma razón por la que hay perros llamados Bench en el parque para perros: los nombres son baratos y el espacio de nombres es plano. La interesante es que la barrera para ejecutar un gusano de la clase de Shai-Hulud acaba de caer. Hasta la semana pasada, hacía falta el utillaje de la banda original, la infraestructura de la banda original, la disciplina de la banda original para no ser atrapados. Hoy, hace falta un clon de GitHub de un repo público, un túnel lhr.life y la paciencia para teclear npm publish. Los cuatro paquetes que encontró OX son, colectivamente, la prueba.

Por qué el número de actores importa más que el número de paquetes.

Un único gusano sofisticado es, en cierto sentido, un adversario tratable. Tiene tics. Tiene infraestructura. Tiene hábitos. Se pueden escribir firmas de detección contra él. Aikido, Socket, Snyk, Wiz — las tiendas de monitoreo de la cadena de suministro de npm que se lanzaron sobre el incidente de @tanstack del 11 de mayo — lo atraparon en cuestión de horas, precisamente porque venían vigilando a la misma familia durante ocho meses.

Una familia de gusanos derivativos escritos por personas que descargaron el código desde un sitio de pegado es una forma distinta. Cada uno exfiltrará hacia un C2 diferente, incrustará una clave RSA diferente, elegirá una combinación diferente de archivos para leer, y elegirá un espacio distinto de typosquats donde vivir. Algunos serán cuidadosos; la mayoría serán chapuceros de un modo que los hará caer rápido; uno de ellos, la próxima vez que escribamos un post como este, será lo suficientemente sofisticado como para que no lo atrapemos en una semana. El problema de detección de los defensores se ensancha de "detectar a Shai-Hulud" a "detectar cualquier cosa que quiera leer ~/.ssh/id_ed25519 desde dentro de un script prepare". Eso es una superficie mucho, mucho más grande.

La forma del camino de instalación también cambió, y esta es la parte que debería preocupar a cualquiera que esté ejecutando un agente de programación. Un desarrollador humano que quisiera chalk-template habría, en 2024, leído el nombre del paquete en un tutorial, lo habría tecleado y se habría dado cuenta del typo cuando chalk-tempalte apareciera con doscientas descargas y un desconocido como publicador. Un agente de programación al que se le pide "agregar algo de color a la salida de mi CLI" en 2026 va a instalar lo que sea que el gestor de paquetes le devuelva. El agente no ve el campo del publicador. El agente no nota que el README tiene tres líneas. El agente está haciendo treinta npm install esta hora porque el usuario está haciendo el trabajo de un pequeño equipo y al agente le pagan por tarea, no por instalación.

Qué hace en realidad un clon de Shai-Hulud sin ofuscación.

El paquete chalk-tempalte está, escribe OX, "casi sin ningún cambio" respecto del código fuente filtrado. Eso significa que aplica la misma mecánica que recorrimos en El gusano que se escribe a sí mismo en .claude, con un nuevo matiz significativo: el canal de exfiltración es el propio GitHub.

El truco característico de Shai-Hulud — preservado por el imitador porque copiar es más fácil que reescribir — es que las credenciales robadas no van a un servidor de recolección oculto. Van a un repositorio público de GitHub recién creado, publicado usando un token de GitHub que el malware acaba de robar a la víctima. Los secretos de la víctima quedan en un repo público, propiedad de la propia cuenta de GitHub de la víctima, a la vista de cualquiera en el mundo para que los rastree, hasta que alguien se da cuenta y el repo es borrado. El manual estándar del defensor — bloquear el dominio de exfiltración, buscar DNS saliente inusual — no atrapa esto, porque el DNS saliente es api.github.com, con el que tu máquina de desarrollo habla doscientas veces por hora de todas formas. El paquete de credenciales sale de tu portátil disfrazado de git push.

Una vez que los secretos son públicos, cualquiera que mire la manguera de GitHub — y varias personas miran la manguera de GitHub para exactamente esta razón — puede recogerlos. El gusano no necesita mantener vivo su C2. GitHub es el C2.

UNA CUENTA npm — deadcode09284814 — CUATRO PAYLOADSUSUARIO npmdeadcode09284814 registrado: 2026-05 paquetes: 4 DL semanales: 2.678PUBLICADOSchalk-tempalte ↳ typo: chalk-templateaxois-utils ↳ typo: axios-utils@…/axios-util ↳ con scope similarcolor-style-utils ↳ nombre genéricolos cuatro llevan un postinstallo un script preparechalk-tempalte → CLON DE SHAI-HULUDlee: ~/.npmrc, ~/.config/gh, $GH_TOKEN, ~/.aws, ~/.sshcrea: repo público de GitHub bajo la cuenta de la víctimacommitea: secretos, cifrados con pubkey RSA embebidaaxois-utils → PHANTOM BOT (DDoS en Golang)persistencia: carpeta de Inicio de Windows + tarea programadacapacidad: flood HTTP/TCP/UDP, ataques de reset TCP@…/axios-util → INFOSTEALER DIRECTOlee: claves SSH, variables de entorno, credenciales AWS/GCP/Azureenvía por POST: paquete en crudo, sin capa de cifradocolor-style-utils → ROBA-BILLETERASlee: IP, geolocalización, billeteras de extensiones de navegadorapunta a: bóvedas en disco de MetaMask, Phantom, KeplrA DÓNDE VA EL BOTÍNchalk-tempalte: api.github.com (token de la víctima) nuevo repo PÚBLICO en cuenta víctima + 87e0bbc636999b.lhr.lifeaxois-utils: b94b6bcfa27554.lhr.life bot en Golang, recibe órdenes@…/axios-util: 80.200.28.28:2222 TCP en crudo, sin TLScolor-style-utils: edcf8b03c84634.lhr.life POST HTTPStres túneles .lhr.life distintos,una IP pelada, un api.github.com:bloquear-el-dominio no alcanza.
A dónde llevan los secretos que leen los cuatro paquetes `deadcode09284814`. chalk-tempalte usa el propio token de GitHub robado a la víctima para publicar un nuevo repositorio público con el botín; el C2 en 87e0bbc636999b.lhr.life es incidental. axois-utils enrola al host en una botnet DDoS de Golang (Phantom Bot) con persistencia vía la carpeta de Inicio de Windows y tareas programadas, recibiendo órdenes desde b94b6bcfa27554.lhr.life. Los otros dos envían por POST un paquete de credenciales a una IP alquilada y a un host tunelizado, respectivamente. Los cuatro paquetes ejecutan su payload desde dentro de `prepare` o post-install, lo que significa que se ejecutan en el momento en que el agente teclea `npm install`, no en el momento en que el usuario lee el diff.

Hay un detalle diminuto en esa imagen sobre el que vale la pena detenerse. El clon de Shai-Hulud, chalk-tempalte, ni siquiera depende de su propia infraestructura para recibir el botín. Usa la propia identidad de GitHub de la víctima para publicar los propios secretos robados de la víctima en un repositorio público de GitHub. El C2 en lhr.life es un respaldo. El canal primario es git push. Para un defensor que mira el egreso, esto es indistinguible del CI normal del desarrollador. Para GitHub, es — hasta que alguien marque el repo — un proyecto público legítimo propiedad de un usuario real. La exfiltración queda lavada a través de la identidad de la víctima.

Lo que el agente está haciendo mientras vos hacés scroll.

Si tenés un agente de programación en tu terminal — Claude Code, Cursor's CLI, Codex CLI, Aider, el que prefieras — hay una probabilidad no nula de que, en el tiempo que tardaste en leer el párrafo anterior, el agente haya corrido un npm install en tu nombre. Quizá dos. Los agentes de programación no se detienen a admirar árboles de dependencias. La razón completa por la que compraste uno es que no se detiene.

Los paquetes que atrapó OX son typosquats, una categoría de error específicamente apta para la velocidad de máquina. Un humano que tiene que tipear chalk-template va a poner las letras en el orden correcto porque lo ha hecho cien veces. Un modelo que ha ingerido cada post de Stack Overflow del planeta ha visto chalk-template y chalk-tempalte en el mismo corpus de entrenamiento — este último típicamente dentro de una captura de pantalla del error de otra persona — y ante un prompt como "agregá salida con color a mi CLI", a veces va a emitir el typo literalmente. El agente no se inmuta. El gestor de paquetes no se inmuta. El script prepare se ejecuta.

Este no es un modo de fallo hipotético. Es el modo de fallo para el que la familia Shai-Hulud fue diseñada. El gusano original se propagaba robando tokens de mantenedores y usándolos para republicar más versiones comprometidas de paquetes legítimamente populares. Los imitadores no tienen todavía los tokens de los mantenedores; lo que tienen es el espacio de nombres de typosquats, en el cual los agentes son singularmente buenos para caer.

Dónde encaja Bromure en esta historia.

Bromure Agentic Coding es la configuración en la que el agente de programación corre dentro de una VM Linux desechable por tarea, con la carpeta del proyecto montada, el egreso intermediado y las credenciales retenidas del lado del host macOS del hipervisor. Recorrimos la arquitectura en detalle en el análisis de Bitwarden CLI y en el análisis de @tanstack. Lo que sigue es lo que específicamente le pasa a cada uno de estos cuatro paquetes dentro de ese límite.

VM BROMURE POR TAREA — lo que ven los cuatro payloadsAGENTE DE PROGRAMACIÓN$ claude> agregá color a mi clitool: bashnpm i chalk-tempalte↳ corre el postinstall↳ dentro del guest↳ la traza lo registraSISTEMA DE ARCHIVOS DEL GUEST — stubs y ausencias~/.aws/credentialsaws_secret = stub-aws-…~/.npmrc$GH_TOKENghp_stub_…~/.ssh/id_ed25519No existe el archivo o directoriobóvedas de MetaMask / Phantom / Keplrno instaladasperfil del navegadorsin navegador host en el guest/etc/cron.d, carpeta Startupen disco desechableRESULTADOS DE LOS PAYLOADSchalk-tempalte: lee stubs, intenta api.github.com → 401axois-utils: el bot escribe en Startup del guest → borrado al resetcolor-style-utils: no hay billeterasHIPERVISOR — bróker de credenciales + proxy de egresoHOST macOS — los secretos reales y el navegador real, nunca cruzaron el límiteBÓVEDA REAL DE CREDENCIALESmacOS Keychainid_ed25519 (privada)~/.aws/credentialsAKIA… real~/.config/gh/hosts.ymlghp_real (con scope a esta tarea)~/.npmrcnpm_… token real de publicación~/Library/.../MetaMask/vault.json (billetera real)perfil de Chromium del hosttu navegador realnada de lo anterior es alcanzable desde el guestPROXY DE EGRESO — solo whitelistedgit push → api.github.com/tu/repo stub ghp_… ⇒ ghp_… real (whitelisted)POST api.github.com/user/repos (CREATE) no whitelisted ⇒ 401 al guestPOST 87e0bbc636999b.lhr.life no whitelisted ⇒ bloqueado, la traza lo registraPOST 80.200.28.28:2222 no whitelisted ⇒ bloqueado, la traza lo registraRadio de explosión = una VM efímera + lo que ya estaba en la carpeta del proyecto montada. Reset, y la siguiente instalación parte desde cero.
El mismo npm install de uno de los typosquats de deadcode09284814, pero el agente está corriendo dentro de una VM Bromure por tarea. chalk-tempalte lee ~/.aws, ~/.npmrc, ~/.ssh dentro del guest y encuentra stubs y archivos faltantes. Después intenta usar $GH_TOKEN para crear un repo público — el token stub devuelve 401 en el proxy de egreso, porque el proxy solo intercambia por el token real para endpoints incluidos en la whitelist que la tarea pidió, y 'crear-un-nuevo-repo-público' no está en esa lista. axois-utils se enrola en el C2 de Phantom Bot; el binario de persistencia en Golang se escribe a sí mismo en el directorio Startup del guest, que vive en el disco desechable. color-style-utils busca una bóveda de MetaMask que no está instalada. El radio de explosión es una VM efímera, más lo que ya estaba en la carpeta del proyecto, punto.

Recorré esto contra los cuatro paquetes, uno a uno, porque los detalles específicos son donde la arquitectura se gana el sueldo.

chalk-tempalte busca $GH_TOKEN.

La jugada característica del clon de Shai-Hulud es tomar el token de GitHub del desarrollador y usarlo para crear un repositorio público en la propia cuenta del desarrollador. Dentro de una VM Bromure, el $GH_TOKEN que lee es un stub — una cadena sintácticamente válida que empieza con ghp_ y existe exactamente por esta razón. La primera acción del runner es POST /user/repos contra api.github.com. El proxy de egreso del lado host reconoce api.github.com como endpoint whitelisted, pero solo para las operaciones que la tarea actual realmente pidió — git push al repo en el que la tarea está trabajando, gh pr create contra ese mismo repo, gh api repos/that/repo/issues. "Crear un nuevo repositorio público en la cuenta del usuario" no está en esa lista, porque el usuario no lo pidió. El proxy se niega a sustituir el token real, y el stub sale como stub. GitHub devuelve 401. El canal de exfiltración del gusano — el canal ingenioso, el diseñado para esquivar el filtrado DNS de egreso — nunca se abre.

El canal de respaldo, el túnel lhr.life en 87e0bbc636999b.lhr.life, tampoco está whitelisted. La traza registra el intento. Los bytes no salen.

axois-utils instala Phantom Bot para persistencia.

El bot en Golang intenta escribirse a sí mismo en la carpeta de Inicio de Windows y crear una tarea programada. La VM Bromure es un guest Linux, así que la persistencia específica de Windows es, gratis, un no-op. En una variante Linux del mismo payload — que alguien va a publicar en breve — el bot se escribiría a sí mismo en /etc/cron.d/ o ~/.config/systemd/user/. Ambas rutas están dentro del disco desechable copy-on-write del guest. El próximo bromure reset, o el final natural de la tarea actual, descarta el disco. La persistencia desaparece sin necesidad de cazarla.

Mientras tanto, la conexión saliente del bot a b94b6bcfa27554.lhr.life no está en la whitelist de egreso de la tarea, porque ninguna tarea legítima de programación habla con un túnel lhr.life recién registrado. El bot llama a casa hacia un socket cerrado. La traza de la sesión registra el intento — útil mañana por la mañana cuando se publique una lista de IOC.

@deadcode09284814/axios-util envía credenciales en crudo por POST.

El más simple de los cuatro payloads es también el que menos tiene para agarrar. El runner lee el ~/.ssh, ~/.aws, las variables de entorno del guest, y los envía por POST a 80.200.28.28:2222. El directorio SSH está vacío. El archivo AWS es un stub. Las variables de entorno son o stubs o no están definidas. La IP de destino no está whitelisted. O la conexión es bloqueada en el proxy, o sale del host llevando un payload de placeholders. Cualquiera de los dos resultados está bien.

color-style-utils busca billeteras que no están ahí.

El ladrón de cripto es el paquete cuyo modelo de amenaza más claramente asume que el propio navegador del desarrollador está en la misma máquina. Lee rutas como ~/Library/Application Support/Google/Chrome/Default/Local Extension Settings/<MetaMask-id>/ y los equivalentes para Phantom y Keplr. Ninguna de esas rutas existe en la VM Bromure. La VM no tiene tu perfil de Chrome adentro. La VM no tiene una extensión de billetera instalada. La VM es, por diseño, el navegador principal de nadie. El runner encuentra un directorio vacío y sigue de largo.

Esta es la parte que no es una historia sobre credenciales que viven en el host. Las billeteras viven en el host porque, en una portátil normal, el navegador del desarrollador y el agente de programación del desarrollador comparten un sistema de archivos. Bromure no hace más fuerte a la billetera; la vuelve inalcanzable desde el lugar donde corre el gusano. El gusano no puede leer lo que no está en su disco.

Lo que sigue doliendo.

Hay rincones de esta historia donde la VM por tarea de Bromure no es una solución, y merecen ser nombrados en voz alta.

La carpeta del proyecto está montada.

Los archivos que el gusano escribe dentro de la carpeta del proyecto — incluida la persistencia estilo .claude/router_runtime.js que cubrimos en el post de @tanstack — son duraderos entre resets de tarea, porque ese es justamente el punto de montar la carpeta del proyecto. La defensa ahí no es la VM. Es git status y un vistazo de cinco segundos al diff antes de hacer push. La traza facilita identificar qué sesiones agregaron archivos inesperados.

La whitelist de egreso es estrecha a propósito.

El bróker de credenciales de Bromure funciona porque la whitelist es estrecha. Si pones en la whitelist npm publish para tu propio scope porque hoy estás publicando un release, y resulta que hoy instalás uno de estos cuatro paquetes, el gusano va a publicar bajo tu scope. Whitelisteá lo que la tarea necesita. Ni un byte más.

Un token bueno para esta tarea sigue siendo un token real.

El token stub de GitHub se intercambia por uno real en el cable para las operaciones que la tarea whitelisted. Si chalk-tempalte lograra convencer al agente de hacer un git push al propio repo del proyecto, ese push iría con un token real. El límite protege las credenciales. No revisa el diff. Leé el diff.

La detección está aguas abajo de la traza.

La traza de la sesión registra cada comando de shell, escritura de archivo y petición saliente. Por sí sola, no clasifica 87e0bbc636999b.lhr.life como malo. Registra que la petición se hizo. Cuando OX publique una lista fresca de IOC mañana por la mañana, tu búsqueda toma dos segundos. Ese es el valor que aporta la traza — no magia, solo recibos.

Una última cosa.

La filtración no es la noticia. La noticia es lo que la filtración vuelve probable en el próximo año, que es una cantidad de pequeños forks medio competentes de un gusano que, incluso en su forma competente, el ecosistema npm apenas atrapó a tiempo. Algunos de los forks van a ser lo suficientemente ruidosos como para obtener un writeup. La mayoría va a quedarse en el registro durante una semana, juntar un par de miles de npm install y desaparecer cuando alguien finalmente presente un reporte de abuso. Las dos mil seiscientas setenta y ocho instalaciones que OX contó en los paquetes deadcode09284814 no son un caso atípico. Son el promedio.

La pregunta honesta no es "¿va a esquivar mi equipo cada typosquat envenenado de npm?". Los agentes tipean rápido. Los nombres son baratos. La pregunta honesta es: cuando el agente instale uno — y a lo largo del próximo año, en un equipo que usa agentes, lo va a hacer — ¿el script de post-install encuentra las manos del desarrollador, o encuentra una caja Linux desechable con stubs en los archivos de credenciales y un proxy que no sabe quién es?

Bromure Agentic Coding es lo segundo. Es gratis, código abierto y enviado hoy. El árbol de forks va a empeorar antes de mejorar.