Volver a todas las publicaciones
Publicado el · por Renaud Deraison

Pidió que lo ejecutaran

Sonatype rastrea 846 paquetes npm maliciosos publicados desde cientos de cuentas desechables con nombres generados por IA. Ninguno lleva script de instalación. El README te dice que cargues el paquete con require(), y el import ejecuta el loader. Las defensas que reconocen un paquete malo por su nombre, por su autor o por su hook declarado ahora compiten con un generador. Bromure Agentic Coding bloquea por la única propiedad que un generador no puede falsificar: la edad del paquete.

Los paquetes no llevan hook de instalación, así que npm no tiene nada que atrapar. El README te dice que los cargues con require(), y el import ejecuta el loader. Cada vez más, el lector que sigue esa instrucción es un agente de código.

Sonatype Research Labs reveló el 5 de agosto una campaña que llama Flooding Dropper, con 846 componentes npm maliciosos contados y la cuenta sigue subiendo. Paul McCarty, de OpenSourceMalware, rastrea la carga bajo el nombre WEL1DROPPER. La cobertura más amplia llegó el 7 de agosto.

Dos detalles la separan de las historias de cadena de suministro de npm del último mes.

El primero es cómo obtienen su nombre los paquetes. El atacante los genera, y hace del slopsquatting una línea de producción. Raíces recurrentes como bigops y bnpl se recombinan con otras palabras en nombres como bigops-api y dolyame-boxy-desktop-bnpl-card-gallery, muchos con un patrón de versión 35.x.y. La publicación se reparte entre cientos de cuentas desechables con un puñado de paquetes cada una, así que no hay ningún autor prolífico al que banear.

Sin script de instalación

Los paquetes no declaran ningún hook de ciclo de vida, así que npm install no ejecuta nada. El texto de McCarty lo dice sin rodeos: “Los paquetes NPM no usan un script preinstall ni postinstall. No lo necesitan.”

Traen un _helpers.js empaquetado que se ejecuta al importar el módulo, junto a un README que le dice al lector que lo importe. Cargue el paquete con require(), dice la documentación, y la documentación tiene razón. La primera etapa toma la huella del sistema operativo y de la arquitectura del procesador, descarga por HTTPS una segunda etapa nativa desde uno de tres hosts de Cloudflare Workers, y recurre a reensamblar el binario a partir de fragmentos base64 alojados en registros DNS TXT bajo wel1.ru cuando la descarga directa falla.

La segunda etapa varía según la plataforma. Linux recibe un ELF empaquetado con UPX que despliega un implante C2 Sliver. macOS recibe comprobaciones antidepuración, una sonda de VMware, un binario baliza y persistencia por LaunchAgent. Windows recibe parcheo de ETW y AMSI, detección de sandbox, ejecución reflexiva en memoria y persistencia mediante una clave Run del Registro y una tarea programada. McCarty y Sonatype leen la campaña como una evolución de “Moika”, la oleada de confusión de dependencias de abril que empujó unos 250 paquetes construidos en torno al robo del entorno.

El atacante movió el disparador del gestor de paquetes al lector.

Tres formas de ser irreconocibleel nombre está generadobigops-api, dolyame-boxy-desktop-bnpl-card-gallery, y ~844 másla deny list necesita el nombreel generador tiene más nombresel publicador es desechablecientos de cuentas, unos pocospaquetes en cada unabanear al autor banea cuatropaquetes de ochocientosno hay script de instalaciónsin preinstall, sin postinstallel README dice: haz require()_helpers.js corre al importarel disparador es el lectorel import ejecuta el loaderhuella de SO y arch, descarga etapa nativaCloudflare Workers, o DNS TXT bajo wel1.ru
La campaña está construida para derrotar al reconocimiento. El atacante genera los nombres en lugar de elegirlos, rota los publicadores por cientos de cuentas desechables y movió el disparador de ejecución del hook de ciclo de vida a una instrucción del README, sin dejar script de instalación que un escáner pueda encontrar ni nombre o autor estable al que una deny list pueda agarrarse.

La automatización le gana a la deny list

El propio resumen de Sonatype es la frase más afilada de toda la cobertura:

Léelo como una afirmación sobre las defensas. Toda capa que te protege reconociendo el paquete malo, ya sea por nombre, por cuenta de publicación o por hook declarado, ahora compite con un generador. Esas capas siguen atrapando lo que ya han visto. Se las califica por reconocer una cadena concreta, y el suministro de cadenas concretas cuesta lo que cuesta ejecutar un script.

El ángulo del agente lo afila. El slopsquatting sale a cuenta, para empezar, porque los modelos de lenguaje inventan nombres de paquete que suenan bien y no existen, y un atacante que registra esos nombres recoge tráfico de instalación gratis. Añade el hook de instalación que falta y tienes un paquete cuyo disparador es una instrucción escrita en un README. Un agente de código es el lector que alucinará el nombre, encontrará un paquete debajo, leerá la documentación y hará lo que la documentación dice. La campaña está moldeada para el agente por los dos extremos.

Elige una propiedad que el generador no pueda falsificar.

La edad no es un nombre

Cada paquete de esta campaña comparte una propiedad que ninguna automatización cambia: es nuevo. Un nombre que un modelo inventó el martes pasado no puede tener debajo un paquete de 2019. Cientos de cuentas desechables publican cientos de versiones recién salidas, porque una cuenta desechable no tiene historial desde el que publicar. El modelo depende de que el paquete esté ahí para cuando alguien lo busque, así que el atacante lo crea con unos pocos días de antelación como mucho.

Bromure Agentic Coding ejecuta el agente dentro de una VM Linux desechable en Apple Silicon y enruta cada byte de su tráfico de red por un proxy en el anfitrión, fuera de la caja en la que corre el agente. Las descargas de paquetes de npm, PyPI, Cargo, RubyGems, Maven, NuGet, módulos Go y Packagist cruzan todas ese proxy, y la primera política con la que se encuentran es un reloj.

La barrera de edad, activa por defecto

Bromure rechaza los paquetes más jóvenes que una edad mínima, fijada en dos días de fábrica. Las referencias flotantes como latest o un rango semver resuelven a la versión más reciente anterior al corte, así que las instalaciones normales siguen funcionando y aterrizan en la publicación de ayer. Una referencia fijada a una versión demasiado fresca recibe un 451 con un error claro de Bromure que nombra lo que ha pasado. Ochocientos paquetes cuestan lo mismo de parar que ocho, porque Bromure no reconoce ninguno.

Actúa antes de que empiece la discusión

La parte ingeniosa de esta campaña, soltar el hook de ciclo de vida y mover la ejecución a require(), describe lo que pasa después de que el tarball llegue al disco. La barrera de edad se sitúa en la descarga, en el proxy del anfitrión, aguas arriba de todo eso. Si la carga se habría ejecutado al instalar o al importar es una pregunta sobre un paquete que nunca llegó. El .npmrc y el pip.conf de dentro de la VM pueden endurecer esta política y no pueden relajarla.

La reputación, detrás del reloj

Con una clave de socket.dev, Bromure verifica cada paquete contra la base de datos de socket.dev antes de dejar pasar la descarga y bloquea los marcados como comprometidos, malware o typosquats. Las consultas OSV bloquean paquetes con vulnerabilidades conocidas a partir de una severidad que tú eliges. Delpi redirige directamente cada petición npm a un registro con filtrado. Estas capas reconocen cosas, lo que las hace útiles y las coloca en segundo lugar.

El README se puntúa por lo que es

“Carga esto con require()” es una instrucción escrita por un desconocido, que llega en texto que el agente lee. Bromure puntúa la salida de herramientas que el agente consume, incluidos contenidos de archivos, páginas web y documentación descargada, con un modelo local de inyección de prompts. Puntúa los archivos de instrucciones que el agente trata como autoridad, CLAUDE.md y AGENTS.md y sus variantes, con un clasificador dedicado más un escáner de cargas en Unicode invisible. Nada sale del Mac para hacerlo.

La carga perfila la máquina equivocada

Supongamos que la descarga pasa igualmente, porque el paquete envejeció más allá del corte o una entrada de allowlist lo exime. Sigue a WEL1DROPPER por un perfil Bromure y míralo hacer su trabajo contra una máquina que ibas a tirar.

El primer movimiento del loader es tomar la huella del anfitrión. En un perfil Bromure ese anfitrión es una VM Ubuntu tras NAT, a un hipervisor de macOS, así que toma la rama Linux y planta allí un implante Sliver. Nunca pide la rama macOS: la antidepuración, la sonda de VMware, la descarga de beacon_mac.bin, el LaunchAgent que habría sobrevivido a un reinicio, el interés por dominios rusos de pago. Todo eso es código para una máquina que el dropper nunca vio. Erase home descarta la capa en la que aterrizó y devuelve el directorio personal a su estado justo después del clonado.

Después la etapa infostealer va a buscar lo que buscaba su antepasado Moika: el entorno. En un perfil Bromure el entorno contiene placeholders brm_…. Ninguna de las claves reales de Anthropic, OpenAI y xAI, ninguno de los tokens de GitHub y GitLab, ningún material de AWS ni credencial de registro está en la memoria de la VM ni en sus dotfiles. Bromure los guarda en el anfitrión y los sustituye en la petición en el proxy, sobre el cable, para el único host de destino al que pertenece cada uno. El robo se ejecuta hasta el final y envía cadenas que solo resuelven en un Mac, que no es la máquina que ejecutó el código.

En el Mac de quien desarrollatoma la huella del anfitriónmacOS, arm64: toma la rama macbeacon_mac.bin, LaunchAgent, aguanta reinicioslee el entornoclaves de API, credenciales git, material cloudtodo real, todo utilizabley se quedala máquina que toma es la que tú usasDentro de un perfil Bromurela descarga topa antes con un relojpublicado hoy, corte de dos días: 451ningún nombre casado, ninguna firmay si alguna vez llegaLinux en una VM, rama mac sin usarel entorno solo tiene placeholders brm_y no se quedaErase home resetea la capa donde cayó
El dropper elige su carga según el anfitrión que encuentra. Dentro de un perfil Bromure ese anfitrión es una VM Linux desechable, así que nunca pide la rama macOS con su persistencia por LaunchAgent, su sonda de VMware y su binario baliza, y la etapa infostealer lee credenciales placeholder cuyos valores reales nunca entraron en la VM.

La caza que recomiendan, ya en marcha

McCarty cierra el texto sobre WEL1DROPPER con consejos defensivos: cazar consultas DNS TXT a wel1.ru, revisar los logs de proxy en busca de tráfico hacia los hosts de Cloudflare Workers, y buscar archivos temporales y LaunchAgents sueltos.

Lee otra vez el segundo punto. Tu portátil no guarda un log de proxy. Un perfil Bromure guarda uno por construcción: cada petición que hace la VM cruza el proxy del anfitrión, y el Security Log y el Trace Inspector registran el host, el estado, la latencia y el informe de sustitución de cada una. Tres destinos *.workers.dev desconocidos apareciendo durante una compilación son tres filas en una lista que puedes leer, en la máquina donde corrió la compilación, sin desplegar nada para obtenerlas.

Los registros seguirán retirando estos paquetes, y las retiradas funcionan: 846 de ellos han desaparecido o están desapareciendo. Pero la campaña ya pasa del millar, publicada desde cuentas que no cuestan nada crear bajo nombres que no cuestan nada inventar, y la siguiente oleada está a una ejecución de script de distancia. El reconocimiento es la competición equivocada.

Un paquete de dos días no merece una confianza profunda que le falte a uno de dos horas. Queda fuera de la ventana en la que opera esta economía, y puedes mover tu grafo de dependencias detrás de esa ventana con un ajuste que ya está activado.

Instala Bromure Agentic Coding, y deja que la descarga se encuentre con un reloj antes de encontrarse con tu máquina.