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 ÊTRE UTILISÉ. 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 indéterminé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înes 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.
print_r(hash_algos());
III-A-2. Mise en place▲
III-A-2-a. Côté serveur▲
Le hash côté serveur empêche, en cas de vol de la base de données 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 deux ou trois 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 côté serveur, il existe la fonction SHA1 incorporée à PHP. On l'appelle lors de l'inscription du membre puis, à chaque connexion, on compare les deux hashs.
// A l'inscription
$pass
=
$_POST
[
'
pass
'
];
$login
=
$_POST
[
'
login
'
];
mysql_query("
INSERT INTO membres (login, pass)
VALUES ('
"
.
mysql_real_escape_string($login
).
"
', '
"
.
sha1($pass
).
"
')
"
);
// 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. Côté client▲
L'utilisation du hash côté client garantit 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 côté 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 côté 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)
<
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 remplit les deux champs texte 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.
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 deux intérêts très distincts suivant qu'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 provenant 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 quelque 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 contenue 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ères 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 trois 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 œuvre ; 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 côté 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 mots de passe enregistrés.
Inconvénients
Difficile à mettre en œuvre, 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 deux GDS, et effectuer côté client l'opération lourde
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 finalement 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 deux 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.
<
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).
<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 le 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 écho. On pourrait établir un flux XML plus propre, mais pour l'exemple, un simple flux texte est suffisant.
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 côté Javascript (dans le fichier getGDS.js).