00 0 x before order
A few months ago, I was writing a blog post about PHP deserialization vulnerabilities and decided to find a real target for the post that would allow me to transfer test data to the PHP unserialize () function for demonstration purposes. So I downloaded a bunch of WordPress plugins and started using grepping to find examples of code to call unserialize () :
$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)) {Copy the code
The problem with this plug-in is that it sends a plaintext HTTP request and passes the request response to the unserialize () function. It’s not the best entry point for a real attack, but if I can trigger the code by providing output to the unserialize () function in this trivial way, that’s enough!
0×01 PHP deserialization attack
Simply put, a deserialization vulnerability occurs when an attacker is able to provide his data to an application that converts the data into a runtime object without proper validation. If attacker data is allowed to control the properties of running objects, then attackers can manipulate any code execution process that uses these object properties, potentially using it to launch attacks. This is a technique called property-oriented programming (POP). A POP gadget can control any piece of code in this way by providing an application with specialized objects that trigger useful behavior when deserialized. If want to know more details, you can see my blog post “Attacking Java Deserialization” (nickbloor. Co. UK / 2017/08/13 /…). , where the general concepts apply to any underlying technology.
In the current state of PHP applications, the POP gadget is best known and most reliable for the __wakeup() method of the class. (The PHP magic method, unserialize() checks for the presence of __wakeup(), and if so, calls __wakeup() first. If a class defines a __wakeup() method, the __wakeup() method is guaranteed to be called whenever an object of that class is deserialized using unserialize (). Another reason is the __destruct() method (which automatically calls __destruct() when the created object is destroyed or a PHP closing flag is encountered, such as when the program has finished executing), such as when the PHP script has finished executing (without fatal errors), The __destruct () method is still almost guaranteed to be called when the deserialized object is out of scope.
In addition to the __wakeup () and __destruct () methods, PHP has other “magic methods” that can be defined in a class or called after deserialization, depending on how the deserialized object is used. In a larger, more complex application, it can be difficult to track where the deserialized object ends and how to use it or call those methods, so it can be difficult to determine which classes can be used to exploit the PHP deserialization vulnerability because the relevant files may not be included at the entry point. Or an autoloader for a class (such as the spl_autoload_register() function) could be registered to further confuse it.
0×02 common 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 magic method is called. Of particular interest are the __get() and __call() magic methods, which are called when an application tries to get attributes that don’t exist or calls methods that don’t exist in that class. The former can be used to identify attributes set on the payload object so that the code for those attributes can be manipulated and used. The latter can be used to identify non-magic methods that POP widgets trigger to use (and can use themselves as POP widgets).
The class’s __wakeup () method also uses the get_declared_classes () function to retrieve and record a list of declared classes that can take advantage of the exploit payload (although this does not reflect classes that are currently undeclared but can be loaded automatically).
<? 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()'); }}}? >Copy the code
0 x 03 PHP detection
To save the above code to a PHP file, we can through this in any other insert a PHP scripts include ‘/ path/to/UniversalPOPGadget. PHP’ statements, and make the class is available. The following Python script will look for all PHP files in a given directory and write statements to the front of the file, effectively detecting the application so that we can provide the UniversalPOPGadget object that provides the serialization for it and use them to investigate the entry point of deserialization.
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() breakCopy the code
0×04 analysis deserialization entry point
Back to just the call unserialize () function of WordPress plugin code fragments, I don’t know how to go to the actual trigger unserialize () function call, all I know is this plugin to api.wordpress.org/plugins/inf… Send the HTTP request, so I use the Python script above to test the WordPress and plug-in code, and then modify the hosts file on the server to point api.wordpress.org to the same server. The following code in the Web root directory/plugins/info / 1.0 / index. The PHP file, in order to provide UniversalPOPGadget content:
<? php include('UniversalPOPGadget.php'); print serialize(new UniversalPOPGadget());Copy the code
After using this technique, I started using the WordPress instance as usual, paying special attention to all the features related to the target WordPress plug-in, and looking at the UniversalPOPGadget log file. Very quickly, a few log files are generated, including the following (many of the available classes have been removed for brevity) :
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)Copy the code
The log file shows that after the UniversalPOPGadget object has been deserialized, the program tries to get or check for the presence of multiple attributes (segment, version, author, and so on). First of all, this tells us, through the specific entry point we can use any available any definition in the class in the __get () or __isset () method in the code for the POP gadgets, secondly it reveals the target application is trying to get a few properties, these properties are almost guarantee affect execution flow, may therefore is useful to the development.
0×05 Sections attribute?
The log file above shows that the first interaction with the deserialized object is an attempt to get a property called sections.
$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)) {Copy the code
Now look at the original target plugin. The first thing it does after calling unserialize () is check for the presence of a property called rating. This log is not generated by the third party plugin I noticed!
0 x 06 POPping WordPress Occurs unexpectedly
Do a quick grep of the WordPress code and, for the HTTP URL mentioned above, show that the request was sent by the WordPress plug-in API in the wp-admin/includes/plugin-install.php file. It is not clear from browsing the code how the deserialized Payload object is used, or exactly where the HTTP request and subsequent call to the unserialize () function are triggered. I continued clicking on the WordPress administration screen and found that the logs were generated from the main control panel, the updates page, and the plugins page. Reloading these pages enabled me to trigger the target HTTP request and provide arbitrary data to the unserialize () function.
I logged some HTTP requests made by WordPress and sent them to the real api.wordpress.org to get the instance response. The result was a serialized object of type stdClass. More importantly, the sample response gave me an exact list of properties THAT I expected WordPress to receive. Each of these attributes has the potential to manipulate the execution flow of some core WordPress code. I modified the fake api.wordpress.org to return the serialized object based on the real response I captured. Here’s a simple example of this:
<? php $payloadObject = new stdClass(); $payloadObject->name = "PluginName"; $payloadObject->slug = "PluginSlug"; $payloadObject->version = "PluginVersion"; print serialize($payloadObject);Copy the code
I started modifying the properties of these objects and refreshing the associated WordPress page to test how the changes affected the resulting page (if any). In some cases, WordPress uses HTML encoding to prevent HTML/JavaScript injection, but eventually I found several fields that can be inserted into arbitrary HTML and JavaScript. Keep in mind that this happens within the admin interface, and if an administrator logs in and views the “Update” or “plug-in” page, an attacker can perform MitM attacks or DNS spoofs on a WordPress site, and possibly exploit this vulnerability for remote code execution.
After a quick try at some JavaScript and Python scripts I have a working proof of the what-if bug. This PoC causes a badge to appear next to the Updates and Plug-ins menu in the WordPress administration interface, indicating that an update is available (and of course, even if it isn’t), which may induce the administrator to click on these links to check and possibly install the update. If an administrator clicks on any link, a JavaScript payload is injected into the page, then a new administrator account is added and a basic PHP command shell is injected into the existing WordPress theme’s index.php.
In most cases this PoC attack is sufficient for code execution, but I’ve also found that I can use a similar approach to send a faulty plugin update to WordPress to attack the click-to-update feature of the WordPress admin interface. If an administrator hits the update button, A ZIP file updated by a fake plug-in is downloaded and extracted to the server.
0 x 7 solutions
Digging deeper into this, I noticed that WordPress was sending HTTP requests like api.wordpress.org even when I wasn’t logged in, and I started a code audit of WordPress to see what was going on and whether it might have been subject to similar attacks. I found the wp_schedule_update_checks() function in wp-includes/update.php.
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');
}Copy the code
WordPress calls wp_version_check (), wP_update_plugins (), and wp_update_themes () twice a day. By default, these update checks can also be triggered by sending an HTTP request through wp-cron.php. I started manually auditing the functions and modifying the code to log various data and the results of branch and function calls to see what was going on and whether the function was doing anything dangerous based on the response from api.wordpress.org.
I eventually managed to forge a few responses from api.wordpress.org to trigger a call to $upgrader->upgrade(), but the old fake plug-in update attack doesn’t seem to work here, and then I found the following comment in the should_update() method:
/** * [...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...] * /Copy the code
It turns out that this is aN attempt by WordPress to upgrade the translation of the built-in Hello Dolly plug-in, and I’ve been trying to download Hello-dolly – 1.6-en_gb.zip from downloads.wordpress.org, instead of requesting my bogus plug-in zip file. I downloaded the original file, added a shell.php file and hosted it on my fake downloads.wordpress.org site. So the next time I visited the wp – cron. PHP and WordPress downloaded forged update and decompression to wp – the content/languages/plugins /, including shell and so on.
Now that an attacker can perform MitM attacks or DNS spoofing on a WordPress site, it can perform zero-interaction attacks against automatic updates and write malicious scripts to the server. It doesn’t have to be a simple attack, but it’s still impossible!
The WordPress team is aware of these issues, but their position seems to be that if HTTPS enablers fail, WordPress will deliberately downgrade to HTTP connections (or install malicious code) in order to allow updates to WordPress sites running on old or broken SSL stack systems…
0 x 08 Precautions/Traps
When requesting to update details and update archives, WordPress tries to connect to api.wordpress.org and downloads.wordpress.org first via HTTPS, but uses a plaintext HTTP connection if HTTPS fails to be enabled for any reason.
If a WordPress PHP script belongs to a different user, then WordPress will not automatically update (and therefore not be vulnerable to the above attacks) by default, such as index.php owned by user Foo, but WordPress is run under user www-data.
* Credit: Nicky Bloor, FreeBuf.COM