El código se ejecutó antes que el modelo
Pluto Security encontró 31 repositorios de Hugging Face que se hacían pasar por pequeños checkpoints de prueba de Qwen2. Ninguno contenía un modelo. Cada uno contenía un archivo Python que se ejecuta en cuanto el modelo se carga con trust_remote_code e instala un minero oculto, hooks de arranque y, en algunas versiones, una reverse shell. El repositorio principal pasó los escáneres de Hugging Face y llegó a 28.412 descargas, y una copia casi idéntica siguió en línea después de la retirada. Los agentes de código recurren a modelos de prueba diminutos cuando escriben tests. En Bromure Agentic Coding, esa carga se ejecuta en una VM desde la que el minero no alcanza su pool, la Security Timeline vincula cada conexión con la llamada a herramienta que la abrió, y un rewind elimina lo que plantó el payload.
El archivo de pesos medía 42 bytes y no tenía ningún tensor. El trabajo lo hacía un archivo Python que se ejecutaba en cuanto se cargaba el modelo, antes de que el código del modelo hiciera nada.
El 29 de octubre, Pluto Security publicó Load/Mine,
su análisis de una campaña activa en Hugging Face y en ModelScope, de
China, desde septiembre. Pluto encontró 31 repositorios maliciosos
repartidos en siete cuentas. Se disfrazan de pequeños checkpoints de
prueba de Qwen2, con nombres como qwen2-tiny, y entre todos sumaban más
de 32.000 descargas registradas. BleepingComputer
lo cubrió el mismo día.
Pluto resume la campaña en una frase: “The code runs before the model does” (el código se ejecuta antes que el modelo).
Cómo cargar un modelo se convierte en un minero
El ataque no usa pesos envenenados ni un exploit de pickle. Usa una
función. Un modelo de Hugging Face puede incluir su propio código Python,
y la biblioteca Transformers ejecuta ese código si quien llama pasa
trust_remote_code=True. El config.json del repositorio indica un
módulo y una clase en una entrada auto_map, como
modeling_evil2.EvilModel. Transformers descarga ese archivo .py y lo
importa. Todo lo que está en el nivel superior del archivo se ejecuta
durante la importación, antes incluso de que se devuelva la clase.
En estos repositorios, model.safetensors mide 42 bytes. No hay ningún
modelo dentro. El archivo Python hace todo el trabajo:
- Se atrinchera. Escribe en
/tmp/.cch/svc.pyun watchdog que relanza el minero cada dos minutos y añade un hook de arranque a~/.bashrc,/root/.bashrcy/root/.profile. Las versiones de la primera oleada también plantan una clave SSH llamadaqx-5090-openauthorized_keysy una tarea cron en/etc/cron.d/.sync-helper. - Descarga el minero y lo esconde. El descargador se construye con
un manejador de proxy vacío, así que ignora los ajustes
HTTP_PROXYyHTTPS_PROXYde los que puede depender una empresa. El binario de 40,6 MB tiene los marcadores UPX alterados para queupx -dlo rechace. Se ejecuta desde un archivo anónimo en memoria, con el nombre de proceso[kworker/u20:1], y mina Pearl a través de Kryptex en el puerto 8048. - Abre una puerta. Tres versiones añaden una reverse shell hacia
212.32.253.3en los puertos 443 o 9443, que se reconecta cada vez que cae. - Va a por el host. Un script relacionado de la segunda oleada monta los discos del host desde un contenedor privilegiado y se da root en el servidor de debajo.
Pasó limpio el escaneo y volvió
Hugging Face analiza las subidas con un antivirus y con herramientas de
socios, de Protect AI y JFrog. Según Pluto, esas herramientas buscan
malware conocido y formatos de modelo inseguros. No miran lo que hace el
archivo Python. El repositorio principal de la segunda oleada,
MENXIANGYU/qwen2-tiny, “scanned clean while it collected 28,412
downloads” (pasó limpio el escaneo mientras acumulaba 28.412 descargas).
El veredicto de un escáner asociado sobre el archivo del payload fue que
era “not a machine-learning model” (no es un modelo de machine learning).
Pluto informó de la primera oleada a Hugging Face y a ModelScope, y ambos
eliminaron las cuentas. En pocos días aparecieron 19 repositorios nuevos
con cuentas nuevas. Horas después del informe de Pluto, el operador
publicó un script de escape de contenedores. Hugging Face desactivó el
repositorio principal, pero BleepingComputer confirmó que una copia casi
idéntica, htt0118/qwen2-tiny, seguía en línea con el mismo minero y la
misma reverse shell.
Un modelo de prueba diminuto es lo que pide tu agente
La campaña se diseñó para servidores con GPU que ejecutan plataformas de
inferencia como Xinference y GPUStack. El cebo también funciona en la
máquina de un desarrollador, porque un agente de código busca un
checkpoint pequeño de Qwen2 cuando escribe tests para código que llama a
un modelo. Pídele a un agente que añada un test a tu pipeline de
embeddings o a tu script de fine-tuning, y saldrá a buscar un modelo lo
bastante pequeño para cargarse en segundos. Los nombres de esta campaña,
qwen2-tiny y sus variantes, están hechos para coincidir con esa
búsqueda.
El agente no tiene que ser descuidado para activar la opción. Cuando un
repositorio incluye código propio, Transformers se detiene y le dice a
quien llama que pase trust_remote_code=True. Para un agente que intenta
arreglar un test que falla, es un error con la solución impresa en el
mensaje. Añade el argumento y vuelve a ejecutar el test. Que el test pase
o no ya da igual: el minero arrancó durante la importación.
En un portátil, ese archivo se ejecuta como tú. El hook de arranque en
~/.bashrc se ejecuta en cada terminal que abras a partir de entonces.
La reverse shell le da al operador un prompt con tu ~/.ssh, tus
credenciales de la nube, tu sesión de gh y tu HF_TOKEN. El consejo de
Pluto para quien haya cargado estos modelos es tratar la máquina como
comprometida y rotar todas las credenciales que el proceso pudiera
alcanzar.
Entre las recomendaciones de bastionado de Pluto, una línea describe la configuración que quieres: ejecutar código de modelos no fiable “with limited privileges, restricted egress and no production secrets” (con privilegios limitados, salida de red restringida y sin secretos de producción).
El mismo test en Bromure Agentic Coding
Bromure Agentic Coding ejecuta Claude Code, Codex, Grok, Kimi y omp en VMs Ubuntu en tu Mac. Cada máquina tiene su propio disco, su propio home y su propia política de red, aplicada en el Mac, donde el agente no llega. Este es el mismo test en una máquina Bromure, una etapa del payload cada vez.
El minero nunca llega a su pool
Cada conexión que abre la VM pasa por el switch virtual y el proxy de Bromure en el Mac. Los puertos 80 y 443 se desvían al proxy en el switch, así que un descargador con un manejador de proxy vacío queda interceptado igualmente. El truco que usa la campaña para saltarse los ajustes de proxy corporativos no tiene nada que esquivar, porque la interceptación no depende de ningún ajuste dentro de la VM.
Pon Unmatched traffic en Deny en Sandbox Policy, o pasa la
máquina al motor OpenShell + Sentry, donde todo se deniega salvo lo
que una regla permite. La política inicial permite GitHub, npm y PyPI en
solo lectura, además de los mirrors de Ubuntu, y tú añades
huggingface.co para las descargas. Entonces la conexión del minero a
prl-us.kryptex.network:8048 no coincide con ninguna regla y se rechaza.
Lo mismo la reverse shell hacia 212.32.253.3, en cualquiera de los dos
puertos. Con Tell the agent when policy blocks it, activado por
defecto en las máquinas OpenShell, el agente también lee en su
conversación qué se bloqueó y que debe parar y preguntarte, en lugar de
pasarse los diez minutos siguientes buscando un rodeo.
En una máquina OpenShell con Enforce binaries, cada conexión también
se asocia al programa que la abrió, y el hash de cada programa se fija la
primera vez que se ejecuta. Un minero lanzado desde un archivo anónimo en
memoria es un programa que ninguna regla nombra. Renombrar su proceso a
[kworker/u20:1] no cambia eso, porque el atestador identifica el
binario, no el nombre que se pone.
Ves qué llamada a herramienta lo hizo
El operador eligió el nombre [kworker/u20:1] para engañar a quien lea
ps o top en un servidor cargado. En una máquina Bromure con el
centinela del kernel, el kernel de la VM informa al Mac de cada
programa que se lanza y de cada conexión, por un canal privado que el
agente no puede cortar. Cada conexión bloqueada llega a la Security
Timeline con la cadena que hay detrás, algo como
Tool pytest tests/test_embed.py: claude → bash → python → TCP 212.32.253.3:443.
Haz doble clic en la fila y network lineage muestra la historia completa: el prompt que diste, lo que el agente escribió antes de la llamada, el comando exacto, cada proceso desde el agente hasta el que se conectó, y qué capa lo rechazó. Cuando una ejecución de tests marca una dirección IP a pelo, obtienes una fila roja con el comando de la carga del modelo.
El watchdog del agente, configurado en Quarantine en Guardrails, actúa sobre las mismas señales sin esperarte. Una ráfaga de bloqueos, o destinos nuevos una vez asentada la sesión, por encima de su umbral en menos de un minuto, y Bromure corta la red de la VM y la pausa en milisegundos. La alerta enumera lo que vio. Keep quarantined reanuda la VM con la red todavía cortada, para que puedas mirar dentro.
Las claves en la VM son sustitutas
Supongamos que una versión del payload llega más lejos, en una máquina
que dejaste con el firewall por defecto. El operador tiene una shell en
una VM Ubuntu, y cada credencial que hay en ella es una sustituta. El
GH_TOKEN del entorno y el token de ~/.git-credentials son falsos, y
el proxy de tu Mac los cambia por el verdadero solo de camino a GitHub.
Añade tu token de Hugging Face en Other API keys con el nombre de
variable HF_TOKEN y un filtro de host huggingface.co, y la VM recibe
una sustituta brm_… que solo funciona allí. Las claves SSH nunca entran
en la VM: el agente puede pedir una firma, y los bytes de la clave se
quedan en el Mac.
Una sustituta también es una trampa. Si el payload envía la sustituta de GitHub a cualquier sitio que no sea GitHub por HTTPS, el proxy bloquea la petición con HTTP 451, pausa la VM y abre una alerta con el nombre de la credencial y el host al que iba.
La limpieza son dos clics
La lista de limpieza de Pluto para un host infectado es larga: encontrar
el watchdog, quitar los hooks de la shell, borrar la tarea cron, sacar la
clave SSH de seis rutas authorized_keys, buscar registros fraudulentos
y reconstruir desde una imagen de confianza si no tienes certeza. Una
máquina Bromure ya tiene esa imagen de confianza. Reset disk vuelve a
clonar el disco del sistema desde la imagen base, lo que se lleva
/etc/cron.d/.sync-helper y todo lo demás que el payload escribió fuera
del home. Rewind home… devuelve /home/ubuntu a un punto de control
de un arranque anterior. Bromure toma uno cada vez que la máquina
arranca, así que el ~/.bashrc de antes del test es uno de ellos. Tus
carpetas compartidas con el host están en el Mac y no se tocan.
En una máquina OpenShell con Enforce binaries, buena parte de esa lista
ni siquiera llega a escribirse. El agente se ejecuta sin root, así que
los hooks en /root/.bashrc y /root/.profile, la tarea cron y las
escrituras en el authorized_keys de root se rechazan, y cada rechazo es
una fila Guest sandbox en la Timeline.
La salida se decide en el Mac
Los puertos 80 y 443 van al proxy digan lo que digan los ajustes de la VM. Con el tráfico sin coincidencia en denegar, el pool y la shell se rechazan.
Cada conexión tiene nombre
El centinela del kernel vincula cada conexión a la llamada a herramienta y a la cadena de procesos que hay detrás. Un minero disfrazado sigue apareciendo como la carga del modelo.
Sustitutas, no claves
Los tokens de GitHub, de la nube y de Hugging Face en la VM son falsos que solo funcionan en sus propios hosts. En otro sitio, disparan una alerta.
Una máquina limpia cuando quieras
Reset disk y Rewind home devuelven la VM a como estaba antes del test, persistencia incluida.
Trata un repositorio de modelos como un paquete
Pluto enumera sus indicadores en el informe. Si cargaste uno de estos
modelos con el código propio activado, busca /tmp/.cch/, el proceso
[kworker/u20:1] con argumentos de minería y los hooks de arranque, y
después rota todas las credenciales que esa máquina pudiera alcanzar.
Antes de activar trust_remote_code para cualquier repositorio, lee su
modeling_*.py y fija el commit que leíste.
El informe de Pluto muestra un repositorio de modelos funcionando como un repositorio de código mientras los escáneres lo revisaban como un formato de archivo. Si tu agente elige modelos para sus tests, acabará cargando uno de estos archivos. En una máquina Bromure, ese archivo se ejecuta en una VM con claves sustitutas y una política de salida aplicada en el Mac, y después puedes devolver la máquina a un punto de control limpio.
Para un caso anterior en el que falló la propia comprobación de
trust_remote_code, lee La flag dijo que no.
Para ver cómo Bromure filtra los paquetes que instala un agente, lee La
versión tenía dos horas.