metasploit-framework icon indicating copy to clipboard operation
metasploit-framework copied to clipboard

ldap_query fails on schema extraction when specifying base_dn as child DC

Open adfoster-r7 opened this issue 1 year ago • 13 comments

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
image

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

adfoster-r7 avatar Dec 31 '22 03:12 adfoster-r7

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

adfoster-r7 avatar Jan 13 '23 00:01 adfoster-r7

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.

gwillcox-r7 avatar Jan 19 '23 18:01 gwillcox-r7

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.

gwillcox-r7 avatar Jan 23 '23 23:01 gwillcox-r7

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.

gwillcox-r7 avatar Jan 23 '23 23:01 gwillcox-r7

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 avatar Jan 23 '23 23:01 adfoster-r7

@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]

gwillcox-r7 avatar Jan 24 '23 00:01 gwillcox-r7

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.

image

gwillcox-r7 avatar Jan 24 '23 00:01 gwillcox-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

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

adfoster-r7 avatar Jan 24 '23 00:01 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

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

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?

gwillcox-r7 avatar Jan 24 '23 00:01 gwillcox-r7

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

adfoster-r7 avatar Jan 24 '23 00:01 adfoster-r7

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.

gwillcox-r7 avatar Jan 24 '23 02:01 gwillcox-r7

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.

gwillcox-r7 avatar Jan 24 '23 02:01 gwillcox-r7

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.

gwillcox-r7 avatar Jan 24 '23 02:01 gwillcox-r7

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.

gwillcox-r7 avatar Feb 13 '23 21:02 gwillcox-r7