In (zero) days gone by - Part 1 - Magento Unauthenticated SQLi (CVE-2011-4781)

Posted on 14th December 2017, by Sam Thomas

Equifax

Theorizing that one could dredge up old vulnerabilities and blog about them, Sam Thomas stepped into the Quantum Leet accelerator - and vanished. ... And so Sam finds himself leaping from vuln to vuln, striving to put right what once went wrong and hoping each time that his next sploit... will pop a root shell.

This will be the first in an occasional series giving the technical details of interesting vulnerabilities we've discovered over the years. So I had thought this might have been the last SQLi in Magento, then I remembered this awesome vulnerability/exploit from checkpoint http://blog.checkpoint.com/2015/04/20/analyzing-magento-vulnerability/. We're not looking at anything nearly as complicated as that :)

A colleague spotted unusual behavior in a particular endpoint in the app he was testing in a black box test:

  • Whenever "%" was used in two specific parameters it caused an error
  • Whenever "%%" was used the app didn't error

As it happened I'd recently been doing some stuff with PHP format strings and recognised that our input was probably being processed as part of a format string. Part way through looking at the issue we established that the app was Magento and we could look at the source code, in all honesty I can't remember if we exploited it blind or with the source code, but it's far easier to understand the issue by looking at the source code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* Add filter by attribute rated price
*
* @param Mage_CatalogSearch_Model_Resource_Advanced_Collection $collection
* @param Mage_Catalog_Model_Resource_Eav_Attribute $attribute
* @param string|array $value
* @param int $rate
* @return bool
*/
public function addRatedPriceFilter($collection, $attribute, $value, $rate = 1)
{
    $adapter = $this->_getReadAdapter();
 
    $conditions = array();
    if (strlen($value['from']) > 0) {
        $conditions[] = $adapter->quoteInto('price_index.min_price %s * %s >= ?', $value['from']);
    }
    if (strlen($value['to']) > 0) {
        $conditions[] = $adapter->quoteInto('price_index.min_price %s * %s <= ?', $value['to']);
    }
    if (!$conditions) {
         return false;
    }
    $collection->addPriceData();
    $select     = $collection->getSelect();
    $response   = $this->_dispatchPreparePriceEvent($select);
    $additional = join('', $response->getAdditionalCalculations());
  
    foreach ($conditions as $condition) {
        $select->where(sprintf($condition, $additional, $rate));
    }
 
    return true;
}

The above function is called with user controlled values in $value. Can you see the problem? - the quoteInto function (which is intended to prevent SQL injection) is run before the output is processed through sprintf function. Any sanitisation function should always be run immediately before the value is used, otherwise any intervening functions might be abused to subvert the value. In this case we need to find a way to abuse the fact that the value is processed through sprintf.

Since $value['from'] and $value['to'] are processed in exactly the same way we'll focus on a single parameter $value['from'], so we end up with "%s * %s <= '#OURVALUE#'" being processed through sprintf where #OURVALUE# has been escaped but is otherwise completely under our control. There is no use our trying to add references to variables of the form %s, %d or anything like that as these variables simply won't be supplied. Fortunately we can refer to any supplied variable using the position specifier notation, so for instance %$2s will format the second argument as a string.

PHP - How do I love thee, let me count the ways

PHP has a lovely habit (from a hackers point of view) of attempting to process or ignore invalid input. If we ask sprintf to format a variable with an unknown format, it decides the best thing to do is output nothing. How does this help us? Let's see what happens if we format a variable with the "\" formatting (which doesn't exist).....

So if we place "$1'" into our input, when escaped it will become "$1\'" which sprintf will view as variable formatted as "\" followed by a single apostrophe..... PHP decides to ignore the $1\ since "\" is not a valid format and leaves behind only the ' - woohoo SQL injection achieved.

Unfortunately the generated SQL is used in multiple queries before user output is produced, so it's not possible to do a simple UNION to extract data. However as long as the magento instance contains at least one product for sale, we can carry out simple boolean queries by accessing a URL of the form:

index.php/catalogsearch/advanced/result/?name=&description=&short_description=&sku=&price[from]=0&price[to]=0%1$%27)+or+1=1--+&search=Search

The output for a false condition

 

The output for a true condition

It's left as an exercise to the reader to extract the Admin hash, use this to login and pop a shell from the admin interface :)

You can download the affected version (1.6.0.0) from https://magento.com/tech-resources/download under RELEASE ARCHIVE.

Secarma Accreditations Crest Accredited CCSS IT Health Check Service ISO 9001 IS0 27001 Cyber Essentials Accreditation Cyber Essentials Trusted Parter Badge