La sécurité est un facteur de plus en plus important pour la viabilité des applications aujourd'hui. C'est un sujet complexe et qui peut devenir très vite extrêmement technique... mais ne vous inquiétez pas, nous partirons de zér0 bien entendu. :)
La robustesse de tout un dispositif ne tient souvent à pas grand-chose. Il faut donc bien être conscient que la sécurité d'un site web tout entier dépend de son maillon le plus faible. Je ne prétends donc pas sécuriser de manière définitive vos applications mais plutôt installer un dispositif supplémentaire pour freiner les pirates. Je vous propose d'étudier dans ce tutoriel une méthode de hacking mise en place par les pirates dans le but de pouvoir la contrer efficacement : l'attaque par force brute. :p
Effrayant comme nom, n'est-ce pas ? Si vous voulez comprendre comment sécuriser vos sites web contre ce type d'attaques, alors suivez le guide !
Je vais maintenant vous présenter plus en détails la stratégie mise au point par les pirates et qui prend le nom technique d'attaque par force brute.
Qu'est-ce qu'une attaque par force brute ?
Une attaque par force brute, ou attaque par exhaustivité, est une méthode utilisée par les pirates et en cryptanalyse pour découvrir le mot de passe ou la clef. Exhaustivité car il s'agit d'essayer toutes les combinaisons possibles ! Ce type d'attaque pirate est en réalité la moins subtile de toutes. Derrière ce concept effrayant de force brute se cache en réalité une technique très simple et très barbare :p .
Illustration par l'exemple
Pour illustrer cette attaque, prenons un petit exemple
Sur votre site web, vous avez une partie cachée au public qui correspond à la partie administration dans laquelle vous pouvez modifier tous les paramètres de votre site web. Le site du Zér0 possède bien entendu ce genre de section privée et de plus en plus, les webmasters possèdent leur partie administrative.
Mettons-nous dans la tête d'un pirate
Imaginons un instant que vous soyez un hackeur : je pense que vous souhaiteriez accéder à cette zone privée pour prendre possession du site entier :) .
Or, pour accéder à cette partie du site, il faut bien entendu montrer patte blanche au serveur en lui fournissant un pseudo et un mot de passe. Nous n'allons quand même pas tenter toutes les combinaisons possibles manuellement (trop fatiguant pour un pirate ^^ ). C'est à ce moment que vous décidez de passer à l'attaque ! Un robot (programme automatisé) se chargera à votre place d'essayer toutes les combinaisons possibles.
Limites de cette attaque
Ouille, mais ça risque de prendre pas mal de temps, tout ça ?
Eh oui ! Mais heureusement pour nous ! D'ailleurs, c'est pour cela que je vous disais que ce type d'attaque n'était pas très subtil. Mais ne vous inquiétez pas pour eux, leurs robots peuvent tenter plusieurs centaines de mots de passe à la seconde et ils ne sont pas obligés non plus de rester devant leur ordinateur : ils ont tout leur temps... Le problème majeur que pose cette attaque, c'est qu'elle finit toujours par trouver le mot de passe si aucune défense n'a été mise en place... ce n'est qu'une question de temps !
Bon : j'espère que je ne vous ai pas trop fait peur, mais je voulais que vous compreniez que la sécurité d'un système est primordiale de nos jours. Rassurez-vous, je ne pense pas que des pirates soient réellement intéressés par des blogs ou des sites amateurs :) .
Pour contrer les techniques mises en place par les pirates, il faut bien entendu comprendre comment elles fonctionnent. Nous savons donc qu'ils envoient des robots sur des parties sensibles d'une application pour essayer de récupérer la clef.
Comment diminuer le nombre de tentatives des pirates ?
L'attaque par force brute se fonde sur le nombre de tentatives : plus les robots essayent des mots de passe et plus leurs chances de trouver la clef augmentent (c'est statistique :lol: ). Nous allons donc les empêcher d'entrer plus de X tentatives par intervalle de temps. En effet, si on limite leur nombre de tentatives à 10 par minute par exemple :
Soit N le nombre de tentatives possibles en 1 an avec la protection
N = 10*60*24*365 = 5 256 000
Soit N' le nombre de tentatives possibles en 1 an sans la protection
N' = 100*60*60*24*365 = 3 153 600 000
Soit r le rapport de N' sur N
r = N' / N = 3 153 600 000 / 5 256 000 = 600
Après un petit calcul, en un an ils auront effectué "seulement" 5 256 000 tentatives : ça peut paraître énorme mais en réalité, ce n'est rien du tout ! Voyez vous-mêmes, sans la protection, ils auraient pu entrer 3 153 600 000 essais avec 100 tentatives par seconde.
On peut donc diminuer leurs chances par 600 de trouver le mot de passe. Autrement dit, pour effectuer autant de tentatives, ils devront faire fonctionner leur ordinateur 600 ans sans interruption (ils ne sont quand même pas suicidaires à ce point :p ).
Par ailleurs, je disais dix tentatives par minute, mais vous pouvez très bien réduire encore le nombre de tentatives : c'est vous qui voyez ^^ .
Dans ce cas, soit ils vous laisseront tranquilles, soit ils utiliseront une autre technique pour obtenir ce qu'ils veulent... une technique moins "bourrine" et plus subtile !
Pour mettre au point notre contre-attaque, je vous conseille fortement de maîtriser les points suivants en PHP :
les sessions ;
les cookies ;
les bases de MySQL.
Si vous êtes prêt(e)s, alors let's script !!
Partie SQL
Réfléchissons ensemble à l'intégration de notre dispositif. Il nous faut tout d'abord une table qui stocke les personnes ayant un accès à la partie administration.
--
-- Structure de la table `administration`
--
CREATE TABLE IF NOT EXISTS `administration` (
`id` tinyint(2) NOT NULL auto_increment,
`pseudo` varchar(50) collate utf8_unicode_ci NOT NULL,
`passe` varchar(32) collate utf8_unicode_ci NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=1 ;
Cryptage des données
Quelques explications s'imposent : on met un VARCHAR 32 pour le passe car nous allons le hacher avec la fonction md5() de PHP qui retourne une chaîne de 32 caractères. Je ne m'étendrai pas sur le principe du hachage dans ce tutoriel. Sachez simplement que si un pirate venait à se procurer le contenu de votre BDD, il n'aurait pas directement accès à vos mots de passe car ils sont hachés, et il est impossible de faire marche arrière (le dé-hachage n'existe pas :p ). Pour les petits curieux : informations sur le principe de hachage de md5.
Petit script qui vous hache votre mot de passe :
<?php
$passe = 'anticonstitutionnellement'; // Passe à hacher
echo md5($passe); // Renvoie : 99c1c137d8d85917f632a0e34a35a5f7
?>
Remarque : pour ceux qui veulent accroître la sécurisation de leurs mots de passe, sachez qu'il aurait fallu rajouter un "salt" ou grain de sel : un grain de sel est un mot ou un passe que vous seuls connaissez et que vous allez ajouter aux mots de passe entrés par les membres. À quoi ça sert ? Si un pirate met la main sur votre BDD, il lui sera beaucoup plus difficile de "déhacher" ces mots md5 avec l'utilisation d'un site qui les recense comme je vous en ai montré un plus tôt. ;) Pour plus d'informations : salage.
Petit exemple :
<?php
// Salage du mot de passe
define("PREFIXE", "zer0");
define("SUFFIXE", "forever");
$passe = 'anticonstitutionnellement'; // Passe à hacher
// Assemblage de notre passe
$passe = PREFIXE . $passe . SUFFIXE;
echo md5($passe);
?>
Et pour les plus paranos d'entre nous, je propose une petite fonction de hachage très performante :
<?php
function hacher($passe)
{
// Nos grains de sel
define("PREFIXE", "zer0");
define("SUFFIXE", "forever");
// Faites tournez le Hachage ^^
$passe = md5( sha1(PREFIXE) . $passe . sha1(SUFFIXE) );
return $passe;
}
?>
Mais je ne comprends pas à quoi elle sert, ta fonction. Comment on l'utilise, concrètement ? o_O
Je m'attendais à cette question, ce n'est pas pour rien qu'on est sur le Site du Zér0 : c'est donc mon devoir de tout vous expliquer depuis zéro. ;)
Une utilisation possible de cette fonction :
<?php
// On hache le passe récupéré à partir d'un formulaire
$passe = hacher($_POST['passe']);
// Formatage de notre requête sql
$requete = "SELECT passe FROM administration WHERE pseudo = 'admin'";
// Envoi de la requête au serveur
$query = mysql_query($requete) or exit(mysql_error());
$resultat = mysql_fetch_assoc($query);
// On compare les deux empreintes
if($resultat['passe'] == $passe)
{
echo 'le mot de passe est correct';
}
else
{
echo 'Erreur : mauvais mot de passe !';
}
?>
Bref, fermons la parenthèse et revenons à notre script. Ensuite, on entre toutes nos données dans la BDD dans la table fraîchement créée ^^ .
INSERT INTO administration(id, pseudo, passe)
VALUES('', 'admin', '99c1c137d8d85917f632a0e34a35a5f7')
Partie XHTML
Passons à présent à la partie XHTML, créons un petit formulaire pour accéder à la partie administration.
Vous pouvez bien entendu rajouter des champs supplémentaires et des fichiers CSS, après c'est à vous de voir :-° .
Algorithme défensif
Allez : on entre vraiment dans le vif du sujet maintenant que les bases sont posées. Codons l'algorithme de base de notre système de défense.
Quoi ? Vous pensiez vraiment que j'allais tout vous donner sur un plateau d'argent :p ? Maintenant c'est à vous de jouer (enfin, de coder). Essayez de le faire par vous-mêmes, ça sera un très bon entraînement car c'est en "forgeant qu'on devient forgeron". Voilà un petit récapitulatif de l'algorithme de base que nous développerons ensuite :
Si le formulaire est correctement validé (champs correctement remplis)
Sécurisation des variables transmises
Formatage de la requête SQL
Bon, vous avez réussi j'espère, ce n'est pas encore trop compliqué. Voilà une solution possible :
<?php
// Si le formulaire est correctement rempli
if(isset($_POST['connexion'])
and !empty($_POST['pseudo'])
and !empty($_POST['passe']))
{
// Sécurisation des variables
$pseudo = mysql_real_escape_string($_POST['pseudo']);
$passe = mysql_real_escape_string(md5($_POST['passe'])); // Hachage
// Formatage de la requête
$requete = "SELECT id
FROM administration
WHERE pseudo = '$pseudo'
AND passe = '$passe'";
}
?>
Utilisation des sessions
Rappelez-vous, nous devons empêcher le hackeur de faire plus de n tentatives par minute. Nous allons donc créer des variables qui vont "compter" le nombre de fois que le formulaire a été soumis. Allez : creusez vous les méninges, je suis sûr que vous allez trouver...
Bien sûr, vous avez raison, nous utiliserons les sessions : c'est l'outil le mieux adapté à nos besoins. Complétez donc le script précédent.
Voilà une solution possible :
<?php
// Si la variable de session qui compte le nombre de soumissions n'existe pas
if(!isset($_SESSION['nombre']))
{
// Initialisation de la variable
$_SESSION['nombre'] = 0;
}
?>
Très bien, maintenant nous allons effectuer la requête SQL sous certaines conditions seulement : il faut que la variable de session soit inférieure à n (nous prendrons ici n = 10, mais vous pouvez très bien modifier cette valeur par la suite).
Voilà ce que j'obtiens :
<?php
// Si on n'essaye pas de nous attaquer par force brute
if($_SESSION['nombre'] < 10)
{
// Connexion à notre base de données
mysql_connect("localhost", "root", "");
mysql_select_db("table");
// Envoie de la requête au serveur
$query = mysql_query($requete) or exit(mysql_error());
// Incrémentation de notre variable de session
$_SESSION['nombre']++;
}
// Si on a dépassé les 10 tentatives, on quitte le script
else
{
exit();
}
?>
N'oubliez pas de rajouter la fonction session_start() en haut de votre code, sinon la session ne fonctionnera pas correctement :) .
Problèmes de sécurité
À ce stade, le script fonctionne : il nous est impossible de lancer plus de 10 tentatives. Cependant, il reste quelques problèmes à régler : si le membre reste sur le site, il conserve ses variables de session et ne peut donc pas se reconnecter. De plus, si on ferme le navigateur et qu'on le relance, il est possible de renvoyer des tentatives...
Comment faire pour parer à ces problèmes ?
Pour le premier problème, nous allons simplement rajouter une variable de session qui enregistre le temps au bout duquel le formulaire sera de nouveau opérationnel.
Voilà ce que je vous propose de rajouter :
<?php
// Si la variable de session qui compte le nombre de soumissions n'existe pas
if(!isset($_SESSION['nombre']))
{
// Initialisation de la variable
$_SESSION['nombre'] = 0;
// Blocage pendant 10 min
$_SESSION['timestamp_limite'] = time() + 60*10;
}
?>
Puis, tout au début du script, rajoutez :
<?php
// Si on a dépassé le temps de blocage
if(isset($_SESSION['nombre'])
and $_SESSION['timestamp_limite'] < time())
{
// Destruction des variables de session
unset($_SESSION['nombre']);
unset($_SESSION['timestamp_limite']);
}
?>
Pour surmonter le second problème, nous allons à présent utiliser les cookies qui constitueront une sécurité complémentaire et nécessaire.
Utilisation des cookies
Principe de la protection : si le membre a entré dix tentatives, on crée un cookie qui nous permettra de le marquer lorsqu'il reviendra sur le site. Ce cookie devra le bloquer pendant X minutes (nous prendrons X = 1). Allez, réfléchissez à la manière de réaliser tout ça : ce n'est pas très compliqué ^^ (le plus dur est déjà passé, ne vous en faites pas).
Voilà une solution :
<?php
// Si on a dépassé les 10 tentatives
else
{
// Si le cookie marqueur n'existe pas, on le crée
if(!isset($_COOKIE['marqueur']))
{
$timestamp_marque = time() + 60; // On le marque pendant une minute
setcookie("marqueur", "marque", $timestamp_marque);
}
// on quitte le script
exit();
}
?>
Remarque : le code que je viens de vous montrer comporte en fait un autre problème de sécurité : il provient du décalage horaire qui peut exister entre votre serveur et la localisation géographique de l'internaute. Par exemple, si vous habitez en Chine et que vous exécutez ce script depuis un serveur français, alors le cookie sera "mort-né". Autrement dit, dès qu'il sera créé, il sera détruit à cause du décalage horaire ^^ .
Pour se conformer à ce petit problème supplémentaire, nous allons tout simplement donner une durée de vie plus importante au cookie et stocker le timestamp limite dans sa valeur.
Voilà ce que ça donne :
<?php
// Si on a dépassé les 10 tentatives
else
{
// Si le cookie marqueur n'existe pas on le crée
if(!isset($_COOKIE['marqueur']))
{
$timestamp_marque = time() + 60; // On le marque pendant une minute
$cookie_vie = time() + 60*60*24; // Durée de vie de 24 heures pour le décalage horaire
setcookie("marqueur", $timestamp_marque, $cookie_vie);
}
// on quitte le script
exit();
}
?>
Parfait ! À présent, il ne nous reste plus qu'à vérifier si le cookie existe avant de lancer quoi que ce soit.
Voilà ce qu'il faut rajouter :
<?php
// Si le cookie n'existe pas
if(!isset($_COOKIE['marqueur']))
{
// Tout notre code
}
// Si le cookie existe
else
{
// Si le temps de blocage a été dépassé
if($_COOKIE['marqueur'] < time())
{
// Destruction du cookie
setcookie("marqueur", "", 0);
}
}
?>
Le pirate ou le robot qui tentera d'accéder à votre script de connexion se bannira lui-même à la condition qu'il prenne en charge les sessions et les cookies ;) . Voilà : le script est terminé. Certes, il est basique mais terriblement efficace contre ce type d'attaques !
Voilà un petit récapitulatif du script anti force brute que nous avons expliqué dans la partie précédente :
<?php
// Démarrage de la session
session_start();
// Si on a dépassé le temps de blocage
if(isset($_SESSION['nombre'])
and $_SESSION['timestamp_limite'] < time())
{
// Destruction des variables de session
unset($_SESSION['nombre']);
unset($_SESSION['timestamp_limite']);
}
// Si le cookie n'existe pas
if(!isset($_COOKIE['marqueur']))
{
// Si le formulaire est correctement rempli
if(isset($_POST['connexion'])
and !empty($_POST['pseudo'])
and !empty($_POST['passe']))
{
// Si la variable de session qui compte le nombre de soumissions n'existe pas
if(!isset($_SESSION['nombre']))
{
// Initialisation de la variable
$_SESSION['nombre'] = 0;
// Blocage pendant 10 min
$_SESSION['timestamp_limite'] = time() + 60*10;
}
// Sécurisation des variables
$pseudo = mysql_real_escape_string($_POST['pseudo']);
$passe = mysql_real_escape_string(md5($_POST['passe']));
// Formatage de la requête
$requete = "SELECT id
FROM administration
WHERE pseudo = '$pseudo'
AND passe = '$passe'";
// Si on n'essaye pas de nous attaquer par force brute
if($_SESSION['nombre'] < 10)
{
// Connexion à notre base de données
mysql_connect("localhost", "root", "");
mysql_select_db("table");
// Envoie de la requête au serveur
$query = mysql_query($requete) or exit(mysql_error());
// Ici, vous traitez les résultats de votre requête à votre guise
// Incrémentation de notre variable de session
$_SESSION['nombre']++;
}
// Si on a dépassé les 10 tentatives
else
{
// Si le cookie marqueur n'existe pas on le crée
if(!isset($_COOKIE['marqueur']))
{
$timestamp_marque = time() + 60; // On le marque pendant une minute
$cookie_vie = time() + 60*60*24; // Durée de vie de 24 heures pour le décalage horaire
setcookie("marqueur", $timestamp_marque, $cookie_vie);
}
// on quitte le script
exit();
}
}
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head>
<title>Administration</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<form method="post" action="">
<p>
<label>Pseudo </label>
<input type="text" name="pseudo" /><br />
<label>Passe </label>
<input type="text" name="passe" /><br />
<input type="submit" name="connexion" value="connexion" />
</p>
</form>
</body>
</html>
<?php
}
// Si le cookie existe
else
{
// Si le temps de blocage a été dépassé
if($_COOKIE['marqueur'] < time())
{
// Destruction du cookie
setcookie("marqueur", "", 0);
}
}
?>
Tout est là, mais ce n'est pas pour ça que le script est parfait (d'ailleurs, un script parfait, ça n'existe pas et ça n'existera jamais :) ). Il y a encore beaucoup de choses à faire pour élever le niveau de sécurité de votre application.
Améliorations possibles
Voilà quelques pistes qui méritent d'être explorées :
protéger votre répertoire par un .htaccess ;
utiliser un blocage en dur par la BDD en complément du blocage par cookie ;
stocker les infos du membre qui a effectué plus de n tentatives ;
ajouter un CAPTCHA pour détourner plus facilement les robots (2 tutos sur le site : ici et ici) ;
vérifier que les données proviennent bien de votre formulaire en le marquant avec un md5 dans un champ caché ;
donner une durée de vie minimale et maximale à votre formulaire ;
etc.
Technique complémentaire
Par ailleurs, vous pouvez très bien mettre au point une technique complémentaire très efficace qui consiste à empêcher simplement de tenter plus de 2 tentatives d'affilée par n secondes : un peu comme un système anti-flood pour un forum, par exemple :) . Le membre ne verra aucune modification si vous prenez une marge de 1 voire 2 secondes, tandis que le robot qui doit rentrer des centaines de tentatives par seconde sera pris au piège :diable: .
Comment je fais ça, moi ?
C'est là que PHP vole à notre secours et nous propose une fonction magique : j'ai nommé la fonction sleep() :magicien: .
<?php
sleep(1); // On "endort" le script pendant une seconde
?>
En temporisant de la sorte votre script, vous ajoutez une protection supplémentaire pour lutter contre l'attaque par force brute. Notez qu'une ligne de code à rajouter pour améliorer la sécurité de son application, c'est une occasion à ne pas manquer ^^ .
Références et approfondissements
Pour approfondir vos connaissances sur le sujet, voilà quelques références :
Une zone d'administration est un point névralgique de toute application web. Je vous ai exposé et expliqué une méthode qui permet de lutter contre les attaques par force brute.
Le point sur les mots de passe
Néanmoins la sécurité doit être abordée d'un point de vue global. Ça ne sert à rien de sécuriser votre panneau d'administration si votre mot de passe n'est pas lui-même sécurisé. Préférez donc un mot de passe mélangeant chiffres et lettres à votre prénom ou votre nom. Par ailleurs, changez-le souvent, c'est essentiel ! Voici quelques conseils pour choisir votre mot de passe :
mélange de chiffres et de lettres ;
mélange de minuscules et de majuscules ;
longueur minimale de 8 voire 10 caractères.
Considérez donc cette approche comme une présentation mais certainement pas comme une fin en soi (ce tutoriel ne prétend pas être exhaustif) : la sécurité est toujours un horizon vers lequel on doit s'efforcer de tendre.