Skip to content

Commit f07efc9

Browse files
committed
Land rapid7#2915, @Meatballs1 improvements for LDAP post mixin
2 parents 7f9b4a4 + f58b66a commit f07efc9

File tree

5 files changed

+142
-32
lines changed

5 files changed

+142
-32
lines changed

lib/msf/core/post/windows/accounts.rb

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,64 @@ module Windows
55

66
module Accounts
77

8+
GUID = [
9+
['Data1',:DWORD],
10+
['Data2',:WORD],
11+
['Data3',:WORD],
12+
['Data4','BYTE[8]']
13+
]
14+
15+
DOMAIN_CONTROLLER_INFO = [
16+
['DomainControllerName',:LPSTR],
17+
['DomainControllerAddress',:LPSTR],
18+
['DomainControllerAddressType',:ULONG],
19+
['DomainGuid',GUID],
20+
['DomainName',:LPSTR],
21+
['DnsForestName',:LPSTR],
22+
['Flags',:ULONG],
23+
['DcSiteName',:LPSTR],
24+
['ClientSiteName',:LPSTR]
25+
]
26+
27+
##
28+
# get_domain(server_name=nil)
29+
#
30+
# Summary:
31+
# Retrieves the current DomainName the given server is
32+
# a member of.
33+
#
34+
# Parameters
35+
# server_name - DNS or NetBIOS name of the remote server
36+
# Returns:
37+
# The DomainName of the remote server or nil if windows
38+
# could not retrieve the DomainControllerInfo or encountered
39+
# an exception.
40+
#
41+
##
42+
def get_domain(server_name=nil)
43+
domain = nil
44+
result = session.railgun.netapi32.DsGetDcNameA(
45+
server_name,
46+
nil,
47+
nil,
48+
nil,
49+
0,
50+
4)
51+
52+
begin
53+
dc_info_addr = result['DomainControllerInfo']
54+
unless dc_info_addr == 0
55+
dc_info = session.railgun.util.read_data(DOMAIN_CONTROLLER_INFO, dc_info_addr)
56+
pointer = session.railgun.util.unpack_pointer(dc_info['DomainName'])
57+
domain = session.railgun.util.read_string(pointer)
58+
end
59+
ensure
60+
session.railgun.netapi32.NetApiBufferFree(dc_info_addr)
61+
end
62+
63+
domain
64+
end
65+
866
##
967
# delete_user(username, server_name = nil)
1068
#

lib/msf/core/post/windows/ldap.rb

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ module LDAP
88

99
include Msf::Post::Windows::Error
1010
include Msf::Post::Windows::ExtAPI
11+
include Msf::Post::Windows::Accounts
1112

1213
LDAP_SIZELIMIT_EXCEEDED = 0x04
1314
LDAP_OPT_SIZELIMIT = 0x03
@@ -83,46 +84,65 @@ def initialize(info = {})
8384
super
8485
register_options(
8586
[
86-
OptString.new('DOMAIN', [false, 'The domain to query.', nil]),
87-
OptInt.new('MAX_SEARCH', [true, 'Maximum values to retrieve, 0 for all.', 50]),
88-
OptString.new('FIELDS', [true, 'FIELDS to retrieve.', nil]),
89-
OptString.new('FILTER', [true, 'Search filter.', nil])
87+
OptString.new('DOMAIN', [false, 'The domain to query or distinguished name (e.g. DC=test,DC=com)', nil]),
88+
OptInt.new('MAX_SEARCH', [true, 'Maximum values to retrieve, 0 for all.', 500]),
9089
], self.class)
9190
end
9291

92+
# Converts a Distinguished Name to DNS name
93+
#
94+
# @param [String] Distinguished Name
95+
# @return [String] DNS name
96+
def dn_to_domain(dn)
97+
if dn.include? "DC="
98+
return dn.gsub(',','').split('DC=')[1..-1].join('.')
99+
else
100+
return dn
101+
end
102+
end
103+
93104
# Performs an ldap query
94105
#
95106
# @param [String] LDAP search filter
96107
# @param [Integer] Maximum results
97108
# @param [Array] String array containing attributes to retrieve
109+
# @param [String] Optional domain or distinguished name
98110
# @return [Hash] Entries found
99-
def query(filter, max_results, fields)
100-
default_naming_context = datastore['DOMAIN']
101-
default_naming_context ||= get_default_naming_context
102-
vprint_status("Default Naming Context #{default_naming_context}")
111+
def query(filter, max_results, fields, domain=nil)
112+
domain ||= datastore['DOMAIN']
113+
domain ||= get_domain
114+
115+
if domain.blank?
116+
raise RuntimeError, "Unable to find the domain to query."
117+
end
118+
103119
if load_extapi
104-
return session.extapi.adsi.domain_query(default_naming_context, filter, max_results, DEFAULT_PAGE_SIZE, fields)
120+
return session.extapi.adsi.domain_query(domain, filter, max_results, DEFAULT_PAGE_SIZE, fields)
105121
else
106-
unless default_naming_context.include? "DC="
107-
raise RuntimeError.new("DOMAIN must be specified as distinguished name e.g. DC=test,DC=com")
122+
if domain and domain.include? "DC="
123+
default_naming_context = domain
124+
domain = dn_to_domain(domain)
125+
else
126+
default_naming_context = get_default_naming_context(domain)
108127
end
109128

110-
bind_default_ldap_server(max_results) do |session_handle|
129+
bind_default_ldap_server(max_results, domain) do |session_handle|
111130
return query_ldap(session_handle, default_naming_context, 2, filter, fields)
112131
end
113132
end
114133
end
115134

116135
# Performs a query to retrieve the default naming context
117136
#
118-
def get_default_naming_context
119-
bind_default_ldap_server(1) do |session_handle|
137+
def get_default_naming_context(domain=nil)
138+
bind_default_ldap_server(1, domain) do |session_handle|
120139
print_status("Querying default naming context")
121140

122141
query_result = query_ldap(session_handle, "", 0, "(objectClass=computer)", ["defaultNamingContext"])
123142
first_entry_fields = query_result[:results].first
124143
# Value from First Attribute of First Entry
125144
default_naming_context = first_entry_fields.first
145+
vprint_status("Default naming context #{default_naming_context}")
126146
return default_naming_context
127147
end
128148
end
@@ -299,13 +319,14 @@ def wldap32
299319
client.railgun.wldap32
300320
end
301321

302-
303322
# Binds to the default LDAP Server
304323
# @param [int] the maximum number of results to return in a query
305324
# @return [LDAP Session Handle]
306-
def bind_default_ldap_server(size_limit)
325+
def bind_default_ldap_server(size_limit, domain=nil)
307326
vprint_status ("Initializing LDAP connection.")
308-
init_result = wldap32.ldap_sslinitA("\x00\x00\x00\x00", 389, 0)
327+
328+
# If domain is still null the API may be able to handle it...
329+
init_result = wldap32.ldap_sslinitA(domain, 389, 0)
309330
session_handle = init_result['return']
310331
if session_handle == 0
311332
raise RuntimeError.new("Unable to initialize ldap server: #{init_result["ErrorMessage"]}")
@@ -321,7 +342,6 @@ def bind_default_ldap_server(size_limit)
321342

322343
bind = bind_result['return']
323344
unless bind == 0
324-
vprint_status("Unbinding from LDAP service")
325345
wldap32.ldap_unbind(session_handle)
326346
raise RuntimeError.new("Unable to bind to ldap server: #{ERROR_CODE_TO_CONSTANT[bind]}")
327347
end

lib/rex/post/meterpreter/extensions/stdapi/railgun/def/def_kernel32.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3668,11 +3668,11 @@ def self.create_dll(dll_path = 'kernel32')
36683668
# ])
36693669

36703670
dll.add_function( 'lstrlenA', 'DWORD',[
3671-
["PCHAR","lpString","in"],
3671+
["LPVOID","lpString","in"],
36723672
])
36733673

36743674
dll.add_function( 'lstrlenW', 'DWORD',[
3675-
["PWCHAR","lpString","in"],
3675+
["LPVOID","lpString","in"],
36763676
])
36773677

36783678

lib/rex/post/meterpreter/extensions/stdapi/railgun/def/def_netapi32.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,19 @@ class Def_netapi32
1212
def self.create_dll(dll_path = 'netapi32')
1313
dll = DLL.new(dll_path, ApiConstants.manager)
1414

15+
dll.add_function('NetApiBufferFree','DWORD',[
16+
["LPVOID","Buffer","in"]
17+
])
18+
19+
dll.add_function('DsGetDcNameA', 'DWORD',[
20+
["PWCHAR","ComputerName","in"],
21+
["PWCHAR","DomainName","in"],
22+
["PBLOB","DomainGuid","in"],
23+
["PWCHAR","SiteName","in"],
24+
["DWORD","Flags","in"],
25+
["PDWORD","DomainControllerInfo","out"]
26+
])
27+
1528
dll.add_function('NetUserDel', 'DWORD',[
1629
["PWCHAR","servername","in"],
1730
["PWCHAR","username","in"],

lib/rex/post/meterpreter/extensions/stdapi/railgun/util.rb

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ def unpack_pointer(packed_pointer)
341341
# See #unpack_pointer
342342
#
343343
def is_null_pointer(pointer)
344-
if pointer.class == String
344+
if pointer.kind_of? String
345345
pointer = unpack_pointer(pointer)
346346
end
347347

@@ -375,6 +375,26 @@ def read_wstring(pointer, length = nil)
375375
return str
376376
end
377377

378+
#
379+
# Reads null-terminated ASCII strings from memory.
380+
#
381+
# Given a pointer to a null terminated array of CHARs, return a ruby
382+
# String. If +pointer+ is NULL (see #is_null_pointer) returns an empty
383+
# string.
384+
#
385+
def read_string(pointer, length=nil)
386+
if is_null_pointer(pointer)
387+
return ''
388+
end
389+
390+
unless length
391+
length = railgun.kernel32.lstrlenA(pointer)['return']
392+
end
393+
394+
chars = read_array(:CHAR, length, pointer)
395+
return chars.join('')
396+
end
397+
378398
#
379399
# Read a given number of bytes from memory or from a provided buffer.
380400
#
@@ -437,7 +457,7 @@ def read_data(type, position, buffer = nil)
437457
return raw.unpack('l').first
438458
end
439459

440-
#If nothing worked thus far, return it raw
460+
#If nothing worked thus far, return it raw
441461
return raw
442462
end
443463

@@ -498,7 +518,7 @@ def is_array_type?(type)
498518

499519
# Returns true if the type passed describes a data structure, false otherwise
500520
def is_struct_type?(type)
501-
return type.class == Array
521+
return type.kind_of? Array
502522
end
503523

504524

@@ -513,10 +533,13 @@ def sizeof_type(type)
513533
return pointer_size
514534
end
515535

516-
if is_array_type?(type)
517-
element_type, length = split_array_type(type)
518-
519-
return length * sizeof_type(element_type)
536+
if type.kind_of? String
537+
if is_array_type?(type)
538+
element_type, length = split_array_type(type)
539+
return length * sizeof_type(element_type)
540+
else
541+
return sizeof_type(type.to_sym)
542+
end
520543
end
521544

522545
if is_struct_type?(type)
@@ -559,18 +582,15 @@ def sizeof_struct(struct)
559582
def struct_offsets(definition, offset)
560583
padding = 0
561584
offsets = []
562-
563585
definition.each do |mapping|
564586
key, data_type = mapping
565-
566587
if sizeof_type(data_type) > padding
567588
offset = offset + padding
568589
end
569590

570591
offsets.push(offset)
571592

572593
offset = offset + sizeof_type(data_type)
573-
574594
padding = calc_padding(offset)
575595
end
576596

@@ -606,12 +626,11 @@ def split_array_type(type)
606626
if type =~ /^(\w+)\[(\w+)\]$/
607627
element_type = $1
608628
length = $2
609-
610629
unless length =~ /^\d+$/
611630
length = railgun.const(length)
612631
end
613632

614-
return element_type, length
633+
return element_type.to_sym, length.to_i
615634
else
616635
raise "Can not split non-array type #{type}"
617636
end

0 commit comments

Comments
 (0)