Skip to main content

Como um scanner de segurança comprometido abriu caminho para backdoor no LiteLLM

Escrito por
illustration hero ai

24 de março de 2026

0 minutos de leitura

Em 24 de março de 2026, duas versões do pacote Python litellm no PyPI foram identificadas com código malicioso. Os pacotes (versões 1.82.7 e 1.82.8) foram publicados pelo agente de ameaças conhecido como TeamPCP, que obteve as credenciais do PyPI do mantenedor após um comprometimento anterior do Trivy, um scanner de segurança de código aberto usado no pipeline de CI/CD do LiteLLM.

As versões maliciosas ficaram disponíveis por aproximadamente três horas, até que o PyPI colocou o pacote em quarentena. O LiteLLM é baixado cerca de 3,4 milhões de vezes por dia.

A Snyk está acompanhando esse incidente. Se você é cliente da Snyk, talvez já tenha visto o alerta no banner do app e recebido uma notificação por e-mail. O registro da vulnerabilidade é SNYK-PYTHON-LITELLM-15762713, e as atualizações de status estão no Snyk Trust Center.

Resumo

Pacote afetado

litellm (PyPI)

Versões afetadas

1.82.7, 1.82.8

Versões seguras

≤ 1.82.6

ID da Snyk

Primeira detecção

10:39 UTC, 24 de março de 2026 (upload da versão 1.82.7)

Quarentena no PyPI

\~13:38 UTC, 24 de março de 2026

Atacante

TeamPCP (também conhecido como PCPcat, Persy_PCP, ShellForce, DeadCatx3)

Vetor de ataque

Cadeia de suprimentos: credenciais comprometidas de publicação no PyPI por meio de uma GitHub Action do Trivy adulterada no CI/CD do LiteLLM

Tipo de payload

Três etapas: coleta de credenciais + exfiltração criptografada + backdoor persistente + worm de Kubernetes

Domínio de exfiltração

models.litellm.cloud (registrado em 23 de março de 2026)

MITRE ATT\&CK

T1546.018 (Python Startup Hooks), T1003 (Credential Dumping), T1610 (Deploy Container)

Principais eventos

Horário (UTC)

Evidência

Evento

Final de fev. de 2026

MegaGame10418 faz um Pwn Request contra o CI do Trivy e explora um workflow pull_request_target para exfiltrar as credenciais do aqua-bot

19 de mar., 17:43 UTC

As tags da GitHub Action do Trivy v0.69.4 são reescritas para apontar para uma versão maliciosa

23 de mar., 12:58 UTC

Endor Labs (capturou os metadados do PyPI antes da exclusão)

GitHub Action do Checkmarx KICS comprometida; domínio de C2 checkmarx.zone e models.litellm.cloud registrados

24 de mar., 10:39 UTC

Endor Labs (capturou os metadados do PyPI antes da exclusão)

litellm 1.82.7 malicioso publicado no PyPI

24 de mar., 10:52 UTC

litellm 1.82.8 malicioso publicado no PyPI (13 minutos após a versão 1.82.7, com um mecanismo de distribuição .pth mais abrangente)

24 de mar., 11:48 UTC

FutureSearch (Callum McMahon) abre uma issue para divulgar o problema

24 de mar., 12:36 UTC

Tópico publicado no HN; chega a 324 pontos

24 de mar., \~12:44 UTC

Issue #24512 no GitHub (visível nos horários dos comentários)

Comentários de bots inundam a issue #24512; a issue é encerrada usando a conta comprometida do mantenedor

24 de mar., 13:03 UTC

FutureSearch (atualização com data e hora)

FutureSearch confirma o encerramento da issue e o spam de bots

24 de mar., 13:48 UTC

Nova issue de acompanhamento aberta

24 de mar., 15:09 UTC

Mantenedor do LiteLLM confirma que todas as chaves do GitHub, Docker e PyPI foram trocadas; as contas dos mantenedores foram migradas para novas identidades

24 de mar., 15:27 UTC

Versões comprometidas excluídas; pacote retirado da quarentena no PyPI

Como o problema foi descoberto

Callum McMahon, da FutureSearch, estava testando um plugin Cursor MCP que incluía litellm como dependência transitiva. Pouco depois da inicialização do Python, a máquina parou de responder por falta de RAM. Ele rastreou o problema até o pacote litellm recém-instalado e encontrou litellm_init.pth, um arquivo de 34.628 bytes em site-packages/, codificado duas vezes em base64.

A falta de RAM foi um efeito colateral do payload, não um recurso intencional. O mecanismo .pth é acionado a cada inicialização do interpretador Python. Como o payload inicia um novo subprocesso Python, e esse novo processo também aciona a execução de .pth, o resultado foi um fork bomb não intencional. McMahon publicou suas descobertas em futuresearch.ai, e a divulgação chegou a r/LocalLLaMA, r/Python e Hacker News em menos de uma hora.

A cadeia de ataque

O ataque ao LiteLLM começou cinco dias antes, com o Trivy.

19 de março: Os atacantes reescreveram tags do Git no repositório da GitHub Action trivy-action para apontar para uma versão maliciosa (v0.69.4) que continha o mesmo payload de coleta de credenciais e a mesma infraestrutura de exfiltração usados em operações posteriores. (Para saber todos os detalhes sobre o comprometimento do Trivy, consulte a cobertura da Snyk sobre o comprometimento da cadeia de suprimentos da GitHub Actions do Trivy.)

23 de março: A mesma infraestrutura foi usada em um ataque separado ao Checkmarx KICS (Keep Infrastructure as Code Secure). O domínio de C2 checkmarx.zone, que se passa pela empresa de segurança Checkmarx, foi registrado e ativado nessa operação.

24 de março: O pipeline de CI/CD do LiteLLM executou o Trivy como parte do processo de build, instalando-o de apt sem fixar uma versão. A action comprometida exfiltrou o token PYPI_PUBLISH do ambiente do runner do GitHub Actions. Com essa credencial, os atacantes publicaram litellm 1.82.7 às 10:39 UTC e 1.82.8 às 10:52 UTC, ambas com payloads maliciosos.

Comentário no Hacker News de um responsável pela manutenção do LiteLLM sobre um comprometimento da cadeia de suprimentos, explicando a vulnerabilidade de CI/CD, o impacto limitado no proxy Docker e o status de quarentena no PyPI.

Resposta inicial do mantenedor do LiteLLM no Hacker News.

Dois mecanismos de distribuição

As duas versões maliciosas usaram técnicas de injeção diferentes:

1.82.7 (injeção no código-fonte): O payload foi codificado em base64 e inserido diretamente em litellm/proxy/proxy_server.py. Ele é executado quando qualquer coisa importa litellm.proxy, o caminho de importação padrão para o modo de servidor proxy do LiteLLM.

1.82.8 (arquivo .pth): Esta versão adicionou litellm_init.pth a site-packages/. O mecanismo .pth é acionado a cada inicialização do interpretador Python, sem necessidade de importação, inclusive quando pip, python -c ou o servidor de linguagem de uma IDE inicia o Python.

O arquivo .pth está declarado corretamente no arquivo RECORD do wheel e, por isso, passa na verificação de hash do pip. O pacote passa por todas as verificações de integridade padrão porque o conteúdo malicioso foi publicado com credenciais legítimas, e não injetado depois. A Snyk Learn apresenta um guia detalhado sobre como esse tipo de ataque funciona em Comprometimento de um pacote legítimo.

Os mantenedores do CPython reconheceram o risco à segurança dos hooks de inicialização .pth na issue #113659, mas nenhuma correção foi aplicada. Essa técnica corresponde à MITRE ATT\&CK T1546.018 (Python Startup Hooks).

O payload em três etapas

Etapa 1: coleta de informações

O script coleta:

  • Dados do sistema: nome do host, variáveis de ambiente, processos em execução, roteamento de rede e registros recentes de autenticação em /var/log/auth.log

  • Credenciais: chaves privadas SSH, arquivos .env, credenciais do Git, chaves de API do histórico do shell, tokens de webhook do Slack e Discord, configurações de CI/CD (Jenkins, Travis CI, Terraform) e hashes de senhas em /etc/shadow

  • Credenciais de nuvem: credenciais e configurações da AWS, arquivos de contas de serviço do GCP e Azure, metadados de nuvem por meio de consultas IMDS diretas, AWS Secrets Manager e SSM Parameter Store (com assinatura completa do IMDSv2 implementada no script)

  • Contêineres e orquestração: config.json do Docker (credenciais de registro), arquivos kubeconfig do Kubernetes, tokens de contas de serviço, segredos e certificados

  • Criptomoedas: arquivos de carteiras e frases-semente de Bitcoin, Ethereum, Solana, Cardano e Monero, além de configurações de carteiras físicas Ledger

Imagem 2

Trecho do código malicioso decodificado, mostrando a iteração estruturada e a coleta de segredos em vários diretórios

Etapa 2: criptografia e exfiltração

Os dados coletados são criptografados antes da transmissão:

  1. Uma chave de sessão AES-256 de 32 bytes é gerada com openssl rand

  2. Os dados são criptografados com AES-256-CBC (derivação de chave PBKDF2)

  3. A chave de sessão é criptografada com uma chave pública RSA de 4096 bits embutida no código (preenchimento OAEP)

  4. Tudo é empacotado em tpcp.tar.gz

  5. O pacote é enviado por POST para https://models.litellm.cloud/ usando curl

models.litellm.cloud é diferente do domínio legítimo litellm.ai e foi registrado em 23 de março, um dia antes da publicação dos pacotes maliciosos.

A chave pública RSA desse payload é idêntica à encontrada nos payloads do Trivy e do KICS. Pesquisadores da Wiz confirmaram essa correspondência nas três operações, estabelecendo o vínculo técnico de atribuição mais forte.

Arquivos temporários criados durante a exfiltração: session.key, payload.enc, session.key.enc, tpcp.tar.gz no diretório temporário do sistema.

Imagem image3

Visualização neutralizada do código malicioso. O nome do arquivo tpcp.tar.gz é um indicador consistente nas operações do Trivy, KICS e LiteLLM.

Etapa 3: persistência e movimento lateral

Persistência local:

  • Grava um script Python de backdoor em ~/.config/sysmon/sysmon.py (ou /root/.config/sysmon/sysmon.py)

  • Cria um serviço de usuário do systemd em ~/.config/systemd/user/sysmon.service com a descrição "System Telemetry Service"

  • Registra e ativa o serviço: systemctl --user daemon-reload && systemctl --user enable --now sysmon.service

O script de persistência consulta https://checkmarx.zone/raw a cada 5 minutos em busca de uma URL, baixa o arquivo para /tmp/pglog, dá permissão de execução e o executa em segundo plano. O estado é registrado em /tmp/.pg_state. No momento da análise, o endpoint retornava URLs do YouTube; o operador pode passar a distribuir payloads ativos quando quiser.

Movimento lateral no Kubernetes: Se o script encontra um token de conta de serviço do Kubernetes no caminho de montagem padrão, ele lê todos os segredos de todos os namespaces. Em seguida, tenta implantar um pod privilegiado em cada nó de kube-system usando alpine:latest. Esses pods montam o sistema de arquivos do host e instalam o backdoor sysmon no nó subjacente.

Os pods maliciosos recebem o nome node-setup-{node_name} (com o nome do nó truncado para 35 caracteres) e contêm um contêiner chamado setup.

Sobre o TeamPCP

O TeamPCP (também identificado como PCPcat, Persy_PCP, ShellForce e DeadCatx3, segundo o Wiz Threat Center) está ativo desde pelo menos dezembro de 2025. O agente mantém canais no Telegram, @Persy_PCP e @teampcp, e inclui a string "TeamPCP Cloud stealer" nos payloads. A Wiz acompanha a campanha completa (Wiz Threat Center; blog da Wiz; ramimac.me).

O comprometimento do LiteLLM é a Fase 09 de uma campanha em andamento. Todas as operações compartilham uma infraestrutura consistente: o mesmo par de chaves RSA, o mesmo nome de pacote tpcp.tar.gz e repositórios do GitHub com prefixo tpcp-docs, usados como pontos de apoio para preparar o C2. Os três domínios dessa operação compartilham o mesmo registrador (Spaceship, Inc.) e provedor de hospedagem (DEMENIN B.V.).

O agente também implantou o CanisterWorm, que usa o Internet Computer Protocol (ICP) como canal de C2. Os canisters do ICP não podem ser desativados por registradores de domínio nem por provedores de hospedagem. Pesquisadores de segurança da Aikido documentam esse caso como o primeiro uso observado do ICP como mecanismo de C2 em uma campanha da cadeia de suprimentos.

Um componente chamado hackerbot-claw usa um agente de IA (openclaw) para direcionar ataques de forma automatizada. Pesquisadores da Aikido documentaram esse caso como um dos primeiros usos operacionais de um agente de IA em um ataque à cadeia de suprimentos.

Supressão de issues

Quando membros da comunidade começaram a relatar o comprometimento na issue #24512 do GitHub, os invasores publicaram 88 comentários de bots usando 73 contas únicas em uma janela de 102 segundos (12:44–12:46 UTC). As contas usadas eram contas de desenvolvedores previamente comprometidas, não perfis criados para esse fim. A análise de Rami McCarthy constatou uma sobreposição de 76% de contas com a botnet usada durante a divulgação do caso Trivy.

Usando a conta de mantenedor comprometida krrishdholakia, os invasores fecharam a issue #24512 como "not planned" e fizeram commits em repositórios sem relação com o caso, usando a mensagem "teampcp update."

A comunidade abriu uma issue paralela para acompanhar o caso (#24518) e continuou a discussão no Hacker News, onde a thread chegou a 324 pontos.

Impacto confirmado

As versões afetadas ficaram no PyPI por aproximadamente 3 horas. Os projetos a seguir abriram PRs ou issues de segurança em 24 de março para fixar a versão e evitar a 1.82.7 e a 1.82.8:

Projeto

Evidência

DSPy

MLflow

PR #21971 mesclado

OpenHands

CrewAI

PR #5040 (desacoplado do litellm); PR #5039

langwatch

strands-agents/sdk-python

Arize Phoenix

nanobot

dreadnode/rigging

CoPaw

Aider

Confirmado como seguro (fixa a versão litellm==1.82.3)

O mecanismo .pth é acionado quando qualquer processo Python é iniciado, inclusive o próprio pip. Em ambientes de CI/CD, isso significa que o payload pode ser executado durante as etapas de build, e não apenas em tempo de execução da aplicação.

Detecção: você foi afetado?

Etapa 1: confira a versão instalada

pip show litellm | grep Version

Se a saída mostrar 1.82.7 ou 1.82.8, considere o sistema comprometido e siga as instruções de correção abaixo. Não basta fazer upgrade: o payload pode já ter sido executado.

Etapa 2: procure artefatos de persistência

# Check for the sysmon backdoor
ls -la ~/.config/sysmon/sysmon.py 2>/dev/null && echo "BACKDOOR FOUND"
ls -la /root/.config/sysmon/sysmon.py 2>/dev/null && echo "ROOT BACKDOOR FOUND"

# Check for the systemd persistence service
systemctl --user status sysmon.service 2>/dev/null
ls -la ~/.config/systemd/user/sysmon.service 2>/dev/null && echo "PERSISTENCE SERVICE FOUND"

# Check for exfiltration archive remnants
ls /tmp/tpcp.tar.gz /tmp/session.key /tmp/payload.enc /tmp/session.key.enc 2>/dev/null && echo "EXFIL ARTIFACTS FOUND"

Etapa 3: procure arquivos .pth maliciosos

# Find .pth files in site-packages with suspicious patterns
find $(python3 -c "import site; print(' '.join(site.getsitepackages()))") \
  -name "*.pth" -exec grep -l "base64\|subprocess\|exec" {} \;

Etapa 4: verifique os hashes dos arquivos

# Check proxy_server.py (1.82.7)
find / -path "*/litellm/proxy/proxy_server.py" 2>/dev/null -exec shasum -a 256 {} \;
# Malicious: a0d229be8efcb2f9135e2ad55ba275b76ddcfeb55fa4370e0a522a5bdee0120b

# Check litellm_init.pth (1.82.8)
find / -name "litellm_init.pth" 2>/dev/null -exec shasum -a 256 {} \;
# Malicious: 71e35aef03099cd1f2d6446734273025a163597de93912df321ef118bf135238

Etapa 5: procure indicadores de rede

grep "litellm.cloud\|checkmarx.zone" /etc/hosts
grep "models.litellm.cloud\|checkmarx.zone" /var/log/syslog 2>/dev/null

Etapa 6: verifique o Kubernetes

kubectl get pods -A | grep "node-setup-"

Etapa 7: faça uma varredura com o Snyk

snyk test --package-manager=pip
Painel de segurança da Snyk mostrando métricas de repositórios, como 61% testados, repositórios inativos e uma lista de repositórios de classe A de alto risco com vulnerabilidades críticas.

O alerta em banner no app do Snyk, com os repositórios afetados exibidos no inventário de ativos. O link do Trust Center leva ao status do incidente em tempo real.

Clientes do Snyk também podem consultar o consultor do pacote litellm e o registro completo da vulnerabilidade em SNYK-PYTHON-LITELLM-15762713.

Correção

Se você NÃO instalou a versão 1.82.7 ou 1.82.8:

Fixe a versão em <=1.82.6 até que uma versão limpa esteja disponível:

pip install "litellm<=1.82.6"
# requirements.txt:
litellm<=1.82.6

Se você instalou a versão 1.82.7 ou 1.82.8:

O payload é executado quando o Python é iniciado, inclusive durante o próprio pip install. Considere o sistema potencialmente comprometido, independentemente de você ter executado algum código de aplicação.

  1. Remova os artefatos de persistência:

rm -f ~/.config/sysmon/sysmon.py
rm -f ~/.config/systemd/user/sysmon.service
systemctl --user disable sysmon.service 2>/dev/null
systemctl --user daemon-reload
rm -f /tmp/tpcp.tar.gz /tmp/session.key /tmp/payload.enc /tmp/session.key.enc /tmp/.pg_state /tmp/pglog
  1. Troque as credenciais no sistema afetado:

  • Chaves privadas SSH: gere novas chaves e revogue as antigas em authorized_keys, GitHub e GitLab

  • Credenciais de nuvem: chaves de acesso da AWS, chaves de conta de serviço do GCP e entidades de serviço do Azure

  • Chaves de API: arquivos .env, variáveis de ambiente do shell e segredos de CI/CD

  • Credenciais de registro do Docker: ~/.docker/config.json

  • Kubernetes: ~/.kube/config, tokens de conta de serviço no cluster

  • Senhas de banco de dados em qualquer arquivo de configuração do sistema

  • Credenciais do Git em ~/.gitconfig ou no armazenamento de credenciais do sistema

  • Frases-semente de carteiras de criptomoedas

  1. Faça uma auditoria no AWS Secrets Manager e no SSM Parameter Store, pois o payload consulta esses serviços diretamente quando os metadados da instância estão acessíveis.

  1. Faça uma auditoria nos segredos do cluster Kubernetes. Se havia um token de conta de serviço, todos os segredos de todos os namespaces podem ter sido lidos. Procure pods node-setup-* em kube-system.

  1. Instale uma versão limpa em um ambiente novo, em vez de fazer upgrade no ambiente atual:

pip install "litellm<=1.82.6"

Por que a verificação de hash do pip não detectou o problema

A verificação de hash confirma que um arquivo corresponde ao que o PyPI anunciou, mas não indica se o conteúdo anunciado é malicioso.

O arquivo litellm_init.pth na versão 1.82.8 está declarado corretamente no arquivo RECORD do wheel, com um hash correspondente. O comando pip install --require-hashes teria sido concluído sem problemas. O pacote passa por todas as verificações de integridade padrão porque o conteúdo malicioso foi publicado com credenciais legítimas: não há divergência de hash, domínios suspeitos nem nome de pacote com erro de digitação.

A única forma de detectar o problema durante a instalação é verificar se um pacote instala arquivos .pth e se esses arquivos contêm padrões como subprocess, base64 ou exec. Atualmente, nenhum plugin do pip amplamente utilizado faz isso automaticamente.

O padrão mais amplo

A seleção de alvos nesta campanha se concentra em ferramentas com acesso elevado a pipelines automatizados: um scanner de contêineres (Trivy), uma ferramenta de varredura de infraestrutura (KICS) e uma biblioteca de roteamento de modelos de IA (LiteLLM). Por definição, cada uma dessas ferramentas precisa de amplo acesso de leitura aos sistemas com que opera (credenciais, configurações e variáveis de ambiente).

O LiteLLM vem sendo cada vez mais usado como gateway centralizado de LLMs que armazena credenciais de API de vários provedores de modelos. Nessa configuração, o conjunto de credenciais acessível a partir de um único host comprometido é mais amplo do que o de uma aplicação típica.

A divulgação inicial se espalhou por comunidades de desenvolvedores de IA (r/LocalLLaMA, r/Python, Hacker News), e não por canais tradicionais de segurança, como r/netsec ou feeds de CVEs.

Para saber mais sobre riscos à cadeia de suprimentos específicos de ferramentas para LLMs, confira o conteúdo do Snyk Learn sobre vulnerabilidades na cadeia de suprimentos de LLMs. O ataque à cadeia de suprimentos do Ultralytics AI Pwn Request, de 2024, também é um caso útil para comparação: outra biblioteca Python de IA amplamente usada foi comprometida por meio de uma exploração de CI/CD, em uma cadeia de ataque semelhante.

Indicadores de comprometimento

Hashes dos arquivos:

Arquivo

SHA-256

litellm_init.pth (1.82.8)

71e35aef03099cd1f2d6446734273025a163597de93912df321ef118bf135238

proxy_server.py (1.82.7)

a0d229be8efcb2f9135e2ad55ba275b76ddcfeb55fa4370e0a522a5bdee0120b

sysmon.py

6cf223aea68b0e8031ff68251e30b6017a0513fe152e235c26f248ba1e15c92a

Rede:

  • Exfiltração: https://models.litellm.cloud/ (POST)

  • Consulta ao C2: https://checkmarx.zone/raw (GET)

Sistema de arquivos:

  • ~/.config/sysmon/sysmon.py ou /root/.config/sysmon/sysmon.py

  • ~/.config/systemd/user/sysmon.service (descrição: "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} em kube-system

  • Nome do contêiner: setup, imagem: alpine:latest

Prefixo da chave pública RSA (codificado nos payloads das três operações):

MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvahaZDo8mucujrT15ry+...

O que fazer agora

  1. Confira a versão do litellm: pip show litellm | grep Version

  2. Fixe a versão em <=1.82.6 em todos os ambientes

  3. Execute snyk test --package-manager=pip

  4. Se você tinha a versão 1.82.7 ou 1.82.8 instalada: troque as credenciais e procure artefatos de persistência

  5. Faça uma auditoria nos pipelines de CI/CD em busca de versões de ferramentas sem fixação, incluindo GitHub Actions

  6. Verifique o Kubernetes: kubectl get pods -A | grep node-setup-

WHITE PAPER

A crise de segurança da IA no seu ambiente Python

Com o ritmo de desenvolvimento acelerando cada vez mais, você sabe o que o seu ambiente de IA pode acessar?