QuietPolyfill, un dropper de NPM que resucitó

QuietPolyfill: Un dropper de NPM que resucitó

TL; DR

Veinte paquetes npm publicados durante dos días a finales de julio y principios de agosto de 2026 se presentan como bibliotecas de componentes de interfaz de usuario internas y utilidades de backend compartidas. Ninguno de ellos implementa la funcionalidad que anuncian. Cada uno incluye una clase stub, un README que describe el paquete como interno y un archivo llamado _polyfill.js.

Lo interesante es desde dónde se invoca ese archivo. No es un gancho de instalación — los manifiestos no declaran ninguna preinstalación, instalación o postinstalación. En cambio, la última línea de index.js dice lo siguiente: try { require("./_polyfill"); } catch (_) {}. La carga útil se ejecuta la primera vez que el código de la aplicación importa el paquete. Instalación con --ignore-scripts, standard consejos para reducir la exposición de la cadena de suministro en el momento de la instalación, no lo impide.

Lo que _polyfill.js Lo que hace es recuperar un ejecutable específico de la plataforma de uno de los tres hosts de Cloudflare Workers, escribirlo en un directorio temporal con un nombre aleatorio, marcarlo como ejecutable y lanzarlo separado del proceso padre. Ese ejecutable es en sí mismo un cargador: enumera los productos de seguridad de endpoints por nombre de proceso, verifica el nombre de usuario actual con las cuentas de entorno de análisis conocidas, parchea las interfaces de telemetría AMSI y ETW en memoria, instala persistencia con el nombre de un componente de audio de Windows y recupera un tercer binario de varios megabytes de tamaño. La tercera etapa llega cifrada y es descifrado y ejecutado en memoria en lugar de escribirse en disco.

El 1 de agosto, aproximadamente cuatro horas después de que se confirmara que los paquetes eran maliciosos tras una revisión manual, diecinueve de los veinte nombres fueron eliminados de npm por su propio editor en una secuencia de veinticinco segundos. El 2 de agosto, uno de esos nombres fue registrado nuevamente, por una cuenta diferente, con el mismo código. La infraestructura de reparto daba servicio a las tres fases en el momento de redactar este texto.

Paquetes20 nombres de npm, ~74 versiones publicadas
Temas de nomenclaturabeaver-ui-* (13) accounts-* (3), más afisha-storybook-default, bcore-bravo-eslint-config, arbocrate-sla-prober-arbocrate-sla-prober-core, a.poltoradnev-package-c
DesencadenarImportación de módulos (require), no es un script de ciclo de vida de npm
Fase 1_polyfill.js — recuperación y lanzamiento
Fase 2Cargador con enlace estático, empaquetado en UPX en Linux; enumeración de seguridad de punto final, aplicación de parches AMSI/ETW, persistencia.
Fase 3beacon_linux.bin / beacon_mac.bin / update_win.exe, 4–13 MB; se entrega cifrado, se ejecuta en memoria
PlataformasLinux x64 y arm64, macOS universal, Windows x64
Publicadores Dos cuentas, un código fuente
Estado19 de 20 nombres no publicados por el operador; 1 republicado y en vivo; infraestructura de entrega en vivo

Anatomía de ataque

Etapa 1: el paquete

Todos los miembros envían los archivos con la misma estructura de tres partes. index.js exporta una clase con un en eso() que establece un valor booleano, un versión(), y un configurar() que fusiona un objeto de opciones. Nada más. README.md proporciona una línea de instalación, una exigir línea, y la frase “Paquete interno. Consulte CONTRIBUTING.md para obtener directrices” — CONTRIBUTING.md no está en el archivo tar.

_polyfill.js Tiene un tamaño ligeramente inferior a 5 KB y se lee como código de infraestructura ordinario. Comienza con tres comprobaciones del entorno:

if (process.env.DISABLE_TELEMETRY ||
    process.env.ANALYTICS_OPT_OUT ||
    process.env.DO_NOT_TRACK)
  return;

Esas variables son controles de privacidad convencionales. Aquí controlan si se descarga y ejecuta un binario. Una máquina que establece NO RASTREAR Se omite por completo.

A continuación viene un sello de ejecución única: un archivo en /tmp/.analytics_state, o estado_analítico bajo %TEMPERATURA% En Windows, cuyo tiempo de modificación se compara con un intervalo de 20,357 segundos. Dentro de ese intervalo, el módulo se ejecuta inmediatamente, por lo que las importaciones repetidas durante la compilación generan un único intento de recuperación en lugar de docenas.

Los hosts de destino no están presentes como literales. Se ensamblan en tiempo de ejecución a partir de fragmentos:

["oob-worker.cf102-","baf.workers.d","ev"].join("") 

Se crean tres hosts de esta manera y se reorganizan antes de su uso, por lo que el orden varía en cada ejecución. La recuperación es una solicitud HTTPS GET fijada a IPv4 con una User-Agent of node-fetch/2.6 — un valor plausible que un proceso de Node puede emitir. Una respuesta inferior a 1,000 bytes se descarta y se intenta con el siguiente host.

Si los tres fallan, el módulo recurre a DNS. Emite una consulta TXT para do. para obtener el recuento de fragmentos, luego realiza solicitudes 0. atravesar norte. En lotes de diez, concatena las respuestas y decodifica el resultado en base64. Esta ruta transmite el mismo ejecutable a través de registros DNS TXT, lo que permite su uso en entornos donde el tráfico HTTP saliente está filtrado, pero la resolución DNS no.

Los bytes recuperados se escriben en /var/tmp/.cache_<8-hex> en Unix, o %TEMP%\dotnet_diag_<8-hex>.exe en Windows: un nombre de archivo que se lee como un artefacto de diagnóstico de .NET. En Unix, al archivo se le da el modo 0755. El lanzamiento es desovar con separado: verdadero, stdio: “ignorar”, y anular referencia(), por lo que el hijo sobrevive al proceso Node que lo inició; en Windows la invocación agrega windowsHide: verdadero.

Dos detalles completan el cuadro. Un ayudante definido como const _l = (…a) => process.stderr.write(“”) se llama en cada paso de la recuperación: escribe una cadena vacía y descarta sus argumentos, ocupando la posición donde se ubicaría el registro. Y cada paquete envía un archivo de 81 KB en lib/telemetría.js cuyo comentario de encabezado describe un “SDK de análisis” con “transporte HTTPS con rotación de punto final y reintentos” y “descubrimiento de servicios basado en DNS para conmutación por error”. En la versión actual, nada lo requiere.

Etapa 2: el cargador

El binario para Linux x64 es un archivo ELF enlazado estáticamente y comprimido con UPX 3.96. La versión para macOS es un binario universal compatible con x86_64 y arm64; la versión para Windows es un ejecutable de consola PE32+ optimizado. Los cuatro binarios se descargaron y analizaron estáticamente; ninguno se ejecutó.

Las importaciones y la tabla de cadenas de la compilación de Windows describen claramente su comportamiento. Resuelve IsDebuggerPresent, Comprobar presencia del depurador remoto, y Proceso de información de NtQuery, y lee SYSTEM\CurrentControlSet\Services\Disk\Enum — una ruta de registro cuyos valores identifican los controladores de disco virtual. Llama Obtener espacio libre en discoExW y Estado de memoria globalEx, en consonancia con la comprobación de si los recursos del host coinciden con una instancia de análisis de corta duración.

Contiene una lista de nombres de usuario: Usuario actual, Administrador de TI, hong lee, CAJA DE PRUEBA, hapubws, mil onzas, timmy, Admin, el malware, muestra, virus, entorno empresarialVarios de ellos son vestigios de entornos de análisis documentados públicamente.

También contiene una lista de productos de seguridad de endpoints y los nombres de sus procesos, enumerados a través de CreateToolhelp32Snapshot y Proceso32PrimeraW/Proceso32SiguienteW:

  • halcón de ataque de multitud (csfalconservice, CSFalconContainer)
  • Carbon negro (cb.exe)
  • Cylance (cylancesvc.exe)
  • Bitdefender (bdagent.exe)
  • Kaspersky (avp.exe, kavfs.exe)
  • Trend Micro y Deep Security (coreServiceShell, ds_agent.exe)
  • FireEye/Trellix (xagt.exe)
  • Tanio (taniumclient.exe)
  • HitmanPro (hmpalert.exe)
  • Elástico (elastic-agent.exe, elastic-endpoint, filebeat.exe, winlogbeat.exe)
  • Wazuh y OSSEC (wazuh-agent.exe, ossec-agent.exe)
  • Check Point
  • windows Defender

Hay dos símbolos más presentes: Búfer de escaneo Amsi, el punto de entrada que Windows expone para el escaneo de contenido en memoria, y Escritura de evento Etw, la función mediante la cual el Seguimiento de eventos para Windows registra los eventos. Ambos son objetivos comunes para la modificación dentro del proceso que suprime la telemetría que esas interfaces producirían de otro modo.

La persistencia aparece dos veces en Windows. El binario se copia a %APPDATA%\Microsoft\Windows\Themes\SystemSoundsService.exey dos mecanismos hacen referencia a él: un valor bajo SOFTWARE\Microsoft\Windows\VersiónActual\Ejecutary una tarea programada registrada como

schtasks /Create /F /SC ONLOGON
  /TN "\Microsoft\Windows\Multimedia\SystemSoundsService"
  /TR "\"%s\""
  /RL HIGHEST

La ruta de la tarea, el nombre de la tarea y el nombre del archivo ejecutable son idénticos a los de un componente de audio original de Windows. /RL MÁS ALTO Solicita el nivel de privilegio más alto disponible.

La compilación de macOS sigue el mismo patrón con nombres apropiados para la plataforma: escribe una lista de propiedades en ~/Library/LaunchAgents/com.apple.windowserver.helper.plist y coloca su ejecutable en ~/.local/share/runtime/com.apple.runtimeComprueba /Aplicaciones/VMware Fusion.app y Biblioteca/Soporte de aplicaciones/Herramientas de VMware.

La capacidad de la red abarca tres protocolos de transporte: WININET.dll para HTTPS con un https://%s:%d%s cadena de formato, WS2_32.dll enchufes y DNSAPI.dll con Consulta DNS_A junto a un c.%s.%s cadena de formato: la misma forma de consulta de recuento de fragmentos que utiliza la primera etapa.

La descripción anterior es lo que muestran las importaciones, cadenas y rutas de registro del binario al inspeccionarlo. Un análisis público multi-motor de la misma muestra de Windows agrega detalles que una lectura estática de una tabla de importación no puede establecer por sí sola, y se atribuye aquí en lugar de repetirse como una observación de primera mano. Ese análisis informa que las referencias AMSI y ETW no solo se resuelven, sino que se parchean. Búfer de escaneo Amsi in amsi.dll y Escritura de evento Etw in ntdll.dll — que suprime el escaneo de contenido en memoria y el rastreo de eventos para el proceso. Informa que las comprobaciones anti-análisis se basan en el tiempo, además de Comprobar presencia del depurador remotoy identifica el Obtener contexto de hilo Llamada como una comprobación de punto de interrupción de hardware que lee los registros de depuración. También informa de un mutex de instancia única, Global\{7A3B9E1F-C2D4-4F8A-B5E6-D7A1F0C3E2B9}, y nombres Servicio de sonidos del sistema como valor de Run-key. Ese valor mutex no estaba entre las cadenas recuperadas aquí (el filtro de extracción utilizado excluía los tokens hexadecimales en mayúsculas entre llaves) y se registra en la tabla de indicadores basándose en ese análisis público en lugar de en la observación directa.

Fase 3

El cargador hace referencia a otro conjunto de rutas en los mismos hosts. Todas eran accesibles durante el análisis: beacon_linux.bin con 5.3 MB, beacon_mac.bin con 13.3 MB, actualizar_win.exe con 10.3 MB y brazo.bin con un tamaño de 4.3 MB. Estos archivos no se recuperaron y se desconoce su comportamiento.

Para la ruta de Windows, el análisis público del cargador describe cómo se maneja esa tercera etapa: se obtiene a través de WinINet, llega encriptado y se descifra en memoria y se ejecuta de forma reflexiva: se asigna memoria, se resuelven las importaciones, se procesan las reubicaciones y el control se pasa a un nuevo hilo a través de Crear hiloPor lo tanto, la tercera etapa nunca se escribe en el sistema de archivos, así que la detección basada en archivos y el análisis forense de discos posterior al incidente no tienen nada que recuperar de ella. Esto también significa que recuperar los bytes del host de entrega produciría texto cifrado, no un ejecutable analizable.

 

Cronograma

Todas las marcas de tiempo están en UTC y provienen de los metadatos del registro de npm, que registra la creación y la eliminación de paquetes independientemente de cualquier observador.

Cuando Lo que
2026-07-31 09:25:05 → 09:29:56 Se crearon diez nombres, cada uno comenzando en la versión 0.0.1con una separación aproximada de trece segundos.
2026-08-01 09:38 → 11:29 Los nombres existentes ascienden a través de números de versión inflados, 6.1.9 atravesar 6.1.12, 9.9.9 atravesar 9.9.12, 8.9.3 atravesar 8.9.6, 5.4.6 atravesar 5.4.9.
2026-08-01 13:04:14 → 13:04:18 Nueve más beaver-ui-* Nombres creados en una ráfaga de cuatro segundos, cada uno publicado directamente en una versión 12.x sin historial previo.
2026-08-01 ~13:00–14:30 Versiones clasificadas como maliciosas tras una revisión manual.
2026-08-01 17:39:49 → 17:40:14 Diecinueve de los veinte nombres no se publicaron en veinticinco segundos, de forma secuencial, con un intervalo de aproximadamente 1.2 segundos entre ellos.
2026-08-02 18:58:41 a.poltoradnev-package-c creado de nuevo bajo una cuenta de mantenedor diferente, versión 0.0.2.
2026-08-02 19:20:28 Versión 33.9.5 publicado con el mismo nombre.
2026-08-02 20:14 Los tres servidores de entrega atienden las etapas dos y tres; el sistema de reserva DNS responde con recuentos de fragmentos.

Merece la pena detenerse en dos de estas entradas.

La ráfaga de las 13:04 puso nueve paquetes en el registro en cuatro segundos, cada uno con una sola versión en los doce y no 0.0.1 Debajo de eso. Los números de versión de esta escala, en nombres que tienen un día de antigüedad, no describen un historial de lanzamientos.

La secuencia de anulación de publicación es igualmente mecánica: diecinueve nombres, veinticinco segundos, espaciado uniforme. npm registra estos eventos como anulaciones de publicación ordinarias, en lugar del marcador de posición que deja una eliminación iniciada por el registro, que asocia la acción con la cuenta de publicación en lugar de con el registro.

 

Indicadores de compromiso

Los hosts y las rutas están desactivados. No los resuelva ni los recupere de un sistema de producción.

Tipo Valor
Repartidor oob-worker[.]cf99-9b3[.]workers[.]dev
Repartidor oob-worker[.]cf102-baf[.]workers[.]dev
Repartidor oob-worker[.]cf103-070[.]workers[.]dev
Transporte DNS tin[.]dl[.]well1[.]site (Linux x64), tina[.] (arm64), ldr[.] (Mac OS), win[.] (Windows) — recuento de fragmentos en c.<domain>, datos en <n>.<domain>
Rutas de la etapa 2 /pkg/package, /pkg/package-arm64, /pkg/loader_mac, /pkg/package.exe
Rutas de la etapa 3 /pkg/beacon_linux.bin, /pkg/beacon_mac.bin, /pkg/update_win.exe, /pkg/arm.bin
Ruta de eliminación (Unix) /var/tmp/.cache_<8-hex>
Ruta de arrastre (Windows) %TEMP%\dotnet_diag_<8-hex>.exe
Persistencia (Windows) %APPDATA%\Microsoft\Windows\Themes\SystemSoundsService.exe; tarea \Microsoft\Windows\Multimedia\SystemSoundsService; Valor de clave de ejecución SOFTWARE\Microsoft\Windows\CurrentVersion\Run\SystemSoundsService
Mutex (Windows) Global\{7A3B9E1F-C2D4-4F8A-B5E6-D7A1F0C3E2B9}
Persistencia (macOS) ~/Library/LaunchAgents/com.apple.windowserver.helper.plist; binario en ~/.local/share/runtime/com.apple.runtime
Sello de una sola ejecución /tmp/.analytics_state, %TEMP%\analytics_state (ventana de 20,357 s)
Solicitar firma HTTPS GET, IPv4-fijado, User-Agent: node-fetch/2.6
Expediente de la etapa 1 _polyfill.js, SHA-256 392dc9ec85042ce1796bfa9b8c7bf6e05c7710579aab8b7b1eb73788683883af
Archivo sin referencia lib/telemetry.js, SHA-256 2266c9b8e199daebe31a9afcb61d9b0d76706b63c92312fc8223d22de42e63c7
Etapa 2 (Linux x64) SHA-256 7e486657f30594afda379b97030252a09a19fe8055e25c9e371544f59bd8e9e3
Etapa 2 (Linux arm64) SHA-256 aafc37589c0d21fb729761a1579ca3b278b95bce152aacfa29243ffd27f5eeb7
Etapa 2 (macOS) SHA-256 c214746c74cae8ece8bdaf69aa05da4db6ce013f9e77452d1eed1a002fd9ba00
Etapa 2 (Windows) SHA-256 d632c2c0c7801c3da57cf849fe4b4ca8351d9a7fe73c31050bf5dbcc96a16791

Nombres de paquetes

cajón de interfaz de usuario del castor botón de acciones de la interfaz de usuario del castor tarjeta de interfaz de usuario de castor grande selector de rango de fechas de la interfaz de usuario del castor formulario de interfaz de usuario del castor modal de formulario de interfaz de usuario del castor objeto de formulario de interfaz de usuario de castor cuadrícula de interfaz de usuario de castor encabezado de interfaz de usuario del castor castor-ui-hooks Icono de interfaz de usuario del castor bloqueado elementos de interfaz de usuario del castor con más diseño de interfaz de usuario del castor formulario final de cuentas estado de carga de cuentas cronograma de cuentas afisha-storybook-default bcore-bravo-eslint-config arbocrate-sla-prober-arbocrate-sla-prober-core a.poltoradnev-package-c

Atribución y comportamiento observado

Hay dos cuentas de npm involucradas. Los diecinueve nombres no publicados fueron publicados por una de las cuentas; a.poltoradnev-package-c Se volvió a registrar el 2 de agosto con un nombre de cuenta y un dominio de correo electrónico diferentes. El código es el mismo en ambos casos, y el segundo registro conserva los mismos servidores de entrega, los mismos dominios DNS de reserva, la misma estructura de ruta de destino y la misma marca de tiempo de ejecución única.

El par republicado también muestra cómo se ensambló. Versiones 0.0.2 y 33.9.5 son idénticos byte a excepción de la cadena de versión en package.json, y index.js enviado como 0.0.2 declara const VERSIÓN = “33.9.5”Ambos artefactos fueron cortados de un mismo árbol fuente y numerados posteriormente. No hay 0.0.2 historial de desarrollo para el 0.0.2 liberación para representar.

La numeración de versiones en todo el conjunto sigue la misma lógica. Los nombres de los paquetes en esta campaña se leen como cosas que existirían dentro de una organización en lugar de en un registro público: un conjunto de componentes de interfaz de usuario, una configuración de ESLint, un comprobador de SLA, un paquete que lleva el nombre de una persona. Cuando una compilación resuelve una dependencia por nombre y consulta un registro público junto con uno privado, el número de versión más alto es el que prevalece. Un nombre creado en 0.0.1 el lunes y llevando 12.9.6 El martes está previsto que se haga esa comparación.

Las dos oleadas de publicación también difieren de una manera que sugiere iteración. El código anterior nombró su archivo de carga útil configuración.js y enumeró cinco hosts de entrega; el archivo actual se llama _polyfill.js y enumera tres. La versión anterior eliminó el archivo descartado cinco segundos después del inicio, dejando el proceso ejecutándose desde un inodo eliminado; la versión actual no lo hace. El archivo sin referencia lib/telemetría.js permanece en el archivo comprimido en ambos lados, todavía se envía, ya no está cableado.

Una observación sobre los interruptores de apagado que vale la pena mencionar antes decisely, porque contradice una interpretación común. DESACTIVAR_TELEMETRÍA, ANÁLISIS_EXCLUIR_DE_LA_SISTEMA_DE_EXCLUSIÓN, y NO RASTREAR Se respetan y se comprueban antes de que ocurra cualquier otra cosa. Un investigador cuyo entorno de análisis configura cualquiera de ellos observa un paquete que no hace absolutamente nada.

Impacto y qué pueden hacer los defensores

La consecuencia práctica del desencadenante en el momento de la importación es limitada y merece la pena mencionarla por separado. npm install –ignore-scripts Esto impide la ejecución de scripts de ciclo de vida, y es un buen consejo. En este caso no tiene efecto, ya que este código no utiliza scripts de ciclo de vida. La carga útil se ejecuta cuando el código de la aplicación, una prueba, un empaquetador o un linter importan el módulo. En un ejecutor de CI que instala y compila inmediatamente, ambos procesos se realizan con segundos de diferencia.

Para la caza de amenazas, las señales duraderas son estructurales más que literales. Los hosts, el nombre del archivo y las rutas de descarga cambiarán; estas formas son más difíciles de variar:

  • Un punto de entrada de módulo cuya instrucción final es una try { require(“./x”); } catch (_) {} alrededor de un archivo que no forma parte de la API exportada. Un paquete con un polyfill genuino lo importa al principio y utiliza su resultado. Ignorar todos los errores de una importación al final del archivo no es un manejo de errores.
  • Cadenas de host ensambladas por .unirse("") a partir de una serie de fragmentos. Un código legítimo no tiene ninguna razón para dividir un nombre de host entre los elementos de una matriz.
  • Un paquete cuyas dependencias declaradas están vacías pero que llega a https, dns, y proceso_niño en el momento de la importación. La recuperación y ejecución en un paquete que anuncia un componente de interfaz de usuario es todo el hallazgo.
  • Los números de versión no coinciden con la antigüedad del paquete. Un nombre publicado por primera vez hace unos días en una versión principal de dos dígitos, sin lanzamientos intermedios, describe algo distinto a su desarrollo.
  • Escribe a / var / tmp or %TEMPERATURA% seguido por un chmod a las 07:55 y un engendro separado. La secuencia es corta, ordenada y rara vez resulta inofensiva en conjunto.

En los hosts de Windows hay dos comprobaciones baratas y de alta confianza. Los artefactos de persistencia son cadenas fijas y ninguno de ellos pertenece a un sistema limpio: una tarea programada en Servicio de sonidos del sistema de Microsoft Windows, un valor Run-key llamado Servicio de sonidos del sistemay un ejecutable de ese nombre bajo %APPDATA%\Microsoft\Windows\Themes\. El servicio de audio genuino de Windows no reside en ninguno de esos lugares. En segundo lugar, el cargador utiliza un mutex fijo de instancia única, Global\{7A3B9E1F-C2D4-4F8A-B5E6-D7A1F0C3E2B9}; su presencia en un host es un indicador, y debido a que el cargador trata un mutex existente como "ya en ejecución", precrearlo es una inoculación viable a corto plazo mientras se programa la remediación.

La detección en la tercera etapa es un problema más complejo y merece ser aclarada. Según los análisis públicos, el archivo se descifra y se almacena en memoria en lugar de escribirse en disco, por lo que el análisis de archivos y el análisis forense de disco no lo detectarán. Lo que sí se puede observar es la huella del propio cargador: las entradas de persistencia, el mutex y las solicitudes salientes a los servidores de entrega o a los dominios DNS-TXT.

Para entornos que resuelven dependencias tanto desde un registro privado como desde uno público, la mitigación no se ve afectada por nada de lo aquí expuesto: se definen los ámbitos de los nombres internos de forma que un nombre público no pueda satisfacerlos, y se fija la resolución al registro interno para esos ámbitos. El comportamiento de inflación de versiones descrito anteriormente solo funciona cuando una versión pública superior puede prevalecer sobre un nombre.

Finalmente, conviene leer detenidamente la información sobre la despublicación, en lugar de considerarla un final. Diecinueve nombres fueron eliminados del registro en veinticinco segundos y uno regresó un día después con una nueva cuenta, sin que la infraestructura de entrega se viera afectada en ningún momento. La eliminación de paquetes no implica la eliminación de la operación; los servidores continuaron prestando servicio en cada etapa mientras se redactaba este informe. Cualquier árbol de dependencias que haya resuelto alguno de estos nombres entre el 31 de julio y el 1 de agosto debe considerarse como si hubiera tenido la oportunidad de ejecutar la primera etapa, independientemente de si el nombre aún existe o no.

Referencias

sca-tools-software-herramientas-de-analisis-de-composicion
Priorice, solucione y proteja sus riesgos de software
Obtén tu cuenta gratuita.
Sin tarjeta de crédito.

Asegure el desarrollo y entrega de software

con la suite de productos Xygeni