Le test de recrutement interdisait les assistants IA
Kaspersky a publié le 1er septembre la campagne de faux recruteurs de Mirage Kitten. L'appât est un test de programmation à faire chez soi, et son README formule trois demandes : terminer en trois heures, ne pas modifier server.js parce qu'il est déjà exempt de bogues, et ne pas utiliser d'assistant IA. La première ligne de server.js importe un paquet npm trojanisé qui n'a jamais été publié sur npm. Il est arrivé dans le zip, déjà installé dans node_modules. Tous les contrôles de chaîne d'approvisionnement jamais conçus surveillent une requête au registre que cette attaque ne fait pas. Bromure Agentic Coding plaide pour l'autre moitié : la machine sur laquelle le code s'exécute.
L'attaquant vous a demandé de ne pas utiliser d'agent de code. Un outil qui ouvre tous les fichiers du dépôt, que les instructions le disent ou non, était la seule chose qui séparait cette campagne d'une porte dérobée, et le README vous en a dissuadé.
Un recruteur vous écrit sur LinkedIn. Le poste a l'air réel, le nom de l'entreprise vous dit quelque chose, et l'étape suivante est un test technique à faire chez vous. Vous recevez un zip. À l'intérieur, une application de gestion de projet appelée TaskFlow, bâtie avec Express, React et Vite : quelques centaines de fichiers et un README à la racine. Corrigez les défauts du frontend. Vous avez trois heures.
C'est la chose la plus ordinaire qui arrive à un ingénieur en activité. C'est
aussi, d'après l'analyse publiée par Kaspersky le 1er septembre,
la façon dont une équipe d'espionnage iranienne que le rapport nomme Mirage
Kitten, suivie ailleurs sous les noms d'UNC1549, Smoke Sandstorm ou Nimbus
Manticore, dépose des portes dérobées multiplateformes chez des développeurs en
Égypte, en Éthiopie et en Afghanistan, avec d'autres soumissions venues d'Inde,
de Turquie, d'Israël, d'Irak, d'Allemagne et d'Irlande. Les archives arrivent
sous des noms comme Front-Technical-Challenge.zip et Task-FullStack.zip,
hébergées sur AWS pour que le téléchargement ressemble à de l'infrastructure
plutôt qu'à une charge utile. The Hacker News
a couvert la campagne le 9 septembre.
Trois règles, toutes au sujet de la lecture
Kaspersky relève trois instructions dans le README. Regardez ce que chacune rapporte à l'attaquant.
Le README imposait également une limite de trois heures et interdisait l'usage d'assistants IA.
Il affirmait aussi que
server.jsétait exempt de bogues et ne devait pas être modifié.
Le chronomètre est la règle évidente : trois heures pour corriger une base de code inconnue, c'est un emploi du temps qui ne laisse aucune place à la curiosité. La deuxième règle dirige votre attention vers le frontend et l'écarte d'un fichier précis. La troisième est la plus intéressante, parce que c'est l'attaquant qui vous dit quel outil lui fait peur.
Un agent de code à qui l'on confie ce dépôt ne respecte pas le périmètre que le
README suggère. Pointez Claude Code ou Codex sur le projet et demandez-lui de
corriger le frontend : il lira quand même server.js, parce que comprendre
comment l'application démarre, c'est comme cela qu'il comprend à quoi le
frontend parle. Il lit l'arborescence entière. Il n'a pas de chronomètre de
trois heures et aucune raison de sauter un fichier que quelqu'un a déclaré sain.
Bannir l'assistant retire de la pièce le seul lecteur qui lit tout, ce qui est
une étrange exigence pour un processus de recrutement.
Et ce qu'il aurait lu, c'est la première ligne.
La dépendance n'a jamais été sur npm
La première ligne de server.js importe un paquet nommé colorized_terminal,
en version 2.1.0. Un échantillon voisin utilise pretty-log, également en
2.1.0. Ni l'un ni l'autre n'est un typosquat de quoi que ce soit, ni une
publication détournée, parce que ni l'un ni l'autre n'est sur le registre :
Les attaquants ont empaqueté la dépendance directement dans le répertoire
node_modulesde l'archive du test, plutôt que de la publier sur le registre npm.
À l'import, le paquet lançait silencieusement un implant depuis
node_modules/.cache/.320697f1/index.jssous forme de processus détaché en arrière-plan.
Aucun npm install ne s'exécute ici, et aucun script postinstall. Il n'y a
aucune entrée de lockfile à vérifier, aucune empreinte d'intégrité à comparer,
aucune attestation de provenance à contrôler, aucune date de publication à faire
vieillir, aucun fournisseur de réputation à interroger, et aucune requête au
registre à laquelle accrocher tout cela. La dépendance était dans le dossier
avant que vous n'ouvriez le dossier. Lancer l'application que le README vous
demande de corriger, ce qui est tout le devoir, signifie un simple require, et
ce require fait bifurquer une porte dérobée.
Trois ans de défenses contre les paquets malveillants reposent toutes sur une requête au registre. Mirage Kitten n'en fait aucune.
Cela ne reste pas dans le dossier du test
NodeRabbit commence petit. La première variante de Kaspersky, issue d'une machine en Afghanistan, porte onze commandes : lire un fichier, écrire un fichier, lister un répertoire, extraire la configuration réseau, démarrer un processus, exécuter un script, se mettre en sommeil. La variante égyptienne ajoute l'évasion de bac à sable, la prise en charge du proxy et la sélection dynamique de port. La troisième, venue d'Éthiopie, atteint vingt-trois, et les nouvelles sont celles où la machine d'un développeur cesse d'être une seule machine.
outlook:emails récolte les artefacts de messagerie. persist:projects:scan
parcourt le disque à la recherche de dépôts git. persist:project:inject écrit
dans ceux qu'il trouve, en atterrissant dans .git/hooks/post-merge et
.git/hooks/post-checkout pour que l'implant se relance au prochain pull.
persist:vscode installe une extension malveillante dans votre éditeur, qui la
charge ensuite dans chaque projet que vous ouvrez. La formulation de Kaspersky
est précise quant à l'intention :
Au-delà des mécanismes de persistance décrits ci-dessus, la variante 3 introduit deux mécanismes supplémentaires qui relancent le logiciel malveillant via des flux de travail courants des développeurs.
Vos autres dépôts sont la cible. La persistance plus banale qui se trouve
dessous est ajustée par plateforme : une tâche planifiée ou une clé Run qui se
déclenche à 10 h chaque jour sous Windows, une entrée cron @reboot sous Linux,
un LaunchAgent dans ~/Library avec RunAtLoad et KeepAlive tous deux à vrai
sous macOS.
Le cousin PollCat arrive par un test React nommé RankChallenge-react dont le
README raccourcit le chronomètre à une session d'une heure, protégée par un code
à six chiffres censé tourner toutes les trente secondes. Même conception de la
pression, moins de temps.
Exécutez-le, ailleurs que sur votre machine
Bromure Agentic Coding exécute les agents de code dans une VM Linux virtualisée
matériellement sur votre Mac, avec tous les contrôles de sécurité du côté hôte
de cette frontière. Le zip d'un inconnu est le travail pour lequel il a été
conçu, parce que les contrôles décrivent la pièce plutôt que l'invité. Aucun
d'eux n'a besoin de reconnaître colorized_terminal.
Un workspace est l'unité d'isolement. Une VM Ubuntu, son propre disque
système, son propre home persistant, ses propres identifiants et ses propres
politiques, et deux workspaces ne partagent jamais un octet. Créez un workspace
jetable pour le test. L'implant s'exécute, l'import se déclenche, le processus
détaché démarre, et tout cela se passe dans une machine qui est venue au monde
ce matin et qui contient un seul fichier zip. Quand vous avez terminé,
Reset disk re-clone le disque système depuis la base et Erase home…
efface l'image du home ; les points de restauration par démarrage derrière
Restore home… le font à une granularité plus fine. L'entrée cron @reboot
que la branche Linux écrit part avec eux.
Les branches de persistance Windows et macOS n'ont aucun terrain où se
poser. Pas d'AppData, pas de clé Run, pas de tâche planifiée, pas de
~/Library, pas de LaunchAgent, pas de messagerie Outlook à lire. L'invité est
un Linux et ce n'est pas votre Linux.
persist:projects:scan ne voit que les dossiers que vous avez partagés. Les
dossiers de l'hôte sont attachés comme montages virtiofs dans
/home/ubuntu/<basename>, plafonnés à huit par workspace, et ils sont la seule
partie du système de fichiers de votre Mac que la VM peut atteindre. Partagez le
dossier du test et rien d'autre, et l'injection de hooks git trouve un seul
dépôt : celui qui était déjà hostile. Gardez la liste courte pour une deuxième
raison. Un dossier partagé est une fenêtre ouverte sur votre Mac, et un effacement
après compromission le laisse intact par conception.
fs:read revient avec des remplaçants. Bromure remplace chaque identifiant
que vous configurez par un faux préservant la structure, dérivé de la vraie
valeur et d'un sel propre à l'installation via HKDF-SHA256 :
sk-ant-api03-brm-…, un jeton ghp_ de la bonne longueur, glpat-…,
brm-k8s-…, brm-docker-…. Les faux sont écrits dans les variables
d'environnement ainsi que dans ~/.git-credentials, ~/.docker/config.json,
~/.kube/config, ~/.aws/config et les configurations MCP, c'est-à-dire la
liste qu'un voleur d'informations énumère. Les vraies valeurs restent chiffrées
sur le Mac, et le proxy de l'hôte les substitue sur le fil, cantonnées à l'hôte
de destination pour lequel chacune a été frappée. AWS va plus loin : l'invité
signe avec une fausse clé secrète et l'hôte resigne, si bien qu'une requête qui
contourne le proxy meurt chez AWS avec InvalidSignatureException. Les octets
des clés privées SSH n'entrent jamais dans la VM, et seules les signatures
traversent. Aucun réglage ne gouverne tout cela, parce que le proxy est la seule
route de la VM vers le réseau.
Les serveurs de commande sont exactement ce à quoi sert une liste
d'autorisation. La plupart des serveurs de commande de NodeRabbit vivent sous
*.azurewebsites.net, et ceux de PollCat aussi. Aucun flux de réputation ne
mettra jamais ce domaine sur liste noire, et aucune heuristique d'âge de domaine
ne le signalera. Une liste d'autorisation s'en moque : dans Guardrails,
réglez Unmatched traffic sur Deny et écrivez les deux ou trois lignes
dont le travail a besoin. L'application est côté hôte et doublée : le
commutateur virtuel apparie chaque flux par IP de destination et par nom d'hôte
espionné dans le DNS, tous protocoles confondus, et le proxy apparie de nouveau
par nom de serveur TLS et méthode HTTP. L'interception transparente détourne le
trafic des ports 80 et 443 de l'invité vers le proxy sans que rien à l'intérieur
de la VM puisse l'annuler, ce qui est la réponse à la variante qui embarquait la
prise en charge du proxy et la sélection dynamique de port.
Et si l'implant tente d'emporter un jeton, le faux devient un fil-piège. Le proxy analyse chaque requête sortante, en-têtes et corps, à la recherche d'un faux qui part vers un hôte pour lequel il n'a pas été frappé. En cas de correspondance, il refuse la requête sans transmettre un seul octet, met la VM en pause sur-le-champ, inscrit une ligne rouge Credential brokering dans la Security Timeline, et marque le workspace comme compromis pour que le prochain lancement efface d'abord le disque et le home. Vous ne changez rien ensuite, parce que ce que l'implant tenait était un remplaçant.
Vous pouvez voir toute la conversation. Avec le traçage sur
Activity only, le proxy écrit une ligne de métadonnées par requête et aucun
corps. bromure-cli trace hostnames affiche chaque hôte distinct que le
workspace a contacté, avec les comptes, si bien que trois adresses
azurewebsites.net que vous n'avez jamais listées sont à une commande de
distance plutôt qu'à une enquête forensique.
Le workspace à créer avant que le zip n'arrive
Un workspace jetable sans aucun identifiant configuré au-delà de la clé de
l'agent lui-même, un seul dossier partagé pour l'archive, et
Unmatched traffic réglé sur Deny dans Guardrails. Ajoutez
allow web registry.npmjs.org et tout ce que le projet récupère par
ailleurs. Enregistrer pousse les règles vers les sessions en cours sans
redémarrage, si bien que vous pouvez les desserrer ligne par ligne à mesure
que le build se plaint.
Les deux commandes une fois que vous avez terminé
bromure-cli trace hostnames <workspace> vous dit où le code est allé, et
bromure-cli trace leaks <workspace> affiche No leaks detected. ✓ quand
rien n'a tenté d'emporter un jeton. Ensuite Reset disk et
Erase home…, et le workspace revient à l'image dont il est parti.
Le lecteur n'a jamais été le contrôle
Lisez server.js quand même. C'est un bon conseil et une mauvaise défense,
parce que la prochaine archive mettra l'import à la ligne 340 d'un fichier que
vous n'avez aucune raison d'ouvrir, ou dans un bundle minifié, ou derrière un
require dynamique assemblé à partir de deux chaînes. Les trois règles de
Mirage Kitten sont une couverture contre votre attention, et l'attention
s'épuise à 16 h un vendredi avec une heure restante au chronomètre.
L'endroit où le code s'exécute, lui, ne s'épuise pas. L'import se déclenche dans les deux cas et le processus détaché démarre dans les deux cas ; ce qu'il vous reste à choisir, c'est ce que ce processus trouve en regardant autour de lui : une machine qui détient votre jeton GitHub, votre kubeconfig, votre clé SSH et quarante dépôts, ou une VM Linux qui détient un fichier zip, un jeu de remplaçants, et une socket réseau qui ne va que là où vous l'avez dit.
Relevez le défi. Utilisez l'agent de code que le README vous disait de ne pas
utiliser, et laissez-le lire server.js en premier.
Installez Bromure Agentic Coding et donnez au zip une machine
que cela ne dérange pas.