Note : Cet article est aussi disponible en anglais 🇬🇧.

Introduction

Kirby est un personnage de jeu vidéo Nitendo CMS open-source PHP orienté pour les créateurs et concepteurs.

La vulnérabilité présentée dans cet article est une XML External Entity (XXE) dans la boite à outils de Kirby.

Revue de code — Identification du code vulnérable

En faisant une rapide revue de code sur la dernière version de Kirby (3.9.5 à l’époque), il s’est vite avéré que la fonction Xml::parse(string $sml) de la boite à outils (src/Toolkit/Xml.php) présente dans Kirby Core semblait vulnérable.

https://github.com/getkirby/kirby/blob/6b5dda600472b59a3a779f7e6b8e9e130cc414a1/src/Toolkit/Xml.php#L272-L281

  public static function parse(string $xml): array|null
  {
    $xml = @simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOENT);

    if (is_object($xml) !== true) {
      return null;
    }

    return static::simplify($xml);
  }

En effet, l’utilisation de l’option LIBXML_NOENT active la substitution d’entités externes.

Le manuel PHP avertit sur le risque de XXE que cela représente.

Revue de code — Détection des emplois

La fonction Xml::parse() fait partie d’une boite à outils dans Kirby Core, c’est-à-dire qu’elle est disponible au sein d’une bibliothèque pour les développeurs. Il n’y a aucun appel à Xml::parse() dans Kirby Core, StarterKit ou PlainKit. Cela veut dire que pour qu’une instance Kirby soit vulnérable, il faut que le client ait fait un développement personnalisé qui utilise cette fonction ou qu’il ait installé une extension qui utilise cette fonction.

On peut voir dans la documentation, un exemple de création de page virtuelle utilisant un flux RSS depuis une source externe où la fonction est utilisée.

D’autre part, il est difficile d’identifier toutes les extensions qui peuvent utiliser cette fonction, mais en voici au moins une : FeedReader.
Cette extension, elle aussi, sert à afficher un flux RSS en utilisant Xml::parse qui propose une interface plus haut niveau.

Selon l’éditeur, Xml::parse() est utilisée dans le gestionnaire de données XML (Xml data handler) dont par exemple Data::decode($string, 'xml'). Celui-ci n’est pas non plus directement utilisé dans Kirby.

Exploitation — Création d’une démonstration / Preuve de concept

Le dépôt Github Acceis/exploit-CVE-2023-38490 contient une application vulnérable sous forme de conteneur docker, une charge utile et la démarche à suivre pour reproduire l’exploitation.

Ce dépôt Github propose une exploitation prête à l’emploi mais je vais détailler ci-dessous la démarche manuelle en créant une application vulnérable avec Kirby.

  1. Déployer une instance Kirby de base avec le StarterKit comme décrit ici https://getkirby.com/docs/guide/quickstart
  2. Reproduire l’exemple de page avec un flux RSS https://getkirby.com/docs/guide/virtual-pages/content-from-rss-feed
  3. Introduire le changement suivant dans /site/models/rssfeed.php afin d’accepter une source contrôlable par l’utilisateur
    modèle rssfeed
  4. Prendre un véritable flux RSS (comme https://www.acceis.fr/feed/), enregistrer le fichier XML et y incorporer une charge utile XXE (ici permettant de lire /etc/passwd)
    charge utile
  5. Servir le fichier malveillant xxe.rss via HTTP
    serveur HTTP
  6. Déclencher la vulnérabilité en fournissant la charge utile malveillante à l’application, ex : http://127.0.0.2:8080/rssfeed?feed=http://127.0.0.42:9999/xxe.rss
    traces d'accès
  7. L’application affiche le fichier local lu sur le système
    Preuve d'exploitation

Corrections — Contenu des correctifs

Pour les branches 3.8 et 3.9 (utilisant PHP 8+), le correctif constitue à supprimer l’option LIBXML_NOENT introduisant la vulnérabilité.

src/Toolkit/Xml.php

-$xml = @simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOENT);
+$xml = @simplexml_load_string($xml);

Pour les branches 3.5, 3.6 et 3.7 (utilisant PHP 7 ou 8+), le correctif désactive aussi l’option problématique LIBXML_NOENT, mais il est, de surcroît, nécessaire d’activer la directive libxml_disable_entity_loader permettant d’empêcher la prise en compte des entités externes lorsque l’application fonctionne avec une version de PHP antérieure à la version 8.

-$xml = @simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOENT);
+$loaderSetting = null;
+if (\PHP_VERSION_ID < 80000) {
+  // prevent loading external entities to protect against XXE attacks;
+  // only needed for PHP versions before 8.0 (the function was deprecated
+  // as the disabled state is the new default in PHP 8.0+)
+  $loaderSetting = libxml_disable_entity_loader(true);
+}
+
+$xml = @simplexml_load_string($xml);
+
+if (\PHP_VERSION_ID < 80000) {
+  // ensure that we don't alter global state by
+  // resetting the original value
+  libxml_disable_entity_loader($loaderSetting);
+}

Chronologie des évènements

Voir la frise chronologique.

Remerciements

Merci à Bastian ALLGEIER et Lukas BESTLE de l’équipe Kirby pour leur chaleureuse réceptivité.

À propos de l’auteur

Article écrit par Alexandre ZANNI alias noraj, Ingénieur en Test d’Intrusion chez ACCEIS.