Exploitation
POPping WordPress
Fun with PHP deserialization and some accidental WordPress bugs.
A few months ago I was putting together a blog post on PHP deserialization vulnerabilities. I decided to look for a real target that would allow me to supply data to the PHP unserialize() function to use for demonstration purposes. I downloaded a load of WordPress plugins and started grepping through the code for calls to unserialize() to find the following example:
1 2 3 4 |
$url = 'http://api.wordpress.org/plugins/info/1.0/'; $response = wp_remote_post ($url, array ('body' => $request)); $plugin_info = @unserialize ($response ['body']); if (isset ($plugin_info->ratings)) { |
The plugin in question made a clear text HTTP request and passed the response to unserialize(). In terms of a real attack it wasn’t the greatest entry point but if I could trigger this code it would be trivial to supply input to unserialize() this way so it was good enough!
Attacking PHP Deserialization
Briefly, deserialization vulnerabilities occur when an attacker can supply data to an application which in turn converts that data into runtime objects without proper validation. If controlling this data allows an attacker to control properties of the resulting runtime objects then the attacker can manipulate the flow of execution of any code that uses those object properties and potentially use it to launch an attack. This is a technique known as property oriented programming (POP). A POP gadget is any snippet of code that can be controlled in this way. Exploitation is achieved by supplying specially crafted objects to an application such that when those objects are deserialized, some useful behaviour is triggered. For more information see my blog post on Attacking Java Deserialization (the general concepts apply regardless of the underlying technology).
In the context of PHP applications the most well known and reliable source for POP gadgets is the __wakeup() method of a class. If a class defines a __wakeup() method then this method is guaranteed to be called whenever an object of that class is deserialized using the unserialize() function. Another reasonably reliable source for PHP POP gadgets is the __destruct() method which is almost guaranteed to be called when the deserialized object goes out of scope, for example when the script has finished executing (unless e.g. a fatal error occurs).
In addition to the __wakeup() and __destruct() methods, PHP has a whole load of other “magic methods” that can be defined in a class and may also be called following deserialization depending on how the deserialized object is used. In a larger or more complex application it can be difficult to trace where a deserialized object ends up and how it is used or what methods are called on it. It can also be difficult to identify which classes can be used in a PHP deserialization exploit because the relevant files may not have been included at the entry point or a class auto loader could have been registered to muddy the waters further.
Universal PHP POP Gadget
To simplify this process I wrote a PHP class that defines all magic methods and writes the details to a log file when any of those magic methods are called. Particularly interesting are the __get() and __call() magic methods which will be called if the application attempts to get a non-existent property or call a non-existent method of the class. The former can be used to identify properties that can be set on the payload object in order to manipulate the code that gets and uses those properties. The latter can be used to identify non-magic methods that can be triggered using a POP gadget (and can themselves hence be used as POP gadgets).
The __wakeup() method of the class also uses the PHP function get_declared_classes() to retrieve and log a list of declared classes that an exploit payload can use (although this wont report classes that aren’t currently declared but which can be auto-loaded).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
<?php if(!class_exists("UniversalPOPGadget")) { class UniversalPOPGadget { private function logEvent($event) { file_put_contents('UniversalPOPGadget.txt', $event . "\r\n", FILE_APPEND); } public function __construct() { $this->logEvent('UniversalPOPGadget::__construct()'); } public function __destruct() { $this->logEvent('UniversalPOPGadget::__destruct()'); } public function __call($name, $args) { $this->logEvent('UniversalPOPGadget::__call(' . $name . ', ' . implode(',', $args) . ')'); } public static function __callStatic($name, $args) { $this->logEvent('UniversalPOPGadget::__callStatic(' . $name . ', ' . implode(',', $args) . ')'); } public function __get($name) { $this->logEvent('UniversalPOPGadget::__get(' . $name . ')'); } public function __set($name, $value) { $this->logEvent('UniversalPOPGadget::__set(' . $name . ', ' . $value . ')'); } public function __isset($name) { $this->logEvent('UniversalPOPGadget::__isset(' . $name . ')'); } public function __unset($name) { $this->logEvent('UniversalPOPGadget::__unset(' . $name . ')'); } public function __sleep() { $this->logEvent('UniversalPOPGadget::__sleep()'); return array(); } public function __wakeup() { $this->logEvent('UniversalPOPGadget::__wakeup()'); $this->logEvent(" [!] Defined classes:"); foreach(get_declared_classes() as $c) { $this->logEvent(" [+] " . $c); } } public function __toString() { $this->logEvent('UniversalPOPGadget::__toString()'); } public function __invoke($param) { $this->logEvent('UniversalPOPGadget::__invoke(' . $param . ')'); } public function __set_state($properties) { $this->logEvent('UniversalPOPGadget::__set_state(' . implode(',', $properties) . ')'); } public function __clone() { $this->logEvent('UniversalPOPGadget::__clone()'); } public function __debugInfo() { $this->logEvent('UniversalPOPGadget::__debugInfo()'); } }} ?> |
Instrumenting PHP
By saving the above code to a PHP file we can insert an include '/path/to/UniversalPOPGadget.php' statement into any other PHP script and make this class available for use. The following Python script will find all PHP files in a given directory and prefix the files with this statement, effectively instrumenting the application so that we can provide serialized UniversalPOPGadget objects to it and use them to investigate deserialization entry points.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import os import sys #Set this to the absolute path to the file containing the UniversalPOPGadget class GADGET_PATH = "/path/to/UniversalPOPGadget.php" #File extensions to instrument FILE_EXTENSIONS = [".php", ".php3", ".php4", ".php5", ".phtml", ".inc"] #Check command line args if len(sys.argv) != 2: print "Usage: GadgetInjector.py <path>" print "" sys.exit() #Search the given path for PHP files and modify them to include the universal POP gadget for root, dirs, files in os.walk(sys.argv[1]): for filename in files: for ext in FILE_EXTENSIONS: if filename.lower().endswith(ext): #Instrument the file and stop checking file extensions fIn = open(os.path.join(root, filename), "rb") phpCode = fIn.read() fIn.close() fOut = open(os.path.join(root, filename), "wb") fOut.write("<?php include '" + GADGET_PATH + "'; ?>" + phpCode) fOut.close() break |
Analyzing a Deserialization Entry Point
Back to that original WordPress plugin code snippet with the call to unserialize(), I had no idea how to actually trigger the call to unserialize(). All I knew was that at some point the plugin should make a HTTP request to http://api.wordpress.org/plugins/info/1.0/. I used the Python script above to instrument the WordPress and plugin code, then I modified the hosts file on the server to point api.wordpress.org back at the same server. The following code was placed in the file /plugins/info/1.0/index.php in the web root in order to deliver a UniversalPOPGadget payload:
1 2 3 |
<?php include('UniversalPOPGadget.php'); print serialize(new UniversalPOPGadget()); |
With this instrumentation in place I started using the WordPress instance as normal, paying particular attention to all functionality relating to the target WordPress plugin whilst watching for the UniversalPOPGadget log files. Soon enough some log files were generated including the following (the large list of available classes has been removed for brevity):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
UniversalPOPGadget::__wakeup() [!] Defined classes: [...Snipped...] UniversalPOPGadget::__get(sections) UniversalPOPGadget::__isset(version) UniversalPOPGadget::__isset(author) UniversalPOPGadget::__isset(requires) UniversalPOPGadget::__isset(tested) UniversalPOPGadget::__isset(homepage) UniversalPOPGadget::__isset(downloaded) UniversalPOPGadget::__isset(slug) UniversalPOPGadget::__get(sections) UniversalPOPGadget::__get(sections) UniversalPOPGadget::__isset(banners) UniversalPOPGadget::__get(name) UniversalPOPGadget::__get(sections) UniversalPOPGadget::__isset(version) UniversalPOPGadget::__isset(author) UniversalPOPGadget::__isset(last_updated) UniversalPOPGadget::__isset(requires) UniversalPOPGadget::__isset(tested) UniversalPOPGadget::__isset(active_installs) UniversalPOPGadget::__isset(slug) UniversalPOPGadget::__isset(homepage) UniversalPOPGadget::__isset(donate_link) UniversalPOPGadget::__isset(rating) UniversalPOPGadget::__isset(ratings) UniversalPOPGadget::__isset(contributors) UniversalPOPGadget::__isset(tested) UniversalPOPGadget::__isset(requires) UniversalPOPGadget::__get(sections) UniversalPOPGadget::__isset(download_link) |
The log file shows that, after this UniversalPOPGadget object was deserialized, the application attempted to get or check for the existence of several properties (sections, version, author etc). Firstly this tells us that with this particular entry point we can use code defined in any __get() or __isset() method of any available class as a POP gadget (hence we should review those methods for useful code). Secondly it reveals several properties that the target application attempts to get. Those properties are almost guaranteed to influence the flow of execution and hence are likely to be useful in exploitation.
Sections?
The log file above shows that the very first interaction with the deserialized object is an attempt to get the property named sections.
1 2 3 4 |
$url = 'http://api.wordpress.org/plugins/info/1.0/'; $response = wp_remote_post ($url, array ('body' => $request)); $plugin_info = @unserialize ($response ['body']); if (isset ($plugin_info->ratings)) { |
Going back to the original target plugin, the first thing it did following the call to unserialize() was to check for the existence of the property named ratings.
This log was not generated by the third-party plugin that I was originally targeting!
Accidentally POPping WordPress
A quick grep over the WordPress code for the HTTP URL mentioned above revealed that the request was issued by the WordPress plugins API found in the file wp-admin/includes/plugin-install.php. Skimming over the code it wasn’t immediately obvious exactly how the deserialized payload object was used or where exactly this HTTP request and subsequent call to unserialize() was triggered from. I continued clicking around the WordPress admin interface and found that the logs were being generated from the main dashboard, the Updates page, and the Plugins page. Reloading these pages allowed me to trigger the target HTTP request and supply arbitrary data to unserialize().
I logged some of the HTTP requests WordPress was issuing and sent them on to the real api.wordpress.org in order to capture sample responses. The responses were serialized objects of the type stdClass. More importantly, the sample responses gave me an exact list of properties that WordPress expected to receive, each of which could potentially be used to manipulate the flow of execution of some core WordPress code. I modified my faked api.wordpress.org to return serialized objects based on the real responses I’d captured. The following is a cut-down example of this:
1 2 3 4 5 6 |
<?php $payloadObject = new stdClass(); $payloadObject->name = "PluginName"; $payloadObject->slug = "PluginSlug"; $payloadObject->version = "PluginVersion"; print serialize($payloadObject); |
I started modifying the properties of these objects and refreshing the relevant WordPress pages to see what (if any) effect the modifications had on the resulting page. In several cases WordPress used HTML encoding to prevent HTML/JavaScript injection, however I eventually found several fields in which I could insert arbitrary HTML and JavaScript. Bear in mind this happens from within the administrative interface. An attacker who is able to perform a MitM attack or spoof DNS to a WordPress site can potentially exploit this to achieve remote code execution if an admin logs in and views the Updates or Plugins pages.
After some quick and dirty JavaScript and Python scripting I had a working proof of concept exploit. The PoC caused the WordPress admin interface to display a badge next to the Updates and Plugins menus indicating that there were updates available (even when there were not). This is likely to encourage an admin to click on those links to review and potentially install those updates. If an admin does click on either link then a JavaScript payload is injected into the page which adds a new administrator uses and injects a basic PHP command shell into the index.php of the active WordPress theme.
In most cases this PoC attack would be enough to achieve code execution, however I also found that I could attack the click-to-update functionality of the WordPress admin interface in a similar way by sending a false plugin update to WordPress which caused it to download a fake plugin update zip file and extract it on the server if the admin clicked the update button.
Translation
Digging into this some more, I noticed that similar HTTP requests to api.wordpress.org were issued by WordPress even without logging in. I started reviewing the WordPress code to work out what was going on here and whether it could be attacked in a similar way and I came across the function wp_schedule_update_checks() in the file wp-includes/update.php.
1 2 3 4 5 6 7 8 9 10 |
function wp_schedule_update_checks() { if ( ! wp_next_scheduled( 'wp_version_check' ) && ! wp_installing() ) wp_schedule_event(time(), 'twicedaily', 'wp_version_check'); if ( ! wp_next_scheduled( 'wp_update_plugins' ) && ! wp_installing() ) wp_schedule_event(time(), 'twicedaily', 'wp_update_plugins'); if ( ! wp_next_scheduled( 'wp_update_themes' ) && ! wp_installing() ) wp_schedule_event(time(), 'twicedaily', 'wp_update_themes'); } |
Twice a day WordPress will call the functions wp_version_check(), wp_update_plugins(), and wp_update_themes(). By default these update checks can also be triggered by issuing a HTTP request for wp-cron.php. I began manually reviewing these functions and modifying the code to log various pieces of data and the outcome of branches and function calls to see what was going on and whether the functions did anything dangerous with the responses from api.wordpress.org.
I eventually managed to falsify several responses from api.wordpress.org in order to trigger calls to $upgrader->upgrade(). The previous false plugin update attack didn’t seem to work here, however. Then I spotted the following comment within the should_update() method:
1 2 3 4 5 6 7 8 9 |
/** * [...Snipped...] * * Generally speaking, plugins, themes, and major core versions are not updated * by default, while translations and minor and development versions for core * are updated by default. * * [...Snipped...] */ |
It turned out that WordPress was attempting to upgrade the translations for the built-in Hello Dolly plugin. Instead of requesting my fake plugin zip archive, it had been attempting to download hello-dolly-1.6-en_GB.zip from downloads.wordpress.org. I downloaded the original file, added a shell.php to it, and hosted it on my fake downloads.wordpress.org site. Next time I requested wp-cron.php WordPress downloaded the fake update and extracted it to wp-content/languages/plugins/, shell and all.
Right before the call to $upgrader->upgrade() was another comment I liked:
1 |
// Boom, This sites about to get a whole new splash of paint! |
An attacker who is in a position to perform a MitM attack or spoof DNS to a WordPress site can hence perform a zero interaction attack against the auto update functionality and write malicious scripts to the server. Granted, it’s not necessarily an easy attack to perform, but this still should not be possible!
The WordPress team are aware of these issues, however their stance seems to be that it’s intentional behaviour for WordPress to downgrade to a HTTP connection if HTTPS fails in order to allow WordPress sites running on systems with old/broken SSL stacks to update (or install malicious code)…
Caveats/Gotchas
When requesting update details and update archives, WordPress does attempt to connect to api.wordpress.org and downloads.wordpress.org over HTTPS first, however it falls back to using a clear text HTTP connection if that fails for any reason.
“Oh, I don’t trust that SSL certificate, I’ll connect over HTTP instead”
WordPress won’t download update archives from RFC 1918 (internal) or loopback IP addresses such as 10.x.y.z or 127.x.y.z.
WordPress will by default fail to auto-update (and hence not be vulnerable to the above attack) if the WordPress PHP scripts are owned by a different user to that which is used to run WordPress. For example if index.php is owned by the user foo but WordPress runs under the context of the user www-data.
Bounty
For those of you who participate in bug bounties and anyone else who might be interested, these issues (complete with PoC exploit code and videos) weren’t worthy of a bug bounty.
Preventing These Attacks
Until WordPress implement mitigations such as forcing updates over HTTPS, a simple way to mitigate these attacks is to filter outbound HTTP/TCP port 80 traffic from any server running WordPress. In doing so, if WordPress cannot update over an HTTPS connection or doesn’t trust the SSL certificate, it won’t be able to attempt an insecure update over HTTP instead.
Discussion