← Volver a todas las publicaciones
Publicado el · por Renaud Deraison

El provider se ejecuta donde están las claves

Aikido encontró un RAT norcoreano distribuido como dos providers de Terraform en el registro de HashiCorp. terraform init descarga un provider, terraform plan lo ejecuta como proceso local, y el provider lee tus credenciales de nube, porque para eso sirve precisamente un provider. La carga solo se descifra para los nombres de variables de una única víctima, así que los escáneres que descargaron el paquete vieron código inerte. Diez de las dieciocho máquinas alcanzadas eran Macs. Bromure Agentic Coding le da a ese binario una VM, una clave secreta falsa y una lista de sitios a los que puede llamar.

Todos los controles que la industria ha construido para los agentes de código quedan por delante del comando: aprobar el plan, leer el diff, restringir el push. Ninguno queda entre un binario de provider descargado y ~/.aws.

Estás añadiendo un contenedor a una stack. Le pides al agente un provider de Docker, escribe cuatro líneas de HCL, y ejecutas terraform init. Terraform resuelve el provider desde el registro, lo descarga, comprueba su firma y lo guarda en caché. Luego ejecutas terraform plan, y Terraform arranca ese binario como un proceso en tu máquina y le habla por RPC. Por el camino se autentica en tu cuenta de nube, con las mismas variables de entorno y el mismo perfil ~/.aws que usas tú.

Cada paso de eso es el diseño documentado, y este mes fue además una ruta de entrega de malware.

Lo que encontró Aikido

El 22 de septiembre Aikido informó de un troyano de acceso remoto escrito en Go, distribuido a través de dos providers de Terraform y dos módulos de Go. The Hacker News lo cubrió al día siguiente, calificándolo como la primera vez que unos atacantes usan el registro que opera HashiCorp para entregar una carga. Checkmarx, JFrog y SafeDep añadieron su análisis.

Los providers son gocommunity-io/dockerd, con 222 descargas, y kreuzwenker/docker, con 1.449. El segundo está a una letra de kreuzwerker/docker, un provider real con 56 millones de descargas declaradas. Los módulos de Go son gocommunity.io/orderedbtree, publicado el 11 de agosto, y gogets.dev/btreex, publicado el 8 de septiembre.

Aikido vincula la campaña con Graphalgo, que ReversingLabs documentó en febrero y atribuyó a operadores norcoreanos. La misma clave de firma aparece en cargas de npm desde abril. A estos paquetes se llega por una oferta de trabajo: una falsa empresa Web3 aborda a alguien que desarrolla en LinkedIn o Facebook y le manda un ejercicio de programación. El repositorio del ejercicio está limpio. La dependencia que arrastra es el malware.

Un provider es un programa, y Terraform le da las claves

npm install ejecuta los scripts de instalación de un paquete y luego se aparta. terraform init trae un programa entero, y ejecutar ese programa es el acto principal.

Un provider de Terraform es un binario ejecutable que Terraform Core invoca por RPC. Terraform lo ejecuta porque ejecutarlo es lo que un provider es. No vas a encontrar un hook de instalación que quitar ni un script de ciclo de vida que auditar, porque aquí la ejecución es la funcionalidad y no un efecto colateral de desempaquetar un tarball.

El binario también se ejecuta en el único punto de tu red con el camino más corto hacia producción. Purple Shield describe la máquina: “Un provider malicioso se ejecuta, por tanto, en la máquina de la persona que despliega tu infraestructura, con el acceso que tenga esa persona.” Y: “un portátil que ejecuta terraform apply suele tener a mano claves de proveedor de nube, tokens de Git, claves SSH y ficheros de estado de Terraform.”

Un ladrón de npm tiene que rebuscar credenciales por tu directorio personal. Terraform se las entrega al provider.

El camino documentado, y la rama que le añadieronel registrokreuzwenker/dockerinitla máquina que despliegaTerraform Coreplan → inicia, y RPCel proceso providerun binario descargadolas credenciales~/.aws, tokens, clavesla rama que nadie pidió, dentro del providersha256(containerName + networkID)coincide → clave AES → descifra → go run .cualquier otra configuración, y todo escánerel hash no coincide; nada se descifra nuncala única configuración a la que iba dirigidaun RAT, informa hostname y hardware, y espera18 máquinas, 725 mensajes: 3 Windows, 5 Linux, 10 macOS
La cadena no tiene ningún exploit. terraform init resuelve un provider desde el registro y guarda el binario en caché; terraform plan lo arranca como proceso local y le habla por RPC; el provider lee la cadena de credenciales del entorno, que es el protocolo. Los providers de Graphalgo añaden una rama: un punto de entrada oculto que aplica hash a dos de tus propias variables de Terraform, y solo cuando el hash coincide con un valor fijado en el código lo usa como clave AES para descifrar una carga escondida en un fixture de ejemplo y ejecutarla con un 'go run' desacoplado. Para cualquier otra configuración el código es inerte.

Dirigido a una sola persona

Estos providers atacan a un objetivo e ignoran a todos los demás que los instalan.

Ambos llevan un punto de entrada oculto en /internal/provider/resource_docker_container_funcs.go que solo se dispara cuando el SHA256 de las variables de Terraform containerName y networkID, concatenadas, es igual a un valor fijado en el código. El código usa entonces ese hash como clave AES para descifrar una ruta de fichero dentro del paquete, descomprime lo que encuentra allí, descifra cada fichero y ejecuta el resultado como paquete de Go con un go run . desacoplado. Escondieron el archivo en el directorio de ejemplos, en un fichero llamado import-resource.sqlite3.

Así que el atacante tiene que conocer los nombres que le diste a dos variables, y este atacante los conocía: el señuelo de la oferta de trabajo es como los aprendió. Todos los investigadores y todos los escáneres que descargaron el paquete obtuvieron código que se queda ahí sin hacer nada. No se puede detectar lo que nunca se descifra.

La segunda etapa es un troyano de acceso remoto. Primero informa de datos del sistema, incluidos atributos de hardware, sistema operativo, hostname y si node está disponible en la máquina, y luego espera instrucciones en dos sitios. Uno es Slack: un workspace llamado portfolio-devs, un canal llamado frontend-devs, y más tarde un segundo workspace con un canal llamado qa-announcements. Aikido contó allí 1.240 mensajes cifrados que se remontan al 16 de julio. El otro es un smart contract en la testnet de Arbitrum Sepolia, en una dirección fijada en el código, con 1.402 transacciones que se remontan al 6 de agosto. El malware consulta la blockchain cada tres segundos y Slack cada diez.

Ambos canales son HTTPS corriente hacia destinos con buena reputación. Tus máquinas de desarrollo hablan con Slack todo el día, y que una de ellas hable con un endpoint RPC público de Ethereum se lee como normal en cualquier semana en la que el equipo entregue algo on-chain. La misma gente usó un contrato de Sepolia en el paquete npm que Checkmarx describió la semana pasada, y lo reutiliza a la vista porque a la vista funciona.

Aikido recuperó la lista de víctimas del propio Slack del atacante: 18 hostnames únicos en 725 mensajes de check-in. Tres Windows, cinco Linux, diez macOS.

Qué cambia en un workspace de Bromure

La campaña venía a por una credencial. En Bromure Agentic Coding, el ~/.aws/config de la VM no contiene tu clave de acceso secreta. Apunta a un helper credential-process que entrega, por un socket virtio, tu identificador de clave de acceso real emparejado con un secreto falso de 40 caracteres. El manual enumera las herramientas que lo adoptan sin configuración alguna: todos los SDK de AWS, la CLI aws, boto3 y terraform. El provider lee la cadena de credenciales como espera, obtiene un perfil bien formado y firma sus peticiones con un secreto que no vale nada.

El host repara esa firma a la salida. Su resigner reconoce el tráfico destinado a *.amazonaws.com, despoja al guest de su condenada firma SigV4 y vuelve a firmar con el secreto real antes de que la petición salga de tu Mac. Tu terraform plan funciona. AWS responde a una copia robada de la credencial con InvalidSignatureException.

Luego el falso intenta salir. El proxy revisa las cabeceras y el cuerpo de cada petición saliente en busca de los valores marcadores del propio workspace, con un autómata de Aho-Corasick lo bastante barato como para correr sobre todas. Manda un falso a un host para el que no fue acuñado, un webhook de Slack por ejemplo, y el proxy responde HTTP 451 y no reenvía ni un byte. La VM se pausa. Una fila roja de Credential brokering aterriza en la Security Timeline, y Bromure marca el workspace como comprometido, de modo que tu siguiente lanzamiento ofrece borrar su disco y su home conservando tus tokens, claves SSH y ajustes.

El C2 todavía tiene que salir de la máquina. La salida es un conjunto ordenado de reglas de permitir y denegar, evaluado de arriba abajo, que termina en default allow o default deny. Un workspace que despliega infraestructura necesita registry.terraform.io, releases.hashicorp.com y tu API de nube, lo que deja portfolio-devs.slack.com y un endpoint RPC de Arbitrum fuera de la lista. El host aplica las reglas dos veces: el switch virtual coteja la IP de destino y el hostname captado por DNS, en TCP y en UDP, y el proxy vuelve a cotejar el nombre de servidor TLS. El switch desvía el puerto 80 y el puerto 443 hacia el proxy sin ninguna variable de entorno de por medio y sin nada que el guest pueda desactivar. Cada veredicto se convierte en una fila de Firewall en la Security Timeline.

En la máquina que despliegael proceso del provider, como túlee ~/.aws → clave secreta reallee tokens git, claves SSH, tfstatetu directorio home es el radiosalida, sin preguntasportfolio-devs.slack.com · cada 10sArbitrum Sepolia RPC · cada 3sambos destinos tienen buena reputaciónno hay anomalía que alertardespués: rotar todo lo que el portátil alcanzabaEn un workspace Bromure Agentic Codingel proceso del provider, en el guestlee ~/.aws → access key ID + secreto falsoUbuntu 24.04, bajo el hipervisorun disco que se cambia desde un menúsalida, cotejada con tus reglasslack.com → sin regla → deny por defectoSepolia RPC → sin regla → deny por defectoy si el falso sale de su ámbitoHTTP 451, ni un byte enviado, VM en pausadespués: borrar y lanzar, sin rotar nada
El mismo binario de provider, dos máquinas. En un portátil se ejecuta como tú, lee una clave secreta real de la cadena de credenciales y alcanza Slack y un endpoint RPC público porque nadie escribió una regla en contra. En un workspace de Bromure se ejecuta dentro de un guest Ubuntu bajo el hipervisor, encuentra un identificador de clave de acceso emparejado con un secreto falso y se topa a la salida con un conjunto ordenado de reglas de egreso; llevar el falso a donde no fue acuñado devuelve HTTP 451 y pausa la VM. Las llamadas a AWS que sí corresponden se vuelven a firmar en el host, así que terraform sigue funcionando, y las acciones destructivas en AWS se detienen en un diálogo que aparece en el Mac.

Tú apruebas los borrados

Un control más se interpone entre el provider y tu cuenta, y es el que cuenta cuando el atacante quiere tu infraestructura en vez de tus secretos.

Las credenciales de AWS llevan una política de escritura, y los workspaces nuevos vienen por defecto en Prompt before write. El proxy clasifica cada llamada por el nombre de acción de la cabecera X-Amz-Target o del parámetro Action=: Get*, List* y Describe* son lecturas y pasan sin una palabra, mientras que Delete*, Terminate*, Revoke* y sus parientes cuentan como destructivas. Una escritura se detiene en un diálogo titulado Allow write on "<scope>" from workspace "<name>"?, que muestra la operación literal en lugar de un resumen, con Allow for 15 minutes, Allow once, Allow for the rest of the session y Don't allow. Rechaza, y el guest recibe una AccessDeniedException que el agente reporta antes de seguir adelante.

Eres tú quien hace clic en ese diálogo, en tu Mac. Un proceso dentro de la VM no puede alcanzarlo, y quien esté al otro lado del canal de Slack tampoco.

Trata la cadena de herramientas como un ejecutor

terraform init y terraform plan son descargar-y-ejecutar. Pregunta qué ejecuta un provider por dentro, porque va a ejecutar.

No dejes ningún secreto vivo al alcance

Dale al provider una cadena de credenciales que le satisfaga y un secreto que no vale nada. El host firma, al otro lado de la frontera, acotado al destino al que pertenece la clave.

Nombra primero los destinos

Un workspace de despliegue necesita una lista corta y estable: el registro, el host de releases, tu API de nube. Escríbela una vez, pon el tráfico sin coincidencia en Deny, y un contrato usado de buzón muerto no tiene nada que consultar.

Haz que lo irreversible pregunte

Deja que las lecturas corran en silencio. Haz que Terminate* te cueste un diálogo en una pantalla que la VM no puede alcanzar.

Aquí nadie encontró una vulnerabilidad

El registro de HashiCorp sirvió el paquete que le pidieron. Terraform verificó la suma de comprobación que le dieron y ejecutó el provider igual que ejecuta todos los providers. Ninguna sandbox se rompió, ningún agente leyó una instrucción envenenada y ningún modelo se portó mal.

Dos providers con 1.671 descargas entre ambos alcanzaron al menos 18 máquinas, la mayoría Macs, y se quedaron ahí consultando un smart contract cada tres segundos. Un comportamiento documentado sostuvo la cadena entera. La única parte novedosa, una carga que sigue cifrada a menos que tus variables se llamen de una forma concreta, existe para que nada pueda encontrarla.

Decide estas cosas antes de la sesión, entonces, porque no las vas a pillar durante. No se puede escanear un provider que no ha descifrado su carga, y no vas a distinguir la exfiltración dentro del tráfico hacia Slack. Ejecuta el despliegue donde las claves sean señuelos, los destinos sean una lista que escribiste tú, y la máquina que acaben rooteando sea una que tiras.