Vos recrutements informatiques

700 000 développeurs, chefs de projets, ingénieurs, informaticiens...

Contactez notre équipe spécialiste en recrutement

Chiffrement et hash en PHP contre l'attaque Man in the middle


précédentsommairesuivant

III. Protection contre le vol de mot de passe

Pour éviter le vol du mot de passe, il faut limiter, voire proscrire, sa transmission et son stockage en clair. À la place, on va manipuler un hash, beaucoup sécurisé car il est (théoriquement) impossible à inverser. En pratique, c'est "réversible". Le hacker utilisera par exemple des dictionnaires pour obtenir le mot de passe en clair. Nous verrons comment lui compliquer la vie avec la technique du grain de sel.

III-A. Hash du mot de passe

III-A-1. Utilité du hash

Le hash est un procédé de chiffrement irréversible. Ceci implique qu'on ne peut appliquer la transformation inverse, et qu'on ne peut donc que comparer les hashs entre eux. L'avantage est que personne, y compris le webmaster, n'a accès aux mots de passe des membres.

Il existe deux principaux algorithmes de hash, le MD5 et le SHA1.

Le MD5 est déprécié et NE DOIT PLUS ETRE UTILISE. En effet, une faille dans l'algorithme a été trouvée, permettant de diminuer considérablement le nombre d'opérations nécessaires à un inversement par rapport à une attaque force brute. De plus, et c'est le plus inquiétant, il a été montré qu'il était possible de créer volontairement des collisions avec un document donné. En effet, les hashs MD5 sont constitués de 32 caractères alphanumériques, soit 3632= 6.3349 combinaisons possibles. Ainsi, le nombre de hashs possibles est limité. Or, nous réalisons des hashs de chaîne de caractères (strings, fichiers...) de taille indeterminée et pour la plupart bien plus grandes que 32 caractères. Par exemple, nous pouvons constituer 21556 chaînes de 1000 caractères alphanumériques.
Nous avons donc une infinité de chaîne de caractères pouvant donner un hash précis et il est maintenant possible de déterminer instantanément les autres chaînes de caractères à partir d'une des chaînes et du hash.

Il est donc recommandé d'utiliser des algorithmes plus robustes, comme le SHA1 ou le SHA-256. Mais il faut bien garder en tête qu'un algorithme est reconnu comme fiable uniquement car il n'a pas encore été craqué. Ainsi, le SHA1 est déjà sur la pente descendante, le nombre d'opérations nécessaires à l'inversion ayant été ramené de 2^80 à 2^63.

Maintenant que nous avons présenté le principe du hash et les principaux algorithmes, nous allons voir un exemple pratique de mise en place côté serveur et côté client.

Vous pouvez tester un code de ce type sur votre serveur pour avoir la liste des hashs supportés par votre installation de PHP.

Afficher la liste des hashs supportés
Sélectionnez

print_r(hash_algos());
Résultat
Sélectionnez

Array
(
    [0] => md4
    [1] => md5
    [2] => sha1
    [3] => sha256
    [4] => sha384
    [5] => sha512
    [6] => ripemd128
    [7] => ripemd160
    ...
);

III-A-2. Mise en place

III-A-2-a. Coté serveur

Le hash coté serveur empêche, en cas de vol de la base de donnée par intrusion sur le serveur, de fournir au hacker tous les mots de passe en clair des membres du site. Il faut savoir que la plupart des membres ne possèdent que 2 ou 3 mots de passe qu'ils utilisent sur presque tous les sites. Le webmaster doit donc s'assurer de son côté de l'intégrité des mots de passe de ses visiteurs. Pour effectuer un hash coté serveur, il existe la fonction SHA1 incorporée à PHP. On l'appelle lors de l'inscription du membre puis, à chaque connexion, on compare les 2 hashs.

Manipulation du hash lors de l'inscription
Sélectionnez

// A l'inscription
$pass = $_POST['pass'];
$login = $_POST['login'];
mysql_query("
    INSERT INTO membres (login, pass) 
    VALUES ('".mysql_real_escape_string($login)."', '".sha1($pass)."')");
Manipulation du hash à la connexion
Sélectionnez

// A la connexion
$pass = $_POST['pass'];
$login = $_POST['login'];
$result = mysql_query("
    SELECT 1 
    FROM membres 
    WHERE (login='".mysql_real_escape_string($login)."')
    AND (pass='".sha1($pass)."')");
if (mysql_num_rows($result) > 0)
{
    // on connecte
}

III-A-2-b. Coté client

L'utilisation du hash coté client garanti l'intégrité du mot de passe lors de son transit depuis le navigateur du client vers le serveur. Il faut donc encoder le mot de passe avant de l'envoyer. Ceci peut être fait par un langage de script comme le Javascript. La mise en place de cette technique est plus lourde que le hash coté serveur (car ces langages de script n'implémentent pas de fonction MD5 ou SHA1), et peut ne pas fonctionner si le client a désactivé Javascript. Il faudra alors prévoir une méthode alternative (transmission du mot de passe en clair). La première étape sera donc d'écrire, ou de récupérer une fonction de hash pour le langage de script que vous aurez choisi. Nous utiliserons ici une implémentation en Javascript de l'algorithme SHA1.

Réaliser un hash coté client n'empêche absolument pas le vol de session, il suffira au hacker d'envoyer le hash au lieu du mot de passe. Il faut bien comprendre que l'utilisation du hash n'est faite que pour protéger le mot de passe.

Plusieurs sites réputés utilisent cette technique. Par exemple, si vous fouillez un peu sur la page d'identification de Yahoo!, vous trouverez un lien vers ce fichier. Cette même implémentation est utilisée dans d'autres scripts, notamment de forum.
Le fichier Javascript que nous utiliserons pour le sha1 provient de la même source dont le lien est disponible en fin d'article.

Un fichier opérationnel est disponible ici (sha1hash.zip)

Formulaire coté client
Sélectionnez

<script type="text/javascript" src="sha1hash.js"></script>
<form method="post" action="" id="connexion-form" name="connexion-form" onsubmit="sha1hash(this.mdp, this.sha1mdp);">
	Pseudo : <input type="text" name="login" id="login" size="20" autocomplete="off">
	Mot de passe : <input type="password" name="mdp" size="20" />
	<input type="hidden" name="sha1mdp" value="" />
	<input value="Valider" id="Valider" type="submit" class="bouton">
</form>
  • L'utilisateur rempli les 2 champs textes login et mdp.
  • Lorsqu'il envoie le formulaire, la fonction sha1hash est appelée. Cette fonction prend en argument les objets correspondant aux champs de formulaire mdp et sha1mdp.
  • La fonction remplit le champ sha1mdp avec le mot de passe hashé et vide le champ correspondant au mot de passe en clair.
  • Le formulaire est envoyé au serveur.
Recéption du formulaire coté serveur
Sélectionnez

// Javascript activé
if ( ! empty ($_POST['sha1mdp']) )
{
	$sha1mdp = $_POST['sha1mdp'];
}
// Javascript désactivé
elseif ( ! empty($_POST['mdp']) )
{
	$sha1mdp = sha1($_POST['mdp']);
}

III-B. Technique du Grain De Sel (GDS)

III-B-1. Utilité du GDS

Le but premier du GDS est de protéger le hash du mot de passe lors de son transfert et de son stockage. En effet, il est de plus en plus facile d'inverser un hash, nous utilisons donc le GDS afin de compliquer cette inversion.
Le grain de sel a 2 intérêts très distincts suivant s'il est utilisé côté client ou serveur.

Côté serveur

Cela signifie en pratique que l'on a hashé le mot de passe de l'utilisateur, préalablement concaténé avec le GDS, avant de l'enregistrer dans la base de données.
Ceci empêche le hacker, en cas de vol de la base de données, d'utiliser la force brute ou un dictionnaire pour inverser les hashs. En effet, comme le GDS est secret, le hacker ne pourra pas constituer de dictionnaire adapté. De plus, la force brute sera vraiment plus longue, car si le GDS est une chaîne de 32 caractères et le mot de passe une chaîne de 8 caractères alors il faudra tester 2.812 combinaisons sans GDS ou 1.862 avec GDS.
Le fondement de cette protection est que le hacker ne connaît pas la valeur du GDS.

Cette utilisation du GDS n'empêche absolument pas la force brute provennant d'un formulaire utilisateur. Pour empêcher cela, il faudra mettre un timer entre chaque tentative ou un bloqueur au bout de x tentatives.

Côté client

Le GDS côté client suppose que le GDS ne soit pas secret. Il ne peut donc pas protéger des attaques par force brute. Par exemple, le mot de passe est "lol" et le GDS "azerty". On appliquera le hash sur toutes les combinaisons possibles de "aaa" à "zzz" pour comparer chaque résultat au hash qu'on souhaite inverser. Avec un GDS connu, il suffira de tester les combinaisons de "azertyaaa" à "azertyzzz". Cela ne complique en rien notre recherche.

Ce type de GDS protège contre les attaques par dictionnaire. En effet, depuis quelques temps, des dictionnaires de hash sont disponibles sur le net, voire directement utilisables en ligne. On peut trouver des dictionnaires en ligne contenant plus de 500 millions de hashs référencés. Grâce à ces dictionnaires, il est possible de retrouver instantanément un mot de passe à partir du hash s'il est dans le dictionnaire.
Toute l'utilité de ce GDS est contenu dans ces mots "s'il est dans le dictionnaire", car si notre mot de passe "lol" y est très certainement, il y a peu de chances que "azertylol" y soit, et encore moins "skndgqzmsrgze6r4gb5q1fde5bhs3df4b3q4df3b4sdfblol".

Avoir un site utilisant une technique de GDS ne change rien au fait que le choix du mot de passe est crucial pour la sécurité. Il est donc conseillé d'utiliser des mots hors dico avec chiffres / majuscules / minuscules / caractère spéciaux.

  Protège Contre
  Base de données Transfert du mdp Attaques par force brute Attaques par Dictionnaire
Côté server X   X X
Côté client   X   X



On voit dans ce tableau récapitulatif la faiblesse du "GDS côté client". Si le hacker arrive à recueillir le GDS et le hash côté client, il pourra utiliser une technique de force brute traditionnelle, sans que nous lui ayons compliqué le travail. Le GDS côté serveur et côté client sont deux techniques complémentaires.

III-B-2. Présentation des 3 techniques possibles

Il existe en réalité beaucoup plus de méthodes, mais elles impliquent toutes -à au moins l'une des étapes- le stockage ou la transmission du mot de passe en clair.
Nous noterons ici pour chaque technique l'efficacité de la protection correspondante :

  • 0 = pas protégé
  • 1 = peu protégé
  • 2 = bien protégé
  • 3 = très bien protégé

III-B-2-a. GDS global

Le principe ici est d'avoir un GDS général pour tout le site et pour tous les utilisateurs. L'avantage est la facilité de mise en oeuvre ; cependant, c'est également la moins sûre. En pratique, on a un GDS donné. À l'inscription on enregistre le mot de passe en base concaténé avec ce GDS et hashé en SHA1. Lorsqu'un visiteur se loggue, on envoie le GDS au client, on hash en SHA1 le mot de passe concaténé avec ce GDS, on envoie au serveur. Celui-ci compare simplement les deux valeurs.

Inconvénients
Il suffit de constituer un seul dictionnaire pour attaquer tous les membres du site. De plus, le GDS n'est pas secret, donc le temps des attaques par force brute n'est pas augmenté.

Avantages
La technique est simple, la base est protégée contre les attaques par dictionnaire et mot de passe ne circule pas en clair.

  Protection contre force brute Protection contre dictionnaires
Côté serveur 1 2
Côté client 0 2

III-B-2-b. GDS par utilisateur

C'est la technique que j'ai adoptée. Elle est similaire à la précédente, sauf qu'à l'inscription, on génère un GDS qui sera propre à l'utilisateur. Ce GDS sera conservé dans la base et envoyé au client à chaque demande de connexion.

Mise en place
  • À l'inscription, on génère le GDS et on le place en base
  • À la connexion, le visiteur entre son login
  • On va chercher le GDS, soit par Ajax, soit en envoyant le login par formulaire, et en renvoyant le GDS
  • On demande le mot de passe, on chiffre coté client et on envoie au serveur

Inconvénients
La récupération du GDS après la saisie du login implique soit deux formulaires (comme dans SPIP), soit un script Ajax

Avantages
Tout est protégé contre les attaques par dictionnaire et le hacker doit constituer un dictionnaire par membre, ce qui est fastidieux et décourageant.
Le GDS de l'utilisateur est enregistré dans la base de données et transite à chaque connexion. Le hacker y a donc accès facilement et peut donc utiliser la force brute pour inverser le hash.

  Protection contre force brute Protection contre dictionnaires
Côté serveur 0 3
Côté client 0 3

III-B-2-c. GDS par session

Cette technique consiste à générer un GDS propre à la session. On ne pourra donc pas stocker le mot de passe en base grâce à ce GDS. Il faudra donc prévoir une deuxième technique (GDS global ou par utilisateur) pour protéger les mot de passes enregistrés.

Inconvénients
Difficile à mettre en oeuvre, car elle nécessite une deuxième technique pour protéger le stockage en base car on ne peut pas enregistrer en base un mot de passe protégé par un GDS qui changera. Il faudrait gérer 2 GDS, et effectuer côté client l'opération lourde

 
Sélectionnez

protected_mdp = SHA1(SHA1(mdp+GDS1)+GDS2)

Dans laquelle SHA1(mdp+GDS1) représente le mot de passe tel qu'il est stocké en base.

Ainsi, toute la base est protégée par le même GDS, et en outrepassant le GDS de session côté client (rendu possible car on doit prendre en compte la possibilité que Javascript ne soit pas activé), on peut réaliser une attaque par dictionnaire dans lequel le dictionnaire serait constitué pour ce GDS spécifique.
On revient au final au principe du GDS global.

Avantages
Probablement la technique la plus sûre si on ne prend pas en compte la possibilité d'avoir le Javascript désactivé, car même si le hacker réussit à trouver le GDS, celui-ci changera pour chaque session (et ainsi pour chaque membre).

  Protection contre force brute Protection contre dictionnaires
Côté serveur 1 2
Côté client 0 3

III-B-3. Mise en place du GDS par utilisateur

Nous allons ici approfondir la technique de protection par GDS propre à un utilisateur car c'est la seule protégeant réellement d'une attaque par dictionnaire, étant donné qu'il faut constituer un dictionnaire par membre. Pour la réaliser, je vais utiliser un peu d'Ajax. Comme je l'ai dit plus haut, il est possible de réaliser 2 formulaires au lieu du script Ajax, c'est le choix qu'a fait le projet SPIP (cf. rubrique lien à la fin de l'article). Le script Ajax utilisé s'appuie sur le tutoriel de Denis Cabasson disponible ici Tutoriel Ajax
Le principe consiste à lire le champ de login et à appeler une page distante à chaque changement observé. Ainsi, à chaque saisie d'un caractère dans le champ login, nous vérifierons si un utilisateur existe avec ce login et si oui, nous renverrons son GDS.

Un fichier JS pour réaliser ceci est disponible ici (getGDS.zip).
Il nous manque maintenant le formulaire et un fichier PHP renvoyant le GDS d'un utilisateur.

Côté client

Il est nécessaire dans la page d'insérer et d'initialiser les scripts de GDS et de hash, puis nous affichons le formulaire.

Insertion du code Javascript dans l'entête de l'HTML
Sélectionnez

<script type="text/javascript">
	var _adresseRecherche = "gds.php"   // l'adresse à interroger pour trouver le GDS
</script>
<script type="text/javascript" src="sha1hash.js"></script>
<script type="text/javascript" src="getGDS.js"></script>
<script type="text/javascript">
	window.onload = function()
	{
		initGetGDS(
			document.getElementById('connexion-form'),
			document.getElementById('login'),
			document.getElementById('Valider'),
			document.getElementById('gds'),
			document.getElementById('info_gds')
			);
	};
</script>

La variable JS _adresseRecherche définit la page distante qu'il faudra appeler pour récupérer le GDS.
Pour initialiser le listener du champ login, nous appelons la fonction initGetGDS en lui passant comme paramètres les objets DOM

  • Du formulaire
  • Du champ login
  • Du bouton de submit
  • Du champ hidden dans lequel placer le GDS
  • Du div dans lequel on pourra indiquer si le GDS est bien réceptionné. (optionnel)
Code HTML du formulaire
Sélectionnez

<form method="post" action="" id="connexion-form" name="connexion-form" onsubmit="sha1hash(this.mdp, this.sha1mdp, this.gds);">
	Pseudo : <input type="text" name="login" id="login" size="20" autocomplete="off" />
	<div id="info_gds">Grain de sel</div><br />
	Mot de passe : <input type="password" name="mdp" size="20" /><br />
	<input type="hidden" name="sha1mdp" value="">
	<input type="hidden" name="gds" id="gds" value="">
	<input value="Valider" id="Valider" type="submit" class="bouton">
</form>

Le reste est identique au modèle présenté dans le chapitre consacré au hash, avec le GDS en plus. Le hash se fera ici selon la formulaire :
sha1mdp = sha1(GDS + mdp + GDS)

Côté serveur

Il s'agit d'un script très simple qui recherche dans la base de données ce login, sélectionne le GDS et en fait un echo. On pourrait établir un flux XML plus propre, mais pour l'exemple, un simple flux texte est suffisant.

Code php du fichier gds.php
Sélectionnez

if(isset($_GET['login']))
{
   $login = urldecode($_GET['login']);
}
else
{
   exit 0;
}
 
$gds = $db->get_var("SELECT me_gds FROM membres WHERE login='".mysql_real_escape_string($login)."'");
if ( !empty($gds) )
{
    echo $gds;
}

Le login est envoyé par GET, on le récupère, on applique un urldecode car on a utilisé urlencode coté Javascript (dans le fichier getGDS.js).


précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 25/01/2007 Guillaume Affringue. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.