Neutron, nuestro motor de IA, obtuvo un 97.1% en el benchmark CyberGym de UC Berkeley. Más información

Análisis a fondo

Análisis a fondo

WFX Phantom: campaña de robo de secretos en GitHub Actions

Una campaña con tokens de GitHub robados introdujo wfx en miles de repositorios y recogió secretos de CI y credenciales cloud desde el 10 de noviembre.

Resumen

  • Una campaña con tokens de GitHub robados ha introducido un stealer de shell llamado wfx en repositorios desde el 2026-11-10. Step Security publicó el análisis el 2026-11-12.
  • El atacante abre pull requests de sondeo, esconde el stealer en workflows o scripts de shell y borra la ejecución del workflow después de que se ejecute. La búsqueda de GitHub mostraba más de 63,000 pull requests de sondeo a las 07:40 UTC del 2026-11-12.
  • El stealer recoge secretos del repositorio, memoria del runner y credenciales cloud. Puede persistir en AWS y Azure. Algunos tokens procedían de la aplicación de GitHub de Fly.io; no se ha establecido una brecha en la infraestructura propia de Fly.io.
  • Elimine los commits y archivos, rote cada secreto y rol cloud al que el workflow pudiera llegar, y compruebe la persistencia. No se asigna ningún CVE, así que no hay entrada en el KEV de CISA al 2026-11-12.

¿Estoy afectado?

Está expuesto si se cumple alguna de estas condiciones:

  • Se abrió en su repositorio una pull request titulada docs: dependency audit notes, desde una rama llamada wfx-docs, el 2026-11-10 o después.
  • Llegó a una rama un commit con uno de los mensajes de sondeo el 2026-11-10 o después. Los mensajes son ci: refresh build cache, ci: restore, ci: cleanup, chore: add ci helper y chore: add dependency audit workflow.
  • Un workflow ejecutó el stealer. El stealer puede ser un paso inyectado, un script de shell añadido, un nuevo .github/workflows/build-audit-<6 characters>.yml, o un .ci-<6 characters>.sh oculto enganchado a postinstall, build o test.
  • El trabajo podía leer secretos de repositorio u organización, un GITHUB_TOKEN capaz de pedir tokens cloud, o credenciales en un runner autoalojado.
  • Instaló una de las versiones de paquete afectadas.

Una pull request de sondeo significa que el token podía escribir en el repositorio. No prueba que el stealer se ejecutara. El atacante solo introduce el stealer donde los permisos del token y la configuración del repositorio lo permiten. El informe estima que eso son miles de repositorios, no las 63,000 sondas.

La señal más fuerte es un número de ejecución ausente. Si el historial de Actions salta de un número al siguiente justo después de uno de los commits de sondeo, es probable que el atacante ejecutara el stealer y borrara las pruebas. El stealer también lee la memoria del proceso Runner.Worker, así que puede tomar secretos que el workflow nunca pasó al trabajo.

Paquete Ecosistema Versión afectada Versión limpia
digitalelvis/neuronai-studio Packagist 3.14.3 3.14.2 o anterior
github.com/onuragtas/docker-env Go v1.0.278 y v1.0.280 Una versión anterior al 2026-11-11
github.com/vkuznet/gocurl Go v0.0.0-20261111095532-8d5aad0cb0da Un commit anterior al 2026-11-11 09:55 UTC

Los dos módulos Go contienen el stealer solo en un archivo de workflow, y las compilaciones de Go nunca ejecutan archivos de workflow. Bloquee esas versiones de todos modos, porque un escaneo o un espejo puede mostrarlas. La versión de Packagist incluye el script .ci-*.sh y un hook postinstall; composer install no lo ejecuta, pero npm install dentro del directorio del paquete sí.

Compruebe un clon local para los commits de sondeo, los scripts ocultos y los marcadores del script:

# Probe commits
git log --all --since=2026-11-10 --oneline \
  --grep='ci: refresh build cache' \
  --grep='ci: restore' \
  --grep='ci: cleanup' \
  --grep='chore: add ci helper' \
  --grep='chore: add dependency audit workflow'

# Hidden scripts and workflows
git log --all --since=2026-11-10 --name-only --pretty=format: \
  | grep -E '\.ci-[a-z0-9]{6}\.sh|build-audit-[a-z0-9]{6}\.yml' | sort -u

# Stealer markers
grep -R -E 'WF_CALLBACK|WF_CTOKEN|WF_EXFIL_|WF_TG_BOTS|wfx-loot|WFX-BEGIN' . --exclude-dir=.git

Compruebe las versiones de los paquetes:

composer show digitalelvis/neuronai-studio 2>/dev/null
go list -m all | grep -E 'onuragtas/docker-env|vkuznet/gocurl'

¿Cómo lo corrijo?

Elimine los cambios del atacante y después rote cada credencial a la que pudiera llegar el workflow. Eliminar el commit no deshace el robo, porque el stealer puede haber enviado ya los secretos y haber plantado persistencia en la nube.

  1. Revierta el commit malicioso con git revert, o restablezca la rama al último commit limpio. Conserve el commit del atacante y los registros del workflow como pruebas antes de reescribir el historial.
  2. Elimine las ramas del atacante, cierre las pull requests de sondeo y retire cualquier colaborador o clave de despliegue que haya añadido.
  3. Rote cada secreto que el trabajo pudiera leer: secretos de repositorio, organización y entorno; tokens de acceso personal; claves de despliegue; credenciales de npm, PyPI, Docker y Vault; claves SSH; y claves de proveedores de IA.
  4. Revogue las sesiones y tokens de la cuenta de GitHub comprometida. Si el token pertenecía a una aplicación de GitHub, retire la instalación de la aplicación y vuelva a conectarla solo cuando la cuenta esté limpia.
  5. Elimine la persistencia en AWS y Azure descrita en «¿Qué hace el código malicioso?».
  6. Reconstruya cada paquete o versión publicado entre el commit malicioso y el revert desde un commit limpio.
  7. Restrinja quién puede hacer push a las ramas por defecto, exija revisiones y configure los permisos de GITHUB_TOKEN como solo lectura por defecto.

Si no puede rotar una credencial de inmediato, reduzca su alcance o desactívela. Una clave de acceso AWS de larga duración o una credencial federada de Azure creada por el atacante sobrevive a un cambio de contraseña.

¿Qué hace el código malicioso?

El stealer es un único script de shell de unas 1,500 líneas con interruptores para cada función. Se ejecuta en cuatro etapas: recoger, escalar, persistir y exfiltrar.

Recoger. Lee cada secreto y variable del repositorio mediante ${{ toJSON(secrets) }} y ${{ toJSON(vars) }}, además del GITHUB_TOKEN del trabajo y su capacidad de pedir tokens cloud. Vuelca la memoria del proceso Runner.Worker, que contiene en forma descifrada cada secreto que el trabajo puede usar. Recoge entornos de proceso y archivos de credenciales de AWS, Azure, GCP, Docker, Kubernetes, npm, PyPI, SSH y Vault. También toma claves de API de proveedores de IA y agentes de código, y, en runners autoalojados, credenciales de metadatos de instancia cloud.

Escalar. Usa el token de OpenID Connect del trabajo para abrir sesiones en AWS, GCP y Azure. Encadena otros roles de AWS nombrados en el repositorio, suplanta cuentas de servicio de GCP y reutiliza tokens de refresco de Azure CLI en caché para llegar a Microsoft Graph y Key Vault.

Persistir. Puede reescribir la política de confianza de un rol de AWS asumido como "Principal": {"AWS": "*"} y llamar a iam:CreateAccessKey para usuarios IAM, lo que da al atacante claves de larga duración. Puede añadir una credencial federada de Azure llamada wfx-ci-<n> a una aplicación de Azure AD. Los workflows del repositorio comprometido pueden entonces iniciar sesión en Azure sin un secreto almacenado. Una tercera función puede marcar las comprobaciones requeridas como superadas; el informe dice que ninguna de las 101 copias examinadas tenía ese interruptor activado.

Exfiltrar. Empaqueta los datos en un único archivo gzip. El informe encontró tres configuraciones de entrega. La primera envía un POST HTTP a 194.150.166[.]244:8080/exfil o 18.143.168[.]96:8080/exfil. La segunda añade un tercer servidor, 153.75.82[.]159:8080/exfil, y una copia de seguridad en Telegram. La tercera, vista por primera vez hacia las 05:30 UTC del 2026-11-12, envía solo a Telegram, mediante cinco tokens de bot, al chat -1004340163187. Todas las copias muestreadas entre las 07:00 y las 07:40 UTC usaban la configuración solo Telegram, así que una lista de bloqueo de IP basada en las direcciones del 10 de noviembre ya no cubre las copias más recientes.

El stealer se esconde en cuatro lugares:

  • Dos pasos añadidos a un workflow existente. El primero entrega los tokens internos del trabajo al siguiente paso, y el segundo ejecuta el stealer con un nombre como Restore cache x9xx.
  • El script añadido a un script de shell existente que un workflow ya ejecuta, como update.sh en nextcloud/docker o tests/retry_env.sh en PancrePal-xiaoyibao/MinerU-xyb.
  • Un nuevo workflow, .github/workflows/build-audit-<6 characters>.yml, en una rama desechable chore/<8 characters>.
  • Un .ci-<6 characters>.sh oculto más un hook en package.json. El informe encontró el hook en postinstall 11 veces, build dos veces y test una vez.

Después de que arranca el workflow, el atacante hace push de un revert (ci: restore, a veces ci: cleanup) y borra la ejecución del workflow con sus registros. En drunomics/nuxtjs-drupal-ce, el stealer se commiteó a las 09:17:23 UTC del 2026-11-11 y el revert llegó 12 segundos después; el historial de Actions pasa de la ejecución #1963 directamente a la #1965, y la ejecución #1964 ha desaparecido.

Cuando el borrado falla, el registro de la ejecución muestra lo que hizo el stealer. En nextcloud/docker, la ejecución superviviente imprimió:

memscan_files 3
gcp_gcloud_auth_empty
gcp_live token=0 projects=0 gcloud=0
selfhosted_collected
az_graph_token_fail
oidc_discovery arn=none gcp=none azure=none
archive_bytes 8971
exfil_no_runtime_vars

Esas líneas muestran un runner autoalojado, un escaneo de memoria que encontró tres archivos y un archivo de 8,971 bytes. También muestran que las rutas de GCP, Azure y OIDC no produjeron un token en esa ejecución. El envío del archivo tuvo éxito, porque el script imprime un mensaje de reintento cuando falla y el registro no tiene ninguno. Esa ejecución recogió datos de la propia máquina, no solo del runner alojado por GitHub.

¿Cómo se explota?

  1. El atacante tiene un token robado con acceso de escritura al repositorio. El token puede pertenecer a un desarrollador, a una cuenta de bot o a una aplicación de GitHub.
  2. El atacante abre la pull request de sondeo desde wfx-docs. La pull request añade un archivo estático, docs/wfx-audit.md, e indica al atacante que el token puede escribir.
  3. El atacante introduce el stealer en uno de los cuatro escondites. El commit parece trabajo de mantenimiento, y el autor suele ser una cuenta de bot que puede saltarse la protección de rama.
  4. Un workflow se ejecuta con el commit. El stealer recoge secretos, pide tokens cloud y envía el archivo. El atacante revierte el commit y borra la ejecución, normalmente en menos de un minuto.
  5. El atacante usa las credenciales cloud robadas o la persistencia plantada después de limpiar el repositorio. El informe dice que el mismo actor puede volver mediante la persistencia incluso después de revocar el token.

El informe atribuye 173 pull requests de sondeo, desde 19 cuentas, a tokens emitidos para la aplicación de GitHub de Fly.io (fly-io, ID de aplicación 34557). La aplicación tiene acceso de escritura a contenidos, acciones y pull requests, suficiente para cada paso.

El informe dice que no ha establecido cómo se obtuvieron esos tokens y que no tiene pruebas de una brecha en la infraestructura propia de Fly.io. La mayor parte de la campaña usa otras credenciales, como tokens de acceso personal.

¿Cómo se desarrolla el ataque?

Un token robado abre una pull request de sondeo, y el atacante introduce un stealer que se ejecuta dentro de un workflow normal. La ejecución envía secretos y tokens cloud antes de que el atacante revierta el commit y borre la ejecución.

flowchart LR
    A["Token de GitHub robado"] --> B["Pull request de sondeo<br/>docs: dependency audit notes"]
    B --> C["Introducir el stealer<br/>workflow, script de shell o hook npm"]
    C --> D["Ejecución de GitHub Actions<br/>lee secretos y memoria de Runner.Worker"]
    D --> E["OIDC cloud<br/>AWS, GCP, Azure"]
    D --> F["Archivo exfiltrado<br/>servidor HTTP o Telegram"]
    E --> G["Persistencia<br/>política AWS, credencial Azure"]
    F --> H["El atacante revierte el commit<br/>y borra la ejecución"]

¿Qué puede hacer un atacante?

Un atacante con un token de escritura puede leer cada secreto al que puede llegar el workflow, incluidos los secretos que el workflow nunca referencia, porque el stealer vuelca la memoria del proceso del runner. Puede usar el token cloud del trabajo para asumir roles en AWS, GCP y Azure, y puede plantar persistencia que sobrevive a la rotación de tokens.

El impacto en un repositorio depende de lo que el workflow pueda hacer. Un repositorio con solo acceso de lectura a su propio código pierde menos que uno con un rol de despliegue cloud o un token de publicación npm. Un runner autoalojado que guarda credenciales de producción es el peor caso. La ejecución superviviente de nextcloud/docker que cita el informe muestra al stealer leyendo la propia máquina del runner.

Una pull request de sondeo por sí sola no da nada de eso al atacante. El stealer debe ejecutarse, y el token debe tener los permisos que usa el workflow. El informe no publica un número de víctimas ni una lista de robos de datos confirmados.

¿Qué hago si mi repositorio se vio afectado?

Si un workflow ejecutó el stealer, trate el repositorio, el runner y cada credencial a la que pudiera llegar el trabajo como comprometidos. Eliminar el paquete o el commit es solo el primer paso.

  1. Aísle el runner autoalojado y conserve su disco, su lista de procesos y sus registros. No lo reutilice hasta haberlo reconstruido desde una imagen limpia.
  2. Conserve los registros de la ejecución del workflow y los commits del atacante. El número de ejecución ausente y el historial de commits son pruebas para la revisión del incidente.
  3. Rote cada secreto y token que el trabajo pudiera leer, desde una máquina limpia. Empiece por los roles cloud y las claves de larga duración, porque el stealer los ataca primero.
  4. Elimine la política de confianza de AWS que permite "AWS": "*", borre las claves de acceso IAM que usted no creó y retire las credenciales federadas de Azure llamadas wfx-ci-<n>.
  5. Busque repositorios, colaboradores, claves de despliegue, webhooks y archivos de workflow nuevos creados con el token robado.
  6. Reconstruya cualquier paquete publicado mientras el stealer estuvo presente y publíquelo de nuevo desde un commit limpio con un número de versión nuevo.

Si solo instaló digitalelvis/neuronai-studio 3.14.3 y el hook postinstall no se ejecutó, retire el paquete y fije 3.14.2 o anterior. Si el hook se ejecutó, siga los mismos pasos de incidente.

¿Cómo compruebo si estuve comprometido?

Empiece por las búsquedas de GitHub y las comprobaciones locales de «¿Estoy afectado?». La pull request de sondeo, los mensajes de commit y los scripts ocultos son pruebas públicas.

# On GitHub: probe pull requests in your org
https://github.com/search?q=%22docs%3A+dependency+audit+notes%22+is%3Apr&type=pullrequests

Busque un número de ejecución ausente en la pestaña Actions justo después de uno de los commits de sondeo. Una ejecución que desapareció es la señal más clara de que el stealer se ejecutó y el atacante limpió.

Busque en el código los servidores de recogida y los marcadores del script:

grep -R -E '194\.150\.166\.244|18\.143\.168\.96|153\.75\.82\.159|WF_CALLBACK|WF_CTOKEN|WF_TG_BOTS' . --exclude-dir=.git

Compruebe la persistencia cloud:

# AWS: a role that trusts every account is the planted back door
aws iam get-role --role-name <role-name> --query 'Role.AssumeRolePolicyDocument'
aws iam list-access-keys --user-name <user-name>

# Azure: federated credentials named wfx-ci-<n>
az ad app federated-credential list --id <application-id>

El informe enumera estos indicadores:

  • Servidores de recogida: 194.150.166[.]244:8080/exfil, 18.143.168[.]96:8080/exfil y 153.75.82[.]159:8080/exfil.
  • Tokens de retorno: b42d70a7aab9140ca19e8249 y 15f44heehnax8jdf41yx, enviados como t=.
  • Telegram: api.telegram.org sendDocument, chat -1004340163187, IDs de bot 8708988906, 9196217635, 9180059428, 8902935164 y 8876515636.
  • Pull request de sondeo: título docs: dependency audit notes, rama wfx-docs, archivo docs/wfx-audit.md, blob de git ee6e5addfe591923fc385553cb66cf0d25634a6b.
  • Ramas y archivos: chore/[a-z0-9]{8}, ci-check-[0-9]{4}, .ci-[a-z0-9]{6}.sh y .github/workflows/build-audit-[a-z0-9]{6}.yml.
  • Marcadores del script: pasos llamados Pass runtime y Restore cache <4 characters> o Run audit; variables WF_CALLBACK, WF_CTOKEN, WF_EXFIL_* y WF_TG_BOTS; carpeta de staging .wfx-$$; notas de registro memscan_files, archive_bytes y deliver_retry.

El informe no publica un número de víctimas, y una ejecución borrada puede dejar solo el número ausente como prueba.

Cronología

  • 2026-11-10: empieza la campaña. Las primeras copias envían datos a 194.150.166[.]244.
  • 2026-11-11 09:17: el stealer se commitea en drunomics/nuxtjs-drupal-ce; el revert llega 12 segundos después y la ejecución #1964 se borra.
  • 2026-11-11 10:02 a 10:36: una ráfaga de pull requests de sondeo usa tokens emitidos para la aplicación de GitHub de Fly.io.
  • 2026-11-12 01:10: aparece un canal de respaldo de Telegram en las muestras.
  • 2026-11-12 05:30: las copias nuevas pasan a entrega solo por Telegram.
  • 2026-11-12 07:40: la búsqueda de GitHub devuelve más de 63,000 pull requests de sondeo.
  • 2026-11-12 10:47: Step Security publica su análisis.

Las primeras 35 horas de la campaña, desde la primera exfiltración HTTP hasta el cambio a Telegram y el hito de las 63,000 sondas.
La cronología de la campaña del 2026-11-10 al informe del 2026-11-12. Fuente: Step Security, consultado el 12 de noviembre de 2026

Preguntas frecuentes

¿Una pull request de sondeo significa que mi repositorio fue comprometido?

No. La sonda solo muestra que el token robado podía escribir en el repositorio. Busque el commit del payload, el script oculto y una ejecución de workflow ausente. Si el stealer se ejecutó, rote los secretos.

¿Sufrieron una brecha GitHub o Fly.io?

El informe dice que no tiene pruebas de una brecha en la infraestructura propia de Fly.io. La aplicación de GitHub de Fly.io emitió tokens a usuarios que conectaron sus cuentas, y el atacante usó algunos de esos tokens. No se ha establecido cómo se obtuvieron los tokens. La plataforma de GitHub no está implicada: la campaña abusa de tokens de GitHub válidos y de funciones normales de los workflows, y por eso tampoco se asigna un CVE.

¿Hay un CVE para esta campaña?

No. La campaña abusa de tokens de GitHub válidos y de funciones normales de los workflows, así que no se asigna ningún CVE. Las versiones de paquete afectadas se listan por sus registros, no por un CVE.

¿Está la campaña en el KEV de CISA?

No. No se asigna ningún CVE, y el KEV de CISA lista CVE. No hay entrada KEV al 2026-11-12.

¿Qué paquetes contienen el stealer?

digitalelvis/neuronai-studio 3.14.3 en Packagist, github.com/onuragtas/docker-env v1.0.278 y v1.0.280, y la pseudo-versión v0.0.0-20261111095532-8d5aad0cb0da de github.com/vkuznet/gocurl. Los dos módulos Go solo lo contienen en un archivo de workflow.

El atacante borró la ejecución. ¿Puedo saber si se ejecutó?

El número de ejecución ausente y el commit de sondeo son las pruebas. Una ejecución que desaparece entre dos ejecuciones numeradas justo después del commit es el patrón. Rote las credenciales aunque no pueda recuperar el registro.

Referencias

  • Step Security, «WFX Phantom: The GitHub Actions Secret Stealer That Deletes Its Tracks», publicado el 2026-11-12: https://www.stepsecurity.io/blog/wfx-phantom-the-github-actions-secret-stealer (consultado el 12 de noviembre de 2026)
  • Packagist, versiones 3.14.2 a 3.14.4 de digitalelvis/neuronai-studio: https://packagist.org/packages/digitalelvis/neuronai-studio (consultado el 12 de noviembre de 2026)
  • Proxy de módulos Go, github.com/onuragtas/docker-env v1.0.278 y v1.0.280: https://pkg.go.dev/github.com/onuragtas/docker-env (consultado el 12 de noviembre de 2026)
  • Proxy de módulos Go, pseudo-versión de github.com/vkuznet/gocurl: https://pkg.go.dev/github.com/vkuznet/gocurl (consultado el 12 de noviembre de 2026)
  • GitHub, pull request de sondeo citada por el informe: https://github.com/EbukaLovesTech/2gether/pull/1 (consultado el 12 de noviembre de 2026)