4.837 cuentas de GitHub fueron utilizadas para probar el acceso de escritura a 61.764 repositorios y, en parte de ellos, instalar un ladrón de secretos en los flujos de integración continua. Aikido Security contabilizó ejecuciones en al menos 1.806 repositorios. La campaña, detectada desde el martes 10 de noviembre, aprovechó credenciales robadas y borró en muchos casos tanto los cambios maliciosos como los registros de ejecución.
El ataque no se limitó a repositorios propiedad de las cuentas comprometidas: las pruebas también alcanzaron proyectos accesibles mediante permisos de colaborador u organización. Y el alcance se mide de varias maneras. SafeDep, en un análisis independiente, encontró commits maliciosos en 8.093 repositorios públicos; WebPulse, a partir de una muestra de Aikido, recoge una estimación de aproximadamente 6.500 repositorios en los que pudo ejecutarse el payload. Son recuentos con métodos y coberturas distintos, no cifras intercambiables.
Una pull request para comprobar permisos
La actividad comenzaba con una pull request titulada docs: dependency audit notes, enviada desde una rama llamada wfx-docs. Su función era comprobar si la cuenta robada podía escribir en el repositorio. Aikido identificó solicitudes de prueba en 61.764 repositorios; algunas recibieron después el código malicioso.
La prueba por sí sola importa incluso cuando no hubo una segunda modificación. Aikido afirma que cualquier cuenta que abriera una de esas pull requests debe considerarse comprometida, aunque el ladrón no llegara a ejecutarse. En la mayoría de los repositorios probados no se empujó nada más, o la carga no funcionó por la configuración del proyecto o por los permisos disponibles.
De las pull requests presentadas con acceso de colaborador u organización, 10.431 llegaron a repositorios que no pertenecían a la cuenta atacada. El 92 % seguía abierto cuando Aikido hizo su comprobación. Eso amplía la búsqueda más allá de los proyectos propios de cada usuario: una cuenta podía utilizar sus permisos delegados para tantear otros repositorios.
La actividad maliciosa adoptó varias formas. En 7.309 repositorios del recuento de SafeDep, el código se añadió a un flujo de trabajo ya existente. En otros 771, llegó como un hook postinstall de npm, que también puede ejecutarse en la máquina de un desarrollador al instalar dependencias.
Una de las modificaciones añadía alrededor de 1.500 líneas a un flujo existente y aparecía con el mensaje de commit ci: refresh build cache. Otras pistas eran el mensaje chore: add ci helper, junto a un script .ci-xxxxxx.sh y un hook de instalación, o un nuevo flujo con nombre parecido a chore: add dependency audit workflow. También se observaron commits con mensajes como ci: restore y ci: cleanup, compatibles con la secuencia de introducción y retirada de cambios.
El cambio desaparecía; los secretos, no necesariamente
Después de introducir el ladrón, el atacante solía revertir el cambio en cuestión de minutos. Los registros del flujo y, a menudo, la propia ejecución también se eliminaban. Aikido comprobó que faltaba el registro en 156 de 159 repositorios de su muestra citados por WebPulse. Por eso, un historial de Actions aparentemente limpio no descarta que el código llegara a correr.
Una señal más útil puede ser un número de ejecución ausente alrededor del commit del atacante. Aikido señala que no encontrar rastros visibles como artefactos llamados wfx-loot, entradas de caché wfx- o resúmenes con WFX-BEGIN tampoco garantiza que no hubiera pérdida de datos.
Las cifras publicadas no describen exactamente el mismo universo. Aikido confirmó ejecución en al menos 1.806 repositorios. WebPulse recoge que la empresa examinó una muestra aleatoria de 1.500 repositorios probados y encontró ejecución en el 10,6 %, con un margen de 1,6 puntos; extrapolado, eso da alrededor de 6.500 repositorios. SafeDep, por su parte, contó 8.093 repositorios que recibieron commits maliciosos entre las 12:46 UTC del martes y las 04:36 UTC del día de su análisis. Ese recuento incluye repositorios donde el ladrón no se ejecutó y se limita a ramas predeterminadas de proyectos públicos, por lo que sus investigadores consideran que el total real es mayor.
La carga buscaba credenciales disponibles para el runner: secretos y variables de GitHub Actions, el token GITHUB_TOKEN, tokens OIDC y del entorno de ejecución de Actions, claves de servicios de IA y secretos presentes en la memoria de procesos. También leía archivos de credenciales de AWS, Azure, GCP, Kubernetes, npm, PyPI, SSH y otras herramientas en sus ubicaciones habituales.
Además, copiaba el repositorio, sus últimos 100 commits y artefactos recientes de los flujos de trabajo. En runners autogestionados podía recoger credenciales de nube mediante metadatos de instancia, así como información del estado de contenedores Docker y unidades de systemd. Aikido encontró opciones para operar en repositorios privados, aunque eso no demuestra por sí solo cuántos fueron afectados.
El acceso podía sobrevivir a la rotación de claves
El ladrón no se limitaba a recopilar secretos para enviarlos fuera. Según Aikido, podía intercambiar tokens OIDC de GitHub por sesiones de AWS, GCP y Azure; en AWS, también crear claves de acceso de larga duración para usuarios IAM. Algunas funciones estaban pensadas para mantener el acceso incluso después de cambiar credenciales.
En AWS, podía modificar la política de confianza de un rol para permitir el acceso desde cualquier cuenta de AWS. En Azure, podía añadir credenciales de identidad federada con el prefijo wfx-ci- a aplicaciones de Entra ID. Así, los flujos del repositorio comprometido podían autenticarse sin un secreto almacenado, incluso después de rotar los tokens habituales.
SafeDep y Aikido recomiendan buscar cambios no reconocidos en esos sistemas: credenciales federadas wfx-ci-* en Entra ID; sesiones o llamadas AssumeRoleWithWebIdentity con nombres wfx-* en CloudTrail, seguidas de CreateAccessKey o UpdateAssumeRolePolicy; llamadas GenerateAccessToken y SignJwt desde el pool de identidad de GitHub en GCP; y accesos JWT desde el emisor OIDC de GitHub en Vault.
SafeDep aconseja revocar los tokens del autor del commit, las concesiones de aplicaciones OAuth, las claves SSH y las sesiones; activar la autenticación de dos factores; y rotar todos los secretos y variables que el repositorio pudiera leer. Si alguien ejecutó npm install sobre un commit afectado, también debe tratar esa máquina como comprometida y cambiar las claves que contenga.
“we don't attribute November to the same actor at this stage”
— Aikido Security
La advertencia de Aikido se refiere a la relación con GhostAction, una campaña de octubre que también utilizó cuentas de GitHub robadas para introducir flujos de trabajo que sustraían secretos. Las cuentas comprometidas entonces tenían unas 1.000 veces más probabilidades que otras cuentas activas de volver a usarse esta semana. En más de 100 repositorios, el nuevo ladrón se añadió a flujos de trabajo de octubre y dejó activo el paso anterior que enviaba datos. Aikido no atribuye por ahora ambas oleadas al mismo actor: señala diferencias en infraestructura, herramientas y métodos, y que las credenciales robadas pueden acabar en manos de varios atacantes.
Paquetes afectados y señales que buscar
La campaña alcanzó al menos un paquete de Packagist y un módulo de Go mediante activadores de publicación automatizados. Al menos 50 de las cuentas comprometidas parecen publicar paquetes para npm, Go, Packagist, PyPI o crates. Aikido considera que el objetivo principal era robar secretos de CI, no propagarse a través de paquetes, pero identificó varias versiones que deben revisarse.
La versión 3.14.3 de digitalelvis/neuronai-studio, publicada el miércoles, es maliciosa. Aikido aconseja considerar comprometida cualquier máquina que la instalara o abriera, bloquear esa versión y fijar la dependencia en la 3.14.2 o anterior. Un composer install normal no ejecuta el ladrón; hacerlo requiere ejecutar npm install dentro del directorio vendor del paquete.
En el módulo de Go github.com/onuragtas/docker-env, las versiones v1.0.278 y v1.0.280 contienen el código en un archivo de flujo de GitHub Actions. El código Go y los binarios publicados no cambiaron, y las compilaciones de Go no ejecutan esos flujos; aun así, Aikido recomienda retirar esas versiones. También detectó el ladrón en el flujo de trabajo de github.com/vkuznet/gocurl, versión seudoversionada v0.0.0-20261111095532-8d5aad0cb0da, aunque ese flujo nunca se había ejecutado. La recomendación es bloquearla.
El código llegó asimismo a 28 repositorios de WebdriverIO mediante commits atribuidos a la cuenta bot wdio-bot. No se vio afectado ningún lanzamiento de paquetes, pero el token del bot debe considerarse robado.
Para buscar la actividad en repositorios accesibles desde el martes 10 de noviembre, las pistas incluyen la rama wfx-docs, ramas breves con nombres chore/[a-z0-9]{8} o ci-check-[0-9]{4}, y la pull request docs: dependency audit notes. También conviene revisar los commits y archivos mencionados, además de las ejecuciones con numeración ausente. The Hacker News incluye entre los indicadores el SHA-256 89695abb392d5cfd4972672961e644bb46baa98aa1ed7173c2a2816c762735cf del script. SafeDep identificó envíos HTTP POST a 194.150.166[.]244:8080/exfil, 18.143.168[.]96:8080/exfil y 153.75.82[.]159:8080, además de un chat de Telegram. En una muestra de 200 commits con cargas, aproximadamente una quinta parte usó solo Telegram.
The Hacker News encontró nuevas pull requests con el mismo título después de que la búsqueda de Aikido terminara el miércoles. Tres solicitudes revisadas, abiertas entre las 05:04 y las 05:38 UTC, procedían de ramas wfx-docs; no se pudo determinar si el ladrón llegó a ejecutarse en ellas. Las comprobaciones publicadas cubren repositorios públicos visibles, mientras que los permisos de escritura también llevaron las pruebas a proyectos de organizaciones y colaboradores.
Fuentes: The Hacker News, WebPulse, Business Upturn, Secure Blog, Hamer Intelligence Services