Ataque dirigido de confusión de dependencias de npm descubierto con las manos en la masa
Snyk Security Research Team
30 de abril de 2022
0 minutos de lecturaCronología de actualizaciones del ataque
10 de mayo de 2022: CodeWhite, una empresa de seguridad especializada en Red Team, se puso en contacto con nosotros por Twitter y asumió la responsabilidad por los paquetes maliciosos. Explicó que formaban parte de un ejercicio de simulación de ataques para sus clientes. ¡Felicitaciones por el elaborado ataque!
4 de mayo de 2022: DigitalOcean respondió que la IP de C2 pertenece a una empresa relacionada con la seguridad y que lo verificarían y nos avisarían.
1 de mayo de 2022: npm eliminó los paquetes maliciosos del registro.
La historia de un malware en npm
En los últimos años, hemos sido testigos de un aumento constante de paquetes maliciosos en distintos ecosistemas. En general, la gran mayoría de estos paquetes son inofensivos: recopilan información, pero no dañan la máquina infectada. Sin embargo, de vez en cuando encontramos un paquete verdaderamente malicioso que tiene un propósito, los medios y está listo para producción. Esta es la historia de uno de ellos.
Encontrar malware real en npm
Como parte del enfoque del equipo de investigación de seguridad de Snyk en la detección proactiva de paquetes maliciosos, nuestro principal objetivo es analizar y alertar sobre estos paquetes lo antes posible después de que aparezcan en el registro de un ecosistema. Para ayudarnos en esta tarea, creamos un sistema sólido con diversos mecanismos diseñados para detectar paquetes maliciosos.
En muy poco tiempo después de configurar nuestra trampa para paquetes maliciosos, ya se habían reportado muchas bibliotecas como maliciosas. Sin embargo, al investigar los hallazgos, descubrimos que muchos de los reportes eran sobre paquetes «ligeramente maliciosos».
Según nuestra definición, un paquete es «ligeramente malicioso» cuando hace una de las siguientes cosas:
Exfiltra información de la máquina mediante consultas DNS (pero no realiza ninguna otra acción)
Usa mineros de criptomonedas (lo cual es malo, pero no es tan interesante desde el punto de vista de la actividad maliciosa)
Son paquetes «ligeramente maliciosos» de otros investigadores, creados principalmente para hacer pruebas
Otras variantes de los elementos de esta lista
Finalmente encontramos un paquete muy interesante — gxm-reference-web-auth-server — y lo marcamos como malicioso. Le echamos un vistazo rápido y notamos algo diferente, así que iniciamos una máquina virtual e investigamos el archivo tarball.
Como el reporte era sobre un paquete de npm, lo primero que revisamos fue el archivo `package.json`. Como esperábamos, tenía un script post-install que ejecutaba un archivo JavaScript del paquete. Si no los conoces, los scripts post-install son una forma muy común de ejecutar scripts cuando npm termina de instalar las dependencias correspondientes, y también son una manera fácil para que los actores maliciosos ejecuten scripts en las máquinas de sus víctimas.
Al revisar el archivo ejecutado, vimos que estaba ofuscado. Sin embargo, la ofuscación no es más que una forma aparentemente sofisticada de «ocultar» el código, pero, en realidad, suele ser relativamente fácil revertirla (al menos en el mundo de JS).
El siguiente archivo del paquete que nos llamó la atención de inmediato estaba cifrado. Se nos encendieron las alarmas: era hora de investigar.
Ingeniería inversa de malware, parte 1: el envoltorio
Como mencionamos, había dos archivos adicionales en el paquete: uno ofuscado y otro cifrado:
Y el archivo package.json incluía un script post-install:
Aunque primero nos enfocamos en el script ejecutado, también notamos las dos dependencias con nombres sin sentido, ldtzstxwzpntxqn y lznfjbhurpjsqmr, que exploraremos más adelante en este artículo.
Al revisar el archivo confsettingsaaa.js, vimos lo siguiente:
Para entender este archivo, usamos un desofuscador que lo hiciera legible. Al revisar el código, vimos que el archivo (al que luego llamaremos P1, porque es la primera parte del malware) exfiltra información del sistema operativo y del paquete mediante consultas a subdominios falsos de pkgio[.]com:
Aunque este código solo exfiltra información y podría parecer inofensivo, estas exfiltraciones son valiosas para el reconocimiento del atacante, ya que le permiten identificar el objetivo y su configuración. Por cierto, los actores abusan de las consultas DNS porque estas suelen estar permitidas a través de firewalls y filtros de red. Así, pueden consultar sus propios servidores DNS y registrar las solicitudes.
Al continuar, encontramos aspectos más interesantes. Primero, todos los bloques try/catch ejecutaban una función `fail` si capturaban una excepción. Luego, vimos que el script usaba una de sus dependencias: lznfjbhurpjsqmr.
Antes de hablar de las dos dependencias, veamos primero la función (o mecanismo) fail. Cuando se invoca, hace dos cosas principales. Primero, limpia todo a su paso. La función «fail» elimina todos los archivos relacionados con el paquete y limpia:
Aunque este mecanismo es ingenioso para el malware, el siguiente paso fue alarmante.
Después de la limpieza, la función crea archivos señuelo package.json e index.js, y muestra este mensaje en la consola: “Please refer to the private registry instead of the public repo; Security Team”
Al leer esto, nos surgieron algunas preguntas: ¿Por qué el paquete malicioso no se elimina por completo? ¿Por qué finge ser un marcador de posición legítimo de un equipo de seguridad? ¿A qué organización apunta este paquete?
Aunque pudimos responder la mayoría de estas preguntas, a pesar de nuestros mejores esfuerzos por identificar y advertir al objetivo, todavía no podemos determinar a qué organización apunta. Con estas preguntas en mente, continuemos.
Las dependencias: ldtzstxwzpntxqn y lznfjbhurpjsqmr
Como se indicó arriba, el paquete instala dos dependencias: ldtzstxwzpntxqn y lznfjbhurpjsqmr. Al visitar sus páginas de npm, vimos que ambas las había publicado el mismo responsable de mantenimiento. Así se veía su página:

Fue interesante verlo y confirmó algunas de nuestras sospechas sobre los paquetes. Sin embargo, las dependencias no eran más que copias de paquetes legítimos:
Nombre del paquete | Paquete original | Propósito |
|---|---|---|
|
| Paquete que ofrece una API más sencilla para npm install (instala elementos mediante programación). |
|
| Requiere npm global como módulo local de Node. |
Como confirmamos más adelante al auditar el resto del código, estos paquetes se usaban tal como se esperaba usar los paquetes originales. ¿Por qué se tomaría el actor el trabajo de crear estos paquetes en vez de usar los originales? No lo sabemos y probablemente nunca lo sabremos.
Filtrado de víctimas, registros privados y transición a P2
Algunas notas rápidas…
En la siguiente sección, cuando escribamos «el código se detiene», significa que se invocó la función
faildescrita anteriormente para hacer la limpieza.Como no podemos determinar a qué organización apunta, nos referiremos a ella como
ORG.
El siguiente paso de P1 es obtener la configuración de autorización para los registros privados del archivo .npmrc (aquí se usa lznfjbhurpjsqmr). Si no encuentra estas configuraciones o el archivo (lo busca en toda la máquina), el código se detiene.
Si encuentra la información de autorización, intenta descargar del registro privado especificado en el archivo de configuración el mismo paquete (por nombre), probando algunas variantes, como estas:
Si no encuentra estos elementos en el registro privado o no logra descargarlos, el código se detiene. Si logra descargar el archivo tarball del registro privado, hace lo siguiente:
1. Ejecuta npm install para instalar el módulo en un subdirectorio llamado .documentation (la instalación se realiza mediante la dependencia ldtzstxwzpntxqn) o detiene la ejecución.
2. Reemplaza el contenido actual del paquete por el contenido del paquete que acaba de instalar, o detiene la ejecución.
3. Exfiltra el contenido de dos archivos relacionados con la red y el nuevo package.json mediante una solicitud POST:
4. Descifra y ejecuta el archivo cifrado en un nuevo proceso independiente (el algoritmo de cifrado, la clave y el IV estaban codificados en el código; hablaremos más de este archivo en la siguiente sección):
5. Limpia todos los archivos correspondientes (en este paso se limpian solo los archivos cifrados y el código de P1)
6. Finaliza.
Ahora que tenemos todo esto claro, podemos ver que, si el paquete gxm-reference-web-auth-server no está en el registro privado o este no es accesible, el malware simplemente lo omitirá.
Esto implica que:
El paquete apunta a una organización específica.
El actor detrás de estos paquetes sabe que este paquete existe en el registro privado de la organización.
Con esto, terminan todos los pasos de P1 y el «envoltorio del malware» finaliza.
Ingeniería inversa de malware, parte 2: el agente
Como el envoltorio tenía codificada la información para descifrar el archivo (suponemos que fue un error del adversario), también pudimos descifrarlo, y eso hicimos.
Al igual que el envoltorio, este archivo también estaba ofuscado y consistía en una sola línea de caracteres sin sentido. Sin embargo, esta vez al desofuscador le costó más trabajo y dejó el código lleno de acertijos:
Probamos algunos otros desofuscadores, pero ninguno produjo un mejor resultado. Nos quedamos con el resultado inicial y empezamos a desofuscar el código manualmente. Tras un poco de trabajo, logramos transformar el archivo en un formato legible. Era hora de investigar el funcionamiento interno del agente.
Registro del agente
Lo primero que se le indica al agente es que se registre con el servidor de comando y control (al que nos referiremos como CNC o C2). Al hacerlo, el agente recibe tres cadenas fundamentales que se usarán para la comunicación posterior: una clave y un IV para cifrar las cargas útiles, y un UUID.
Una vez que se completa esta solicitud, todas las comunicaciones posteriores entre el servidor C2 y el agente se cifran y descifran con estas claves y este IV (el algoritmo estaba codificado en el código). Inmediatamente después, el agente envía al servidor una solicitud POST con información sobre su entorno:
Una vez enviada esta solicitud, el agente pasa al siguiente paso.
El ciclo de ejecución
El ciclo de ejecución del agente es bastante sencillo. Incluye unas cuantas condiciones if-else para varios comandos y los ejecuta según las instrucciones del servidor C2. Por ejemplo, el agente puede eliminarse si recibe el comando delete o evaluar un fragmento de código si el C2 se lo envía:
En conjunto, el agente responde a los siguientes comandos:
Al revisar la lista, ten en cuenta que exec o eval pueden ejecutar un shell inverso que le daría al atacante control total sobre la máquina infectada. También ten en cuenta que la opción register, si se vuelve a invocar, permite que el agente y el servidor C2 intercambien su información de cifrado y generen información nueva, por si quieren cambiarla.
Aunque esta funcionalidad pueda parecer única o sofisticada, en el mundo de los agentes C2 es una función estándar de un agente que instalarías en la máquina de una víctima. En cualquier caso, no pudimos relacionar el código fuente del agente con ningún framework C2 conocido.
Conclusiones sobre el malware
Ahora que entendemos por completo el malware, podemos concluir lo siguiente:
El malware apunta a una sola empresa, aún desconocida. Sin embargo, según la información obtenida durante la ingeniería inversa, se espera que esta empresa tenga el paquete “gxm-reference-web-auth-server” en su registro privado.
Como el paquete se busca a sí mismo en el registro privado de la víctima, también podemos clasificar esto como un ataque de confusión de dependencias, un tipo de ataque a la cadena de suministro.
Es probable que los atacantes tuvieran información sobre la existencia de este paquete en el registro privado de la empresa.
En este punto, aunque entendíamos claramente cómo funcionaba el paquete, decidimos comprobar si el servidor C2 respondía y si se trataba de una campaña activa. Era hora de hacernos pasar por alguien más.
Jugar «Among Us» con un adversario
La idea para nuestro siguiente experimento era sencilla. Queríamos saber si había alguien al otro lado de la línea y, de ser así, averiguar si estaba activo. Para hacerlo, teníamos que:
Usar el agente sin el wrapper (P1 filtraría nuestro cliente por falta de credenciales de .npmrc, etc.)
Interceptar y registrar todo el tráfico HTTP/S (queríamos ver qué enviaba y recibía el servidor C2)
Transferirnos los datos registrados de forma unidireccional, irreversible e imposible de rastrear (esto es importante porque el atacante también puede acceder a la máquina)
Pero, antes de llevar a cabo estos pasos, queríamos recopilar información sobre el servidor.
¿Hay alguien ahí?
Recopilamos información sobre el servidor mediante análisis estándar de WHOIS y Nmap. Los resultados de WHOIS indicaron que el servidor está alojado en DigitalOcean (a quienes reportamos la IP del servidor), y los análisis de Nmap mostraron lo siguiente:
El servidor no es muy seguro y está activo y escuchando.
El impostor
Volviendo a nuestro plan, teníamos que preparar lo siguiente:
Código del agente: Lo obtuvimos durante la fase de descifrado y podemos ejecutarlo por separado del wrapper (P1).
Interceptores: Teníamos que implementarlos en el entorno NodeJS, ya que el agente usaba módulos HTTP/S de NodeJS.
Canalización de registro: Decidimos usar Pipedream porque permite gestionar las solicitudes HTTP/S con mucho detalle y ya incluye varias integraciones (como almacenamiento de valores, integración con Slack y más).
Para la interceptación, usamos la biblioteca @gr2m/http-recorder, que hacía exactamente lo que buscábamos: capturar y manipular por completo las solicitudes HTTP/S.
Al combinar los pasos 1 y 2, obtuvimos el siguiente fragmento:
Solo faltaba enviarlo. Con Pipedream, configuramos la función send para que enviara la carga útil a nuestro endpoint y pudiéramos procesarla.
Para procesar los datos, guardamos el IV y la clave para poder descifrar los mensajes. También integramos la canalización de Pipedream con un espacio de trabajo anónimo de Slack y configuramos el registro de los mensajes allí. Así, cada vez que el agente y el servidor C2 intercambiaban mensajes, recibíamos una notificación de Slack con toda la información descifrada y legible. A continuación, se muestra una ilustración de la configuración de nuestra máquina infectada:

Y aquí hay un ejemplo de los resultados en Slack:

Y así, nuestro pseudo honeypot estaba listo.
Hola, soy yo
Poco después de que nuestro agente falso se activara y se comunicara con el servidor C2, empezaron a llegar comandos. El primer comando fue un ls, seguido de un intento del actor de usar cat para leer todos los archivos visibles y explorar el sistema. Este es un ejemplo de uno de los comandos recibidos (tras decodificarlo desde base64):

En ese momento, decidimos detener el experimento, ya que habíamos logrado nuestro objetivo: determinar si había alguien al otro lado.
Esto implica que hay una campaña activa contra los propietarios del “gxm-reference-web-auth-server” original/privado.
Pero espera, hay más
Justo antes de concluir el experimento, notamos algo interesante. Aparte de la dirección C2, había pocos valores codificados de forma fija; uno de ellos era el valor de la propiedad “engine” en la llamada inicial a /register:
Como ya no nos preocupaba pasar desapercibidos, decidimos probar distintos valores para ver si había otros tipos de objetivos. Al cabo de un tiempo, encontramos más tipos de engine:
Esto implica que hay al menos un agente de navegador y uno de Golang, y no es descabellado suponer que hay más.
Divulgación y próximos pasos
Aunque conocemos el funcionamiento interno de este malware, no podemos saber qué organización o empresa es el objetivo. Por eso, queremos usar esta plataforma (junto con Twitter, etc.) para pedirle a la comunidad que advierta sobre este paquete y se mantenga alerta. También pueden contactarnos (o enviarnos un mensaje directo a @snyksec) si tienen preguntas o inquietudes al respecto.
También nos comunicamos con DigitalOcean para pedirles que retiraran el servidor C2 de su servicio, y con npm para informarles sobre los paquetes maliciosos y pedirles que los retiraran.
Dicho esto, aquí termina nuestro recorrido. Los ataques son complejos y sofisticados, pero queremos señalar que el vector de infiltración inicial de este ataque es un simple conflicto de dependencias de paquetes, que es fácil de mitigar. Para ver el flujo de trabajo completo de este ataque, consulta la imagen de abajo. ¡Cuídate y mantente a salvo!

Protege tus dependencias de código abierto
Las herramientas de Snyk, diseñadas para desarrolladores, generan pull requests de corrección con un clic para dependencias vulnerables de código abierto y sus dependencias transitivas.
