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 chimrod (site web personnel) . Évalué à 5. Dernière modification le 14 janvier 2020 à 12:14.
Le mot de passe est dans le fichier .passwd !
[^] # Re: Je sais !
Posté par lovasoa (site web personnel) . É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 chimrod (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 lovasoa (site web personnel) . É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 serge_sans_paille (site web personnel) . Évalué à 7.
Premier indice, tiré de la doc mentionnée dans le journal :
Deuxième indice, lié à l'introspection :
[^] # Re: Indices
Posté par lovasoa (site web personnel) . Évalué à 2. Dernière modification le 14 janvier 2020 à 12:48.
Bravo ! Je ne pensais pas que quelqu'un trouverait si vite.
[^] # Re: Indices
Posté par THE_ALF_ . É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 RB . Évalué à 2.
Pareil :-)
[^] # Re: Indices
Posté par lovasoa (site web personnel) . É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 Shuba . É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 lovasoa (site web personnel) . Évalué à 3.
Pour éviter les interactions manuelles, on peut faire un script
Et ensuite, on peut exécuter le script avec :
[^] # Re: Indices
Posté par brendel . Évalué à 2.
C'est quasiment la réponse du coup
# Fonctionne pas
Posté par barmic 🦦 . Évalué à 1.
J'ai des problèmes pour exécuter ton script.
De base il me dit :
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 :https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll
[^] # Re: Fonctionne pas
Posté par lovasoa (site web personnel) . Évalué à 2.
Si vous n'avez pas de version récente de python sur votre ordinateur, vous pouvez faire tourner le script en ligne: https://repl.it/repls/SimultaneousLonelyTransformation
[^] # Re: Fonctionne pas
Posté par elf32 . Évalué à 6.
Il faut Python 3.6 pour les f-strings je crois.
[^] # Re: Fonctionne pas
Posté par louiz’ (site web personnel) . Évalué à 3.
Ça va, python 3.6 n’est sorti que depuis 3 ans, t’as le temps.
# Erreur de syntaxe
Posté par goeb . É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 lovasoa (site web personnel) . É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 benoar . É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 THE_ALF_ . É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 lovasoa (site web personnel) . É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 :
Et la faille de sécurité sera toujours présente.
[^] # Re: Erreur de syntaxe
Posté par barmic 🦦 . Évalué à 5.
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. ;)
https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll
# Un indice si l'on est pas familier avec les f-strings
Posté par Colin Pitrat (site web personnel) . Évalué à 4.
Indice 1
En modifiant le programme pour faire
juste avant le
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 Anonyme . Évalué à 6.
Je sais, c’est parce que tu utilise
random
au lieu desecrets
.J’ai bon ? :p
[^] # Re: Random
Posté par lovasoa (site web personnel) . Évalué à 3. Dernière modification le 14 janvier 2020 à 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 Anonyme . Évalué à 3.
Autre chose, ça me semble bien complexe de faire un
random.choice
sur des entiers de 0 à 9 que tujoin
alors que tu pourrais utiliserrandom.getrandbits
.[^] # Re: Random
Posté par elf32 . Évalué à 1. Dernière modification le 15 janvier 2020 à 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 lovasoa (site web personnel) . É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 parrandom
.# Je suis triste
Posté par Guillaum (site web personnel) . Évalué à 4.
Parce que
PyF
https://github.com/guibou/PyF, une librairie Haskell qui implémente l'équivalent desf
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 Matthieu Moy (site web personnel) . Évalué à 3. Dernière modification le 16 janvier 2020 à 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()
.[^] # Re: Je suis triste
Posté par Guillaum (site web personnel) . Évalué à 2.
Hello !
En effet. Merci. Je suis donc moins triste ;)
# Bookmark
Posté par batisteo . Évalué à 2.
Pour ceux qui ne connaissent pas : https://pyformat.info/
# ValueError: Too many decimal digits in format string
Posté par steph1978 . Évalué à 2.
Ça doit pas être loin de la solution …
Raa, toujours pasValueError: Single '}' encountered in format string
[^] # Re: ValueError: Too many decimal digits in format string
Posté par steph1978 . Évalué à 2.
J'ai une solution qui semble fonctionner.
Je pose sa signature ici, des fois qu'elle soit valide.
Cher OP, comment se fait la validation des solutions ?
[^] # Re: ValueError: Too many decimal digits in format string
Posté par lovasoa (site web personnel) . Évalué à 3.
Voilà, j'ai fait un petit dépôt github qui permet de tester sa solution:
https://github.com/lovasoa/pyformat-challenge
[^] # Re: ValueError: Too many decimal digits in format string
Posté par mistiru . Évalué à 3.
Ta solution est visible sur ton dépôt, c'est voulu ?
[^] # Re: ValueError: Too many decimal digits in format string
Posté par lovasoa (site web personnel) . Évalué à 3.
Oui, j'ai fait une branche "solution" qui contient une solution, et une pull request qui va avec.
[^] # Re: ValueError: Too many decimal digits in format string
Posté par steph1978 . Évalué à 3.
Ma solution est ici : https://framagit.org/snippets/4751
Elle reprend le même principe que la solution que tu as publié.
[^] # Re: ValueError: Too many decimal digits in format string
Posté par lovasoa (site web personnel) . Évalué à 3.
Bravo ! Et je ne connaissais pas
coproc
en bash, je viens de regarder la doc et ça a l'air super utile. Merci ![^] # Re: ValueError: Too many decimal digits in format string
Posté par steph1978 . Évalué à 3.
Oui j'ai trouvé ça un jour où je me prenais la tête avec de FIFO pour faire communiquer deux processus en bidirectionnel. La syntaxe est un peu bizarre et ça n'est disponible qu'en bash 4+ mais en pratique ça marche bien.
Suivre le flux des commentaires
Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.