1- import json
2- import re
31from impacket .ldap import ldap , ldaptypes , ldapasn1 as ldapasn1_impacket
42from impacket .ldap .ldap import LDAPSearchError
5- from impacket .ldap .ldaptypes import SR_SECURITY_DESCRIPTOR
63from ldap3 .protocol .microsoft import security_descriptor_control
74from nxc .parsers .ldap_results import parse_result_attributes
85
@@ -25,7 +22,8 @@ class NXCModule:
2522 multiple_hosts = True
2623
2724 def __init__ (self ):
28- self .sccm_sites = []
25+ self .sccm_site_servers = [] # List of dns host names of the SCCM site servers
26+ self .sccm_sites = {}
2927 self .base_dn = ""
3028
3129 def options (self , context , module_options ):
@@ -40,12 +38,10 @@ def on_login(self, context, connection):
4038 self .base_dn = connection .ldapConnection ._baseDN if not self .base_dn else self .base_dn
4139 self .sc = ldap .SimplePagedResultsControl ()
4240
43- search_filter = f"(distinguishedName=CN=System Management,CN=System,{ self .base_dn } )"
44- controls = security_descriptor_control (sdflags = 0x04 )
45- context .log .display (f"Starting LDAP search with search filter '{ search_filter } '" )
46-
4741 try :
48-
42+ search_filter = f"(distinguishedName=CN=System Management,CN=System,{ self .base_dn } )"
43+ controls = security_descriptor_control (sdflags = 0x04 )
44+ context .log .display (f"Looking for the SCCM container with filter: '{ search_filter } '" )
4945 result = connection .ldapConnection .search (
5046 searchFilter = search_filter ,
5147 attributes = ["nTSecurityDescriptor" ],
@@ -55,14 +51,76 @@ def on_login(self, context, connection):
5551 )
5652 for item in result :
5753 if isinstance (item , ldapasn1_impacket .SearchResultEntry ):
58- raw_sec_descriptor = str (item [1 ][0 ][1 ][0 ]).encode ("latin-1" )
59- principal_security_descriptor = ldaptypes .SR_SECURITY_DESCRIPTOR (data = raw_sec_descriptor )
60- context .log .highlight (f"Found SCCM object: { item [0 ]} " )
61- self .parse_dacl (principal_security_descriptor ["Dacl" ])
62- self .context .log .highlight (f"Found sccm_sites: { self .sccm_sites } " )
54+ self .context .log .success (f"Found SCCM object: { item [0 ]} " )
55+ self .get_site_servers (item )
56+ self .get_sites ()
57+ self .get_management_points ()
58+
59+ self .context .log .success ("Site Servers:" )
60+ for site in self .sccm_site_servers :
61+ ip = self .connection .resolver (site )
62+ self .context .log .highlight (f"{ site } - { ip ['host' ] if ip else 'unknown' } " )
63+ self .context .log .success ("SCCM Sites:" )
64+ for site in self .sccm_sites :
65+ self .context .log .highlight (f"{ self .sccm_sites [site ]['cn' ]} " )
66+ self .context .log .highlight (f" Site Code: { site .rjust (14 )} " )
67+ self .context .log .highlight (f" Assignment Site Code: { self .sccm_sites [site ]['AssignmentSiteCode' ].rjust (3 )} " )
68+ self .context .log .highlight (" Management Points:" )
69+ for mp in self .sccm_sites [site ]["ManagementPoints" ]:
70+ self .context .log .highlight (f"\t CN:{ ' ' :<12} { mp ['cn' ]} " )
71+ self .context .log .highlight (f"\t DNS Hostname:{ ' ' :<2} { mp ['dNSHostName' ]} " )
72+ self .context .log .highlight (f"\t IP Address:{ ' ' :<4} { mp ['IPAddress' ]} " )
73+ self .context .log .highlight (f"\t Default MP:{ ' ' :<4} { mp ['mSSMSDefaultMP' ]} " )
74+ self .context .log .highlight ("" )
6375
6476 except LDAPSearchError as e :
65- context .log .fail (f"Obtained unexpected exception: { e } " )
77+ context .log .fail (f"Got unexpected exception: { e } " )
78+
79+ def get_management_points (self ):
80+ """Searches for all SCCM management points in the Active Directory and maps them to their SCCM site."""
81+ try :
82+ response = self .connection .ldapConnection .search (
83+ searchFilter = "(objectClass=mSSMSManagementPoint)" ,
84+ attributes = "*" ,
85+ )
86+ response_parsed = parse_result_attributes (response )
87+ self .context .log .success ("SCCM Management Points:" )
88+ for mp in response_parsed :
89+ ip = self .connection .resolver (mp ["dNSHostName" ])
90+ self .sccm_sites [mp ["mSSMSSiteCode" ]]["ManagementPoints" ].append ({
91+ "cn" : mp ["cn" ],
92+ "dNSHostName" : mp ["dNSHostName" ],
93+ "IPAddress" : ip if ip else "-" ,
94+ "mSSMSDefaultMP" : mp ["mSSMSDefaultMP" ],
95+ })
96+
97+ except LDAPSearchError as e :
98+ self .context .log .error (f"Error searching for management points: { e } " )
99+
100+ def get_sites (self ):
101+ """Searches for all SCCM sites in the Active Directory."""
102+ try :
103+ response = self .connection .ldapConnection .search (
104+ searchFilter = "(objectClass=mSSMSSite)" ,
105+ attributes = ["cn" , "mSSMSSiteCode" , "mSSMSAssignmentSiteCode" ],
106+ )
107+ response_parsed = parse_result_attributes (response )
108+ for site in response_parsed :
109+ self .sccm_sites [site ["mSSMSSiteCode" ]] = {
110+ "cn" : site ["cn" ],
111+ "AssignmentSiteCode" : site ["mSSMSAssignmentSiteCode" ],
112+ "ManagementPoints" : []
113+ }
114+
115+ except LDAPSearchError as e :
116+ self .context .log .error (f"Error searching for sites: { e } " )
117+
118+ def get_site_servers (self , item ):
119+ """Extracts the site servers from the SCCM object."""
120+ raw_sec_descriptor = str (item [1 ][0 ][1 ][0 ]).encode ("latin-1" )
121+ principal_security_descriptor = ldaptypes .SR_SECURITY_DESCRIPTOR (data = raw_sec_descriptor )
122+ self .parse_dacl (principal_security_descriptor ["Dacl" ])
123+ self .sccm_site_servers = set (self .sccm_site_servers ) # Make list unique
66124
67125 def parse_dacl (self , dacl ):
68126 """Parses a DACL and extracts the dns host names with full control over the SCCM object."""
@@ -97,7 +155,7 @@ def resolve_SID(self, sid):
97155
98156 if int (parsed_result ["sAMAccountType" ]) == SAM_MACHINE_ACCOUNT :
99157 self .context .log .debug (f"Found object with full control over SCCM object. SID: { sid } , dns_hostname: { parsed_result ['dNSHostName' ]} " )
100- self .sccm_sites .append (parsed_result ["dNSHostName" ])
158+ self .sccm_site_servers .append (parsed_result ["dNSHostName" ])
101159 elif int (parsed_result ["sAMAccountType" ]) == SAM_GROUP_OBJECT :
102160 if isinstance (parsed_result ["member" ], list ):
103161 for member in parsed_result ["member" ]:
@@ -115,19 +173,11 @@ def resolve_SID(self, sid):
115173
116174 def dn_to_sid (self , dn ) -> str :
117175 """Tries to resolve a DN to a SID."""
118- try :
119- result = self .connection .ldapConnection .search (
120- searchBase = self .base_dn ,
121- searchFilter = f"(distinguishedName={ dn } )" ,
122- attributes = ["sAMAccountName" , "objectSid" ],
123- )
124- parsed_result = parse_result_attributes (result )[0 ]
125- self .context .log .highlight (f"Found object for DN { dn } : { parsed_result [0 ]} " )
126- if not parsed_result :
127- return ""
128- else :
129- parsed_result = parsed_result [0 ]
130- return parsed_result ["objectSid" ]
131- except Exception as e :
132- self .context .log .debug (f"DN not found in LDAP: { dn } , { e } " )
133- return ""
176+ result = self .connection .ldapConnection .search (
177+ searchBase = self .base_dn ,
178+ searchFilter = f"(distinguishedName={ dn } )" ,
179+ attributes = ["sAMAccountName" , "objectSid" ],
180+ )
181+
182+ sid_raw = bytes (result [0 ][1 ][0 ][1 ].components [0 ])
183+ return ldaptypes .LDAP_SID (data = sid_raw ).formatCanonical ()
0 commit comments