mercredi 22 décembre 2010

Injection SQL : contourner un système d'authentification

Premier article d'une série sur les injections SQL.
Je ne montrerais que des exemples sur des applications PHP avec MySQL comme système de base de données.
Ce que nous allons apprendre :
  • Outrepasser un système d'authentification simple en PHP
  • Forger une requête SQL correcte
  • Comment outrepasser des systèmes plus complexes


    Pré-requis :
    • Un minimum de connaissance en PHP et en SQL (requête de type SELECT)

    http://www.mygeekpal.com/

      1 - Principe

      Il nous est tous arrivé de vouloir se connecter sur un site. Que ce soit le dernier site de votre ami, Facebook ou même Gmail. Pour cela, il nous faut souvent 2 choses essentielles : un identifiant et un mot de passe.
      Ce couple identifiant/mot de passe est souvent stocké dans une base de données de type SQL. Notre but va être de pouvoir accéder à notre compte, sans pour autant avoir un couple d'identifiants valides.
      Nous allons pour cela imaginer une requête SQL de ce type :
      SELECT * FROM membres WHERE login = '...' AND pass = '...'
      Comme vous voyez, il nous faut donc le login, et le mot de passe. Ces deux informations sont entre simples guillemets : '.
      Nous avons donc un test : il faut donc qu'il y ai (WHERE) un login (login = '...') et (AND) un mot de passe (pass = '...'). Notre but va être de rajouter une condition à ce test, par exemple "et jour = lundi" pour que l'on puisse se connecter que le lundi. Ou plus simplement : "ou 1=1" qui se traduit en SQL par :
      OR 1=1
      J'utilise ici 1=1 qui est une condition toujours vrai. Le but est de proposer une alternative à la validation de l'identifiant/mot de passe par une autre condition qui serra toujours vraie (typiquement 1=1, mais cela peut être 50 = 25*2 ou même 'a' = 'a'). Nous choisissons d'insérer cette condition dans notre champ login, et 1234 dans notre champ pass. Notre requête actuellement ressemble à ceci :
      SELECT * FROM membres WHERE login = 'OR 1=1' AND pass='1234'
      Mais alors il y a un problème : actuellement la requête cherche un utilisateur qui s'appelle 'OR 1=1' avec un mot de passe qui est 1234. Nous n'avons pas passé le système d'authentification et n'avons pas réellement modifié la structure de la requête SQL.
      Pour cela, il va falloir sortir de la valeur de login, c'est-à-dire sortir du login = '...'. Pour cela, rien de plus simple : il suffit de sortir de la chaine, c'est-à-dire d'ajouter un ' dans notre OR 1=1. Nous avons donc comme login : ' OR 1=1 et toujours comme mot de passe 1234. Actuellement, notre requête est donc :
      SELECT * FROM membres WHERE login = '' OR 1=1 AND pass='1234'
      Bien, nous avons bien avancé. Vous suivez toujours ? Si vous avez bien compris, il reste un dernier problème à régler. La requête cherche quelqu'un avec un login qui est égal à rien (vide) OU  1=1 (ce qui est toujours vrai !) et un mot de passe qui est égal à 1234.
      Et oui, voilà le problème : comment trouver un mot de passe qui existe dans la base de données ? Comment être sûr qu'un utilisateur ai un mot de passe qui soit 1234. Si les mots de passes ont été généré aléatoirement, il y a peu de chance que ce soit le cas. Il faudrait trouver un moyen d'enlever la dernière condition du mot de passe : AND pass = '...'. Rien de plus simple. Il suffit de commenter la fin de la requête !
      Pour rappel, on utilise la séquence '--' pour commenter quelque chose en SQL. Notre login devient dont ' OR 1=1 -- et notre mot de passe reste 1234. Notre requête devient :
      SELECT * FROM membres WHERE login = '' OR 1=1 -- AND pass='1234'
      Et est en réalité interprétée comme ceci :
      SELECT * FROM membres WHERE login = '' OR 1=1
      Donc au final on cherche dans la base de données toutes les informations concernant un utilisateur, n'importe lequel puisqu'il faut seulement respecter la condition 1=1, qui est par définition toujours vraie. Nous avons donc réussi ! Au final, nous avons à entrer comme login : ' OR 1=1 -- et comme mot de passe : 1234.
      Toujours avec moi ? Nous allons voir une application concrète avec un petit script PHP.

      2 - Application

      On va pour cela utiliser un script PHP simple :
      /* Structure de la table :
       * CREATE TABLE membres (
       *     login VARCHAR(256) NOT NULL DEFAULT '',
       *     pass VARCHAR(256) NOT NULL DEFAULT '',
       *     PRIMARY(`pass`)
       * );
       */
      
      /* Connexion à la base de donnée */
      mysql_connect(...);
      mysql_select_database(...);
      
      /* Vérification des identifiants */
      $requete = mysql_select("SELECT * FROM membres WHERE login = '".$_POST['login']."' AND pass = '".$_POST['pass']."'");
      $resultat = mysql_fetch_assoc($requete);
      
      if (isset($resultat['login']) && !empty($resultat['login']))
          echo 'Connexion réussie !';
      else
          echo 'Mauvais couple d\'identifiants.';
      
      /* Traitements des informations... */
      Je vous laisse essayer. On aura donc $_POST['login'] = "' OR 1=1 --" et $_POST['pass'] = "1234".
      Dans notre script, on aura ceci :
      $requete = mysql_select("SELECT * FROM membres WHERE login = '' OR 1=1");
      Avec ce bout de code on récupère les données pour le premier login rentrée en base de données, souvent celui de l'administrateur. Il y a donc des chances qu'on récupère l'accès au compte de l'administrateur. Avec tous ces droits spéciaux !
      Voilà, vous avez réussi à vous connecter sur le site sans aucun compte, bravo !

      3 - Se protéger

      Maintenant que nous avons vu comment passer le système d'authentification, nous allons voir comment le protéger. Et oui, cette méthode n'est pas universelle. Sinon quid de la sécurité de nos informations privées sur internet ?
      Je ne traite ici que d'application en PHP. Nous allons donc utiliser la fonction mysql_real_escape_string (et aucune autre contre les injections SQL !*)
      Cette fonction va ajouter intelligemment des \ là où il faut. C'est à dire avant notre simple guillemet '. Cela va échapper notre ', et ce que nous avons tapé comme login sera toujours traité comme un login.
      Notre requête SQL va donc devenir :
      SELECT * FROM membres WHERE login = '\' OR 1=1 -- ' AND pass = '1234'
      Et à mon avis, il n'y a personne qui a un pseudo ' OR 1=1 --.
      Notre code PHP devient donc le suivant :
      /* Connexion à la base de donnée */
      mysql_connect(...);
      mysql_select_database(...);
      
      /* Vérification des identifiants */
      $requete = mysql_select("SELECT * FROM membres WHERE login = '".mysql_real_escape_string($_POST['login'])."' AND pass = '".mysql_real_escape_string($_POST['pass'])."'");
      $resultat = mysql_fetch_assoc($requete);
      
      if (isset($resultat['login']) && !empty($resultat['login']))
          echo 'Connexion réussie !';
      else
          echo 'Mauvais couple d\'identifiants.';
      
      /* Traitements des informations... */
      Voilà très simplement comment vous protéger contre ce type d'injection SQL. Il représente 90% des attaques SQL.
      * Utilisez toujours mysql_real_escape_string et non d'autres fonctions tel que addslashes. Les autres fonctions ne protègent pas totalement des injections et sous souvent contournables.

      4 - Cas particuliers

      Voici quelques cas particuliers qui peuvent vous arriver sur ce type d'injection. Quelqu'un qui aura bien compris le principe de l'injection sera adapter son entrée en fonction de la requête qu'il y a derrière.
      Si le script PHP vérifie qu'un seul résultat correspond dans la base de données :
      if (mysql_num_rows($requete) == 1)
      Dans ce cas là, il vous suffit de rajouter LIMIT 0, 1 avant le commentaire :
      SELECT * FROM membres WHERE login = '' OR 1=1 LIMIT 0,1--
      Si le login est encodé , comme ceci par exemple :
      SELECT * FROM membres WHERE login = SHA1('...') AND pass = SHA1('...')
      Votre login deviendra ') OR 1=1 --'. Nous aurons une requête comme ceci :
      SELECT * FROM membres WHERE login = SHA1('') OR 1=1 --
      Enfin pour aller un peu plus loin, imaginons un champ supplémentaire dans la base de donnée, nommé admin. Si ce champ vaut 1, alors ce membre et admin, sinon c'est un simple utilisateur.
      Libre à vous de rajouter une condition supplémentaire à la requête pour qu'elle sélectionne un compte admin. Exemple (je ne vous mets que la requête finale).
      SELECT * FROM membres WHERE login = '' OR 1=1 AND admin = 1--
      Il y a ici une histoire de priorité d'opérateur : plus d'information ici

      Conclusion

      Voilà que ce premier article touche à sa fin. Prochainement nous verrons comment exploiter un système de news (ou une galerie d'image), avec dans certains cas la possibilité de passer outre mysql_real_escape_string !
      Voilà 2-3 liens pour approfondir le langage SQL et le langage PHP pour ceux qui auraient eu du mal :
      Voilà que ce premier article touche à sa fin. Je vous remercie de m'avoir lu jusqu'à la fin. Je suis ouvert à toute critique ou conseil. Si vous ne comprenez pas un point ou avez besoin de plus d'explication, n'hésitez pas à utiliser les commentaires.

      Aucun commentaire:

      Enregistrer un commentaire