Casse-tête à base de popchain PHP

Write-up du challenge Unserial killer du dghack (édition 2022)

Le

Bonjour et bienvenu sur ce troisième write-up portant sur les challenges de la DGHACK, édition 2022, organisé par la Direction Générale de l’Armement du Ministère des Armées.

Description du challenge

Une entreprise vient de se faire attaquer par des hackers ayant récupéré la configuration d'un de leur serveur web.

Auditez le code source du serveur web et trouvez comment ils ont pu y accéder.

Ce challenge repose sur l’exploitation d’une vulnérabilité de type PHP Object Injection, rendue possible par la désérialisation d’une donnée fournie par l’utilisateur. L’attaque s’appuie sur une POP chain PHP.

Pas d'inquiétude si vous ne comprenez vous tous ces termes, nous aurons l'occasion de revenir plus en détail sur chacun d'entre eux au fil de l’article.

Bon, et bien allons-y !

Une première vulnérabilité : la désérialisation

On commence par se connecter au site web. On peut y télécharger son code source depuis un lien prévu à cet effet. En effet, le challenge s'effectue en whitebox, c'est-à-dire que l'on à accès au code de l'application.

On remarque un premier point intéressant, situé la fonction main du fichier functions.php.

Code PHP de functions.php : la fonction main désérialise le paramètre data fourni par l'utilisateur après décodage base64

functions.php

L'application accepte un paramêtre data, pouvant être passée en variable d'URL GET ou en donnée POST. Le contenu de cette variable est désérialisé via un appel à la fonction unserialize.


Mais qu'est-ce que la sérialisation/deserialisation ?

En informatique, la sérialisation est le codage d'une information sous la forme d'une suite d'informations plus petites (dites atomiques, voir l'étymologie de atome) pour, par exemple, sa sauvegarde (persistance) ou son transport sur le réseau (proxy, RPC…). L'activité réciproque, visant à décoder cette suite pour créer une copie conforme de l'information d'origine, s'appelle la désérialisation.

Source: Wikipedia

Pour faire simple, la sérialization permet de représenter un objet et ses attributs sous la forme d'une chaine de caractère par exemple, plus facilement stockable en base de donnée et transportable entre différents systèmes.

Schéma : la sérialisation transforme un objet en flux d'octets stockable, la désérialisation le reconstruit en objet

En PHP, deux fonctions permettent d'utiliser la sérialisation d'objet.

Les objets sérialisés sont structurés de la manière suivante

  • Le type
  • La longueur (pour les chaînes notamment)
  • Le contenu
  • La structure (tableaux, objets…)
  • Éventuellement le nombre d’éléments

avec

Type PHP Exemple sérialisé Signification
entier (int) i:42; i = integer
booléen b:1; b = boolean
chaîne (string) s:5:"hello"; s = string (5 chars)
tableau a:2:{...} a = array, 2 éléments
objet O:8:"MyClass":2:{...} O = object, nom de classe, nombre de propriétés

Par exemple, la sérialisation d'un objet PHP de classe a, disposant d'un attribut value de type string valant "100" donnerait une chaine de caractère de la forme :

O:1:"a":1:{s:5:"value";s:3:"100";}

Revenons maintenant au challenge : on peut envoyer un objet PHP sérialisé à l'application (data=O:1:"a":1:{s:5:"value";s:3:"100";} par exemple), qu'elle désérialisera.

Cependant, rien ne semble directement exploitable pour l'instant. Je poursuis mon investigation 🕵️ ...

Exploitation via une Pop Chain

Les méthodes magiques PHP

Avant de parler de Pop Chain, parlons méthodes magiques PHP 🧚‍♀️. Les méthodes magiques PHP sont des fonctions spéciales préfixées par __ (deux underscore), qui sont appeller automatiquement pour gérer certains comportements implicites, comme la création, la destruction, l’accès aux propriétés, l’invocation ou la sérialisation d’objets. Elles écrasent l'action par défaut de PHP (https://www.php.net/manual/fr/language.oop5.magic.php).

Je vous en ai mis quelques-unes dans le tableau ci-dessous :

Méthode magique Déclencheur / Rôle
__construct() Appelée à la création d’un objet.
__destruct() Appelée à la destruction d’un objet.
__call($name, $args) Appelée quand on invoque une méthode inaccessible ou inexistante.
__callStatic($name, $args) Pareil que __call, mais pour les méthodes statiques.
__get($name) Accès à une propriété inaccessible ou inexistante.
__set($name, $value) Affectation à une propriété inaccessible ou inexistante.
__isset($name) Appelée par isset() ou empty() sur une propriété inaccessible.
__unset($name) Appelée par unset() sur une propriété inaccessible.
__sleep() Avant la sérialisation : retourne les propriétés à sérialiser.
__wakeup() Après désérialisation : restauration d’état ou initialisations.
__serialize() Nouveau mécanisme de sérialisation introduit en PHP 7.4+.
__unserialize($data) L’équivalent côté désérialisation.
__toString() Transformation de l’objet en chaîne, ex. lors d’un echo.
__invoke() Appelé quand on traite l’objet comme une fonction.
__set_state($array) Appelé par var_export() pour reconstruire l’objet.
__clone() Appelé lors du clonage d’un objet (clone).
__debugInfo() Informations à afficher lors d’un var_dump().

Le casse tête des Pop Chain

L'un des moyens d'exploiter une vulnérabilité de type unserialize est l'utilisation de Pop Chain.

Une popchain PHP est une méthode d'exploitation utilisé lors de la désérialisation d’un objet malveillant qui se base sur une suite de méthodes invoquées automatiquement via des méthodes magiques des objets PHP (méthodes __wakeup, __destruct, __call, etc.). L'exploitation peut permettre à un attaquant d'obtenir des primitives d'exploitation avancé, tel que la lecture/écriture de fichier arbitraire, de l'execution de code/commande etc.

Prenons un exemple de code simple pour illustrer ce qu'est une Pop chain.

<?php
class Logger {
    public $file;

    function write_log() {
        file_put_contents($this->file, "Hacked!\n", FILE_APPEND);
    }
}

class User {
    public $logger;

    function __wakeup() {
        $this->logger->write_log();
    }
}
unserialize($_GET['data']);

On envoie l'objet sérialisé malveillant suivant dans $_GET['data].

O:4:"User":1:{
    s:6:"logger";
    O:6:"Logger":1:{
        s:4:"file";
        s:9:"pwned.txt";
    }
}

La déserialisation va instancier un objet User, dont l'attribut logger de cet objet est un objet de classe Logger, qui a lui même un attribut file de type string valant pwned.txt.

Au moment où l’objet User est recréé, sa méthode magique __wakeup() s’exécute automatiquement. Elle appelle ensuite write_log() sur l’objet Logger. Cela permet d’invoquer une méthode normalement inaccessible depuis l’extérieur, et d’obtenir l’action finale recherchée, ici, l’écriture dans un fichier arbitraire.

Vous avez compris l'objectif des Pop chain : chainer des objets entre eux afin d'obtenir une primitive interessante.

Point de départ et arrivé

Revenons maintenant au challenge. Après un rapide analyse du code, je m'aperçois qu'il y a des fichiers qui ont été modifiés dans le package vendor/guzzlehttp/psr7 (je me base sur les sources de GitHub à la même version 1.8.5 disponible ici).

Terminal : un diff révèle que trois fichiers de la bibliothèque guzzlehttp psr7 ont été modifiés par rapport à l'original

En comparant ces 3 fichiers avec les fichiers originaux, on s'aperçoit qu'il y a quelques fonctions qui ont été modifié / ajouté.

  • Fichier FnStream.php

Code PHP FnStream : la méthode getContents lit et affiche un fichier, point d'arrivée de la POP chain

FnStream->getContents

Cette fonction est plus que très intéressante, car si on l'appelle avec les paramètres de l'objet display_content = true et _fn_getContents = "flag", on sera en mesure de lire notre fichier (il sera directement affiché sur la page). Elle constitue le point d'arrivé de notre Pop Chain.

  • Fichier Stream.php

Code PHP Stream : la méthode __destruct appelle closeContent sur customMetadata, point de départ de la POP chain

Stream->destruct

Cette fonction est également très utile, car elle sera appelée à la destruction de notre objet. Elle constitue le point de départ de la Pop Chain.

Vous l'aurez compris, il ne reste plus qu'à passer les bons attributs à l'objet stream pour qu'il call la fonction getContents d'un objet FnStream avec les bons attributs.

Elaboration d'une Pop Chain (1)

Maintenant que l'on sait d'où on part et vers quoi on doit arriver, il ne nous reste plus qu'à régler le problème du comment. Le challenge se résume maintenant à un casse-tête.

Premièrement, il faut donner une suite à la méthode __destruct de l'objet Stream. Pour cela, il faudrait que l'attribut customMetadata de l'objet Stream soit un objet qui dispose d'une méthode closeContents. Malheureusement, il n'y a aucun objet qui dispose d'une telle méthode. En nous renseignant un peu plus sur les méthodes magiques (sur la doc officielle de PHP) on tombe sur la méthode magique __call. Elle est appelée quand on cherche à exécuter une méthode non définie sur un objet, ce qui est notre cas. Laissez-moi donc vous présenter le troisième fichier modifié dont on a pas encore parlé, j'ai nommé StreamDecoratorTrait.php

Code PHP StreamDecoratorTrait : la méthode magique __call enchaîne les méthodes listées dans custom_method

StreamDecoratorTrait->__call

Comme on peut le voir sur la capture, StreamDecoratorTrait dispose d'une méthode magic __call. Disséquons-la.

  • la ligne 71 vérifie que l'attribut stream de l'objet StreamDecoratorTrait est bien un objet et qu'il dispose d'une méthode decorate.

  • Les lignes 72-73 assignent la valeur de l'attribut custom_method de l'objet StreamDecoratorTrait à une variable nommée method

  • Les lignes 75-76 transforment la variable method en un array si elle n'en est pas déjà un.

  • La ligne 79 récupère le premier élément de la liste d'arguments fournie à __call, et place la valeur dans la variable args

  • Les lignes 81-86 vont appeler toutes les méthodes définies dans l'array methods avec les arguments de $args. A chaque itération, on retirer le premier élément de la liste $args.

Pour résumer: si l'on créé un objet Stream, qu'on fixe son attribut size sur null et son attribut customMetadata sur un objet StreamDecoratorTrait dont l'attribut custom_method est un string contenant "getContents" et l'attribut stream est un objet FnStream, alors

Stream->__destruct va appeler StreamDecoratorTrait->__call qui va appeler Stream->getContents.

Sauf que: pour que getContents affiche le fichier flag.php il faut paramétrer l'attribut display_content à True et l'attribut _fn_getContents sur la string "flag.php".

Elaboration d'une Pop Chain (2)

On y est presque, il faut maintenant juste fixer les attributs de l'objet FnStream, seulement c'est un peu plus compliqué que ça.

Code PHP FnStream : la méthode __wakeup supprime tous les attributs sensibles de l'objet après désérialisation

fnstream.php

FnStream->__wakeup va être appelé quand on va instancier notre objet Stream, et donc tout nos attributs dont le nom est cité sur la capture ci-dessus seront retirés de l'objet. Mince ...

Code PHP FnStream : les méthodes register et allow_attribute permettent de contourner la liste des attributs interdits

fnstream.php

Ces deux fonctions nous permettent de rajouter des attributs à notre objet FnStream. La première fonction "register" permet d'ajouter n'importe quel attribut à FnStream, dans qu'il ne se trouve pas dans la liste des attributs interdits (self::$forbidden_attributes). La deuxième permet de retirer des attributs de la liste des attributs interdits. Alléluia, on est tiré d'affaire, je m'explique:

Il suffit d’appeler FnStream->allow_attribute("_fn_getContents"), puis FnStream->register("_fn_getContents","flag.php") et on aura fixé la valeur de _fn_getContents malgré la restriction. Tout cela est rendu possible au niveau de la fonction __call de l'objet StreamDecoratorTrait en fixant son attribut customMethod sur un array comportant le nom des méthodes que l'on va appeler (c'est-à-dire allow_attribute, register et getContents)

Remarque: J'ai considéré l'objet StreamDecoratorTrait comme un objet, mais il s'agit en réalité d'un "trait". Ceci dit ça ne change rien pour le challenge, on prend simplement un objet qui utilise ce trait et c'est réglé.

VS Code : les classes du paquet psr7 utilisant le trait StreamDecoratorTrait, dont LimitStream retenue pour la chaîne

Pour finir...

Pour faciliter l'exploit, j'ai mis au point 2 scripts:

<?php
class FnStream {

    public $display_content = true;

}

class LimitStream {
    public $custom_method = ["allow_attribute","allow_attribute","register","getContents"];
        function __construct($stream) {
                $this->stream = $stream;
        }
}

class Stream {
    public $size = [ ["_fn_getContents"],["getContents"],["_fn_getContents","/../../../../config"],[] ];
        function __construct($customMetadata) {
                $this->customMetadata = $customMetadata;
        }
}

$a = new Stream(new LimitStream(new FnStream));
echo serialize($a);
import subprocess
import base64
result = subprocess.run(['php', 'unserial.php'], stdout=subprocess.PIPE)
result = result.stdout.decode('utf-8')

result = result.replace('O:6:"Stream"','O:22:"GuzzleHttp\Psr7\Stream"')
result = result.replace('O:11:"LimitStream"','O:27:"GuzzleHttp\Psr7\LimitStream"')
result = result.replace('O:8:"FnStream"','O:24:"GuzzleHttp\Psr7\FnStream"')

print("[-] Serialized string:")
print(result)
print("[-] Base64 Serialized string:")
print(base64.b64encode(result.encode()).decode('utf-8'))

On place la payload en base64 dans l'argument GET data, et le tour est joué !

Accès Restreint

Cet article n'est pas (encore) publique.
Pour obtenir un accès anticipé, contactez-moi.

[[ passwordError ]]

[[ post.title ]]

[[ post.description ]]

Le [[ post.date ]]

[[ tag ]]
[[ currentMatchIndex + 1 ]] / [[ searchMatches.length ]] 0 / 0

Sommaire

Page principale