## # ## This file is part of the Metasploit Framework and may be subject to # Current source: https://github.com/rapid7/metasploit-framework ## require 'rex' require 'msf/core' require 'msf/core/auxiliary/report' class Metasploit3 < Msf::Post include Msf::Auxiliary::Report def initialize(info={}) super( update_info( info, 'Name' => 'Windows Gather AD Enumerate Computers', 'Description' => %q{ This module will enumerate computers in the default AD directory. Optional Attributes: objectClass, cn, description, distinguishedName, instanceType, whenCreated, whenChanged, uSNCreated, uSNChanged, name, objectGUID, userAccountControl, badPwdCount, codePage, countryCode, badPasswordTime, lastLogoff, lastLogon, localPolicyFlags, pwdLastSet, primaryGroupID, objectSid, accountExpires, logonCount, sAMAccountName, sAMAccountType, operatingSystem, operatingSystemVersion, operatingSystemServicePack, serverReferenceBL, dNSHostName, rIDSetPreferences, servicePrincipalName, objectCategory, netbootSCPBL, isCriticalSystemObject, frsComputerReferenceBL, lastLogonTimestamp, msDS-SupportedEncryptionTypes }, 'License' => MSF_LICENSE, 'Author' => [ 'Ben Campbell ' ], 'Platform' => [ 'win' ], 'SessionTypes' => [ 'meterpreter' ] )) register_options([ OptInt.new('MAX_SEARCH', [true, 'Maximum values to retrieve, 0 for all.', 20]), OptBool.new('STORE', [true, 'Store file in loot.', false]), OptString.new('ATTRIBS', [true, 'Attributes to retrieve.', 'dNSHostName,distinguishedName,description,operatingSystem']) ], self.class) end def read_value(addr) val_size = client.railgun.memread(addr-4,4).unpack('V*')[0] value = client.railgun.memread(addr, val_size) return value.strip end def run print_status("Connecting to default LDAP server") session_handle = bind_default_ldap_server return false unless session_handle print_status("Querying default naming context") query_result = query_ldap(session_handle, "", 0, "(objectClass=computer)", ["defaultNamingContext"]) first_entry_attributes = query_result[0]['attributes'] defaultNamingContext = first_entry_attributes[0]['values'] # Value from First Attribute of First Entry print_status("Default Naming Context #{defaultNamingContext}") attributes = datastore['ATTRIBS'].split(',') print_status("Querying computer objects - Please wait...") results = query_ldap(session_handle, defaultNamingContext, 2, "(objectClass=computer)", attributes) print_status("Unbinding from LDAP service.") wldap32.ldap_unbind(session_handle) results_table = Rex::Ui::Text::Table.new( 'Header' => 'AD Computers', 'Indent' => 1, 'SortIndex' => -1, 'Columns' => attributes ) results.each do |result| row = [] result['attributes'].each do |attr| if attr['values'].nil? row << "" else row << attr['values'] end end results_table << row end print_line results_table.to_s if datastore['STORE'] stored_path = store_loot('ad.computers', 'text/plain', session, results_table.to_csv) print_status("Results saved to: #{stored_path}") end end def wldap32 return client.railgun.wldap32 end def bind_default_ldap_server vprint_status ("Initializing LDAP connection.") session_handle = wldap32.ldap_sslinitA("\x00\x00\x00\x00", 389, 0)['return'] vprint_status("LDAP Handle: #{session_handle}") if session_handle == 0 print_error("Unable to connect to LDAP server") wldap32.ldap_unbind(session_handle) return false end vprint_status ("Binding to LDAP server.") bind = wldap32.ldap_bind_sA(session_handle, nil, nil, 0x0486)['return'] #LDAP_AUTH_NEGOTIATE 0x0486 if bind != 0 print_error("Unable to bind to LDAP server") wldap32.ldap_unbind(session_handle) return false end return session_handle end def query_ldap(session_handle, base, scope, filter, attributes) vprint_status ("Searching LDAP directory.") search = wldap32.ldap_search_sA(session_handle, base, scope, filter, nil, 0, 4) vprint_status("search: #{search}") if search['return'] != 0 print_error("No results") wldap32.ldap_msgfree(search['res']) return end search_count = wldap32.ldap_count_entries(session_handle, search['res'])['return'] if(search_count == 0) print_error("No entries retrieved") wldap32.ldap_msgfree(search['res']) return end print_status("Entries retrieved: #{search_count}") vprint_status("Retrieving results...") entries = {} entry_results = [] if datastore['MAX_SEARCH'] == 0 max_search = search_count else max_search = [datastore['MAX_SEARCH'], search_count].min end 0.upto(max_search - 1) do |i| print '.' if(i==0) entries[0] = wldap32.ldap_first_entry(session_handle, search['res'])['return'] else entries[i] = wldap32.ldap_next_entry(session_handle, entries[i-1])['return'] end if(entries[i] == 0) print_error("Failed to get entry.") wldap32.ldap_unbind(session_handle) wldap32.ldap_msgfree(search['res']) return end vprint_status("Entry #{i}: #{entries[i]}") attribute_results = [] attributes.each do |attr| vprint_status("Attr: #{attr}") pp_value = wldap32.ldap_get_values(session_handle, entries[i], attr)['return'] vprint_status("ppValue: 0x#{pp_value.to_s(16)}") if pp_value == 0 vprint_error("No attribute value returned.") else count = wldap32.ldap_count_values(pp_value)['return'] vprint_status "Value count: #{count}" value_results = [] if count < 1 vprint_error("Bad Value List") else 0.upto(count - 1) do |j| p_value = client.railgun.memread(pp_value+(j*4), 4).unpack('V*')[0] vprint_status "p_value: 0x#{p_value.to_s(16)}" value = read_value(p_value) vprint_status "Value: #{value}" if value.nil? value_results << "" else value_results << value end end value_results = value_results.join('|') end end if pp_value != 0 vprint_status("Free value memory.") wldap32.ldap_value_free(pp_value) # wldap32.ldap_memfree(attr) No need to free attributes as these are hardcoded end attribute_results << {"name" => attr, "values" => value_results} end entry_results << {"id" => i, "attributes" => attribute_results} end print_line return entry_results end end