rapid7/metasploit-framework

View on GitHub
modules/auxiliary/scanner/smb/smb_enumusers.rb

Summary

Maintainability
D
2 days
Test Coverage
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Auxiliary

  # Exploit mixins should be called first
  include Msf::Exploit::Remote::SMB::Client
  include Msf::Exploit::Remote::SMB::Client::Authenticated

  include Msf::Exploit::Remote::DCERPC

  # Scanner mixin should be near last
  include Msf::Auxiliary::Report
  include Msf::Auxiliary::Scanner

  def initialize
    super(
      'Name'        => 'SMB User Enumeration (SAM EnumUsers)',
      'Description' => 'Determine what local users exist via the SAM RPC service',
      'Author'      => 'hdm',
      'License'     => MSF_LICENSE,
      'DefaultOptions' => {
        'DCERPC::fake_bind_multi' => false
      }
    )

    register_options(
      [
        OptBool.new('DB_ALL_USERS', [ false, "Add all enumerated usernames to the database", false ]),
      ])

    deregister_options('RPORT')
  end

  def rport
    @rport || super
  end

  def smb_direct
    @smbdirect || super
  end

  # Locate an available SMB PIPE for the specified service
  def smb_find_dcerpc_pipe(uuid, vers, pipes)
    found_pipe   = nil
    found_handle = nil
    pipes.each do |pipe_name|
      connected = false
      begin
        connect
        smb_login
        connected = true

        handle = dcerpc_handle(
          uuid, vers,
          'ncacn_np', ["\\#{pipe_name}"]
        )

        dcerpc_bind(handle)
        return pipe_name

      rescue ::Interrupt => e
        raise e
      rescue ::Exception => e
        raise e if not connected
      end
      disconnect
    end
    nil
  end

  def smb_pack_sid(str)
    [1,5,0].pack('CCv') + str.split('-').map{|x| x.to_i}.pack('NVVVV')
  end

  def smb_parse_sam_domains(data)
    ret = []
    idx = 0

    cnt = data[8, 4].unpack("V")[0]
    return ret if cnt == 0
    idx += 20
    idx += 12 * cnt

    1.upto(cnt) do
      v = data[idx,data.length].unpack('V*')
      l = v[2] * 2

      while(l % 4 != 0)
        l += 1
      end

      idx += 12
      ret << data[idx, v[2] * 2].gsub("\x00", '')
      idx += l
    end
    ret
  end

  def smb_parse_sam_users(data)
    ret = {}
    rid = []
    idx = 0

    cnt = data[8, 4].unpack("V")[0]
    return ret if cnt == 0
    idx += 20

    1.upto(cnt) do
      v = data[idx,12].unpack('V3')
      rid << v[0]
      idx += 12
    end

    1.upto(cnt) do
      v = data[idx,32].unpack('V*')
      l = v[2] * 2

      while(l % 4 != 0)
        l += 1
      end

      uid = rid.shift

      idx += 12
      ret[uid] = data[idx, v[2] * 2].gsub("\x00", '')
      idx += l
    end

    ret
  end

  @@sam_uuid     = '12345778-1234-abcd-ef00-0123456789ac'
  @@sam_vers     = '1.0'
  @@sam_pipes    = %W{ SAMR LSARPC NETLOGON BROWSER SRVSVC }

  # Fingerprint a single host
  def run_host(ip)

    [[139, false], [445, true]].each do |info|

    @rport = info[0]
    @smbdirect = info[1]

    sam_pipe   = nil
    sam_handle = nil
    begin
      # Find the SAM pipe
      sam_pipe = smb_find_dcerpc_pipe(@@sam_uuid, @@sam_vers, @@sam_pipes)
      break if not sam_pipe

      # Connect4
      stub =
        NDR.uwstring("\\\\" + ip) +
        NDR.long(2) +
        NDR.long(0x30)

      dcerpc.call(62, stub)
      resp = dcerpc.last_response ? dcerpc.last_response.stub_data : nil

      if ! (resp and resp.length == 24)
        print_error("Invalid response from the Connect5 request")
        disconnect
        return
      end

      phandle = resp[0,20]
      perror  = resp[20,4].unpack("V")[0]

      if(perror == 0xc0000022)
        disconnect
        return
      end

      if(perror != 0)
        print_error("Received error #{"0x%.8x" % perror} from the OpenPolicy2 request")
        disconnect
        return
      end

      # EnumDomains
      stub = phandle + NDR.long(0) + NDR.long(8192)
      dcerpc.call(6, stub)
      resp = dcerpc.last_response ? dcerpc.last_response.stub_data : nil
      domlist = smb_parse_sam_domains(resp)
      domains = {}

      # LookupDomain
      domlist.each do |domain|
        next if domain == 'Builtin'

        # Round up the name to match NDR.uwstring() behavior
        dlen = (domain.length + 1) * 2

        # The SAM functions are picky on Windows 2000
        stub =
          phandle +
          [(domain.length + 0) * 2].pack("v") + # NameSize
          [(domain.length + 1) * 2].pack("v") + # NameLen (includes null)
          NDR.long(rand(0x100000000)) +
          [domain.length + 1].pack("V") +          # MaxCount (includes null)
          NDR.long(0) +
          [domain.length + 0].pack("V") +          # ActualCount (ignores null)
          Rex::Text.to_unicode(domain)          # No null appended

        dcerpc.call(5, stub)
        resp    = dcerpc.last_response ? dcerpc.last_response.stub_data : nil
        raw_sid = resp[12, 20]
        txt_sid = raw_sid.unpack("NVVVV").join("-")

        domains[domain] = {
          :sid_raw => raw_sid,
          :sid_txt => txt_sid
        }
      end


      # OpenDomain, QueryDomainInfo, CloseDomain
      domains.each_key do |domain|

        # Open
        stub =
          phandle +
          NDR.long(0x00000305) +
          NDR.long(4) +
          [1,4,0].pack('CvC') +
          domains[domain][:sid_raw]

        dcerpc.call(7, stub)
        resp    = dcerpc.last_response ? dcerpc.last_response.stub_data : nil
        dhandle = resp[0,20]
        derror  = resp[20,4].unpack("V")[0]

        # Catch access denied replies to OpenDomain
        if(derror != 0)
          next
        end

        # Password information
        stub = dhandle + [0x01].pack('v')
        dcerpc.call(8, stub)
        resp = dcerpc.last_response ? dcerpc.last_response.stub_data : nil
        if(resp and resp[-4,4].unpack('V')[0] == 0)
          mlen,hlen = resp[8,4].unpack('vv')
          domains[domain][:pass_min] = mlen
          domains[domain][:pass_min_history] = hlen
        end

        # Server Role
        stub = dhandle + [0x07].pack('v')
        dcerpc.call(8, stub)
        if(resp and resp[-4,4].unpack('V')[0] == 0)
          resp = dcerpc.last_response ? dcerpc.last_response.stub_data : nil
          domains[domain][:server_role] = resp[8,2].unpack('v')[0]
        end

        # Lockout Threshold
        stub = dhandle + [12].pack('v')
        dcerpc.call(8, stub)
        resp = dcerpc.last_response ? dcerpc.last_response.stub_data : nil

        if(resp and resp[-4,4].unpack('V')[0] == 0)
          lduration = resp[8,8]
          lwindow   = resp[16,8]
          lthresh   = resp[24, 2].unpack('v')[0]

          domains[domain][:lockout_threshold] = lthresh
          domains[domain][:lockout_duration]  = Rex::Proto::SMB::Utils.time_smb_to_unix(*(lduration.unpack('V2')))
          domains[domain][:lockout_window]    = Rex::Proto::SMB::Utils.time_smb_to_unix(*(lwindow.unpack('V2')))
        end

        # Users
        stub = dhandle + NDR.long(0) + NDR.long(0x10) + NDR.long(1024*1024)
        dcerpc.call(13, stub)
        resp  = dcerpc.last_response ? dcerpc.last_response.stub_data : nil
        if(resp and resp[-4,4].unpack('V')[0] == 0)
          domains[domain][:users] = smb_parse_sam_users(resp)
        end


        # Close Domain
        dcerpc.call(1, dhandle)
      end

      # Close Policy
      dcerpc.call(1, phandle)


      domains.each_key do |domain|

        # Delete the no longer used raw SID value
        domains[domain].delete(:sid_raw)

        # Store the domain name itself
        domains[domain][:name] = domain

        # Store the domain information
        report_note(
          :host => ip,
          :proto => 'tcp',
          :port => rport,
          :type => 'smb.domain.enumusers',
          :data => domains[domain]
        )

        users = domains[domain][:users] || {}
        extra = ""
        if (domains[domain][:lockout_threshold])
          extra = "( "
          extra << "LockoutTries=#{domains[domain][:lockout_threshold]} "
          extra << "PasswordMin=#{domains[domain][:pass_min]} "
          extra << ")"
        end
        print_good("#{domain.upcase} [ #{users.keys.map{|k| users[k]}.join(", ")} ] #{extra}")
        if datastore['DB_ALL_USERS']
          users.each { |user|
            store_username(user, domain, ip, rport, resp)
          }
        end
      end

      # cleanup
      disconnect
      return
    rescue ::Timeout::Error
    rescue ::Interrupt
      raise $!
    rescue ::Rex::ConnectionError
    rescue ::Rex::Proto::SMB::Exceptions::LoginError
      next
    rescue ::Exception => e
      print_line("Error: #{ip} #{e.class} #{e}")
    end
    end
  end


  def store_username(username, domain, ip, rport, resp)
    service_data = {
      address: ip,
      port: rport,
      service_name: 'smb',
      protocol: 'tcp',
      workspace_id: myworkspace_id,
      proof: resp
    }

    credential_data = {
      origin_type: :service,
      module_fullname: fullname,
      username: username[1],
      realm_key: Metasploit::Model::Realm::Key::ACTIVE_DIRECTORY_DOMAIN,
      realm_value: domain,
    }.merge(service_data)

    login_data = {
      core: create_credential(credential_data),
      status: Metasploit::Model::Login::Status::UNTRIED
    }.merge(service_data)

    create_credential_login(login_data)
  end

end