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 fonctionnement d'un OTP

Le challenge consiste à fournir en réponse le code qui a été envoyé par le canal secondaire.

Construction d'un Challenge

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

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.

= > Notre serveur

Envoi au client

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

A vous de jouer !

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

Login

On regarde les cookies et on récupère le hash

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.

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Scanner;

public class BreakKnownSalt {

	public static void main(String[] args) {
		@SuppressWarnings("resource")
		Scanner scan = new Scanner(System.in);
		String s = scan.nextLine();

		String login = "admin";
		String salt = "SALT";
		long now = System.currentTimeMillis();
		
		MessageDigest digest;
		try {
			digest = MessageDigest.getInstance("SHA-256");
		} catch (NoSuchAlgorithmException e) {
			throw new RuntimeException(e);
		}
		
		int nbn = 0;
		for (int i = 0; i < 10_000; i++) {
			for (int code = 0; code < 10_000; code++) {
				String text = login + code + salt + (now - i);
				nbn++;
				byte[] hash = digest.digest(text.getBytes(StandardCharsets.UTF_8));
				String string = ByteHex.byteToHex(hash);
				
				if (string.equals(s)) {
					System.out.println("Found :" + code);
					System.out.println(nbn + " hashes has been computed");
					System.exit(0);
				}
			}		
		}
	}
}

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.

welcome admin

Passe moi le sel ? pas là !

= > Notre serveur Part2

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

Le compte demo permet d’afficher le SMS sur la page de connexion au lieu de le recevoir sur un téléphone.

SMS demo

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.

import java.io.IOException;
import java.net.CookieManager;
import java.net.CookiePolicy;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpRequest.BodyPublishers;
import java.net.http.HttpResponse.BodyHandlers;

public class BreakUnknownSalt {
	
	public static void main(String[] args) throws IOException, InterruptedException {
		String uri = args[0]; 
		
		CookieManager cookiesManager = new CookieManager();
		cookiesManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL);
		
		HttpClient client = HttpClient
				.newBuilder()
				.cookieHandler(cookiesManager)
				.build();
		String body = "username=demo";
		HttpRequest request = HttpRequest
				.newBuilder(URI.create(uri))
				.POST(BodyPublishers.ofString(body ))
				.build();
		System.out.println("Start:" + System.currentTimeMillis());
		var reponse = client.send(request, BodyHandlers.ofString());
		System.out.println(reponse.body());
		cookiesManager.getCookieStore().getCookies().forEach(c -> System.out.println(c));
		System.out.println("End:" + System.currentTimeMillis());
	}
}
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.

Collab GPU

!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 :

collab succeed

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.