Injection SQL
De nombreux développeurs web ne sont pas conscients des possibilités
de manipulation des requêtes SQL, et supposent que les requêtes SQL
sont des commandes sûres. Cela signifie qu'une requête SQL est
capable de contourner les contrôles et vérifications, comme les
identifications, et parfois, les requêtes SQL ont accès aux commandes
d'administration.
L'injection SQL directe est une technique où un pirate modifie une requête
SQL existante pour afficher des données cachées, ou pour écraser des
valeurs importantes, ou encore exécuter des commandes dangereuses pour la
base. Cela se fait lorsque l'application prend les données envoyées par
l'internaute, et l'utilise directement pour construire une requête SQL. Les
exemples ci-dessous sont basés sur une histoire vraie, malheureusement.
Avec le manque de vérification des données de l'internaute et la connexion
au serveur avec des droits de super utilisateur, le pirate peut créer des
utilisateurs, et créer un autre super utilisateur.
Exemple #1
Séparation des résultats en pages, et créer des administrateurs
(PostgreSQL et MySQL)
<?php
$offset = $argv[0]; // Attention, aucune validation!
$query = "SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET $offset;";
$result = pg_query($conn, $query);
?>
Un utilisateur normal clique sur les boutons 'suivant' et 'précédent',
qui sont alors placés dans la variable
$offset,
encodée dans l'
URL. Le script s'attend à ce que la variable
$offset soit alors un nombre décimal. Cependant,
il est possible de modifier l'
URL en ajoutant une nouvelle valeur,
au format
URL, comme ceci :
Exemple #2 Exemple d'injection SQL
0;
insert into pg_shadow(usename,usesysid,usesuper,usecatupd,passwd)
select 'crack', usesysid, 't','t','crack'
from pg_shadow where usename='postgres';
--
Si cela arrive, le script va créer un nouveau super utilisateur.
Notez que la valeur
0;
sert à terminer la requête
originale et la terminer correctement.
Note:
C'est une technique répandue que de forcer l'analyseur SQL à ignorer le
reste de la requête, en utilisant les symboles --
pour
mettre en commentaires.
Un moyen disponible pour accéder aux mots de passe est de contourner
la recherche de page. Ce que le pirate doit faire, c'est simplement
voir si une variable du formulaire est utilisée dans la requête, et
si elle est mal gérée. Ces variables peuvent avoir été configurées
dans une page précédente pour être utilisées dans les clauses
WHERE, ORDER BY, LIMIT
et OFFSET
des
requêtes SELECT
. Si votre base de données supporte
les commandes UNION
, le pirate peut essayer d'ajouter
une requête entière pour lister les mots de passe dans n'importe quelle
table. Utiliser la technique des mots de passe chiffrés est fortement
recommandé.
Exemple #3
Liste d'articles ... et ajout de mot de passe
<?php
$query = "SELECT id, name, inserted, size FROM products
WHERE size = '$size'";
$result = odbc_exec($conn, $query);
?>
La partie statique de la requête, combinée avec une autre
requête
SELECT
, va révéler les mots de passe :
Exemple #4 Révélation des mots de passe
<?php
'
union select '1', concat(uname||'-'||passwd) as name, '1971-01-01', '0' from usertable;
--
?>
Si cette requête, exploitant les
'
et
--
est affectée à une variable utilisée dans
$query, une injection SQL va se produire.
Les commandes UPDATE
sont aussi sujettes à des
attaques de votre base de données. Ces requêtes peuvent aussi introduire
toute une nouvelle requête dans votre commande initiale. Mais en plus,
le pirate peut jouer sur la commande SET
. Dans ce cas,
il doit connaître un peu votre base de données. Cela peut se deviner
en examinant les noms de variables dans les formulaires, ou simplement,
en testant les cas les plus classiques. Il n'y a pas beaucoup de
conventions de noms pour stocker des noms d'utilisateurs et des mots de
passe.
Exemple #5 Modifier un mot de passe ... et gain de droits!
<?php
$query= "UPDATE usertable SET pwd='$pwd' WHERE uid='$uid';";
?>
Mais un internaute fourbe peut envoyer une valeur telle que
' or uid like'%admin%
dans
$uid
pour modifier le mot de passe utilisateur, ou simplement, utiliser la
variable
$pwd avec la valeur
hehehe', trusted=100, admin='yes
pour obtenir des droits supplémentaires. La requête devient alors :
Exemple #6 Une requête et son injection
<?php
// $uid == ' or uid like '%admin%
$query = "UPDATE usertable SET pwd='...' WHERE uid='' or uid like '%admin%';";
// $pwd: hehehe', trusted=100, admin='yes
$query = "UPDATE usertable SET pwd='hehehe', trusted=100, admin='yes' WHERE
...;";
?>
C'est un exemple terrible d'acquisition de droits d'administrateur sur un
serveur de base de données.
Exemple #7 Attaque d'un serveur de bases de données (MSSQL Server)
<?php
$query = "SELECT * FROM products WHERE id LIKE '%$prod%'";
$result = mssql_query($query);
?>
Si le pirate injecte la valeur
a%' exec master..xp_cmdshell 'net user test testpass /ADD' --
dans la variable
$prod, alors la requête
$query devient :
Exemple #8 Attaque d'un serveur de base de données (MSSQL Server) - 2
<?php
$query = "SELECT * FROM products
WHERE id LIKE '%a%'
exec master..xp_cmdshell 'net user test testpass /ADD' --%'";
$result = mssql_query($query);
?>
MSSQL Server exécute les requêtes SQL en lot, y compris la commande
d'ajout d'un nouvel utilisateur à la base de données locale. Si cette
application fonctionnait en tant que
sa
et que le
service MSSQLSERVER disposait de niveau de droits suffisant, le pirate
dispose désormais d'un compte avec accès au serveur.
Note:
Certains des exemples ci-dessus sont spécifiques à certains serveurs de
bases de données. Cela n'empêche pas des attaques similaires d'être
possibles sur d'autres produits. Votre base de données sera alors
vulnérable d'une autre manière.
Image de
» xkcd
Techniques de contournement
Bien qu'il semble évident qu'un pirate doit posséder quelques connaissances
de l'architecture de la base de données afin de conduire avec succès une
attaque, il est souvent très simple de les obtenir. Par exemple, si la
base de données fait partie d'un paquet open source ou disponible publiquement,
ces informations sont complètement ouvertes et disponibles. Ces informations
peuvent aussi être divulgués pour des codes sources fermés - y compris si
ce code est encodé, occulté, ou compilé - aux travers des messages d'erreurs.
D'autres méthodes consistent à deviner l'utilisateur de table commune ainsi
que des noms des colonnes. Par exemple, un formulaire d'identification
qui utilise la table 'users' avec les colonnes de noms
'id', 'username', et 'password'.
Ces attaques sont généralement basées sur l'exploitation de code qui
n'est pas écrit de manière sécuritaire. N'ayez aucune confiance dans
les données qui proviennent de l'utilisateur, même si cela provient d'un
menu déroulant, d'un champ caché ou d'un cookie. Le premier exemple montre
comment une requête peut causer un désastre.
-
Ne nous connectez jamais sur une base de données en tant que super
utilisateur ou propriétaire de la base. Utilisez toujours un utilisateur
adapté, avec des droits très limités.
-
Utilisez des requêtes préparées avec des variables liées. Elles sont
disponibles avec PDO,
MySQLi
ainsi que d'autres bibliotèques.
-
Vérifiez que les données ont bien le type attendu. PHP dispose
d'un éventail de fonction de validation large, depuis les plus
simples, de la section Variables et
la section Caractères
(e.g. is_numeric(), ctype_digit()
respectivement) aux fonctions avancées de
Expression rationnelle Perl.
-
Si l'application attend une entrée numérique, vérifiez vos données
avec la fonction ctype_digit(), ou bien modifiez
automatiquement le type avec la fonction settype(),
ou encore avec sprintf().
Exemple #9 Une navigation de fiches plus sécuritaire
<?php
settype($offset, 'integer');
$query = "SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET $offset;";
// notez que %d dans la chaîne de format : %s serait inutile
$query = sprintf("SELECT id, name FROM products ORDER BY name LIMIT 20 OFFSET %d;",
$offset);
?>
-
Si la couche de base de données ne suppose pas les variables liées,
alors, mettez entre guillemets toutes les valeurs non numériques qui sont
passées à la base de données avec la fonction spécifique à la base de
données d'échappement de caractères (e.g.
mysql_real_escape_string(),
sqlite_escape_string(), etc.).
Les fonctions génériques comme addslashes() sont utiles
uniquement dans un environnement très spécifique (i.e. MySQL avec un jeu
de caractères sur un seul octet avec NO_BACKSLASH_ESCAPES
désactivé), aussi, il est préférable de ne pas les utiliser.
-
N'affichez jamais d'informations spécifiques à la base, et notamment
des informations concernant le schéma. Voyez aussi la section
Rapport d'erreur et le chapitre
Gestion des erreurs.
-
Vous pouvez avoir des procédures stockées et des curseurs prédéfinis qui
font que les utilisateurs n'ont pas un accès direct aux tables ou vues,
mais cette solution a d'autres impacts.
À côté de ces conseils, il est recommandé d'enregistrer vos requêtes, soit
dans vos scripts, soit dans la base elle-même, si elle le supporte.
Évidemment, cet enregistrement ne sera pas capable d'empêcher une attaque,
mais vous permettra de retrouver la requête qui a fauté. L'historique
n'est pas très utile par lui-même, mais au niveau des informations qu'il
contient. Plus vous avez de détails, mieux c'est.