sprayhound icon indicating copy to clipboard operation
sprayhound copied to clipboard

Error while getting users

Open rodry7 opened this issue 1 year ago • 3 comments

Having this error while trying to perform an authenticated password spraying using this comand: sprayhound -p PASS -d domain.com -dc 10.10.10.10 -lu USER -lp 'PASSWORD' -t 1 -vv

[X] An error occurred while looking for users via LDAP
[X] Failed getting ldap credentials
[X] An error occurred while executing SprayHound
Traceback (most recent call last):
  File "/usr/local/bin/sprayhound", line 8, in <module>
    sys.exit(run())
             ^^^^^
  File "/usr/local/lib/python3.12/dist-packages/sprayhound/core.py", line 228, in run
    CLI().run()
  File "/usr/local/lib/python3.12/dist-packages/sprayhound/core.py", line 218, in run
    ).run()
      ^^^^^
  File "/usr/local/lib/python3.12/dist-packages/sprayhound/core.py", line 68, in run
    self.get_ldap_credentials(lower=self.lower, upper=self.upper)
  File "/usr/local/lib/python3.12/dist-packages/sprayhound/core.py", line 93, in get_ldap_credentials
    ret = self.ldap.get_users(self)
          ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/dist-packages/sprayhound/modules/ldapconnection.py", line 129, in get_users
    bad_password_count=0 if 'badPwdCount' not in entry['attributes'] else int(entry['attributes']['badPwdCount']),
                                                                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: int() argument must be a string, a bytes-like object or a real number, not 'list'

rodry7 avatar Feb 14 '25 22:02 rodry7

Hello!

This seems to be related to pull request #5, where the LDAP library changed. I have never seen it return a list during testing for the badbadPwdCount attribute, unlike with the previous library. I'll look further into the issue, and try to push a fix over the weekend.

In the meantime, could you share a bit more information about the setup where the failure occured ? Maybe the version of Windows used, and if the DC used was the primary domain controller?

BabdCatha avatar Mar 12 '25 13:03 BabdCatha

Hi,

This was some time ago, this was a real environment with updated domain controllers and I don't remember exactly if this occur on the primary domain controller but I remember trying different domain controllers.

rodry7 avatar Mar 12 '25 22:03 rodry7

I think I may have an explanation:

According to Microsoft documentation, the badPwdCount attribute is not synchronized between domain controllers, but only forwarded by secondary Domain Controllers to the Domain Controller with the PDCe role.

This means that when using a DC that is not the PDCe, and the user has never entered a bad password when authenticating on this specific DC, the badPwdCount attribute will not be set. With the LDAP3 library, this means that the attribute is an empty list, causing the error that you saw.

I tried this using a similar setup, with two Domain Controllers. The account "misato.katsuragi" was the only one to ever enter a bad password when connecting to this DC, the other account have always entered the correct one:

Image

This is probably what the bad_password_count=0 if 'badPwdCount' not in entry in the previous version was checking for.

I will add a second check to verify that the attribute is not an empty list, and treat it as if it was 0 in this case.

BabdCatha avatar Mar 15 '25 14:03 BabdCatha

I solved it in that way.

--- a/sprayhound/modules/ldapconnection.py
+++ b/sprayhound/modules/ldapconnection.py
@@ -120,7 +120,53 @@
     def get_users(self, dispatcher, users=None, disabled=True):
         filters = ["(objectClass=User)"]
         if users:
             if len(users) == 1:
                 filters.append("(samAccountName={})".format(users[0].lower()))
             else:
                 filters.append("(|")
                 filters.append("".join("(samAccountName={})".format(user.lower()) for user in users))
                 filters.append(")")
         if not disabled:
             filters.append("(!(userAccountControl:1.2.840.113556.1.4.803:=2))")
 
         if len(filters) > 1:
             filters = '(&' + ''.join(filters) + ')'
         else:
             filters = filters[0]
 
         try:
             self.log.debug("Looking in {}".format(self.domain_dn))
             ldap_attributes = ['samAccountName', 'badPwdCount', 'msDS-ResultantPSO']
             self.log.debug("Users will be retrieved using paging")
             res = self.get_paged_users(filters, ldap_attributes)
 
-            results = [
-                Credential(
-                    samaccountname=entry['attributes']['sAMAccountName'],
-                    bad_password_count=0 if 'badPwdCount' not in entry['attributes'] else int(entry['attributes']['badPwdCount']),
-                    threshold=self.domain_threshold if entry['dn'] not in self.granular_threshold else self.granular_threshold[entry['dn']],
-                    pso=True if 'msDS-ResultantPSO' in entry['attributes'] and isinstance(entry['attributes']['msDS-ResultantPSO'], str) and entry['attributes']['msDS-ResultantPSO'].upper().startswith('CN=') else False
-                ) for entry in res if isinstance(entry, dict) and 'attributes' in entry and entry['attributes']['sAMAccountName'][-1] != '$'
-            ]
+            results = []
+            for entry in res:
+                if not isinstance(entry, dict) or 'attributes' not in entry:
+                    continue
+                if entry['attributes']['sAMAccountName'][-1] == '$':
+                    continue
+
+                bad_password_attr = entry['attributes'].get('badPwdCount', 0)
+                if isinstance(bad_password_attr, list):
+                    bad_password_attr = bad_password_attr[0] if bad_password_attr else 0
+                bad_password_count = int(bad_password_attr) if bad_password_attr not in [None, ''] else 0
+
+                results.append(
+                    Credential(
+                        samaccountname=entry['attributes']['sAMAccountName'],
+                        bad_password_count=bad_password_count,
+                        threshold=self.domain_threshold if entry['dn'] not in self.granular_threshold else self.granular_threshold[entry['dn']],
+                        pso=True if 'msDS-ResultantPSO' in entry['attributes']
+                             and isinstance(entry['attributes']['msDS-ResultantPSO'], str)
+                             and entry['attributes']['msDS-ResultantPSO'].upper().startswith('CN=') else False
+                    )
+                )
+
+            dispatcher.credentials = results
+            return ERROR_SUCCESS
+        except Exception as e:
+            self.log.error("An error occurred while looking for users via LDAP")
+            raise

kalinathalie avatar Jan 16 '26 17:01 kalinathalie