Cómo un escáner de seguridad comprometido se convirtió en la clave para insertar una puerta trasera en LiteLLM
24 de marzo de 2026
0 minutos de lecturaEl 24 de marzo de 2026, se descubrió que dos versiones del paquete de Python litellm en PyPI contenían código malicioso. Las versiones 1.82.7 y 1.82.8 fueron publicadas por un actor de amenazas conocido como TeamPCP, que obtuvo las credenciales de PyPI del responsable del paquete mediante un compromiso previo de Trivy, un escáner de seguridad de código abierto que se usa en el pipeline de CI/CD de LiteLLM.
Las versiones maliciosas estuvieron disponibles durante aproximadamente tres horas, hasta que PyPI puso el paquete en cuarentena. LiteLLM se descarga alrededor de 3.4 millones de veces al día.
Snyk ha estado siguiendo este incidente. Si eres cliente de Snyk, es posible que ya hayas visto la alerta en el banner de la aplicación y recibido una notificación por correo electrónico. El registro de la vulnerabilidad es SNYK-PYTHON-LITELLM-15762713, y las actualizaciones de estado están en Snyk Trust Center.
En resumen
Paquete afectado |
|
Versiones afectadas | 1.82.7, 1.82.8 |
Versiones seguras | ≤ 1.82.6 |
ID de Snyk | |
Primera detección | 10:39 UTC, 24 de marzo de 2026 (carga de la versión 1.82.7) |
Cuarentena de PyPI | \~13:38 UTC, 24 de marzo de 2026 |
Atacante | TeamPCP (también: PCPcat, Persy_PCP, ShellForce, DeadCatx3) |
Vector de ataque | Cadena de suministro: credenciales comprometidas de publicación en PyPI mediante una GitHub Action de Trivy manipulada en CI/CD de LiteLLM |
Tipo de carga maliciosa | Tres etapas: recolector de credenciales + exfiltración cifrada + puerta trasera persistente + gusano de Kubernetes |
Dominio de exfiltración |
|
MITRE ATT\&CK | T1546.018 (ganchos de inicio de Python), T1003 (volcado de credenciales), T1610 (implementación de contenedores) |
Eventos principales
Hora (UTC) | Evidencia | Evento |
|---|---|---|
Finales de febrero de 2026 |
| |
19 de mar., 17:43 UTC | Se reescriben las etiquetas de la GitHub Action de Trivy | |
23 de mar., 12:58 UTC | Endor Labs (capturó los metadatos de PyPI antes de su eliminación) | Se compromete la GitHub Action de Checkmarx KICS; se registran el dominio C2 |
24 de mar., 10:39 UTC | Endor Labs (capturó los metadatos de PyPI antes de su eliminación) | Se publica |
24 de mar., 10:52 UTC | Se publica | |
24 de mar., 11:48 UTC | FutureSearch (Callum McMahon) abre un problema para informar la vulnerabilidad | |
24 de mar., 12:36 UTC | Se publica un hilo en HN; alcanza 324 puntos | |
24 de mar., \~12:44 UTC | Problema #24512 en GitHub (visible en las marcas de tiempo de los comentarios) | Una avalancha de comentarios de bots inunda el problema #24512; el problema se cierra desde la cuenta comprometida del responsable |
24 de mar., 13:03 UTC | FutureSearch (actualización con marca de tiempo) | FutureSearch confirma el cierre del problema y el spam de bots |
24 de mar., 13:48 UTC | Se abre un problema limpio para dar seguimiento | |
24 de mar., 15:09 UTC | El responsable de LiteLLM confirma que se rotaron todas las claves de GitHub, Docker y PyPI; las cuentas de responsables se migraron a nuevas identidades | |
24 de mar., 15:27 UTC | Se eliminan las versiones comprometidas; PyPI quita el paquete de cuarentena |
Cómo se descubrió
Callum McMahon, de FutureSearch, estaba probando un plugin MCP de Cursor que incluía litellm como dependencia transitiva. Poco después de que Python se iniciara, su computadora dejó de responder porque se agotó la memoria RAM. McMahon rastreó el problema hasta el paquete litellm recién instalado y encontró litellm_init.pth, un archivo de 34,628 bytes en site-packages/, codificado dos veces en base64.
El agotamiento de la memoria RAM fue un efecto secundario de la carga maliciosa, no una función intencional. El mecanismo .pth se activa cada vez que se inicia el intérprete de Python. Como la carga maliciosa inicia un nuevo subproceso de Python, y ese proceso también activa la ejecución de .pth, se produjo una bomba de bifurcación involuntaria. McMahon publicó sus hallazgos en futuresearch.ai y, en menos de una hora, la información se difundió en r/LocalLLaMA, r/Python y Hacker News.
La cadena de ataque
El ataque a LiteLLM comenzó cinco días antes con Trivy.
19 de marzo: Los atacantes reescribieron etiquetas de Git en el repositorio de la GitHub Action trivy-action para que apuntaran a una versión maliciosa (v0.69.4) que contenía la misma carga para robar credenciales y la misma infraestructura de exfiltración que se usaron en operaciones posteriores. (Para obtener todos los detalles del compromiso de Trivy, consulta la cobertura de Snyk sobre el compromiso de la cadena de suministro de las GitHub Actions de Trivy.)
23 de marzo: Se usó la misma infraestructura en un ataque separado contra Checkmarx KICS (Keep Infrastructure as Code Secure). Durante esta operación, se registró y activó el dominio C2 checkmarx.zone, que suplanta a la empresa de seguridad Checkmarx.
24 de marzo: El pipeline de CI/CD de LiteLLM ejecutó Trivy como parte de su proceso de compilación y lo obtuvo de apt sin fijar una versión. La acción comprometida exfiltró el token PYPI_PUBLISH del entorno del ejecutor de GitHub Actions. Con esas credenciales, los atacantes publicaron litellm 1.82.7 a las 10:39 UTC y 1.82.8 a las 10:52 UTC; ambas versiones contenían cargas maliciosas.

Respuesta inicial del responsable de LiteLLM en Hacker News.
Dos mecanismos de distribución
Las dos versiones maliciosas usaron distintas técnicas de inyección:
1.82.7 (inyección en el código fuente): La carga se codificó en base64 y se insertó directamente en litellm/proxy/proxy_server.py. Se ejecuta cuando cualquier elemento importa litellm.proxy, la ruta de importación estándar para el modo de servidor proxy de LiteLLM.
1.82.8 (.pth archivo): Esta versión agregó litellm_init.pth a site-packages/. El mecanismo .pth se activa cada vez que se inicia el intérprete de Python, sin necesidad de importar nada; esto incluye cuando se inicia Python desde pip, python -c o el servidor de lenguaje de un IDE.
El archivo .pth está declarado correctamente en el archivo RECORD de la rueda, por lo que supera la verificación de hash de pip. El paquete pasa todas las comprobaciones de integridad estándar porque el contenido malicioso se publicó con credenciales legítimas, no se inyectó posteriormente. Snyk Learn ofrece una guía detallada sobre cómo funciona este tipo de ataque en Compromise of a Legitimate Package.
Los responsables de CPython han reconocido el riesgo de seguridad de los ganchos de inicio .pth en el problema #113659, pero todavía no se ha aplicado ningún parche. Esta técnica corresponde a MITRE ATT\&CK T1546.018 (Python Startup Hooks).
La carga maliciosa de tres etapas
Etapa 1: recopilación de información
El script recopila:
Datos del sistema: nombre de host, variables de entorno, procesos en ejecución, rutas de red y registros de autenticación recientes de
/var/log/auth.logCredenciales: claves privadas SSH, archivos
.env, credenciales de Git, claves de API del historial del shell, tokens de webhooks de Slack y Discord, configuraciones de CI/CD (Jenkins, Travis CI, Terraform) y hashes de contraseñas de/etc/shadowCredenciales en la nube: credenciales y configuración de AWS, archivos de cuentas de servicio de GCP y Azure, metadatos de la nube mediante consultas directas a IMDS, AWS Secrets Manager y SSM Parameter Store (el script implementa por completo la firma de IMDSv2)
Contenedores y orquestación:
config.jsonde Docker (credenciales de registro), archivoskubeconfigde Kubernetes, tokens de cuentas de servicio, secretos y certificadosCriptomonedas: archivos de billetera y frases semilla de Bitcoin, Ethereum, Solana, Cardano y Monero; configuraciones de billeteras de hardware Ledger

Fragmento del código malicioso decodificado que muestra la iteración estructurada y la recopilación de secretos en varios directorios
Etapa 2: cifrado y exfiltración
Los datos recopilados se cifran antes de transmitirse:
Se genera una clave de sesión AES-256 de 32 bytes mediante
openssl randLos datos se cifran con AES-256-CBC (derivación de clave PBKDF2)
La clave de sesión se cifra con una clave pública RSA de 4096 bits codificada en el script (relleno OAEP)
Todo se agrupa en
tpcp.tar.gzEl paquete se envía mediante POST a
https://models.litellm.cloud/concurl
models.litellm.cloud es distinto del dominio legítimo litellm.ai y se registró el 23 de marzo, un día antes de que se publicaran los paquetes maliciosos.
La clave pública RSA de esta carga es idéntica a la que se encontró en las cargas de Trivy y KICS. Investigadores de Wiz confirmaron esta coincidencia en las tres operaciones, lo que la convierte en el vínculo técnico individual más sólido para atribuir los ataques.
Archivos temporales creados durante la exfiltración: session.key, payload.enc, session.key.enc y tpcp.tar.gz en el directorio temporal del sistema.

Vista neutralizada del código malicioso. El nombre del archivo tpcp.tar.gz es un indicador constante en las operaciones de Trivy, KICS y LiteLLM.
Etapa 3: persistencia y movimiento lateral
Persistencia local:
Escribe un script de Python de puerta trasera en
~/.config/sysmon/sysmon.py(o/root/.config/sysmon/sysmon.py)Crea un servicio de usuario de systemd en
~/.config/systemd/user/sysmon.servicecon la descripción "System Telemetry Service"Lo registra y habilita:
systemctl --user daemon-reload && systemctl --user enable --now sysmon.service
El script de persistencia consulta https://checkmarx.zone/raw cada 5 minutos para obtener una URL, descarga el archivo en /tmp/pglog, le asigna permisos de ejecución y lo ejecuta como proceso en segundo plano. El estado se registra en /tmp/.pg_state. En el momento del análisis, el endpoint devolvía URL de YouTube; el operador puede cambiar a la entrega de cargas en vivo cuando quiera.
Movimiento lateral en Kubernetes: Si el script encuentra un token de cuenta de servicio de Kubernetes en la ruta de montaje estándar, lee todos los secretos de cada espacio de nombres. Luego intenta implementar un pod con privilegios en cada nodo de kube-system mediante alpine:latest. Estos pods montan el sistema de archivos del host e instalan la puerta trasera sysmon en el nodo subyacente.
Los pods maliciosos se llaman node-setup-{node_name} (el nombre del nodo se trunca a 35 caracteres) y contienen un contenedor llamado setup.
Acerca de TeamPCP
TeamPCP (también identificado como PCPcat, Persy_PCP, ShellForce y DeadCatx3, según Wiz Threat Center) ha estado activo desde, al menos, diciembre de 2025. El actor mantiene canales de Telegram en @Persy_PCP y @teampcp, e incluye la cadena "TeamPCP Cloud stealer" en sus cargas. Wiz ha seguido toda la campaña (Wiz Threat Center; blog de Wiz; ramimac.me).
El compromiso de LiteLLM es la fase 09 de una campaña en curso. Todas las operaciones comparten una infraestructura constante: el mismo par de claves RSA, el mismo nombre de paquete tpcp.tar.gz y repositorios de GitHub con el prefijo tpcp-docs que se usan como puntos de entrega temporal para preparar C2. Los tres dominios de esta operación comparten registrador (Spaceship, Inc.) y proveedor de alojamiento (DEMENIN B.V.).
El actor también ha desplegado CanisterWorm, que usa Internet Computer Protocol (ICP) como canal C2. Los registradores de dominios y los proveedores de alojamiento no pueden dar de baja los canisters de ICP. Investigadores de seguridad de Aikido documentan este caso como el primer uso observado de ICP como mecanismo C2 en una campaña de cadena de suministro.
Un componente llamado hackerbot-claw usa un agente de IA (openclaw) para automatizar la selección de objetivos de ataque. Investigadores de Aikido documentaron este caso como uno de los primeros usos operativos de un agente de IA en un ataque a la cadena de suministro.
Supresión de problemas
Cuando los miembros de la comunidad comenzaron a reportar el compromiso en el issue #24512 de GitHub, los atacantes publicaron 88 comentarios de bots desde 73 cuentas únicas en una ventana de 102 segundos (12:44-12:46 UTC). Las cuentas utilizadas eran cuentas de desarrolladores previamente comprometidas, no perfiles creados para este fin. El análisis de Rami McCarthy encontró un 76 % de coincidencia de cuentas con la botnet utilizada durante la divulgación de Trivy.
Con la cuenta de mantenedor comprometida krrishdholakia, los atacantes cerraron el issue #24512 con el estado "not planned" e hicieron commits en repositorios no relacionados con el mensaje "teampcp update."
La comunidad abrió un issue paralelo de seguimiento (#24518) y continuó el debate en Hacker News, donde el hilo alcanzó 324 puntos.
Impacto confirmado
Las versiones afectadas estuvieron en PyPI durante aproximadamente 3 horas. Los siguientes proyectos presentaron PR de seguridad o issues el 24 de marzo para fijar una versión distinta de 1.82.7 y 1.82.8:
Proyecto | Evidencia |
|---|---|
DSPy | PR #9498 integrada; informe de falla de CI |
MLflow | PR #21971 integrada |
OpenHands | |
CrewAI | |
langwatch | |
strands-agents/sdk-python | |
Arize Phoenix | |
nanobot | |
dreadnode/rigging | |
CoPaw | |
Aider | Confirmado como seguro (fija |
El mecanismo .pth se activa cada vez que se inicia un proceso de Python, incluido el propio pip. En entornos de CI/CD, esto significa que la carga maliciosa puede ejecutarse durante los pasos de compilación, no solo al ejecutar la aplicación.
Detección: ¿estás afectado?
Paso 1: Comprueba la versión instalada
Si el resultado muestra 1.82.7 o 1.82.8, considera que el sistema está comprometido y sigue los pasos de corrección que se indican a continuación. No te limites a actualizar: es posible que la carga maliciosa ya se haya ejecutado.
Paso 2: Busca artefactos de persistencia
Paso 3: Busca archivos .pth maliciosos
Paso 4: Verifica los hashes de los archivos
Paso 5: Busca indicadores de red
Paso 6: Revisa Kubernetes
Paso 7: Analiza con Snyk

La alerta en el banner de la aplicación de Snyk muestra los repositorios afectados en el inventario de activos. El enlace al Trust Center lleva al estado del incidente en tiempo real.
Los clientes de Snyk también pueden consultar el asesor de paquetes de litellm y el registro completo de la vulnerabilidad en SNYK-PYTHON-LITELLM-15762713.
Corrección
Si NO instalaste 1.82.7 ni 1.82.8:
Fija la versión en <=1.82.6 hasta que haya una versión limpia:
Si SÍ instalaste 1.82.7 o 1.82.8:
La carga maliciosa se ejecuta al iniciar Python, incluso durante el propio pip install. Considera que el sistema podría estar comprometido, independientemente de si ejecutaste código de alguna aplicación.
Elimina los artefactos de persistencia:
Rota las credenciales del sistema afectado:
Claves privadas SSH: genera claves nuevas y revoca las anteriores en
authorized_keys, GitHub y GitLabCredenciales de la nube: claves de acceso de AWS, claves de cuentas de servicio de GCP y entidades de servicio de Azure
Claves de API: archivos
.env, variables de entorno del shell y secretos de CI/CDCredenciales del registro de Docker:
~/.docker/config.jsonKubernetes:
~/.kube/config, tokens de cuentas de servicio dentro del clústerContraseñas de bases de datos en cualquier archivo de configuración del sistema
Credenciales de Git de
~/.gitconfigo del almacén de credenciales del sistemaFrases semilla de billeteras de criptomonedas
Audita AWS Secrets Manager y SSM Parameter Store, ya que la carga maliciosa consulta estos servicios directamente si puede acceder a los metadatos de la instancia.
Audita los secretos del clúster de Kubernetes. Si había un token de cuenta de servicio, es posible que se hayan leído todos los secretos de todos los espacios de nombres. Busca pods
node-setup-*enkube-system.
Instala una versión limpia en un entorno nuevo en lugar de actualizar la instalación existente:
Por qué la verificación de hashes de pip no detectó el problema
La verificación de hashes confirma que un archivo coincide con lo que PyPI anunció, pero no indica si el contenido anunciado es malicioso.
El archivo litellm_init.pth de la versión 1.82.8 está declarado correctamente en el archivo RECORD del wheel y tiene un hash coincidente. pip install --require-hashes habría finalizado sin errores. El paquete supera todas las comprobaciones de integridad estándar porque el contenido malicioso se publicó con credenciales legítimas: no hay discrepancias de hash, dominios sospechosos ni nombres de paquetes mal escritos.
La única forma de detectar el problema durante la instalación es comprobar si un paquete instala archivos .pth y si estos contienen patrones como subprocess, base64 o exec. Actualmente, ningún complemento de pip de uso generalizado lo hace automáticamente.
El patrón más amplio
La selección de objetivos de esta campaña se centra en herramientas con acceso elevado a canalizaciones automatizadas: un escáner de contenedores (Trivy), una herramienta de análisis de infraestructura (KICS) y una biblioteca de enrutamiento de modelos de IA (LiteLLM). Por diseño, cada una de estas herramientas requiere amplio acceso de lectura a los sistemas con los que opera (credenciales, configuraciones, variables de entorno).
LiteLLM se implementa cada vez más como una puerta de enlace centralizada para LLM que almacena credenciales de API de varios proveedores de modelos. En esa configuración, el conjunto de credenciales al que se puede acceder desde un único host comprometido es más amplio que en una aplicación típica.
La divulgación inicial se difundió a través de comunidades de desarrolladores de IA (r/LocalLLaMA, r/Python, Hacker News), en lugar de los canales de seguridad tradicionales, como r/netsec o los feeds de CVE.
Para leer más sobre los riesgos de la cadena de suministro específicos de las herramientas de LLM, Snyk Learn aborda el tema en Vulnerabilidades de la cadena de suministro en los LLM. El ataque a la cadena de suministro de Ultralytics AI Pwn Request de 2024 también es un caso de comparación útil: otra biblioteca de Python para IA de uso extendido que se vio comprometida mediante un exploit de CI/CD, con una cadena de ataque similar.
Indicadores de compromiso
Hashes de archivos:
Archivo | SHA-256 |
|---|---|
|
|
|
|
|
|
Red:
Exfiltración:
https://models.litellm.cloud/(POST)Consultas C2:
https://checkmarx.zone/raw(GET)
Sistema de archivos:
~/.config/sysmon/sysmon.pyo/root/.config/sysmon/sysmon.py~/.config/systemd/user/sysmon.service(descripción: "System Telemetry Service")/tmp/tpcp.tar.gz,/tmp/session.key,/tmp/payload.enc,/tmp/session.key.enc/tmp/.pg_state,/tmp/pglog
Kubernetes:
Pods:
node-setup-{node_name}enkube-systemNombre del contenedor:
setup, imagen:alpine:latest
Prefijo de la clave pública RSA (codificado de forma fija en las cargas de las tres operaciones):
Qué hacer ahora mismo
Comprueba la versión de litellm:
pip show litellm | grep VersionFija la versión en
<=1.82.6en todos los entornosEjecuta
snyk test --package-manager=pipSi tenías instalada la versión 1.82.7 o 1.82.8: rota las credenciales y busca artefactos de persistencia
Audita las canalizaciones de CI/CD para detectar versiones de herramientas sin fijar, incluidas las GitHub Actions
Revisa Kubernetes:
kubectl get pods -A | grep node-setup-
DOCUMENTO TÉCNICO
La crisis de seguridad de IA en tu entorno de Python
Con el ritmo de desarrollo acelerándose a un ritmo vertiginoso, ¿sabes realmente a qué puede acceder tu entorno de IA?
