Journal Petit défi Python

Posté par  . Licence CC By‑SA.
29
14
jan.
2020

Aujourd’hui, je propose à ceux qui s’ennuient un petit défi de cybersécurité en Python.
Voici un script Python qui semble trivial, et qui contient une faille de sécurité :

#!/usr/bin/env python3
import random

SECRET = ''.join(random.choice("0123456789") for i in range(64))

class Sandbox:

    def ask_age(self):
        self.age = input("How old are you ? ")
        self.width = input("How wide do you want the nice box to be ? ")

    def ask_secret(self):
        if input("What is the secret ? ") == SECRET:
            print("You found the secret ! I thought this was impossible.")
        else:
            print("Wrong secret")

    def run(self):
        while True:
            self.ask_age()
            to_format = f"""
Printing a {self.width}-character wide box:
[Age: {{self.age:{self.width}}} ]"""
            print(to_format.format(self=self))
            self.ask_secret()

Sandbox().run()

Le script est très simple : il vous demande votre âge, puis un nombre n. Ensuite, il vous affiche votre âge dans un petit cadre long de n caractères. Ensuite, il vous demande un mot de passe.

L’objectif du défi est de réussir à tromper le programme au moment de donner son âge et le nombre n, de manière à récupérer le mot de passe.

Bonne chance !

P.‑S. — Vous pouvez donner des indices, mais ne donnez pas la solution dans les commentaires. Je publierai la solution dans quelques jours, si personne ne trouve… La page de documentation du formatage des chaînes de caractères de Python pourra également vous être utile…

  • # Je sais !

    Posté par  (site Web personnel) . Évalué à 5. Dernière modification le 14/01/20 à 12:14.

    Le mot de passe est dans le fichier .passwd !

    • [^] # Re: Je sais !

      Posté par  . Évalué à 1.

      Le but du défi, c'est de trouver le mot de passe depuis l'intérieur du programme. La réponse au défi est une valeur ou une suite de valeurs à donner au programme pour récupérer le mot de passe. Vous n'avez pas accès à la machine qui exécute le programme en dehors du programme lui-même.

      • [^] # Re: Je sais !

        Posté par  (site Web personnel) . Évalué à 3.

        Tu as pris mon message au pied de la lettre, mais j'avais bien compris que le but était de casser le programme en faussant les données saisies.

        (Et tu as modifié le script publié sur github, au moment ou j'ai écrit mon commentaire, il allait lire dans un fichier présent dans le répertoire courant, du coup mon commentaire n'est plus drôle du tout…)

        • [^] # Re: Je sais !

          Posté par  . Évalué à 2.

          Ah ! J'ai cru qu'il y avait un problème de compréhension dû au fait que le script sur github était légèrement différent. Voilà le lien vers l'ancienne version, pour que le commentaire regagne toute sa valeur humoristique.

  • # Indices

    Posté par  (site Web personnel) . Évalué à 7.

    Premier indice, tiré de la doc mentionnée dans le journal :

    A format_spec field can also include nested replacement fields within it.

    Deuxième indice, lié à l'introspection :

    w3m -dump -cols 1000 https://docs.python.org/3/reference/datamodel.html | grep __globals__ | head -n1
    
    • [^] # Re: Indices

      Posté par  . Évalué à 2. Dernière modification le 14/01/20 à 12:48.

      Bravo ! Je ne pensais pas que quelqu'un trouverait si vite.

      • [^] # Re: Indices

        Posté par  . Évalué à 2.

        Le plus dur est de tromper la ligne "[Age: …" sans faire planter la ligne "Printing…". J'ai pu trouver une solution qui marche en enlevant la ligne "Printing…", mais sinon je bloque :D

        • [^] # Re: Indices

          Posté par  . Évalué à 2.

          Pareil :-)

        • [^] # Re: Indices

          Posté par  . Évalué à 2.

          Oui, la difficulté est que width doit être à la fois un spécificateur de formatage valide et une chaîne valide à passer à str.format. La documentation du fonctionnement des chaînes de formatage listée dans la dépêche donne une piste.

        • [^] # Re: Indices

          Posté par  . Évalué à 3.

          Ça peut se contourner en n'obtenant qu'un chiffre du secret à la fois. Mais c'est pénible vu que du coup ça demande 64 interactions manuelles.

          • [^] # Re: Indices

            Posté par  . Évalué à 3.

            Pour éviter les interactions manuelles, on peut faire un script

            password = ""
            for i in range(64):
                print(0)
                print("hack everything")
                input()
                input()
                password += extract_password_char(input())
                print(password)
                input()

            Et ensuite, on peut exécuter le script avec :

            mkfifo fifo
            python3 crack.py < fifo | python3 challenge.py | tee fifo
    • [^] # Re: Indices

      Posté par  . Évalué à 2.

      C'est quasiment la réponse du coup

  • # Fonctionne pas

    Posté par  . Évalué à 1.

    J'ai des problèmes pour exécuter ton script.

    De base il me dit :

      File "/tmp/plouf.py", line 23
        [Age: {{self.age:{self.width}}} ]"""
                                           ^
    SyntaxError: invalid syntax
    
    

    J'utilise python 3.5.2. J'ai essayé en enlevant le f avant les """ et maintenant il s'exécute, mais je ne crois pas que ce soit de la façon prévu. Si je lui donne un age 12 et une taille 50 il me dit :

    How old are you ? 12
    How wide do you want the nice box to be ? 50
    
    Printing a 50-character wide box:
    [Age: {self.age:50} ]
    What is the secret ?
    
  • # Erreur de syntaxe

    Posté par  . Évalué à 0.

    Ce script contient une erreur de syntaxe, non ?

    File "t.py", line 24
    [Age: {{self.age:{self.width}}} ]"""
    ^
    SyntaxError: invalid syntax

    Apparemment c'est le f avant le """ qui la provoque.
    J'ai Python 3.5.3.

    • [^] # Re: Erreur de syntaxe

      Posté par  . Évalué à 4.

      Python 3.5 n'est plus supporté depuis 2017. Si vous n'avez pas de version récente de python sur votre ordinateur, vous pouvez exécuter le script en ligne sur https://repl.it/repls/SimultaneousLonelyTransformation

      • [^] # Re: Erreur de syntaxe

        Posté par  . Évalué à 2.

        Alors je suis allé voir la doc sur ce nouveau type de chaîne littérale :
        https://docs.python.org/3/reference/lexical_analysis.html#formatted-string-literals

        En fait, ce sont des chaînes qui sont évaluées automatiquement, au lieu d'avoir à passer par le classique .format(). Et c'est là qu'on commence à entrevoir la faille avec tes doubles accolades qui me laissaient perplexes au début…

        Mais franchement, ce genre de feature « magique », ça sent vraiment pas bon et je n'aime pas, ça n'est pas quelque-chose que j'utiliserai dans mon code.

        • [^] # Re: Erreur de syntaxe

          Posté par  . Évalué à 6.

          PEP3101: "The best way to use string formatting in a way that does not create potential security holes is to never use format strings that come from an untrusted source."

        • [^] # Re: Erreur de syntaxe

          Posté par  . Évalué à 2.

          Les f-strings sont au contraire plus sûres que .format() ! Comme on marque explicitement la chaîne à formater, elle ne peut pas être contrôlée par un attaquant. Ici, la faille de sécurité vient du fait que l'on appelle .format() sur une chaîne qui est en partie contrôlée par l'utilisateur.

          On peut très bien remplacer la f-string par :

          to_format = """
          Printing a {width}-character wide box:
          [Age: {{self.age:{width}}} ]""".format(width = self.width)

          Et la faille de sécurité sera toujours présente.

      • [^] # Re: Erreur de syntaxe

        Posté par  . Évalué à 5.

        Python 3.5 n'est plus supporté depuis 2017.

        C'est plus compliqué que ça. Il n'est plus supporté par la communauté python upstream depuis 2017. Ubuntu supporte sa version 16.4 jusqu'en avril 2021 et redhat doit aussi avoir une distribution python 3.5 encore en vie. ;)

  • # Un indice si l'on est pas familier avec les f-strings

    Posté par  (site Web personnel) . Évalué à 4.

    Indice 1

    En modifiant le programme pour faire

    print(to_format)

    juste avant le

    print(to_format.format(self=self))

    on comprend mieux ce qui se passe.

    En gros, on veut réussir à produire une f-string dont le format-specifier

    Indice 2
    Ensuite, on peut essayer avec juste la première ligne (le "Printing a {self.width}-character wide box:") et sans la deuxième. C'est assez facile de lui faire afficher ce qu'on veut (en s'aidant des indices déjà donnés).

    Indice 3
    Finalement il faut se demander comment obtenir la même chose sans avoir à enlever la deuxième ligne. Personnellement je bloque là dessus. Je me dis que ça doit être quelque chose du genre de https://xkcd.com/327/

  • # Random

    Posté par  (site Web personnel) . Évalué à 6.

    Je sais, c’est parce que tu utilise random au lieu de secrets.

    J’ai bon ? :p

    • [^] # Re: Random

      Posté par  . Évalué à 3. Dernière modification le 14/01/20 à 17:16.

      Si vous arrivez à exploiter le manque d'entropie de random pour trouver le mot de passe en un temps raisonnable, alors vous avez bon…

      En tout cas, je ne connaissais pas secrets, visiblement introduit dans python 3.6. Merci pour la découverte !

      • [^] # Re: Random

        Posté par  (site Web personnel) . Évalué à 3.

        Autre chose, ça me semble bien complexe de faire un random.choice sur des entiers de 0 à 9 que tu join alors que tu pourrais utiliser random.getrandbits.

      • [^] # Re: Random

        Posté par  . Évalué à 1. Dernière modification le 15/01/20 à 01:53.

        Est-ce que le défi aurait été impossible en utilisant secrets ou autre méthode de génération aléatoire adaptée à la cryptographie ?

        • [^] # Re: Random

          Posté par  . Évalué à 1.

          Non, bien sûr ! La faille de sécurité est dans l'utilisation de .format() sur une chaîne de caractères contrôlée par l'utilisateur, pas dans la génération du secret.

          Pour pouvoir prédire la sortie des nombres générés par random, il faudrait déjà avoir accès à un grand nombre de valeurs générées précédemment, ce qui n'est pas le cas ici. Si ce sujet vous intéresse, vous pouvez aller voir Python-random-module-cracker, un module qui permet de prévoir les nombres sortis par random.

  • # Je suis triste

    Posté par  (site Web personnel) . Évalué à 4.

    Parce que PyF https://github.com/guibou/PyF, une librairie Haskell qui implémente l'équivalent des f string de python (parce que c'est tellement bien en python qu'il me fallait la même chose en Haskell), ne souffre pas de ce bug, car je n'ai jamais été capable d’implémenter la récursion dans les paramètres de remplacement.

    • [^] # Re: Je suis triste

      Posté par  (site Web personnel) . Évalué à 3. Dernière modification le 16/01/20 à 08:16.

      Hello Guillaum ;-),

      Attention, là il n'y a pas de récursion, mais une f-string d'abord, et un .format appliqué à la f-string. Du point de vue de la f-string, les {{ sont juste des { échappés, et qui du coup sont utilisés par .format().

  • # Bookmark

    Posté par  . Évalué à 2.

    Pour ceux qui ne connaissent pas : https://pyformat.info/

  • # ValueError: Too many decimal digits in format string

    Posté par  . Évalué à 2.

    Ça doit pas être loin de la solution …

    ValueError: Single '}' encountered in format string
    Raa, toujours pas

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n’en sommes pas responsables.