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.
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.
- Déployer une instance Kirby de base avec le StarterKit comme décrit ici https://getkirby.com/docs/guide/quickstart
- Reproduire l’exemple de page avec un flux RSS https://getkirby.com/docs/guide/virtual-pages/content-from-rss-feed
- Introduire le changement suivant dans
/site/models/rssfeed.php
afin d’accepter une source contrôlable par l’utilisateur
- 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
)
- Servir le fichier malveillant
xxe.rss
via HTTP
- 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
- L’application affiche le fichier local lu sur le système
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.