Moodle Blind SQL injection via MNet authentication

23-11-2021 - rekter0

Moodle is an opensource learning management system, popular in universities and workplaces largely used to manage courses, activities and learning content, with about 200 million users
Versions affected 3.10 to 3.10.3, 3.9 to 3.9.6, 3.8 to 3.8.8, 3.5 to 3.5.17
CVE identifier CVE-2021-32474

# Summary

What is Mnet?
The Moodle network feature allows a Moodle administrator to establish a link with another Moodle or a Mahara site and to share some resources with the users of that Moodle. Official documentation: https://docs.moodle.org/310/en/MNet

How ?
Mnet communicate with peers through xmlrpc, and uses encrypted and signed messages with RSA 2048

So what ?
auth/mnet/auth.php/keepalive_server xmlrpc method used to pass unsanitized user supplied parameters to SQL query => SQL injection

Attack scenario ?
1- You compromised one moodle instance, and use it to launch attack on its peers
2- An evil moodle instance decides to attack its peers
3- For one reason or another some mnet instance keypairs are leaked

# Vulnerability analysis

Moodle uses singed and encrypted xmlrpc messages to communicate via MNet protocol

/mnet/xmlrpc/client.php

function send($mnet_peer) {
    global $CFG, $DB;


    if (!$this->permission_to_call($mnet_peer)) {
        mnet_debug("tried and wasn't allowed to call a method on $mnet_peer->wwwroot");
        return false;
    }

>   $this->requesttext = xmlrpc_encode_request($this->method, $this->params, array("encoding" => "utf-8", "escaping" => "markup"));
>   $this->signedrequest = mnet_sign_message($this->requesttext);
>   $this->encryptedrequest = mnet_encrypt_message($this->signedrequest, $mnet_peer->public_key);

    $httprequest = $this->prepare_http_request($mnet_peer);
>   curl_setopt($httprequest, CURLOPT_POSTFIELDS, $this->encryptedrequest);

xmlrpc message is first singed using private key

/mnet/lib.php

function mnet_sign_message($message, $privatekey = null) {
    global $CFG;
    $digest = sha1($message);

    $mnet = get_mnet_environment();
    // If the user hasn't supplied a private key (for example, one of our older,
    //  expired private keys, we get the current default private key and use that.
    if ($privatekey == null) {
        $privatekey = $mnet->get_private_key();
    }

    // The '$sig' value below is returned by reference.
    // We initialize it first to stop my IDE from complaining.
    $sig  = '';
    $bool = openssl_sign($message, $sig, $privatekey); // TODO: On failure?

    $message = '<?xml version="1.0" encoding="iso-8859-1"?>
    <signedMessage>
        <Signature Id="MoodleSignature" xmlns="http://www.w3.org/2000/09/xmldsig#">
            <SignedInfo>
                <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
                <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
                <Reference URI="#XMLRPC-MSG">
                    <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
                    <DigestValue>'.$digest.'</DigestValue>
                </Reference>
            </SignedInfo>
            <SignatureValue>'.base64_encode($sig).'</SignatureValue>
            <KeyInfo>
                <RetrievalMethod URI="'.$CFG->wwwroot.'/mnet/publickey.php"/>
            </KeyInfo>
        </Signature>
        <object ID="XMLRPC-MSG">'.base64_encode($message).'</object>
        <wwwroot>'.$mnet->wwwroot.'</wwwroot>
        <timestamp>'.time().'</timestamp>
    </signedMessage>';
    return $message;
}

The xml envelope along signature is then encrypted

/mnet/lib.php

function mnet_encrypt_message($message, $remote_certificate) {
    $mnet = get_mnet_environment();

    // Generate a key resource from the remote_certificate text string
    $publickey = openssl_get_publickey($remote_certificate);

    if ( gettype($publickey) != 'resource' ) {
        // Remote certificate is faulty.
        return false;
    }

    // Initialize vars
    $encryptedstring = '';
    $symmetric_keys = array();

    //        passed by ref ->     &$encryptedstring &$symmetric_keys
    $bool = openssl_seal($message, $encryptedstring, $symmetric_keys, array($publickey));
    $message = $encryptedstring;
    $symmetrickey = array_pop($symmetric_keys);

    $message = '<?xml version="1.0" encoding="iso-8859-1"?>
    <encryptedMessage>
        <EncryptedData Id="ED" xmlns="http://www.w3.org/2001/04/xmlenc#">
            <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#arcfour"/>
            <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
                <ds:RetrievalMethod URI="#EK" Type="http://www.w3.org/2001/04/xmlenc#EncryptedKey"/>
                <ds:KeyName>XMLENC</ds:KeyName>
            </ds:KeyInfo>
            <CipherData>
                <CipherValue>'.base64_encode($message).'</CipherValue>
            </CipherData>
        </EncryptedData>
        <EncryptedKey Id="EK" xmlns="http://www.w3.org/2001/04/xmlenc#">
            <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5"/>
            <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
                <ds:KeyName>SSLKEY</ds:KeyName>
            </ds:KeyInfo>
            <CipherData>
                <CipherValue>'.base64_encode($symmetrickey).'</CipherValue>
            </CipherData>
            <ReferenceList>
                <DataReference URI="#ED"/>
            </ReferenceList>
            <CarriedKeyName>XMLENC</CarriedKeyName>
        </EncryptedKey>
        <wwwroot>'.$mnet->wwwroot.'</wwwroot>
    </encryptedMessage>';
    return $message;
}

On the other side the server receives the xmlrpc request and processes it via verifying signature then decrypting the envelope

/mnet/xmlrpc/server.php

try {
    $plaintextmessage = mnet_server_strip_encryption($rawpostdata);
    $xmlrpcrequest = mnet_server_strip_signature($plaintextmessage);
} catch (Exception $e) {
    mnet_debug('encryption strip exception thrown: ' . $e->getMessage());
    exit(mnet_server_fault($e->getCode(), $e->getMessage(), $e->a));
}
[...]
[...]
// Have a peek at what the request would be if we were to process it
> $params = xmlrpc_decode_request($xmlrpcrequest, $method);
mnet_debug("incoming mnet request $method");
[...]
[...]
if ((($remoteclient->request_was_encrypted == true) && ($remoteclient->signatureok == true))
    || (($method == 'system.keyswap') || ($method == 'system/keyswap'))
    || (($remoteclient->signatureok == true) && ($remoteclient->plaintext_is_ok() == true))) {
    try {
        // main dispatch call.  will echo the response directly
>       mnet_server_dispatch($xmlrpcrequest);
        mnet_debug('exiting cleanly');
        exit;
    } catch (Exception $e) {
        mnet_debug('dispatch exception thrown: ' . $e->getMessage());
        exit(mnet_server_fault($e->getCode(), $e->getMessage(), $e->a));
    }
}

Then moodle dispatch xmlrequest method to the appropriate functions.

Blind SQL Injection
keepalive_server method used to pass client supplied parameters to SQL query unsanitized

/auth/mnet/auth.php

function keepalive_server($array) {
        global $CFG, $DB;
        $remoteclient = get_mnet_remote_client();

        // We don't want to output anything to the client machine
        $start = ob_start();

        // We'll get session records in batches of 30
        $superArray = array_chunk($array, 30);

        $returnString = '';

        foreach($superArray as $subArray) {
            $subArray = array_values($subArray);
>           $instring = "('".implode("', '",$subArray)."')";
>           $query = "select id, session_id, username from {mnet_session} where username in $instring";

>           $results = $DB->get_records_sql($query);

            if ($results == false) {
                // We seem to have a username that breaks our query:
                // TODO: Handle this error appropriately
                $returnString .= "We failed to refresh the session for the following usernames: \n".implode("\n", $subArray)."\n\n";
            } else {
                foreach($results as $emigrant) {
                    \core\session\manager::touch_session($emigrant->session_id);
                }
            }
        }

        $end = ob_end_clean();

        if (empty($returnString)) return array('code' => 0, 'message' => 'All ok', 'last log id' => $remoteclient->last_log_id);
        return array('code' => 1, 'message' => $returnString, 'last log id' => $remoteclient->last_log_id);
    }

array parameters for keepalive_server used to be processed via implode and concatinated into the SQL query leading to blind SQL injection risks.

# Impact

Blind SQL injection risks in keepalive_server xmlrpc method for MNet Authentication, Successful exploitation could have led to compromising the targeted moodle instance with RCE possibility.

# Timeline

24-01-2021 - Reported
05-02-2021 - Vendor confirmed
17-05-2021 - Fixed in new release


moodle - sql injection - mnet - php

CONTACT



rekter0 © 2022