Volver a todas las publicaciones
Publicado el · por Renaud Deraison

La prueba de código prohibía los asistentes de IA

Kaspersky publicó el 1 de septiembre la campaña de falsos reclutadores de Mirage Kitten. El cebo es una prueba de programación para hacer en casa, y su README plantea tres peticiones: terminar en tres horas, no modificar server.js porque ya está libre de errores, y no usar un asistente de IA. La primera línea de server.js importa un paquete npm troyanizado que nunca se publicó en npm. Venía dentro del zip, ya instalado en node_modules. Todos los controles de cadena de suministro jamás construidos vigilan una petición al registro que este ataque no hace. Bromure Agentic Coding defiende la otra mitad: la máquina donde se ejecuta el código.

El atacante te pidió que no usaras un agente de código. Una herramienta que abre todos los archivos del repositorio, digan lo que digan las instrucciones, era lo único que se interponía entre esta campaña y una puerta trasera, y el README te convenció de renunciar a ella.

Un reclutador te escribe por LinkedIn. El puesto parece bastante real, el nombre de la empresa te suena, y el siguiente paso es una prueba técnica para hacer en casa. Recibes un zip. Dentro hay una aplicación de gestión de proyectos llamada TaskFlow, construida con Express, React y Vite: unos cientos de archivos y un README en la raíz. Corrige los defectos del frontend. Tienes tres horas.

Es lo más ordinario que le ocurre a un ingeniero en activo. También es, según el análisis que Kaspersky publicó el 1 de septiembre, la forma en que un grupo de espionaje iraní al que el informe llama Mirage Kitten, rastreado en otros sitios como UNC1549, Smoke Sandstorm o Nimbus Manticore, viene dejando puertas traseras multiplataforma en desarrolladores de Egipto, Etiopía y Afganistán, con envíos adicionales desde India, Turquía, Israel, Irak, Alemania e Irlanda. Los archivos llegan con nombres como Front-Technical-Challenge.zip y Task-FullStack.zip, alojados en AWS para que la descarga parezca infraestructura y no una carga maliciosa. The Hacker News cubrió la campaña el 9 de septiembre.

Tres reglas, todas sobre leer

Kaspersky recoge tres instrucciones en el README. Fíjate en lo que le reporta cada una al atacante.

El README también imponía un límite de tres horas y prohibía el uso de asistentes de IA.

Afirmaba además que server.js estaba libre de errores y no debía modificarse.

El reloj es la regla evidente: tres horas para arreglar una base de código desconocida es un calendario que no deja hueco para la curiosidad. La segunda regla dirige tu atención hacia el frontend y lejos de un archivo concreto. La tercera es la interesante, porque es el atacante diciéndote de qué herramienta tiene miedo.

Un agente de código al que se le entrega este repositorio no respeta el alcance que sugiere el README. Apunta Claude Code o Codex al proyecto y pídele que arregle el frontend: leerá igualmente server.js, porque averiguar cómo arranca la aplicación es la manera en que averigua con qué habla el frontend. Lee el árbol entero. No tiene reloj de tres horas ni motivo para saltarse un archivo que alguien declaró sano. Prohibir el asistente elimina de la sala al único lector que lo lee todo, que es una exigencia extraña en un proceso de selección.

Y lo que habría leído es la primera línea.

La dependencia nunca estuvo en npm

La primera línea de server.js importa un paquete llamado colorized_terminal, en la versión 2.1.0. Una muestra hermana usa pretty-log, también 2.1.0. Ninguno es un typosquat de nada, y ninguno es una publicación secuestrada, porque ninguno está en el registro:

Los atacantes empaquetaron la dependencia directamente en el directorio node_modules del archivo de la prueba, en lugar de publicarla en el registro npm.

Al importarse, el paquete lanzaba silenciosamente un implante desde node_modules/.cache/.320697f1/index.js como proceso desacoplado en segundo plano.

Aquí no se ejecuta ningún npm install, ni ningún script postinstall. No hay entrada de lockfile que verificar, ni hash de integridad que comparar, ni atestación de procedencia que comprobar, ni marca de publicación que envejecer, ni proveedor de reputación al que consultar, ni petición al registro de la que colgar nada de eso. La dependencia estaba en la carpeta antes de que abrieras la carpeta. Ejecutar la aplicación que el README pide arreglar, que es todo el ejercicio, significa un simple require, y ese require bifurca una puerta trasera.

Tres años de defensas contra paquetes maliciosos se apoyan todas en una consulta al registro. Mirage Kitten no hace ninguna.

La cadena de suministro sin registroFront-Technical-Challenge.zipREADME.mdtres horas · sin IA“server.js está sin errores”server.jslínea 1: require('colorized_terminal')el archivo que debías saltarnode_modules/[email protected]ya instalado · fuera de npmejecutas la appel import lanza un implantenode_modules/.cache/.320697f1/index.jsproceso en segundo planoNodeRabbit llama a casaplugplay.azurewebsites.netrgbteller.azurewebsites.net11 comandos, luego 23lo que nunca se dispara, y por quéninguna petición al registroantigüedad — sin fechaintegridad — sin lockfileprocedencia — nada se publicóreputación — ningún paquete que mirarscripts quitados — sin instalacióntyposquat — ningún nombre que compararqueda la máquina que lo ejecuta
El archivo llega con sus dependencias ya instaladas, de modo que la cadena de suministro nunca toca un registro. La primera línea del archivo que el README te decía que no abrieras importa un paquete que no existe en ningún otro sitio, e importarlo bifurca un implante desde un directorio oculto dentro de node_modules. Sin paso de instalación, sin comprobación de lockfile, sin marca de publicación: los controles que vigilan las descargas de paquetes no tienen descarga alguna que vigilar.

No se queda en la carpeta de la prueba

NodeRabbit empieza pequeño. La primera variante de Kaspersky, de una máquina en Afganistán, lleva once comandos: leer un archivo, escribir un archivo, listar un directorio, volcar la configuración de red, arrancar un proceso, ejecutar un script, dormir. La variante egipcia añade evasión de sandbox, soporte de proxy y selección dinámica de puerto. La tercera, de Etiopía, llega a veintitrés, y las nuevas son el punto donde la máquina de un desarrollador deja de ser una sola máquina.

outlook:emails cosecha artefactos de correo. persist:projects:scan recorre el disco buscando repositorios git. persist:project:inject escribe en los que encuentra, aterrizando en .git/hooks/post-merge y .git/hooks/post-checkout para que el implante se relance en el siguiente pull. persist:vscode instala una extensión maliciosa en tu editor, que luego la carga en cada proyecto que abres. La formulación de Kaspersky es exacta en cuanto a la intención:

Más allá de los mecanismos de persistencia descritos arriba, la Variante 3 introduce dos mecanismos adicionales que relanzan el malware a través de flujos de trabajo habituales de los desarrolladores.

Tus otros repositorios son el objetivo. La persistencia más llana que hay debajo está afinada por plataforma: una tarea programada o una clave Run que se dispara a las 10 de la mañana cada día en Windows, una entrada cron @reboot en Linux, un LaunchAgent en ~/Library con RunAtLoad y KeepAlive ambos en verdadero en macOS. El hermano PollCat llega por una prueba de React llamada RankChallenge-react cuyo README acorta el reloj a una sesión de una hora, protegida tras un código de seis dígitos que afirma rotar cada treinta segundos. El mismo diseño de presión, menos tiempo.

en el portátilel implante hereda toda tu cuentapersist:projects:scancada repo del disco recibe un hookpersist:vscodeuna extensión que carga en cada proyectooutlook:emails · fs:read~/.aws · ~/.kube · ~/.git-credentialslimpiezabuscar los hooks, rotar las claves, esperaren un workspaceuna VM Ubuntu, su disco, su homeel escaneo solo ve lo que montastesolo carpetas compartidas, ocho como muchola persistencia cae en un disco desechableReset disk · Erase home · checkpointscada archivo de credenciales lleva un falsoghp_… · brm-k8s-… · sk-ant-api03-brm-…limpiezaborrar disco y home, no rotar nada
Un archivo, dos radios de daño. En un portátil, el implante hereda la cuenta entera: cada repositorio git que encuentra recibe un hook, el editor recibe una extensión, el correo y la configuración de nube son legibles, y las credenciales del entorno del shell son las de verdad. En un workspace de Bromure Agentic Coding los mismos comandos se ejecutan contra una VM Linux que guarda credenciales de mentira, monta solo las carpetas que has compartido y alcanza la red a través de un socket en el anfitrión.

Ejecútalo, en algún sitio que no sea tu máquina

Bromure Agentic Coding ejecuta agentes de código dentro de una VM Linux virtualizada por hardware en tu Mac, con todos los controles de seguridad del lado anfitrión de esa frontera. El zip de un desconocido es justo el trabajo para el que se construyó, porque los controles describen la habitación y no al huésped. Ninguno de ellos tiene que reconocer colorized_terminal.

Un workspace es la unidad de aislamiento. Una VM Ubuntu, su propio disco de sistema, su propio home persistente, sus propias credenciales y sus propias políticas, y dos workspaces nunca comparten un byte. Crea un workspace desechable para la prueba. El implante se ejecuta, el import se dispara, el proceso desacoplado arranca, y todo eso ocurre en una máquina que vino al mundo esta mañana y guarda un único archivo zip. Cuando termines, Reset disk vuelve a clonar el disco de sistema desde la base y Erase home… borra la imagen del home; los checkpoints de reversión por arranque detrás de Restore home… lo hacen con más grano fino. La entrada cron @reboot que escribe la rama Linux se va con ellos.

Las ramas de persistencia de Windows y macOS no tienen suelo donde pisar. Sin AppData, sin clave Run, sin tarea programada, sin ~/Library, sin LaunchAgent, sin almacén de Outlook que leer. El invitado es Linux y no es tu Linux.

persist:projects:scan solo ve las carpetas que has compartido. Las carpetas del anfitrión se adjuntan como montajes virtiofs en /home/ubuntu/<basename>, con un tope de ocho por workspace, y son la única parte del sistema de archivos de tu Mac que la VM alcanza. Comparte la carpeta de la prueba y nada más, y la inyección de hooks git encuentra un repositorio: el que ya era hostil. Mantén la lista corta por una segunda razón. Una carpeta compartida es una ventana abierta a tu Mac, y un borrado por compromiso la deja intacta por diseño.

fs:read vuelve con marcadores de posición. Bromure sustituye cada credencial que configuras por un falso que preserva la estructura, derivado del valor real y de una sal propia de la instalación mediante HKDF-SHA256: sk-ant-api03-brm-…, un token ghp_ de la longitud correcta, glpat-…, brm-k8s-…, brm-docker-…. Los falsos se escriben en las variables de entorno y en ~/.git-credentials, ~/.docker/config.json, ~/.kube/config, ~/.aws/config y las configuraciones MCP, que es justo la lista que enumera un infostealer. Los valores reales siguen cifrados en el Mac, y el proxy del anfitrión los intercambia en el cable, acotados al host de destino para el que se acuñó cada uno. AWS va más lejos: el invitado firma con una clave secreta falsa y el anfitrión vuelve a firmar, de modo que una petición que se salte el proxy muere en AWS con InvalidSignatureException. Los bytes de las claves privadas SSH nunca entran en la VM, y solo cruzan las firmas. Ningún interruptor gobierna nada de esto, porque el proxy es la única ruta de la VM hacia la red.

Los hosts de mando son exactamente para lo que sirve una lista de permitidos. La mayoría de los servidores de mando de NodeRabbit viven bajo *.azurewebsites.net, y los de PollCat también. Ningún feed de reputación pondrá jamás ese dominio en lista negra, y ninguna heurística de antigüedad de dominio lo señalará. A una lista de permitidos le da igual: en Guardrails, pon Unmatched traffic en Deny y escribe las dos o tres líneas que el trabajo necesita. La aplicación es del lado anfitrión y va doblada: el conmutador virtual empareja cada flujo por IP de destino y por nombre de host espiado en DNS, en todos los protocolos, y el proxy vuelve a emparejar por nombre de servidor TLS y método HTTP. La interceptación transparente desvía el tráfico de los puertos 80 y 443 del invitado hacia el proxy sin que nada dentro de la VM pueda deshacerlo, que es la respuesta a la variante que traía soporte de proxy y selección dinámica de puerto.

Y si el implante intenta sacar un token, el falso se convierte en un cable trampa. El proxy examina cada petición saliente, cabeceras y cuerpo, en busca de un falso que vaya hacia un host para el que no fue acuñado. Si hay coincidencia, rechaza la petición sin reenviar un byte, pausa la VM en el acto, registra una fila roja de Credential brokering en la Security Timeline, y marca el workspace como comprometido para que el siguiente lanzamiento borre antes el disco y el home. Después no rotas nada, porque lo que el implante tenía era un marcador.

Puedes ver la conversación entera. Con el trazado en Activity only el proxy escribe una fila de metadatos por petición y ningún cuerpo. bromure-cli trace hostnames imprime cada host distinto que el workspace contactó, con sus cuentas, de modo que tres direcciones azurewebsites.net que nunca listaste quedan a un comando de distancia en lugar de a una investigación forense.

El workspace que crear antes de que llegue el zip

Un workspace desechable sin ninguna credencial configurada más allá de la clave del propio agente, una carpeta compartida para el archivo, y Unmatched traffic en Deny dentro de Guardrails. Añade allow web registry.npmjs.org y lo que el proyecto descargue además. Guardar empuja las reglas a las sesiones en marcha sin reiniciar, así que puedes aflojarlas línea a línea según se queje la compilación.

Los dos comandos para cuando hayas terminado

bromure-cli trace hostnames <workspace> te dice adónde fue el código, y bromure-cli trace leaks <workspace> imprime No leaks detected. ✓ cuando nada intentó sacar un token. Luego Reset disk y Erase home…, y el workspace vuelve a la imagen de la que partió.

El lector nunca fue el control

Lee server.js de todos modos. Es un buen consejo y una defensa pobre, porque el siguiente archivo pondrá el import en la línea 340 de un fichero que no tienes motivo para abrir, o en un bundle minificado, o detrás de un require dinámico montado a partir de dos cadenas. Las tres reglas de Mirage Kitten son una apuesta contra tu atención, y la atención se agota a las cuatro de la tarde de un viernes con una hora restante en el reloj.

El lugar donde se ejecuta el código no se agota. El import se dispara igual y el proceso desacoplado arranca igual, así que lo que aún te toca elegir es lo que ese proceso encuentra cuando mira alrededor: una máquina con tu token de GitHub, tu kubeconfig, tu clave SSH y cuarenta repositorios, o una VM Linux con un archivo zip, un juego de marcadores y un socket de red que solo va adonde tú dijiste.

Acepta el reto. Usa el agente de código que el README te dijo que no usaras, y deja que lea server.js primero. Instala Bromure Agentic Coding y dale al zip una máquina a la que no le importe.