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.jsestaba 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_modulesdel 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.jscomo 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.
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.
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.