OTP - Mot de passe à usage unique, the wrong way
Les mots de passe à usage uniques, One time password (OTP) sont maintenant presque partout. On les retrouve souvent en SMS (pour le moment), par email, etc..
Certains ont déjà montrés leur faiblesse, SIM swapping, ou autre.
Je m’intéresse ici plus à l’implémentation de l’OTP et aux mauvaises pratiques qui peuvent réduire son efficacité.
Principes
Le challenge consiste à fournir en réponse le code qui a été envoyé par le canal secondaire.
Le challenge est construit à partir de données vérifiables, le login, le code qui correspond à l’OTP, un sel de hachage et le timestamp.
En base de données, on conserve le nombre d’utilisations du challenge afin qu’il soit bien unique, ainsi que le timestamp pour avoir une durée de validité.
Voici donc le shéma de la table Challenge
Le challenge ne doit pas circuler en clair, on en calcule un hash qui servira de clé primaire pour être sauvé en base.
Notre Serveur
Nous allons jouer avec une page de login conçue volontairement pour ce sujet.
Nous avons une implémentation un peu particulière.
Le client reçoit dans notre cas le hash dans un cookie.
Ainsi lors de la réponse au challenge, le serveur pourra chercher la ligne correspondante en base, récupérer le nombre d’essais restants et le timestamp. Le serveur valide le challenge en calculant le hash et le comparant avec celui fourni, il vérifie aussi le timestamp et le login.
Découverte
Reçu un mail d’un développeur
Bonjour, l’authentification par OTP est en place, j’ai implémenté le hash comme convenu : Login + Code + SALT + timestamp, en SHA-256 comme convenu avec le responsable sécurité
Les 4 chiffres du code sont bien envoyés par SMS sur le téléphone enregistré par l’utilisateur
Le compte admin est ainsi le premier sécurisé
Il me reste une question :
A quoi sert le mot SALT dans le calcul du hash ?
(C’est bien évidemment un échange fictif)
Ainsi, pour avoir une histoire intéressante à raconter, nous avons plusieurs informations intéressantes.
- il existe un compte admin
- le hash est un SHA-256
- le template du hash Login+Code+
SALT
+timestamp - le code est de 4 chiffres
Nous allons donc essayer de nous connecter comme admin, récupérer le cookie avec le challenge hashé, casser off-line le hash, puis saisir le code
Connection comme admin
On regarde les cookies et on récupère le hash
Il faut maintenant casser le hash. J’ai codé quelques lignes en Java qui vont faire ça rapidement. Je colle le hash dans la console, et le programme tourne un peu avant de me fournir le code.
Sans beaucoup de subtilités, il va essayer tous les codes possibles (brute force) pour chaque milliseconde depuis maintenant en remontant le temps.
Le code ici présenté n’est pas très rapide, sur mon pc il calcule environ 500k hash/s. Mais il suffit amplement, malgré le copier coller manuel, etc, à retrouver le code à 4 chiffres en quelques dizaines de secondes.
Passe moi le sel ? pas là !
Il est temps pour notre développeur inconnu de continuer son travail. Nous découvrons le mail suivant :
Bonjour,
Autant pour moi, je n’avais pas compris le SALT, c’est à présent une valeur en configuration.
L’équipe de production a choisi une chaîne de caractères sûre, majuscules, minuscules, chiffres et caractères spéciaux.
La chaîne de caractères fait toujours 4 caractères, mais c’est maintentant impossible à deviner
Il y a un compte
demo
qui a été créé pour montrer le fonctionnement de notre authentification sûre.
Cordialement
(L’échange est bien évidemment tout autant fictif que le précédent)
Bien sûr, ce compte demo ne sert aussi qu’à l’exercice en cours.
Le compte demo permet d’afficher le SMS sur la page de connexion au lieu de le recevoir sur un téléphone.
Ce qui permet de se connecter.
Nous avons donc à disposition :
-
Un login connu
demo
-
Un code connu, reçu par SMS sur la page
-
Le hash du challenge
-
Il nous manque le timestamp exact et la chaîne utilisée comme SALT
Première étape, récupérer le hash et une approximation du timestamp.
Start:1592145505591
{"smsCode": 5511}
OTP=F9C72EC4D1244D081577174FD342A9081A5ABEF3C0062C1719B1CB440E1E02F6
UN=demo
End:1592145505841
Le code ici fait ce job. La précision sur le timestamp reste toute relative, il ne faut pas compter sur System.currentTimeMillis pour cela, mais cela nous permet de réduire à quelques chiffres l’imprécision sur le timestamp.
Il nous manque quelques décimales sur le timestamp et les 4 caractères sur le SALT.
Chaque caractère fait parti de l’ensemble minuscules, majuscules, chiffres et caractères spéciaux… on peut approximer 100 possibilitées, ce qui nous fait : \(100^4 \times 10^3 = 100000000000 = 10^{11}\)
Le code Java non optimisé pour calculer les SHA-256, et ses 500k hash/s, ne sera pas suffisant. Le code est en effet executé sur un seul coeur du CPU, ce qui était suffisant, mais sera trop long à présent.
Heureusement les cartes graphiques aujourd’hui calculent beaucoup plus vite les hash que les cpu, et Google Collab nous propose un environnement gratuit avec GPU
Je sélectionne GPU comme accélérateur matériel, j’installe hashcat et je plonge dans la doc.
!apt install cmake build-essential -y && apt install checkinstall git -y && git clone https://github.com/hashcat/hashcat.git && cd hashcat && git submodule update --init && make && make install
Je vérifie que l’installation est correcte
!hashcat -I
Je crée un fichier hash.txt
pour y mettre le hash, et j’éxécute
!hashcat -a 3 -m 1400 hash.txt demo5511?a?a?a?a1592145505?d?d?d
Les options se lisent ainsi :
- -a 3 pour un bruteforce
- -m 1400 pour du SHA-256
Le masque se lit ainsi :
- demo le login
- 5511 le code reçu par SMS
- ?a?a?a?a 4 caractères alphanumériques plus caractères spéciaux
- 1592145505 le timestamp sans les 3 derniers chiffres
- ?d?d?d les 3 derniers chiffres du timestamp
Au bout de quelques minutes :
Les 4 caractères ont été crackés, malgré le timestamp peu précis (sur 1 seconde), sur un GPU gratuit dans le cloud, il faut au pire 10 minutes, avec une vitesse de calcul d’environ 150 millions de hash par secondes.
Il reste maintenant avec le hash, à refaire la manip du premier exemple et se logguer comme admin.
Epilogue
Notre développeur malheureux, aura finalement appris qu’il ne faut pas (jamais) transmettre au client les informations qui permettent de reconstruire le hash. Finalement, il générera un unique id pour chaque challenge et ne transmettra que l’id au client. Le cassage d’un hash ne demande que des ressources, ce qui peut être fait de plus en plus vite et de moins en moins cher.
Conclusion
Entre les lignes ici, sous couvert de parler Hash et sécurité, j’ai aussi montré un HttpClient
avec son cookie manager associé, j’aime beaucoup cette API.
L’algorithme SHA-256 est considéré comme fiable, mais quelques erreurs de design ici ont réduit à quasi néant son efficacité.
Ne jamais envoyer au client des données dont il n’a pas absolument besoin, et comme notre développeur, ne pas prendre la sécurité à la légère.