metasploit-framework
metasploit-framework copied to clipboard
ldap_query fails on schema extraction when specifying base_dn as child DC
Steps to reproduce
Running ldap query results in a crash:
msf6 auxiliary(gather/ldap_query) > run domain=za.tryhackme.loc [email protected] password=Mwtv3419 ZA.TRYHACKME.LOC base_dn='DC=za,DC=tryhackme,DC=loc' action=ENUM_CONSTRAINED_DELEGATION
[*] Running module against 10.200.60.101
[*] User-specified base DN: DC=za,DC=tryhackme,DC=loc
[-] Auxiliary aborted due to failure: unexpected-reply: The LDAP search with (|(LDAPDisplayName=cn)(LDAPDisplayName=samaccountname)(LDAPDisplayName=serviceprincipalname)(LDAPDisplayName=objectcategory)(LDAPDisplayName=msds-allowedtodelegateto)) failed cause the operation targeted an entity within the base DN that does not exist.
[*] Auxiliary module execution completed
From a breakpoint I can see that the entries are returned successfully, but the metadata query fails:
msf6 auxiliary(gather/ldap_query) > run domain=za.tryhackme.loc [email protected] password=Mwtv3419 ZA.TRYHACKME.LOC base_dn='DC=za,DC=tryhackme,DC=loc' action=ENUM_CONSTRAINED_DELEGATION
[*] Running module against 10.200.60.101
[*] User-specified base DN: DC=za,DC=tryhackme,DC=loc
From: /mnt/hgfs/metasploit-framework/modules/auxiliary/gather/ldap_query.rb:299 Msf::Modules::Auxiliary__Gather__Ldap_query::MetasploitModule#normalize_entries:
294: entries.each do |entry|
295: attributes.merge!(entry.to_h)
296: end
297:
298: require 'pry-byebug'; binding.pry
=> 299: attribute_properties = query_attributes_data(ldap, attributes)
300:
301: entries.each do |entry|
302: # Convert to a hash so we get the raw data we need from within the Net::LDAP::Entry object
303: entry = entry.to_h
304: entry.each_key do |attribute_name|
[1] pry(#<Msf::Modules::Auxiliary__Gather__Ldap_query::MetasploitModule>)> ldap
=> #<Net::LDAP:0x00005576873f1d00 ...>
[2] pry(#<Msf::Modules::Auxiliary__Gather__Ldap_query::MetasploitModule>)> attributes
=> {:dn=>["CN=IIS Server,CN=Users,DC=za,DC=tryhackme,DC=loc"],
:cn=>["IIS Server"],
:samaccountname=>["svcIIS"],
:serviceprincipalname=>["HTTP/svcServWeb.za.tryhackme.loc"],
:objectcategory=>["CN=Person,CN=Schema,CN=Configuration,DC=tryhackme,DC=loc"],
:"msds-allowedtodelegateto"=>
["WSMAN/THMSERVER1.za.tryhackme.loc",
"WSMAN/THMSERVER1",
"http/THMSERVER1.za.tryhackme.loc",
"http/THMSERVER1"]}
Specifically it fails here as the calculated base dn is wrong:
attributes_data = perform_ldap_query(ldap, filter, attributes, base: ['CN=Schema,CN=Configuration', @base_dn].join(','))
Values which crash:
From: /mnt/hgfs/metasploit-framework/modules/auxiliary/gather/ldap_query.rb:141 Msf::Modules::Auxiliary__Gather__Ldap_query::MetasploitModule#perform_ldap_query:
139: def perform_ldap_query(ldap, filter, attributes, base: nil)
140: base ||= @base_dn
=> 141: returned_entries = ldap.search(base: base, filter: filter, attributes: attributes)
142: query_result = ldap.as_json['result']['ldap_result']
143:
144: validate_query_result!(query_result, filter)
145:
146: if returned_entries.nil? || returned_entries.empty?
147: print_error("No results found for #{filter}.")
148: nil
149: else
150: returned_entries
151: end
152: end
[11] pry(#<Msf::Modules::Auxiliary__Gather__Ldap_query::MetasploitModule>)> base
=> "CN=Schema,CN=Configuration,DC=za,DC=tryhackme,DC=loc"
[12] pry(#<Msf::Modules::Auxiliary__Gather__Ldap_query::MetasploitModule>)> filter
=> "(|(LDAPDisplayName=cn)(LDAPDisplayName=samaccountname)(LDAPDisplayName=serviceprincipalname)(LDAPDisplayName=objectcategory)(LDAPDisplayName=msds-allowedtodelegateto))"
[13] pry(#<Msf::Modules::Auxiliary__Gather__Ldap_query::MetasploitModule>)> attributes
=> ["LDAPDisplayName", "isSingleValued", "oMSyntax", "attributeSyntax"]
I fixed with a local hack to unblock me:
diff --git a/modules/auxiliary/gather/ldap_query.rb b/modules/auxiliary/gather/ldap_query.rb
index 5e3e4a76e9..49312592fe 100644
--- a/modules/auxiliary/gather/ldap_query.rb
+++ b/modules/auxiliary/gather/ldap_query.rb
@@ -271,7 +271,9 @@ class MetasploitModule < Msf::Auxiliary
end
filter += ')'
attributes = ['LDAPDisplayName', 'isSingleValued', 'oMSyntax', 'attributeSyntax']
- attributes_data = perform_ldap_query(ldap, filter, attributes, base: ['CN=Schema,CN=Configuration', @base_dn].join(','))
+ # attributes_data = perform_ldap_query(ldap, filter, attributes, base: ['CN=Schema,CN=Configuration', @base_dn].join(','))
+ # With the @base_dn set this becomes 'CN=Schema,CN=Configuration,DC=za,DC=tryhackme,DC=loc', but we need to query with 'CN=Schema,CN=Configuration,DC=tryhackme,DC=loc'
+ attributes_data = perform_ldap_query(ldap, filter, attributes, base: ['CN=Schema,CN=Configuration', 'DC=tryhackme,DC=loc'].join(','))
entry_list = {}
for entry in attributes_data do
@@ -294,6 +296,7 @@ class MetasploitModule < Msf::Auxiliary
entries.each do |entry|
attributes.merge!(entry.to_h)
end
+
attribute_properties = query_attributes_data(ldap, attributes)
entries.each do |entry|
For context - this is the list DN:
CN=Configuration,DC=tryhackme,DC=loc
CN=Schema,CN=Configuration,DC=tryhackme,DC=loc
DC=DomainDnsZones,DC=za,DC=tryhackme,DC=loc
DC=ForestDnsZones,DC=tryhackme,DC=loc
DC=za,DC=tryhackme,DC=loc

Were you following a specific guide/tutorial or reading documentation?
https://tryhackme.com/room/exploitingad
Expected behavior
No crash
Current behavior
Crash
Metasploit version
msf6 auxiliary(gather/ldap_query) > version
Framework: 6.2.33-dev-a8957bce49
Console : 6.2.33-dev-a8957bce49
This also seems to spit out errors after running against a DC without auth:
msf6 auxiliary(gather/ldap_query) > run verbose=true rhosts=192.168.123.13
[*] Running module against 192.168.123.13
[+] Successfully bound to the LDAP server!
[*] Discovering base DN automatically
[*] 192.168.123.13:389 Getting root DSE
dn:
namingcontexts: DC=adf3,DC=local
namingcontexts: CN=Configuration,DC=adf3,DC=local
namingcontexts: CN=Schema,CN=Configuration,DC=adf3,DC=local
...etc etc...
supportedldapversion: 2
supportedsaslmechanisms: GSSAPI
supportedsaslmechanisms: GSS-SPNEGO
supportedsaslmechanisms: EXTERNAL
supportedsaslmechanisms: DIGEST-MD5
[+] 192.168.123.13:389 Discovered base DN: DC=adf3,DC=local
[-] Auxiliary aborted due to failure: unknown: An LDAP operational error occurred on (|(|(|(objectClass=organizationalPerson)(sAMAccountType=805306368))(objectcategory=user))(objectClass=user)). It is likely the client requires authorization! The error was: 000004DC: LdapErr: DSID-0C0909AF, comment: In order to perform this operation a successful bind must be completed on the connection., data 0, v3839
[*] Auxiliary module execution completed
I don't believe that's related, but thought it might be easier to mention that as part of the same issue
This also seems to spit out errors after running against a DC without auth:
msf6 auxiliary(gather/ldap_query) > run verbose=true rhosts=192.168.123.13 [*] Running module against 192.168.123.13 [+] Successfully bound to the LDAP server! [*] Discovering base DN automatically [*] 192.168.123.13:389 Getting root DSE dn: namingcontexts: DC=adf3,DC=local namingcontexts: CN=Configuration,DC=adf3,DC=local namingcontexts: CN=Schema,CN=Configuration,DC=adf3,DC=local ...etc etc... supportedldapversion: 2 supportedsaslmechanisms: GSSAPI supportedsaslmechanisms: GSS-SPNEGO supportedsaslmechanisms: EXTERNAL supportedsaslmechanisms: DIGEST-MD5 [+] 192.168.123.13:389 Discovered base DN: DC=adf3,DC=local [-] Auxiliary aborted due to failure: unknown: An LDAP operational error occurred on (|(|(|(objectClass=organizationalPerson)(sAMAccountType=805306368))(objectcategory=user))(objectClass=user)). It is likely the client requires authorization! The error was: 000004DC: LdapErr: DSID-0C0909AF, comment: In order to perform this operation a successful bind must be completed on the connection., data 0, v3839 [*] Auxiliary module execution completed
I don't believe that's related, but thought it might be easier to mention that as part of the same issue
This seems like expected behavior, the error message is stating that this operation requires authentication to perform.
After some further poking around I see line 269 of ldap.rb aka https://github.com/rapid7/metasploit-framework/blob/feature-kerberos-authentication/lib/msf/core/exploit/remote/ldap.rb#L269 seems to be part of the issue as it will naturally assume that the first entry is the valid DN unless you supply a DN yourself.
Looking at when this was last edited it was 3 years back by Will Vu so I can only presume this may be been a quick throw up and we didn't want to spend the time back then to properly validate the data and see that the first DN=
part of the string is also equal to the domain name we are after.
Well bummer, problem is if we implement that it means redefining the function and then whilst we can update it mostly, we would still break modules/auxiliary/gather/vmware_vcenter_vmdir_ldap.rb
and modules/auxiliary/admin/ldap/rbcd.rb
, which presently don't have a DOMAIN definition defined.
I think this just requires a code change here in the ldap module:
https://github.com/rapid7/metasploit-framework/blob/8368accd5592392a6d8449490f222f4b269e3a3c/modules/auxiliary/gather/ldap_query.rb#L274
Similar to the workaround that I implemented
diff --git a/modules/auxiliary/gather/ldap_query.rb b/modules/auxiliary/gather/ldap_query.rb
index 5e3e4a76e9..49312592fe 100644
--- a/modules/auxiliary/gather/ldap_query.rb
+++ b/modules/auxiliary/gather/ldap_query.rb
@@ -271,7 +271,9 @@ class MetasploitModule < Msf::Auxiliary
end
filter += ')'
attributes = ['LDAPDisplayName', 'isSingleValued', 'oMSyntax', 'attributeSyntax']
- attributes_data = perform_ldap_query(ldap, filter, attributes, base: ['CN=Schema,CN=Configuration', @base_dn].join(','))
+ # attributes_data = perform_ldap_query(ldap, filter, attributes, base: ['CN=Schema,CN=Configuration', @base_dn].join(','))
+ # With the @base_dn set this becomes 'CN=Schema,CN=Configuration,DC=za,DC=tryhackme,DC=loc', but we need to query with 'CN=Schema,CN=Configuration,DC=tryhackme,DC=loc'
+ attributes_data = perform_ldap_query(ldap, filter, attributes, base: ['CN=Schema,CN=Configuration', 'DC=tryhackme,DC=loc'].join(','))
entry_list = {}
for entry in attributes_data do
@@ -294,6 +296,7 @@ class MetasploitModule < Msf::Auxiliary
entries.each do |entry|
attributes.merge!(entry.to_h)
end
+
attribute_properties = query_attributes_data(ldap, attributes)
entries.each do |entry|
@adfoster-r7 I have updated the code mentioned at https://github.com/rapid7/metasploit-framework/blob/feature-kerberos-authentication/lib/msf/core/exploit/remote/ldap.rb#L269 so that in theory it should now grab the right base DN, might need some more tweaking though but here is my initial attempt:
# NOTE: Find the first entry that starts with `DC=` as this will likely be the base DN.
naming_contexts.select! {|context| context =~ /^(DC=[A-Za-z0-9-]+,?)+$/}
naming_contexts.reject! {|context| context =~ /(Configuration)|(Schema)|(ForestDnsZones)/}
if naming_contexts.blank?
print_error("#{peer} A base DN matching the expected format could not be found!")
return
end
base_dn = naming_contexts[0]
Alright nm finally got what you were saying. Seems bug is in part due to what I mentioned above and in part due to the line you were pointing out. That should in theory work with my fix however the issue is that the schema information is contained in the parent DN, not in the child DN. Therefore we end up trying to get the current base DN and apply that to the query, when in reality we have to always query with the DN of the parent DC in order to retrieve such information.
Image below showcases this as the earlier query works but when we go to retrieve the schema information it fails since there is no CN=Configuration
CN under DC=CHILD,DC=daforest,DC=com
; it only exists under DC=daforest,DC=com
.
I have updated the code mentioned at https://github.com/rapid7/metasploit-framework/blob/feature-kerberos-authentication/lib/msf/core/exploit/remote/ldap.rb#L269
Just a heads up to use perma-links for future travellers, you can press y
on your keyboard for a shortcut to generate a URL such as https://github.com/rapid7/metasploit-framework/blob/d356b34422cd2e5a374d117cc44c4b62ee717fc0/lib/msf/core/exploit/remote/ldap.rb#L269
which Github's UI will also automatically show the code for:
https://github.com/rapid7/metasploit-framework/blob/d356b34422cd2e5a374d117cc44c4b62ee717fc0/lib/msf/core/exploit/remote/ldap.rb#L269
I have updated the code mentioned at https://github.com/rapid7/metasploit-framework/blob/feature-kerberos-authentication/lib/msf/core/exploit/remote/ldap.rb#L269
Just a heads up to use perma-links for future travellers, you can press
y
on your keyboard for a shortcut to generate a URL such ashttps://github.com/rapid7/metasploit-framework/blob/d356b34422cd2e5a374d117cc44c4b62ee717fc0/lib/msf/core/exploit/remote/ldap.rb#L269
which Github's UI will also automatically show the code for:https://github.com/rapid7/metasploit-framework/blob/d356b34422cd2e5a374d117cc44c4b62ee717fc0/lib/msf/core/exploit/remote/ldap.rb#L269
Weird I don't seem to get anything when pressing Y, and CTRL+Y doesn't come up with anything either. Wonder if the bindings are different on Mac vs Windows?
Should be the same across browsers:
- https://docs.github.com/en/repositories/working-with-files/using-files/getting-permanent-links-to-files
- https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-a-permanent-link-to-a-code-snippet
Oh I didn't see that you had to do this when viewing the file within GitHub itself. Thought you could do this when pasting the link into the chat as some shortcut, which would be nice, but looks like that isn't supported.
So Net::LDAP doesn't seem to like handling redirects too well so thats another bug that we might have to address....not sure how to fix that at the moment since it seems to involve yeilding to a block we provide but I'm not sure how to formulate that right now.
In the meantime, here is a solution that seems to fix the issue being mentioned with a new function I created:
def find_root_dn(ldap)
filter = '(objectClass=*)'
attributes = ['instanceType']
@root_dn = @base_dn
loop do
attributes_data = perform_ldap_query(ldap, filter, attributes, base: @root_dn)
for entry in attributes_data do
int_instance_type = entry[:instancetype][0].to_i
# Check if this is a root of a naming context (NC).
if (int_instance_type & IT_NC_HEAD)
# Check if this has a naming context above this one
if (int_instance_type & IT_NC_ABOVE)
# If it does then set the @root_dn value to that and try again to see if there
# are entries higher in the tree.
parts = @root_dn.split(',')
if parts.length <= 2
print_good("Discovered root DN: #{@root_dn}")
return @root_dn
end
@root_dn = parts[1..].join(',')
else
# We have found the root naming context (NC)
print_good("Discovered root DN: #{@root_dn}")
@root_dn
end
end
end
end
end
We then can update the line to be attributes_data = perform_ldap_query(ldap, filter, attributes, base: ['CN=Schema,CN=Configuration', @root_dn].join(','))
and this seems to solve our issue.
Oh and before I forget to mention that, I also added the constant definitions in as well:
# https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-drsr/5e821e19-c93c-4e61-9cf1-453d6bdfec56
# Is the root of a Naming Context (NC)
IT_NC_HEAD = 0x1
# The domain controller hosts the Naming Context above this one.
IT_NC_ABOVE = 0x8
My concerns atm is whether or not this is a robust enough solution though.
As a side note, the code above could use some optimization but when I tried to do that, that was when I ran into the issue with redirects so I have reverted the code for the time being. As its now 9 pm locally though think I'm going to call it a night and give optimization another crack tomorrow. Feel free to let me know if you find anything odd with the solution above in the meantime.
This should now be fixed with https://github.com/rapid7/metasploit-framework/pull/17532. Feel free to reopen this if this issue still occurs for you.