From d7aa77eacaf950793b64b7c1b9897a283e50b0c2 Mon Sep 17 00:00:00 2001 From: pba Date: Fri, 2 Jun 2023 12:59:52 +0200 Subject: [PATCH 1/5] Added support for sMSAs in get-adserviceaccount --- pywerview/functions/net.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/pywerview/functions/net.py b/pywerview/functions/net.py index a4be1e5..febd694 100644 --- a/pywerview/functions/net.py +++ b/pywerview/functions/net.py @@ -52,6 +52,7 @@ def get_adobject(self, queried_domain=str(), queried_sid=str(), def get_adserviceaccount(self, queried_domain=str(), queried_sid=str(), queried_name=str(), queried_sam_account_name=str(), ads_path=str(), resolve_sids=False): + # First, searching for gMSAs filter_objectclass = '(ObjectClass=msDS-GroupManagedServiceAccount)' attributes = ['samaccountname', 'distinguishedname', 'objectsid', 'description', 'msds-managedpassword', 'msds-groupmsamembership', 'useraccountcontrol'] @@ -71,6 +72,21 @@ def get_adserviceaccount(self, queried_domain=str(), queried_sid=str(), adserviceaccounts = self._ldap_search(object_filter, adobj.GMSAAccount, attributes=attributes) + # Now searching for sMSAs + filter_objectclass = '(ObjectClass=msDS-ManagedServiceAccount)' + attributes = ['samaccountname', 'distinguishedname', 'objectsid', 'description', + 'msds-hostserviceaccountbl', 'useraccountcontrol'] + for attr_desc, attr_value in (('objectSid', queried_sid), ('name', escape_filter_chars(queried_name)), + ('samAccountName', escape_filter_chars(queried_sam_account_name))): + if attr_value: + object_filter = '(&({}={}){})'.format(attr_desc, attr_value, filter_objectclass) + break + else: + object_filter = '(&(name=*){})'.format(filter_objectclass) + + smsas = self._ldap_search(object_filter, adobj.GMSAAccount, attributes=attributes) + + # In this loop, we resolve SID (if true) and we populate 'enabled' attribute for i, adserviceaccount in enumerate(adserviceaccounts): if resolve_sids: @@ -86,6 +102,9 @@ def get_adserviceaccount(self, queried_domain=str(), queried_sid=str(), adserviceaccounts[i].add_attributes({'msds-groupmsamembership': results}) adserviceaccounts[i].add_attributes({'Enabled': 'ACCOUNTDISABLE' not in adserviceaccount.useraccountcontrol}) adserviceaccounts[i]._attributes_dict.pop('useraccountcontrol') + + # Finally, adding the sMSAs to the gMSA list + adserviceaccounts += smsas return adserviceaccounts @LDAPRPCRequester._ldap_connection_init From 89cd892afe755b89378b8a710e60c6ab0c6eeedd Mon Sep 17 00:00:00 2001 From: pba Date: Fri, 2 Jun 2023 13:32:35 +0200 Subject: [PATCH 2/5] Handling --resolve-sids when sMSAs are present --- pywerview/functions/net.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/pywerview/functions/net.py b/pywerview/functions/net.py index febd694..c1bdc08 100644 --- a/pywerview/functions/net.py +++ b/pywerview/functions/net.py @@ -86,25 +86,28 @@ def get_adserviceaccount(self, queried_domain=str(), queried_sid=str(), smsas = self._ldap_search(object_filter, adobj.GMSAAccount, attributes=attributes) + # Finally, adding the sMSAs to the gMSA list + adserviceaccounts += smsas # In this loop, we resolve SID (if true) and we populate 'enabled' attribute for i, adserviceaccount in enumerate(adserviceaccounts): if resolve_sids: results = list() - for sid in getattr(adserviceaccount, 'msds-groupmsamembership'): - try: - resolved_sid = self.get_adobject(queried_sid=sid, queried_domain=self._queried_domain, - attributes=['distinguishedname'])[0].distinguishedname - except IndexError: - self._logger.warning('We did not manage to resolve this SID ({}) against the DC'.format(sid)) - resolved_sid = sid - results.append(resolved_sid) - adserviceaccounts[i].add_attributes({'msds-groupmsamembership': results}) + try: + for sid in getattr(adserviceaccount, 'msds-groupmsamembership'): + try: + resolved_sid = self.get_adobject(queried_sid=sid, queried_domain=self._queried_domain, + attributes=['distinguishedname'])[0].distinguishedname + except IndexError: + self._logger.warning('We did not manage to resolve this SID ({}) against the DC'.format(sid)) + resolved_sid = sid + results.append(resolved_sid) + adserviceaccounts[i].add_attributes({'msds-groupmsamembership': results}) + except AttributeError: + pass adserviceaccounts[i].add_attributes({'Enabled': 'ACCOUNTDISABLE' not in adserviceaccount.useraccountcontrol}) adserviceaccounts[i]._attributes_dict.pop('useraccountcontrol') - # Finally, adding the sMSAs to the gMSA list - adserviceaccounts += smsas return adserviceaccounts @LDAPRPCRequester._ldap_connection_init From e2f59585dc38622ae85e83a765d6cb571941f862 Mon Sep 17 00:00:00 2001 From: ThePirateWhoSmellsOfSunflowers Date: Tue, 13 Jun 2023 14:29:25 +0200 Subject: [PATCH 3/5] split get-adserviceaccount in two functions --- pywerview/cli/helpers.py | 17 ++++++++-- pywerview/cli/main.py | 34 ++++++++++++++------ pywerview/functions/net.py | 58 ++++++++++++++++++---------------- pywerview/objects/adobjects.py | 2 ++ 4 files changed, 73 insertions(+), 38 deletions(-) diff --git a/pywerview/cli/helpers.py b/pywerview/cli/helpers.py index 9ee7378..3cd40cd 100644 --- a/pywerview/cli/helpers.py +++ b/pywerview/cli/helpers.py @@ -36,7 +36,7 @@ def get_adobject(domain_controller, domain, user, password=str(), queried_sam_account_name=queried_sam_account_name, ads_path=ads_path, attributes=attributes, custom_filter=custom_filter) -def get_adserviceaccount(domain_controller, domain, user, password=str(), +def get_netgmsa(domain_controller, domain, user, password=str(), lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False, user_cert=str(), user_key=str(), queried_domain=str(), queried_sid=str(), queried_name=str(), @@ -44,11 +44,24 @@ def get_adserviceaccount(domain_controller, domain, user, password=str(), requester = NetRequester(domain_controller, domain, user, password, lmhash, nthash, do_kerberos, do_tls, user_cert, user_key) - return requester.get_adserviceaccount(queried_domain=queried_domain, + return requester.get_netgmsa(queried_domain=queried_domain, queried_sid=queried_sid, queried_name=queried_name, queried_sam_account_name=queried_sam_account_name, ads_path=ads_path, resolve_sids=resolve_sids) +def get_netsmsa(domain_controller, domain, user, password=str(), + lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False, + user_cert=str(), user_key=str(), + queried_domain=str(), queried_sid=str(), queried_name=str(), + queried_sam_account_name=str(), ads_path=str()): + requester = NetRequester(domain_controller, domain, user, password, + lmhash, nthash, do_kerberos, do_tls, + user_cert, user_key) + return requester.get_netsmsa(queried_domain=queried_domain, + queried_sid=queried_sid, queried_name=queried_name, + queried_sam_account_name=queried_sam_account_name, + ads_path=ads_path) + def get_objectacl(domain_controller, domain, user, password=str(), lmhash=str(), nthash=str(), do_kerberos=False, do_tls=False, queried_domain=str(), queried_sid=str(), queried_name=str(), diff --git a/pywerview/cli/main.py b/pywerview/cli/main.py index fc8433f..6ad7670 100644 --- a/pywerview/cli/main.py +++ b/pywerview/cli/main.py @@ -129,24 +129,40 @@ def main(): default=[], help='Object attributes to return') get_adobject_parser.set_defaults(func=get_adobject) - # Parser for the get-adserviceaccount command - get_adserviceaccount_parser = subparsers.add_parser('get-adserviceaccount', help='Returns a list of all the '\ + # Parser for the get-netgmsa command + get_netgmsa_parser = subparsers.add_parser('get-netgmsa', help='Returns a list of all the '\ 'gMSA of the specified domain. To retrieve passwords, you need a privileged account and '\ 'a TLS connection to the LDAP server (use the --tls switch).', parents=[ad_parser, logging_parser, json_output_parser, certificate_parser]) - get_adserviceaccount_parser.add_argument('--sid', dest='queried_sid', + get_netgmsa_parser.add_argument('--sid', dest='queried_sid', help='SID to query (wildcards accepted)') - get_adserviceaccount_parser.add_argument('--sam-account-name', dest='queried_sam_account_name', + get_netgmsa_parser.add_argument('--sam-account-name', dest='queried_sam_account_name', help='samAccountName to query (wildcards accepted)') - get_adserviceaccount_parser.add_argument('--name', dest='queried_name', + get_netgmsa_parser.add_argument('--name', dest='queried_name', help='Name to query (wildcards accepted)') - get_adserviceaccount_parser.add_argument('-d', '--domain', dest='queried_domain', + get_netgmsa_parser.add_argument('-d', '--domain', dest='queried_domain', help='Domain to query') - get_adserviceaccount_parser.add_argument('-a', '--ads-path', + get_netgmsa_parser.add_argument('-a', '--ads-path', help='Additional ADS path') - get_adserviceaccount_parser.add_argument('--resolve-sids', dest='resolve_sids', + get_netgmsa_parser.add_argument('--resolve-sids', dest='resolve_sids', action='store_true', help='Resolve SIDs when querying PrincipalsAllowedToRetrieveManagedPassword') - get_adserviceaccount_parser.set_defaults(func=get_adserviceaccount) + get_netgmsa_parser.set_defaults(func=get_adserviceaccount) + + # Parser for the get-netsmsa command + get_netsmsa_parser = subparsers.add_parser('get-adserviceaccount', help='Returns a list of all the '\ + 'sMSA of the specified domain.', + parents=[ad_parser, logging_parser, json_output_parser, certificate_parser]) + get_netsmsa_parser.add_argument('--sid', dest='queried_sid', + help='SID to query (wildcards accepted)') + get_netsmsa_parser.add_argument('--sam-account-name', dest='queried_sam_account_name', + help='samAccountName to query (wildcards accepted)') + get_netsmsa_parser.add_argument('--name', dest='queried_name', + help='Name to query (wildcards accepted)') + get_netsmsa_parser.add_argument('-d', '--domain', dest='queried_domain', + help='Domain to query') + get_netsmsa_parser.add_argument('-a', '--ads-path', + help='Additional ADS path') + get_netsmsa_parser.set_defaults(func=get_adserviceaccount) # Parser for the get-objectacl command get_objectacl_parser = subparsers.add_parser('get-objectacl', help='Takes a domain SID, '\ diff --git a/pywerview/functions/net.py b/pywerview/functions/net.py index c1bdc08..ec605fe 100644 --- a/pywerview/functions/net.py +++ b/pywerview/functions/net.py @@ -49,10 +49,9 @@ def get_adobject(self, queried_domain=str(), queried_sid=str(), return self._ldap_search(object_filter, adobj.ADObject, attributes=attributes) @LDAPRPCRequester._ldap_connection_init - def get_adserviceaccount(self, queried_domain=str(), queried_sid=str(), + def get_netgmsa(self, queried_domain=str(), queried_sid=str(), queried_name=str(), queried_sam_account_name=str(), ads_path=str(), resolve_sids=False): - # First, searching for gMSAs filter_objectclass = '(ObjectClass=msDS-GroupManagedServiceAccount)' attributes = ['samaccountname', 'distinguishedname', 'objectsid', 'description', 'msds-managedpassword', 'msds-groupmsamembership', 'useraccountcontrol'] @@ -70,9 +69,31 @@ def get_adserviceaccount(self, queried_domain=str(), queried_sid=str(), else: object_filter = '(&(name=*){})'.format(filter_objectclass) - adserviceaccounts = self._ldap_search(object_filter, adobj.GMSAAccount, attributes=attributes) + gmsa = self._ldap_search(object_filter, adobj.GMSAAccount, attributes=attributes) + + # In this loop, we resolve SID (if true) and we populate 'enabled' attribute + for i, adserviceaccount in enumerate(gmsa): + if resolve_sids: + results = list() + for sid in getattr(adserviceaccount, 'msds-groupmsamembership'): + try: + resolved_sid = self.get_adobject(queried_sid=sid, queried_domain=self._queried_domain, + attributes=['distinguishedname'])[0].distinguishedname + except IndexError: + self._logger.warning('We did not manage to resolve this SID ({}) against the DC'.format(sid)) + resolved_sid = sid + results.append(resolved_sid) + gmsa[i].add_attributes({'msds-groupmsamembership': results}) + gmsa[i].add_attributes({'Enabled': 'ACCOUNTDISABLE' not in adserviceaccount.useraccountcontrol}) + gmsa[i]._attributes_dict.pop('useraccountcontrol') + + return gmsa + + @LDAPRPCRequester._ldap_connection_init + def get_netsmsa(self, queried_domain=str(), queried_sid=str(), + queried_name=str(), queried_sam_account_name=str(), + ads_path=str()): - # Now searching for sMSAs filter_objectclass = '(ObjectClass=msDS-ManagedServiceAccount)' attributes = ['samaccountname', 'distinguishedname', 'objectsid', 'description', 'msds-hostserviceaccountbl', 'useraccountcontrol'] @@ -84,31 +105,14 @@ def get_adserviceaccount(self, queried_domain=str(), queried_sid=str(), else: object_filter = '(&(name=*){})'.format(filter_objectclass) - smsas = self._ldap_search(object_filter, adobj.GMSAAccount, attributes=attributes) - - # Finally, adding the sMSAs to the gMSA list - adserviceaccounts += smsas + smsas = self._ldap_search(object_filter, adobj.SMSAAccount, attributes=attributes) - # In this loop, we resolve SID (if true) and we populate 'enabled' attribute - for i, adserviceaccount in enumerate(adserviceaccounts): - if resolve_sids: - results = list() - try: - for sid in getattr(adserviceaccount, 'msds-groupmsamembership'): - try: - resolved_sid = self.get_adobject(queried_sid=sid, queried_domain=self._queried_domain, - attributes=['distinguishedname'])[0].distinguishedname - except IndexError: - self._logger.warning('We did not manage to resolve this SID ({}) against the DC'.format(sid)) - resolved_sid = sid - results.append(resolved_sid) - adserviceaccounts[i].add_attributes({'msds-groupmsamembership': results}) - except AttributeError: - pass - adserviceaccounts[i].add_attributes({'Enabled': 'ACCOUNTDISABLE' not in adserviceaccount.useraccountcontrol}) - adserviceaccounts[i]._attributes_dict.pop('useraccountcontrol') + # In this loop, we populate 'enabled' attribute + for i, adserviceaccount in enumerate(smsas): + smsas[i].add_attributes({'Enabled': 'ACCOUNTDISABLE' not in adserviceaccount.useraccountcontrol}) + smsas[i]._attributes_dict.pop('useraccountcontrol') - return adserviceaccounts + return smsas @LDAPRPCRequester._ldap_connection_init def get_objectacl(self, queried_domain=str(), queried_sid=str(), diff --git a/pywerview/objects/adobjects.py b/pywerview/objects/adobjects.py index e8a9d28..0362334 100644 --- a/pywerview/objects/adobjects.py +++ b/pywerview/objects/adobjects.py @@ -185,3 +185,5 @@ class GPOLocation(ADObject): class GMSAAccount(ADObject): pass +class SMSAAccount(ADObject): + pass From 0e9155ca21c0acdbc3d6bbd45203726a6bdb4dd7 Mon Sep 17 00:00:00 2001 From: ThePirateWhoSmellsOfSunflowers Date: Tue, 13 Jun 2023 21:31:14 +0200 Subject: [PATCH 4/5] fix wrong function name --- pywerview/cli/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pywerview/cli/main.py b/pywerview/cli/main.py index 6ad7670..7c7a681 100644 --- a/pywerview/cli/main.py +++ b/pywerview/cli/main.py @@ -146,7 +146,7 @@ def main(): help='Additional ADS path') get_netgmsa_parser.add_argument('--resolve-sids', dest='resolve_sids', action='store_true', help='Resolve SIDs when querying PrincipalsAllowedToRetrieveManagedPassword') - get_netgmsa_parser.set_defaults(func=get_adserviceaccount) + get_netgmsa_parser.set_defaults(func=get_netgmsa) # Parser for the get-netsmsa command get_netsmsa_parser = subparsers.add_parser('get-adserviceaccount', help='Returns a list of all the '\ @@ -162,7 +162,7 @@ def main(): help='Domain to query') get_netsmsa_parser.add_argument('-a', '--ads-path', help='Additional ADS path') - get_netsmsa_parser.set_defaults(func=get_adserviceaccount) + get_netsmsa_parser.set_defaults(func=get_netsmsa) # Parser for the get-objectacl command get_objectacl_parser = subparsers.add_parser('get-objectacl', help='Takes a domain SID, '\ From 59068a789ccd59098e1b85219600c4dc60639e75 Mon Sep 17 00:00:00 2001 From: ThePirateWhoSmellsOfSunflowers Date: Tue, 13 Jun 2023 21:35:36 +0200 Subject: [PATCH 5/5] get-adserviceaccount -> ge-netsmsa --- pywerview/cli/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pywerview/cli/main.py b/pywerview/cli/main.py index 7c7a681..3ec15ef 100644 --- a/pywerview/cli/main.py +++ b/pywerview/cli/main.py @@ -149,7 +149,7 @@ def main(): get_netgmsa_parser.set_defaults(func=get_netgmsa) # Parser for the get-netsmsa command - get_netsmsa_parser = subparsers.add_parser('get-adserviceaccount', help='Returns a list of all the '\ + get_netsmsa_parser = subparsers.add_parser('get-netsmsa', help='Returns a list of all the '\ 'sMSA of the specified domain.', parents=[ad_parser, logging_parser, json_output_parser, certificate_parser]) get_netsmsa_parser.add_argument('--sid', dest='queried_sid',