O código rodou antes do modelo
A Pluto Security encontrou 31 repositórios no Hugging Face disfarçados de pequenos checkpoints de teste do Qwen2. Nenhum continha um modelo. Cada um continha um arquivo Python que roda no instante em que o modelo é carregado com trust_remote_code, instalando um minerador oculto, hooks de inicialização e, em algumas versões, um reverse shell. O repositório principal passou pelos scanners do Hugging Face e chegou a 28.412 downloads, e uma cópia quase idêntica continuou no ar depois da remoção. Agentes de código recorrem a pequenos modelos de teste quando escrevem testes. No Bromure Agentic Coding, esse carregamento roda em uma VM de onde o minerador não alcança seu pool, a Security Timeline liga cada conexão à chamada de ferramenta que a abriu, e um rewind remove o que o payload plantou.
O arquivo de pesos tinha 42 bytes e nenhum tensor. O trabalho era feito por um arquivo Python que rodava assim que o modelo era carregado, antes que qualquer código do modelo fizesse alguma coisa.
Em 29 de outubro, a Pluto Security publicou Load/Mine,
sua análise de uma campanha que roda no Hugging Face e no ModelScope, da
China, desde setembro. A Pluto encontrou 31 repositórios maliciosos em
sete contas. Eles se apresentam como pequenos checkpoints de teste do
Qwen2, com nomes como qwen2-tiny, e juntos somavam mais de 32.000
downloads registrados. O BleepingComputer
noticiou no mesmo dia.
A Pluto resume a campanha em uma frase: “The code runs before the model does” (o código roda antes do modelo).
Como carregar um modelo vira um minerador
O ataque não usa pesos envenenados nem um exploit de pickle. Usa um
recurso. Um modelo do Hugging Face pode trazer seu próprio código Python,
e a biblioteca Transformers executa esse código se quem chama passar
trust_remote_code=True. O config.json do repositório indica um módulo
e uma classe em uma entrada auto_map, como modeling_evil2.EvilModel.
O Transformers baixa esse arquivo .py e o importa. Tudo o que está no
nível superior do arquivo roda durante o import, antes mesmo de a classe
ser devolvida.
Nesses repositórios, model.safetensors tem 42 bytes. Não há modelo
nenhum ali. O arquivo Python faz todo o trabalho:
- Ele se instala. Grava em
/tmp/.cch/svc.pyum watchdog que relança o minerador a cada dois minutos e acrescenta um hook de inicialização a~/.bashrc,/root/.bashrce/root/.profile. As versões da primeira onda também plantam uma chave SSH chamadaqx-5090-opemauthorized_keyse um cron job em/etc/cron.d/.sync-helper. - Ele baixa o minerador e o esconde. O downloader é montado com um
handler de proxy vazio, então ignora as configurações
HTTP_PROXYeHTTPS_PROXYdas quais uma empresa pode depender. O binário de 40,6 MB tem os marcadores UPX alterados para queupx -do recuse. Ele roda a partir de um arquivo anônimo em memória, com o nome de processo[kworker/u20:1], e minera Pearl pela Kryptex na porta 8048. - Ele abre uma porta. Três versões acrescentam um reverse shell para
212.32.253.3nas portas 443 ou 9443, que se reconecta sempre que cai. - Ele vai atrás do host. Um script relacionado da segunda onda monta os discos do host a partir de um contêiner privilegiado e se dá root no servidor por baixo.
Passou limpo pelo scanner e voltou
O Hugging Face verifica os uploads com antivírus e com ferramentas de
parceiros, da Protect AI e da JFrog. Segundo a Pluto, essas ferramentas
procuram malware conhecido e formatos de modelo inseguros. Elas não olham
o que o arquivo Python faz. O repositório principal da segunda onda,
MENXIANGYU/qwen2-tiny, “scanned clean while it collected 28,412
downloads” (passou limpo pelo scanner enquanto acumulava 28.412
downloads). O veredito de um scanner parceiro sobre o arquivo do payload
foi que ele era “not a machine-learning model” (não é um modelo de
machine learning).
A Pluto reportou a primeira onda ao Hugging Face e ao ModelScope, e os
dois removeram as contas. Em poucos dias, 19 novos repositórios apareceram
em contas novas. Horas depois do relatório da Pluto, o operador publicou
um script de fuga de contêiner. O Hugging Face desativou o repositório
principal, mas o BleepingComputer confirmou que uma cópia quase idêntica,
htt0118/qwen2-tiny, continuava no ar com o mesmo minerador e o mesmo
reverse shell.
Um modelo de teste minúsculo é o que seu agente procura
A campanha foi feita para servidores com GPU que rodam plataformas de
inferência como Xinference e GPUStack. A isca também funciona na máquina
de um desenvolvedor, porque um agente de código procura um checkpoint
pequeno do Qwen2 quando escreve testes para código que chama um modelo.
Peça a um agente para adicionar um teste ao seu pipeline de embeddings ou
ao seu script de fine-tuning, e ele vai procurar um modelo pequeno o
bastante para carregar em segundos. Os nomes desta campanha, qwen2-tiny
e suas variantes, foram feitos para casar com essa busca.
O agente não precisa ser descuidado para ativar a flag. Quando um
repositório traz código próprio, o Transformers para e diz a quem chama
para passar trust_remote_code=True. Para um agente tentando consertar
um teste que falha, isso é um erro com a correção impressa na mensagem.
Ele adiciona o argumento e roda o teste de novo. Se o teste passa ou não,
já não importa: o minerador começou durante o import.
Em um notebook, esse arquivo roda como você. O hook de inicialização em
~/.bashrc roda em todo terminal que você abrir depois disso. O reverse
shell dá ao operador um prompt com o seu ~/.ssh, suas credenciais de
nuvem, seu login do gh e o seu HF_TOKEN. O conselho da Pluto para
quem carregou esses modelos é tratar a máquina como comprometida e
rotacionar todas as credenciais que o processo podia alcançar.
Entre as recomendações de hardening da Pluto, uma linha descreve a configuração que você quer: rodar código de modelo não confiável “with limited privileges, restricted egress and no production secrets” (com privilégios limitados, saída de rede restrita e nenhum segredo de produção).
O mesmo teste no Bromure Agentic Coding
O Bromure Agentic Coding roda Claude Code, Codex, Grok, Kimi e omp em VMs Ubuntu no seu Mac. Cada máquina tem seu próprio disco, sua própria home e sua própria política de rede, aplicada no Mac, fora do alcance do agente. Veja o mesmo teste rodando em uma máquina Bromure, uma etapa do payload por vez.
O minerador nunca chega ao pool
Toda conexão que a VM abre passa pelo switch virtual e pelo proxy do Bromure no Mac. As portas 80 e 443 são desviadas para o proxy no switch, então um downloader com um handler de proxy vazio é interceptado do mesmo jeito. O truque que a campanha usa para escapar das configurações de proxy corporativas não tem o que contornar, porque a interceptação não depende de nenhuma configuração dentro da VM.
Coloque Unmatched traffic em Deny em Sandbox Policy, ou passe
a máquina para o motor OpenShell + Sentry, em que tudo é negado a
menos que uma regra permita. A política inicial libera GitHub, npm e PyPI
somente leitura, além dos mirrors do Ubuntu, e você adiciona
huggingface.co para os downloads. Aí a conexão do minerador com
prl-us.kryptex.network:8048 não casa com nenhuma regra e é recusada. O
reverse shell para 212.32.253.3 também, em qualquer das portas. Com
Tell the agent when policy blocks it, ativado por padrão em máquinas
OpenShell, o agente também lê na conversa o que foi bloqueado e que deve
parar e perguntar a você, em vez de passar os próximos dez minutos
tentando dar a volta.
Em uma máquina OpenShell com Enforce binaries, cada conexão também é
associada ao programa que a abriu, e o hash de cada programa é fixado na
primeira vez que ele roda. Um minerador iniciado a partir de um arquivo
anônimo em memória é um programa que nenhuma regra nomeia. Renomear o
processo para [kworker/u20:1] não muda isso, porque o atestador
identifica o binário, não o nome que ele dá a si mesmo.
Você vê qual chamada de ferramenta fez isso
O operador escolheu o nome [kworker/u20:1] para enganar quem lê ps ou
top em um servidor ocupado. Em uma máquina Bromure com a sentinela do
kernel, o kernel da VM informa ao Mac cada programa iniciado e cada
conexão, por um canal privado que o agente não consegue derrubar. Cada
conexão bloqueada entra na Security Timeline com a cadeia por trás
dela, algo como
Tool pytest tests/test_embed.py: claude → bash → python → TCP 212.32.253.3:443.
Dê dois cliques na linha e a network lineage mostra a história inteira: o prompt que você deu, o que o agente escreveu antes da chamada, o comando exato, cada processo do agente até o que se conectou, e qual camada recusou. Quando uma execução de testes disca um endereço IP puro, você ganha uma linha vermelha com o comando do carregamento do modelo.
O watchdog do agente, configurado como Quarantine em Guardrails, age sobre os mesmos sinais sem esperar por você. Uma rajada de bloqueios, ou destinos novos depois que a sessão se estabilizou, acima do limite em menos de um minuto, e o Bromure corta a rede da VM e a pausa em milissegundos. O alerta lista o que ele viu. Keep quarantined retoma a VM com a rede ainda cortada, para você olhar lá dentro.
As chaves na VM são substitutas
Digamos que uma versão do payload vá mais longe, em uma máquina que você
deixou no firewall padrão. O operador tem um shell em uma VM Ubuntu, e
toda credencial ali é uma substituta. O GH_TOKEN no ambiente e o token
em ~/.git-credentials são falsos, e o proxy no seu Mac só os troca pelo
verdadeiro a caminho do GitHub. Adicione seu token do Hugging Face em
Other API keys com o nome de variável HF_TOKEN e um filtro de host
huggingface.co, e a VM recebe uma substituta brm_… que só funciona
lá. Chaves SSH nunca entram na VM: o agente pode pedir uma assinatura, e
os bytes da chave ficam no Mac.
Uma substituta também é uma armadilha. Se o payload mandar a substituta do GitHub para qualquer lugar que não seja o GitHub via HTTPS, o proxy bloqueia a requisição com HTTP 451, pausa a VM e abre um alerta com o nome da credencial e o host para onde ela ia.
A limpeza são dois cliques
A lista de limpeza da Pluto para um host infectado é longa: achar o
watchdog, remover os hooks do shell, apagar o cron job, tirar a chave SSH
de seis caminhos authorized_keys, procurar registros indevidos e
reconstruir a partir de uma imagem confiável se você não tiver certeza.
Uma máquina Bromure já tem essa imagem confiável. Reset disk clona de
novo o disco do sistema a partir da imagem base, o que leva junto
/etc/cron.d/.sync-helper e tudo o mais que o payload gravou fora da
home. Rewind home… volta /home/ubuntu a um checkpoint de uma
inicialização anterior. O Bromure tira um toda vez que a máquina inicia,
então o ~/.bashrc de antes do teste é um deles. Suas pastas
compartilhadas com o host ficam no Mac e não são tocadas.
Em uma máquina OpenShell com Enforce binaries, boa parte dessa lista nem
chega a ser gravada. O agente roda sem root, então os hooks em
/root/.bashrc e /root/.profile, o cron job e as gravações no
authorized_keys do root são recusados, e cada recusa vira uma linha
Guest sandbox na Timeline.
A saída é decidida no Mac
As portas 80 e 443 vão para o proxy, digam o que disserem as configurações da VM. Com o tráfego não correspondido em negar, o pool e o shell são recusados.
Toda conexão tem um nome
A sentinela do kernel liga cada conexão à chamada de ferramenta e à cadeia de processos por trás dela. Um minerador disfarçado ainda aparece como o carregamento do modelo.
Substitutas, não chaves
Tokens do GitHub, de nuvem e do Hugging Face na VM são falsos que só funcionam nos próprios hosts. Em outro lugar, disparam um alerta.
Uma máquina limpa quando quiser
Reset disk e Rewind home devolvem a VM ao estado de antes do teste, persistência incluída.
Trate um repositório de modelo como um pacote
A Pluto lista seus indicadores no relatório. Se você carregou um desses
modelos com código próprio ativado, procure /tmp/.cch/, o processo
[kworker/u20:1] com argumentos de mineração e os hooks de
inicialização, e depois rotacione todas as credenciais que aquela máquina
podia alcançar. Antes de ativar trust_remote_code para qualquer
repositório, leia o modeling_*.py dele e fixe o commit que você leu.
O relatório da Pluto mostra um repositório de modelo funcionando como um repositório de código enquanto os scanners o verificavam como um formato de arquivo. Se o seu agente escolhe modelos para os testes, uma hora vai carregar um desses arquivos. Em uma máquina Bromure, esse arquivo roda em uma VM com chaves substitutas e uma política de saída aplicada no Mac, e depois você pode voltar a máquina a um checkpoint limpo.
Para um caso anterior em que a própria verificação de trust_remote_code
falhou, leia A flag disse não. Para ver como o
Bromure filtra os pacotes que um agente instala, leia A versão tinha
duas horas.