TL; DR
Un grupo de cinco paquetes npm, publicado en dos cuentas, envió un postinstall gancho que lee las credenciales de la nube del host y las envía fuera del dispositivo. Los paquetes llevan nombres fantasma y piratas. coral-wraith, ecto-corsair-whisper-6f3b9, ecto-corsair-flag-x9m4, ecto-rust-read-f3a9c1, ecto-nightly-spirit — y serializar todo lo que recolecten en una falsificación ecto_module: Manifiesto YAML antes de transmitirlo. Hacemos un seguimiento del clúster como Ectoplasma.
La carga útil solo se ejecuta cuando detecta un entorno específico: un host cuyo nombre es un cadena hexadecimal de 12 caracteres y un directorio de trabajo en /app/node_modules — la forma de una compilación en contenedores o un trabajador de CI. Cuando esa puerta pasa, el gancho consulta el Servicio de metadatos de instancias de AWS (IMDSv2) para credenciales de rol de IAM, enumera Administrador de AWS Secretos en tres regiones, vuelca las variables de entorno, lee archivos en /appy rastrea cadenas de captura de bandera. Luego exfiltra el resultado de dos maneras: una baliza a una webhook.site recolector y un manifiesto PUT a un punto final de IP sin procesar, con una lista de alternativas de localhost-first.
Las descripciones de paquetes posteriores indican "Carga útil CTF para pruebas de la cadena de suministro de verdaccio". Presentamos esta autodescripción como un hecho observable. El comportamiento en sí —salida en tiempo real a una IP pública, lecturas reales de credenciales IMDS, llamadas reales a Secretos Manager— es lo que es, independientemente de la etiqueta, y es la razón por la que estas versiones fueron clasificadas como maliciosas.
Un nombre en el grupo, coral-wraith, no se detuvo en un solo lanzamiento. Se republicó en rápida sucesión a través de docenas de versiones en cuestión de horas — 1.0.0 escalando a 6.0.0 — y una edición anterior del mismo nombre había utilizado cifras infladas. 9999.0.x números de versión, la forma clásica de un intento de confusión de dependenciaDurante ese proceso, la carga útil maduró visiblemente: pasó de ser una baliza de enumeración de host de un solo uso a un pivote completo de credenciales de AWS envuelto en comprobaciones de entorno que lo mantienen silencioso fuera de su objetivo previsto.
| Paquetes | 5 nombres; coral-wraith sola republicada en docenas de versiones |
| Ecosistema | npm |
| Instalar vector | postinstall script del ciclo de vida |
| Objetivo principal | Credenciales de rol de AWS IAM + Valores de Secretos Manager, variables de entorno, /app archivos |
| exfilar | webhook.site baliza + IP sin procesar C2 PUT |
| Puerta de activación | Nombre de host hexadecimal de 12 bits + /app/node_modules cwd, más una comprobación de entorno que suprime la carga útil fuera de ese contexto. |
| Gravedad | high — divulgación de credenciales en la nube y Secreto administrado de entornos de compilación y ejecución en contenedores |
Anatomía de ataque
Cada paquete en el clúster se construye de la misma manera: un casi vacío index.js (módulo.exports = {}), una línea package.json guion - “postinstall”: “node postinstall.js” — y la carga útil en postinstall.js. Basta con instalar el paquete para ejecutar el hook; no se requiere ninguna importación ni llamada.
La puerta de puntería. Antes de hacer nada, la carga útil de la familia ecto comprueba su entorno:
function isAppWorker(): host = os.hostname() if host does NOT match /^[0-9a-f]{12}$/ -> exit if cwd does NOT contain "/app/node_modules" -> exit if cwd contains "/tmp/npm-safe" -> exit otherwise -> proceed Un nombre de host hexadecimal de 12 caracteres es la forma predeterminada que Docker asigna a un contenedor, y /app/node_modules es una ruta de instalación convencional dentro de un contenedor. La tercera cláusula se cancela si la ruta parece un directorio de extracción aislado. El efecto neto es que la carga útil permanece inactiva en una computadora portátil de desarrollador o un entorno aislado de análisis y solo se activa dentro de un trabajador de compilación o tiempo de ejecución en contenedores, el tipo de entorno con más probabilidades de contener credenciales de nube en vivo. El paquete más antiguo en el clúster, espectro coralino, no tiene tal puerta y ejecuta su colección (más simple) incondicionalmente.
ColecciónCuando la puerta pasa, el gancho sale disparado. ejecutarFileSync(“/bin/sh”, [“-c”,…]) y ejecuta un único comando compuesto que, en orden:
1. PUT /latest/api/token to 169.254.169.254 (IMDSv2 token request) 2. GET .../iam/security-credentials/ (IAM role name) 3. GET .../iam/security-credentials/<role> (temporary credentials) 4. dump env | sort (environment variables) 5. list /app (excl. node_modules) + cat first 15 (application files) 6. aws Secretosmanager list-Secretos (us-east-1, eu-west-1, eu-central-1) 7. scrape readable files for HTB{...} (capture-the-flag strings) Los pasos 1 a 3 son una recuperación IMDSv2 de libro de texto: solicitar un token de sesión y luego adjuntarlo como el X-aws-ec2-metadata-token encabezado para obtener el rol de IAM de la instancia y las claves de acceso temporales de ese rol. La elección de implementar IMDSv2 en lugar del IMDSv1 no autenticado más simple. Cabe destacar que esto significa que la carga útil funciona incluso en instancias configuradas para requerir acceso a metadatos basado en tokens, que es el endurecimiento recomendado por AWS. Las credenciales devueltas en el paso 3 tienen una vida útil corta. ID de clave de acceso/Clave de acceso secreto/Token Tripletas con alcance limitado al rol de la instancia; todo lo que ese rol puede hacer, el poseedor de esas claves puede hacerlo durante la vigencia de la credencial.
Los pasos 4 a 6 amplían la toma. env El volcado captura todo lo que el proceso de compilación o de tiempo de ejecución haya heredado; en la práctica, aquí es donde suelen residir los tokens de registro, las cadenas de conexión a la base de datos y las claves de API. / aplicación El recorrido de archivos lee hasta quince archivos de aplicación externos. nodo_módulos, que puede configuración de superficie, .env archivos o fuente. Llamadas del paso 6 Lista de secretos de AWS Secretosmanager en tres regiones; las credenciales obtenidas en los pasos 1 a 3 son exactamente lo que autentica esas llamadas, por lo que la lectura de IMDS y la enumeración de Secretos Manager se unen en una única escalada: rol de instancia → inventario de Managed-Secreto. El paso 7 es un guiño al marco de captura de la bandera: cuando un HTB{…} Si se encuentra una bandera, se envía por separado; de lo contrario, el bloque recopilado en bruto se divide en cuatro partes y se envía.
Las versiones posteriores en el clúster llevan la escalada más allá. En lugar de detenerse en un inventario, analizan la respuesta IMDS y exportan las claves temporales como AWS_ACCESS_KEY_ID / CLAVE DE ACCESO SECRETA DE AWS / TOKEN DE SESIÓN DE AWS variables de entorno, confirme la identidad con aws sts obtener-identidad-de-llamaday luego iterar sobre cada Secreto devuelto por lista-Secretos llamar aws Secretosmanager obtener-valor-secreto en cada uno — recuperando el contenido de Secreto, no solo sus nombres. Las mismas versiones también leen binarios de bandera sustituidos por procesos (/readflag y amigos) y un intento de recorrido de carga contra cualquier proyecto Rust encontrado en / aplicación, ampliando la recopilación más allá de las credenciales en la nube a todo lo que exponga el entorno de compilación.
Estas versiones posteriores también se restringen de forma más agresiva. Además del nombre de host hexadecimal de 12 bits y /app/node_modules Durante las comprobaciones, la carga útil inspecciona la configuración del registro de paquetes activo y la ruta del directorio de trabajo, y finaliza silenciosamente cuando indican un contexto de análisis o de réplica en lugar de un objetivo real. El resultado es una carga útil que no realiza ninguna acción observable en la mayoría de los entornos de inspección y ejecuta su recopilación completa únicamente cuando se considera que se encuentra en un host de contenedores genuino.
exfiltraciónLos datos recopilados salen del host por dos canales. Primero, una baliza. PUBLICAR a un fijo webhook.sitio recolector, que contiene el nombre de host, UID numérico, directorio de trabajo y hasta 120 KB de datos recopilados. En segundo lugar, los datos se pliegan en un "manifiesto de módulo" YAML falso y PUT a /api/modules/ en un servidor de destino:
ecto_module: name: "<flag-or-chunk-0>" version: "1.0.0" power_level: "<chunk-1>" ship_deck: "<chunk-2>" cargo_hold: "<chunk-3>" Los nombres de los campos del manifiesto (nivel_de_potencia, cubierta del barco, bodega de carga) son decoración: los datos robados viajan dentro de los valores de cadena, razón por la cual un monitor de red ve lo que parece una carga benigna del manifiesto del registro de paquetes en lugar de un volcado de datos obvio. El canal de baliza transporta más: el PUBLICAR cuerpo a webhook.sitio Incluye el nombre de host, el UID numérico, el directorio de trabajo y hasta 120 KB del blob recopilado, por lo que incluso una sola baliza exitosa proporciona la información completa. webhook.sitio Es un servicio gratuito de inspección de solicitudes; al usarlo como recolector, el operador nunca tiene que instalar su propia infraestructura de recepción para ese canal, y las solicitudes registradas permanecen en la papelera del servicio.
El manifiesto PUT recorre una lista de reserva que comienza con varios 127.0.0.1/localhost puertos y luego cae a tres direcciones públicas en el `154.57.164.0/24` El rango se detiene en el primer punto final que responde con un estado 2xx. El orden de prioridad localhost es coherente con la autodescripción de "pruebas de verdaccio" (un registro local en loopback), pero las alternativas de IP pública implican que los datos salen del host siempre que loopback no esté escuchando, es decir, en cualquier máquina que no sea el propio equipo de prueba del autor.
Cronograma
El clúster muestra un crecimiento incremental de la capacidad en lugar de una caída puntual. Lo ordenamos según el comportamiento observado, no según el tiempo transcurrido publicado:
| Fase | Paquetes / Versiones | Comportamiento |
|---|---|---|
| Carrera temprana | coral-wraith 9999.0.x | Números de versión inflados, consistentes con un intento de confusión de dependencias; enumeración y exfiltración en tiempo de instalación. |
| Semilla | coral-wraith 1.0.0 | postinstall recopila archivos id/env/flag; PUT único a 154[.]57[.]164[.]71:30782, marcador ECT-472839 |
| Iteración rápida | coral-wraith 1.0.1 → 6.0.0 | Docenas de lanzamientos en horas; la carga útil gana la isAppWorker() puerta, extracción de credenciales IMDSv2, el completo get-Secreto-value Pivote, comprobación del entorno de registro/ruta y marcadores de doble destino |
| Nombres paralelos | ecto-corsair-whisper-6f3b9 1.0.14–1.0.18 | Misma carga útil controlada; webhook.site Lista de respaldo para balizas y puntos finales múltiples |
| variantes | ecto-rust-read-f3a9c1 1.0.1–1.0.2 | Añade marcadores adicionales para el fregadero. ECT-987654, ECT-654321, ECT-839201 |
| variantes | ecto-corsair-flag-x9m4 1.0.0, ecto-nightly-spirit 1.1.0 | Misma carga útil controlada, mismo C2 y baliza. |
El rasgo definitorio del clúster es su cadencia de publicación: en lugar de un paquete y una versión, el mismo nombre se vuelve a publicar una y otra vez en rápida sucesión, cada lanzamiento es una pequeña variación del anterior, junto con un puñado de hermanos con nombres diferentes que llevan la misma carga útil. Dentro del susurro La familia del código se dividió en dos huellas digitales cercanas: un conjunto que activa dos detecciones críticas, otro tres (un sumidero de lectura de archivos adicional), pero ambos se resuelven en la misma carga útil; la diferencia es una deriva del código, no una bifurcación de comportamiento. Versiones de susurro fuera del rango analizado (hasta al menos 1.0.25 en el momento de escribir esto) se observaron en vivo en el registro, y el espectro coralino El nombre continuó ascendiendo por su propia escalera de versiones sobre la misma ventana.
Indicadores de compromiso
Todos los indicadores que aparecen a continuación se extrajeron del código fuente del paquete en el disco. Los indicadores de red se han desactivado.
Network
| Indicador | Rol |
|---|---|
hxxp://154[.]57[.]164[.]71:30782 | Objetivo C2 PUT (coral-wraith) |
hxxp://154[.]57[.]164[.]80:30543 | C2 PUT de reserva (ecto-*) |
hxxp://154[.]57[.]164[.]82:31250 | C2 PUT de reserva (ecto-*) |
hxxp://154[.]57[.]164[.]71:31289 | C2 PUT de reserva (ecto-*) |
hxxps://webhook[.]site/602a4c72-7033-4e28-92ea-dc66e59206e5 | Recolector de balizas |
169[.]254[.]169[.]254/latest/... | Lectura de credenciales IMDSv2 (lado del destino, metadatos de AWS) |
Comportamiento/archivo
| Indicador | Rol |
|---|---|
"postinstall": "node postinstall.js" | Instalar vector |
ecto_module: YAML con power_level / ship_deck / cargo_hold claves | Esquema de manifiesto de Exfil |
Marcadores de fregadero ECT-472839, ECT-987654, ECT-654321, ECT-839201 | Segmento de ruta C2 /api/modules/<marker> |
isAppWorker() puerta: anfitrión /^[0-9a-f]{12}$/, cwd contiene /app/node_modules | Condición de activación |
aws Secretosmanager list-Secretos más del us-east-1, eu-west-1, eu-central-1 | enumeración de secretos |
HTB{...} extracción de expresiones regulares | Cosecha de captura de bandera |
Hashes de archivos (sha256, capturados en el momento del análisis)
| Archivo | sha256 |
|---|---|
coral-wraith/postinstall.js | ce5ff035cfdfed1d0015446424b352c27b66bcb77e9fdb0a51e4245199146824 |
ecto-corsair-whisper-6f3b9 1.0.18/postinstall.js | b58432acba376aa6976f0490d9a1c04257ccdbc856d8390260c50322d63e31c3 |
Atribución y comportamiento observado
Los cinco nombres de paquetes se publicaron bajo dos identificadores de cuenta de npm, pero comparten suficiente infraestructura como para tratarlos como un solo clúster: el mismo ecto_module esquema manifiesto, el mismo ECT-472839 marcador de fregadero principal, el mismo webhook.sitio ID del recolector y puntos finales C2 en el mismo Bloque 154.57.164.0/24`. El paquete semilla (`coral-wraith`), más simple y sin compuertas) y la ectofamilia con compuertas se lee así como iteraciones de un conjunto de herramientas en lugar de esfuerzos independientes.
Los paquetes se describen a sí mismos, en versiones posteriores, como “Carga útil CTF para pruebas de la cadena de suministro de Verdaccio.” Presentamos esa etiqueta como un hecho observable y no la reformulamos como un hallazgo sobre el propósito. Lo que hace el código es inequívoco e independiente de cómo se etiquete: lee las credenciales de rol de IAM del servicio de metadatos de instancia, enumera los Secretos administrados en tres regiones de AWS y transmite los resultados a una IP pública y a un recolector de webhooks de terceros. Un entorno de prueba que solo utilice bucles de retorno no necesitaría la lista de reserva de IP pública, las lecturas de credenciales de IMDS ni las llamadas al Administrador de Secretos entre regiones. Dado que la salida y el alcance de las credenciales son reales, las versiones restringidas se clasificaron como maliciosas.
La restricción de acceso solo a contenedores es la característica más destacable desde el punto de vista operativo. Se trata, a la vez, de una medida de evasión —permaneciendo en silencio en portátiles y entornos de análisis— y de una medida de selección de objetivos, activándose únicamente donde es más probable que existan un rol IAM real y Secretos en funcionamiento. Los analistas que ejecuten estos paquetes en un entorno de análisis genérico no observarán nada; el comportamiento solo se manifiesta bajo un nombre de host de estilo Docker y una ruta de instalación dentro de un contenedor.
Impacto, tendencias y orientación para los defensores
La exposición aquí es la divulgación de credenciales en la nube y de Secreto dentro de los contenedores de compilación y ejecución. Una credencial de rol de IAM obtenida de IMDS conlleva todos los permisos que tenga ese rol; Secretosmanager:ListSecretos (y cualquier continuación) ObtenerValorSecretoEsto se extiende a la aplicación almacenada Secretos. Los volcados de variables de entorno suelen contener tokens de registro, URL de bases de datos y claves API. En un contexto de CI o contenedor —exactamente para lo que se selecciona la puerta— una sola instalación transitiva de uno de estos paquetes es suficiente para filtrar ese material.
Ectoplasm se ajusta a un patrón que seguimos observando: cargas útiles de instalación que acceden a metadatos en la nube y a Secretos gestionados, en lugar de a archivos locales, y que se limitan a ejecutarse únicamente en entornos de alto valor. A continuación, se presentan dos observaciones sobre medidas de seguridad.
- La forma es detectable. Un gancho de instalación de npm/PyPI cuyo gráfico de llamadas llega tanto a una API Secretos en la nube (Administrador de secretos de AWS, gcloud Secretos, az keyvault) o la dirección IMDS y un sumidero de salida de red es un patrón estrecho y de alta señal; casi nunca ocurre en un script de ciclo de vida legítimo. Análisis de flujo estático Se puede marcar sin depender de ningún dominio o IP específicos.
- El endurecimiento ambiental lo atenúa. Aplicar IMDSv2 con un límite de salto de 1 impide que las cargas de trabajo de los contenedores alcancen los metadatos de la instancia; limitar el alcance de los roles de IAM al mínimo privilegio limita el radio de explosión de cualquier credencial que se filtre; y ejecutar instalaciones con –ignorar-scripts En CI, se elimina por completo el vector install-hook para los paquetes que no lo necesitan.
Para los defensores, las comprobaciones prácticas son: alertar sobre conexiones salientes desde contenedores de compilación/CI a direcciones IP públicas no permitidas durante npm instalar; esté atento al acceso a IMDS que se origine desde scripts del ciclo de vida del paquete; y trate cualquier gancho de instalación que ejecute una interfaz de línea de comandos en la nube como sospechoso hasta que se demuestre lo contrario.
Dos notas adicionales específicas para este clúster. Primero, debido a que la activación está restringida a entornos de contenedores, un paquete que parece inerte cuando se verifica en una estación de trabajo aún puede estar activo en producción; la verificación debe reproducir las condiciones del nombre de host y la ruta del contenedor, o leer el código fuente directamente, en lugar de confiar en "Lo instalé y no pasó nada". Segundo, el uso de un servicio público de inspección de solicitudes como recolector de balizas significa que parte de los datos exfiltrados pueden recuperarse para la respuesta a incidentes: una organización que encuentre uno de estos paquetes en su árbol de dependencias puede razonar sobre lo que una baliza exitosa habría contenido a partir de la lógica de recolección de la carga útil, y debe rotar cualquier credencial de rol de IAM, tokens de registro y Secretos administrados que fueran accesibles desde el entorno de compilación o tiempo de ejecución afectado. Rotación de credencialesLa solución efectiva una vez que se ha ejecutado la instalación es la eliminación del paquete, no la desinstalación.





