Check Point researchers recently discovered a critical RCE (remote code execution) vulnerability in the Magento web e-commerce platform that can lead to the complete compromise of any Magento-based store, including credit card information as well as other financial and personal data, affecting nearly two hundred thousand online shops.

 

Check Point privately disclosed the vulnerabilities together with a list of suggested fixes to eBay prior to public disclosure. A patch to address the flaws was released on February 9, 2015 (SUPEE-5344 available here). Store owners and administrators are urged to apply the patch immediately if they haven’t done so already.
For a visual demonstration of one way the vulnerability can be exploited, please see our video here.

What kind of attack is it?

The vulnerability is actually comprised of a chain of several vulnerabilities that ultimately allow an unauthenticated attacker to execute PHP code on the web server. The attacker bypasses all security mechanisms and gains control of the store and its complete database, allowing credit card theft or any other administrative access into the system.

This attack is not limited to any particular plugin or theme. All the vulnerabilities are present in the Magento core, and affects any default installation of both Community and Enterprise Editions. Check Point customers are already protected from exploitation attempts of this vulnerability through the IPS software blade.

 

How did Check Point discover this vulnerability?

These vulnerabilities were discovered by Netanel Rubin of the Check Point Malware & Vulnerability Research Group. Check Point researchers often function as “white hat hackers”, discovering and protecting against security vulnerabilities before malicious actors are able to exploit them. This publication is part of Check Point’s ongoing efforts toward public security awareness and education of consumers and enterprises.

 

How can I protect against the vulnerability?

Magento-based e-Commerce businesses are advised to apply the designated patch SUPEE-5344 released by Magento. Although Check Point did not witness any exploitation attempts of this vulnerability in the wild, administrators are advised to monitor logs for patterns matching the technical description.

Check Point IPS currently protects against exploitation attempts of this vulnerability.

 

Vulnerable Versions

Confirmed vulnerable: 1.9.1.0 CE and 1.14.1.0 EE (Latest releases as of this writing).

 

Synopsis

Magento is a popular eCommerce platform purchased by eBay in 2011. It has 2 versions:
• A community version, which is open-sourced and contains code contributed from the community.
• An enterprise version, which offers more features as well as customer support and other premium benefits.

We discovered a vulnerability-chain which ultimately allows an unauthenticated attacker to execute PHP code in the vulnerable server. This chain consists of a number of vulnerabilities, which are described further in the technical description.

These vulnerabilities have been assigned CVE-2015-1397, CVE-2015-1398, CVE-2015-1399.

 

Disclosure Timeline

January 14, 2015 – First contact with Magento Security
January 15, 2015 – Provided complete vulnerability report including suggested fixes
February 9, 2015 – Patch Released by Magento (SUPEE-5344 available here)
April 22, 2015 – Public Disclosure

 

Technical Description

Before we begin the technical walkthrough resulting in the discovery of each vulnerability, we need to briefly explain how Magento extensively uses reflection-style code as a dynamic way of providing content.

Magento uses modules, which are directories that contain different pieces of functionality. Each module contains controllers, which are PHP class files that define actions, public methods in that class.

Each incoming request is parsed to understand which module, controller and action is requested by the user. The PATH_INFO variable (the section of the URI after the resource/script file name) contains the requested model, controller, and action name, in this format:

GET /index.php/[MODULE_NAME]/[CONTROLLER_NAME]/[ACTION_NAME] HTTP/1.1

The dynamic controller loading logic uses this algorithm:
1. Determine if [MODULE_NAME] exists in the modules white list.
2. If it exists, construct a class name in this format: Mage_[MODULE_NAME]_[CONTROLLER_NAME]Controller
3. Find a class file by replacing every ‘_’ with ‘/’ and appending a ‘php’ extension.
4. If the file is found, include it.

For example, if we send this request:

GET /index.php/downloadable/file/ HTTP/1.1

The following class is loaded:

Mage_Downloadable_FileController

Controlled Injections

When an administrator user attempts to load a controller, the system appends the string ‘Adminhtml’ after the module name.

Therefore, if we add the ‘admin’ prefix to our request:

GET /index.php/admin/downloadable/file/ HTTP/1.1

We get the following class name (Note: the reflection loader repeats the module name after the ‘Adminhtml’ prefix):

Mage_Downloadable_Adminhtml_Downloadable_FileController

Therefore, in addition to the obvious LFI of any file with a ‘Controller.php’ suffix (‘/index.php/downloadable/……file’), we can bypass admin authentication if we send this request:

GET /index.php/downloadable/Adminhtml_Downloadable_File/ HTTP/1.1

That will try to load this class:

Mage_Downloadable_Adminhtml_Downloadable_FileController

The weakness exposed here is that Magento’s code validates admin sessions only if a request contains the ‘/admin/’ prefix, and fails to detect this controller injection technique.

However, as mentioned earlier, a predefined module white list constrains the module names we can attempt to load. This list differs between user and admin contexts. Therefore, the controller injection technique (loading an admin-context module while in user-context) will only work for modules that exist in both white lists (controllers containing user and admin functionality). This greatly limits our attack surface.

A second drawback for attackers is that some modules implement additional checks for specific privileges. As we don’t have any privileges, these checks will fail.

These well-implemented safeguards really constrain our attack surface, and we could not find any critical (RCE, LFI with no suffix) vulnerabilities with this approach.

 

Best Foot ‘Forwarded’

Continuing in attacker mode, we next examined how the system determines if we are administrators when a controller is loaded.

For authentication-required controllers, we discovered that the system does not block access (using exit(), die; or similar) for unauthenticated users. Instead, the requested controller is replaced with a ‘login’ controller.

This code can be viewed here:

$requestedActionName = $request->getActionName();
$openActions = array(
    LIST_OF_OPEN_ACTIONS (login, reset password, etc.)
);

if (in_array($requestedActionName, $openActions)) {
    $request->setDispatched(true); // An open actions
} else { // Should check authentication
    if($user) { // A user exist
        $user->reload(); // Check validity of user
    }
    if (!$user || !$user->getId()) { // No user or no user ID
        if ($request->getPost('login')) { // Try to login
            TRY_TO_LOGIN
        }
	     // No active user session AND no user login - access denied
        if (!$request->getParam('forwarded')) {
            if ($request->getParam('isIframe')) {
                CHANGE_CONTROLLER
            } elseif($request->getParam('isAjax')) {
                CHANGE_CONTROLLER
            } else {
                CHANGE_CONTROLLER
            }
            return false;
        }
    }
}

Mage_Admin_Model_Observer::actionPreDispatchAdmin()

Note the highlighted ‘if’ statement that occurs after the system has detected that we are not authenticated and we are not attempting to log in.

• If the statement is true, the system changes our controller to one of the ‘denied access’ controllers, blocking us from the admin panel.
• If the statement is false, our controller remains unchanged and the system continues normally.

This is a simple control flow logic flaw, and we can bypass the checks by the simple addition of a ‘forwarded’ parameter to our request.

We believe this flaw resulted from an attempt to allow controllers to authenticate users bypassing the built-in login mechanism. The fact that this parameter can be controlled externally may have been overlooked.

This finding vastly widens our attack surface, as we can now access any admin module regardless of whether or not it offers functionality to regular users.

However, we are left with the restriction of using modules that do not implement additional privilege checking.

Mage: Level 90

There are very few modules that never check for privileges, and those modules do not usually do anything that would be of interest to an attacker. However, let’s take a look at the ‘Cms_Wysiwyg’ controller in the ‘Adminhtml’ module. This controller has only one action named ‘directive’, which loads an image using a given path.

This is the relevant code from the controller:

// Get the ___directive parameter
$directive = $this->getRequest()->getParam('___directive');

// base64_decodes the input
$directive = Mage::helper('core')->urlDecode($directive);

// Filter the input
$url = Mage::getModel('cms/adminhtml_template_filter')->filter($directive);

// Try to load the image
try {
    $image = Varien_Image_Adapter::factory('GD2');
    $image->open($url);
    $image->display();
}

Mage_Adminhtml_Cms_WysiwygController::directiveAction()

Excerpt from the “What You See is What You Gain access to” controller

 

Any attempt to load a file that is not a valid image (such as a configuration file, typically sought after by attackers) results in an exception being raised, and a default image is displayed instead. As a result, we can’t use this controller for a local file disclosure attack.

However, let’s examine the ‘filter()’ function. In this controller, ‘filter()’ is defined as a method in ‘cms/adminhtml_template_filter.’

The ‘filter()’ function reviews the input and performs these steps:
1. Locate blocks with this format: ‘{{FUNC_NAME PARAM_NAME=PARAM_VALUE}}’
2. If found, attempt to call the function ‘[FUNC_NAME]Directive’ in this class.
3. If called successfully, replace the block with the function output.

As part of an attack, we can attempt to use any filter that admin templates use, and call any class method named ‘[FUNC]Directive.’ Unfortunately, many of these methods operate only if there are template parameters. As our template parsing instance doesn’t contain any, most of these methods are useless for our purposes.

Luckily, one function turned out very valuable: ‘blockDirective()’, which creates a Magento-“block” class under our control.

The class name is defined:

Mage_X_Block_Y

where ‘X’ and ‘Y’ are both under our current control.

Apart from the LFI this causes (triggered by the auto loading mechanism), we can thus create any block class we want.

Note: The LFI is no use to us on its own; most of the system files are class files, while other files do not contain code that we can exploit. As in the previous LFI, a null byte can be useful for this situation, but we don’t want our exploit to work only on old versions of PHP (null byte injections were fixed since 5.3.4).

After the system initializes the requested block, it continues to set its parameters using the function ‘setDataUsingMethod()’:

public function setDataUsingMethod($key, $args=array())
{
 $method = 'set'.$this->_camelize($key); // Parse the method name
 $this->$method($args); // Call the method
    return $this; // Return (Wow, such dynamics, very $this)
}

Mage_Core_Model_Email_Template_Filter::blockDirective ()

Where ‘$key’ is the name of one of the filter parameters we have entered and ‘$args’ is its value.

At this point, an attacker can call any method that starts with the ‘set’ prefix (e.g. ‘setData()’), passing an argument that he controls.

After the block is initialized and has its parameters set, ‘blockDirective()’ lets us call any method we want (from the block class)with no arguments.

We can now create an instance of any block class we want, set some of its properties, and then call any class method without arguments. The drawback for attackers is that blocks are usually responsible for the GUI parts of the system, and the majority of them can’t be used for anything potentially dangerous.

 

A needle in a Magestack

Determined to discover an exploitable function, we set out on another code-wide search. We found the ‘getCsvFile()’ method from the ‘Mage_Adminhtml_Block_Widget_Grid’ class, which exports the data contained in the block (data extracted from the DB) and saves it into a CSV file.Unfortunately for attackers, neither the file name nor its extension can be controlled.

Magento offers the option of filtering what data is extracted, by adding different conditions to the actual SQL query.

This is the routine logic:

1. Retrieve the ‘filter’ parameter from the request.
2. Parse it using ‘parse_str()’.
3. Loop on each parameter name and check for a matching condition name.
4. If a match is found, parse and escape the parameter so it will be safe to use in the query based on the condition type used.
5. Add the parsed condition to the SQL query.

For a regular text string used as a filter in an SQL condition, the condition parsing is performed by inserting the string into a ‘LIKE’ statement with two surrounding ’%’ chars. The resulting ‘WHERE’ statement is similar to:

WHERE `attr` LIKE ‘%escaped_text_string%’

Other filtering conditions (‘id’, ‘range’) are processed differently. For example, the class ‘Mage_Adminhtml_Block_Report_Search_Grid’ displays every search query that is performed. One of its condition parameters, ‘popularity’, is considered a ‘range’ condition. This means we can insert 2 parameters, ‘from’ and ‘to’, to create a range operation on the ‘popularity’ column of the search term.

The condition parsing is seen here (where we control ‘$condition’):

$conditionKeyMap = array(
    // A dictionary containing operation and their matching SQL
    CONDITION_DICTIONARY_MAP 
);

$query = '';
// If the condition is an array
if (is_array($condition)) { 
    // If there's a 'field_expr' field, assign it to $fieldName
    if (isset($condition['field_expr'])) { 
        $fieldName = str_replace('#?', $this->quoteIdentifier($fieldName), $condition['field_expr']);
        unset($condition['field_expr']);
    }
    …
    // Add the start condition
    if (isset($condition['from'])) { 
        $from = $this->_prepareSqlDateCondition($condition, 'from');
        $query = $this->_prepareQuotedSqlCondition($conditionKeyMap['from'], $from, $fieldName);
    }
    // Add the end condition
    if (isset($condition['to'])) { 
        $query .= empty($query) ? '' : ' AND ';
        $to = $this->_prepareSqlDateCondition($condition, 'to');
        $query = $this->_prepareQuotedSqlCondition($query . $conditionKeyMap['to'], $to, $fieldName);
    }
    …
}

Varien_Db_Adapter_Pdo_Mysql::prepareSqlCondition ()

Note the highlighted line where ‘$fieldname’ is replaced with a value arriving from ‘$condition.’ This means we can control the field name on which the condition is performed if we can assign a ‘field_expr’ key in the condition dictionary. Since the field name is considered secure, it’s not escaped.

As attackers, we want to control the entire dictionary. The system attempts to validate our input by passing it into a validator function as seen here:

// This is the actual array from the request parameter
$value = $this->getData('value');

// Checks for a 'from' and 'to' keys (I edited the ‘if’ statement a little bit so it’ll be convenient to read)
if (strlen($value['from']) > 0) || strlen($value['to']) > 0) {
return $value; // Just return the dictionary as it!
}

Mage_Adminhtml_Block_Widget_Grid_Column_Filter_Range::getValue()

Therefore, if there are ‘from’ and ‘to’ keys in the dictionary, the function returns the dictionary as is, without unsetting any of its elements.

Now that we control the entire dictionary, we can inject any SQL we want. Fortunately for attackers, Magento uses PHP PDO, which by default supports multiple queries in a single statement. In our case, Magento assigns ‘PDO::ATTR_EMULATE_PREPARES’ to ‘true’, which forces PDO to enable multiple queries support. This means we can go beyond ‘union select’ statement injections, and inject any query we want.

There is no shortage of effective SQLi exploitation ideas. For example, we could add another administrator account in the DB and could completely compromise the system.
Such a change is too visible, however, and is still not as deep as an RCE could potentially go. We decided to keep looking for vectors to gain access to the RCE holy grail.

We can use the SQLi to add another file to the server by inserting our data to the ‘core_file_storage’ table which stores media files locally. When we access the ‘get.php’ page and request the file we just inserted, the files are created in the actual file system under the ‘media’ directory.

 

Dodging Detection

A drawback for attackers is that the system disables CGI execution in this directory using an ‘.htaccess’ file explicitly declaring ‘Options –ExecCGI.’ In order to bypass that safeguard, we can create another ‘.htaccess’ file in a subdirectory and override the parent declaration. However, this is a common exploitation trick, easily detected when looking for suspicious red flags in the server.
Similarly, we will avoid using the previously mentioned LFI as we don’t want to introduce the very suspicious artifact of a ‘.php’ file in an image directory.We therefore need to find a creative way to execute a ‘.jpg’ file without using another ‘.htaccess.’

The ‘Mage_Core_Block_Template_Zend’ class has 2 methods: ‘setScriptPath()’ and ‘fetchView().’ ‘fetchView()’ takes a template file as an argument and calls the ‘render()’ method at ‘Zend_View_Abstract’. It then parses the template file’s full path and includes it without forcing another extension.

Examine the following code:

// Check for any path traversal in the template name
if ($this->isLfiProtectionOn() && preg_match('#\.\.[\\\/]#', $name)) {
   DIE
}
// Check that we have at least one include directory
if (0 == count($this->_path['script'])) {
   DIE
}
// Try to include the file by concating each directory with the file name
foreach ($this->_path['script'] as $dir) {
// If the file is readable, return it (and later include it without further checks)
   if (is_readable($dir . $name)) {
       return $dir . $name;
   }
}
DIE

Zend_View_Abstract::_script()

At first glance, this appears to be a great opportunity for us to LFI our way to the file we have created. However, even though we can call any method we want, we are still limited to calling it without any arguments. This means we have to rely on another method to call ‘fetchView()’ with an argument we can control.

Fortunately for the attackers, such a method does exist: ‘renderView()’, which calls ‘fetchView()’ with a template file. We can control the template name property because of the ‘setDataUsingMethod()’ behavior we examined earlier. The problem is that this function calls ‘setScriptPath()’ before the ‘fetchView()’ call, which adds the ‘./app/design’ directory to the ‘$this->_path’ array. This means that path is now prepended to our file path; due to the LFI protection at the start of the function, we cannot insert any path traversal strings and escape the ‘design’ directory.

If we call ‘setScriptPath()’ ourselves with our directory before the call to ‘renderView()’ (using ‘setDataUsingMethod()’), ‘renderView()’ will override us and won’t let us use our directory.

However, if we call ‘setScriptPath()’ with our directory name and ‘fetchView()’ directly (without going through ‘renderView()’), ‘fetchView()’ is called with a blank file name. When the system appends the file name to the directory, the resulting include path contains only our directory. WIN.

Unfortunately, ‘setScriptPath()’ appends a forward slash (‘/’) to our directory name, forcing our path to be treated as a directory in the include statement. This is a problem, as directories cannot be included.

However, as we control anything prior to that slash, we also control the stream wrapper (e.g. ‘http://’). We can therefore potentially RFI our way out of this situation. However, RFI is disabled by default in PHP versions greater than 5.2, so we need to find a different, unique wrapper to exploit, one that will allow us to treat a directory path as a valid file and is not affected by PHP’s ‘allow_url_include.’

 

Phar Fetched Exploit

The ‘phar://’ wrapper handles PHP archive files. Similar to JAR files, these are archives containing compressed PHP files. The unique feature of the Phar wrapper for our purposes is that it allows us to include a file despite an ending forward slash in the file path. That file will be properly included due to the fact the wrapper interprets the last forward slash as the root directory inside the Phar archive.

Therefore, if we try to include a path similar to ‘phar://lolz.file/’, the Phar parser will execute the file’s ‘bootstrap stub’, an executable PHP code that is loaded whenever the phar file is included directly (without trying to access the files it contains). Additionally, the Phar wrapper supports different stat functions such as ‘is_readable()’, allowing us to pass the validity checks of the included file. Finally, the Phar parser disregards bytes prior to its header, as it considers them part of the bootstrap stub. We can therefore append our code to a valid image file – a very stealthy approach that is hard to detect. If we want to go the extra mile, Phar supports GZ and BZ2 compressions, as well as ZIP and TAR archives.

Using the authentication bypass, and combining both the file upload/SQLi vulnerability and the LFI/RFI vulnerability, we can ultimately execute any PHP code on the system, without any authentication.

You may also like