As part of a course on Information Security, part of the course assesment came from a semester project. We had to exploit an application, preferably a web application, of our choice. Given that our university uses the Moodle platform for the course web pages, Moodle became an obvious first choice. Moodle is an excellent target because it is an old project, written in PHP, full of obscure features that are questionably maintained. Therefore it has a huge attack surface, and there are plenty of known vulnerabilities to exploit. Besides, PHP makes it easy to write insecure code: plenty of dynamic-language fun like eval() and unserialize(), magic methods, an extremely poor type system, and a long legacy of functions that require input sanitization before use.

While I had no trouble exploiting several XSS vulnerabilities, which make for great session stealers, and implementing a published SQL / Privilege Escalation exploits, when I started trying to CVE-2018-14630 I ran into problems. I was using Moodle 3.2, which is not on the vulnerable list of this exploit despite not receiving the vendor patch. I downgraded to 3.1.13, and it still didn’t work, which wasn’t surprising considering that it had the exact same vulnerable code.

I uploaded the Proof of Concept XML into the question importer, and the very first problem I ran into was simple. You can’t have any whitespace in your serialized string.

Next, I found that Moodle was trying to access an object in the payload that isn’t defined. At this point I thought it was a pretty poor Proof of Concept, since it could never have worked without this simple addition.

No problem, I simply added the object to the payload.


Sucess? Not really, if the payload had run, we would’ve gotten the username running the PHP interpreter. Now there’s no errors, at least. To get to the bottom of this, I added a strategic var_dump() in the vulnerable Moodle code, to find this:

If you’re a sharp reader, you would notice that the keys from the payload are duplicated, there’s "key":protected and "key". Clearly, the PHP interpreter references the protected variables as Moodle executes, and our payload never gets executed.

To understand why this is happening, we need to understand how PHP serializes objects. If an object is public, it’s name is written as it is: s:5:"slots", where s is the type (string), 5 is the length of the variable, the "slots" is the name. The object following it is the value of this variable, in our case a:1:{i:0;i:1337;}. Same spiel: a for array, 1 for its length, braces for its members, i for integer, first integer is the index, the second is the value. All objects follow this pattern, but if a variable is private, its name is written in the format “\0NAME\0”, where \0 is the NUL character. Protected variables are prepended with \0*\0, that’s an asterisk surrounded by NULs.

Clearly, when this payload was written, this was not taken in to account. My next step was to correct the payload so that all the variable lengths are incremented by 3 for the 3 new characters I prepend to all the variable names (the \0*\0). But I quickly ran into a roadblock I could not pass: XML is relatively forgiving, but one rule that can never be broken is that you can never have a NUL character in a valid XML file. Therefore, Moodle’s XML parser would immediatly stop upon the first \0. To confirm my theory, I modified Moodle’s source code to directly load my payload rather than the user uploaded file, and it worked!

My second approach, was to change all the protected variables in Moodle to public variables, allowing the payload without any protected variables to work.

Sucess! Our payload ran.