Skip to content

Commit cee469b

Browse files
committed
Created gpp_privileges module
1 parent 05ad3c6 commit cee469b

1 file changed

Lines changed: 295 additions & 0 deletions

File tree

nxc/modules/gpp_privileges.py

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
import ssl
2+
import ldap3
3+
import re
4+
from io import BytesIO
5+
6+
7+
class NXCModule:
8+
"""
9+
Module to retrieve privileges assigned via Group Policy Objects (GPOs) by parsing GptTmpl.inf files
10+
and resolving SIDs using LDAP.
11+
"""
12+
13+
name = "gpp_privileges"
14+
description = "Extracts privileges assigned via GPOs and resolves SIDs via LDAP."
15+
supported_protocols = ["smb"]
16+
opsec_safe = True
17+
multiple_hosts = True
18+
19+
WELL_KNOWN_SIDS = {
20+
"S-1-0": "Null Authority",
21+
"S-1-0-0": "Nobody",
22+
"S-1-1": "World Authority",
23+
"S-1-1-0": "Everyone",
24+
"S-1-2": "Local Authority",
25+
"S-1-2-0": "Local",
26+
"S-1-2-1": "Console Logon",
27+
"S-1-3": "Creator Authority",
28+
"S-1-3-0": "Creator Owner",
29+
"S-1-3-1": "Creator Group",
30+
"S-1-3-2": "Creator Owner Server",
31+
"S-1-3-3": "Creator Group Server",
32+
"S-1-3-4": "Owner Rights",
33+
"S-1-5-80-0": "All Services",
34+
"S-1-4": "Non-unique Authority",
35+
"S-1-5": "NT Authority",
36+
"S-1-5-1": "Dialup",
37+
"S-1-5-2": "Network",
38+
"S-1-5-3": "Batch",
39+
"S-1-5-4": "Interactive",
40+
"S-1-5-6": "Service",
41+
"S-1-5-7": "Anonymous",
42+
"S-1-5-8": "Proxy",
43+
"S-1-5-9": "Enterprise Domain Controllers",
44+
"S-1-5-10": "Principal Self",
45+
"S-1-5-11": "Authenticated Users",
46+
"S-1-5-12": "Restricted Code",
47+
"S-1-5-13": "Terminal Server Users",
48+
"S-1-5-14": "Remote Interactive Logon",
49+
"S-1-5-15": "This Organization",
50+
"S-1-5-17": "This Organization",
51+
"S-1-5-18": "Local System",
52+
"S-1-5-19": "NT Authority",
53+
"S-1-5-20": "NT Authority",
54+
"S-1-5-32-544": "Administrators",
55+
"S-1-5-32-545": "Users",
56+
"S-1-5-32-546": "Guests",
57+
"S-1-5-32-547": "Power Users",
58+
"S-1-5-32-548": "Account Operators",
59+
"S-1-5-32-549": "Server Operators",
60+
"S-1-5-32-550": "Print Operators",
61+
"S-1-5-32-551": "Backup Operators",
62+
"S-1-5-32-552": "Replicators",
63+
"S-1-5-64-10": "NTLM Authentication",
64+
"S-1-5-64-14": "SChannel Authentication",
65+
"S-1-5-64-21": "Digest Authority",
66+
"S-1-5-80": "NT Service",
67+
"S-1-5-83-0": "NT VIRTUAL MACHINE\\Virtual Machines",
68+
"S-1-16-0": "Untrusted Mandatory Level",
69+
"S-1-16-4096": "Low Mandatory Level",
70+
"S-1-16-8192": "Medium Mandatory Level",
71+
"S-1-16-8448": "Medium Plus Mandatory Level",
72+
"S-1-16-12288": "High Mandatory Level",
73+
"S-1-16-16384": "System Mandatory Level",
74+
"S-1-16-20480": "Protected Process Mandatory Level",
75+
"S-1-16-28672": "Secure Process Mandatory Level",
76+
"S-1-5-32-554": "BUILTIN\\Pre-Windows 2000 Compatible Access",
77+
"S-1-5-32-555": "BUILTIN\\Remote Desktop Users",
78+
"S-1-5-32-557": "BUILTIN\\Incoming Forest Trust Builders",
79+
"S-1-5-32-556": "BUILTIN\\Network Configuration Operators",
80+
"S-1-5-32-558": "BUILTIN\\Performance Monitor Users",
81+
"S-1-5-32-559": "BUILTIN\\Performance Log Users",
82+
"S-1-5-32-560": "BUILTIN\\Windows Authorization Access Group",
83+
"S-1-5-32-561": "BUILTIN\\Terminal Server License Servers",
84+
"S-1-5-32-562": "BUILTIN\\Distributed COM Users",
85+
"S-1-5-32-569": "BUILTIN\\Cryptographic Operators",
86+
"S-1-5-32-573": "BUILTIN\\Event Log Readers",
87+
"S-1-5-32-574": "BUILTIN\\Certificate Service DCOM Access",
88+
"S-1-5-32-575": "BUILTIN\\RDS Remote Access Servers",
89+
"S-1-5-32-576": "BUILTIN\\RDS Endpoint Servers",
90+
"S-1-5-32-577": "BUILTIN\\RDS Management Servers",
91+
"S-1-5-32-578": "BUILTIN\\Hyper-V Administrators",
92+
"S-1-5-32-579": "BUILTIN\\Access Control Assistance Operators",
93+
"S-1-5-32-580": "BUILTIN\\Remote Management Users",
94+
}
95+
96+
97+
def options(self, context, module_options):
98+
""" """
99+
pass
100+
101+
def options(self, context, module_options):
102+
"""
103+
Define module options.
104+
- no_ldap: If set to True, disables LDAP queries for resolving SIDs.
105+
"""
106+
self.no_ldap = module_options.get("NO_LDAP", False)
107+
108+
def on_login(self, context, connection):
109+
shares = connection.shares()
110+
for share in shares:
111+
if share["name"] == "SYSVOL" and "READ" in share["access"]:
112+
context.log.info("Found SYSVOL share")
113+
context.log.display("Searching for GptTmpl.inf files")
114+
115+
paths = connection.spider("SYSVOL", pattern=["GptTmpl.inf"])
116+
117+
if not paths:
118+
context.log.warning("No GptTmpl.inf files found in SYSVOL.")
119+
return
120+
121+
for path in paths:
122+
if "6AC1786C-016F-11D2-945F-00C04fB984F9" in path: # Default Domain Policy
123+
context.log.success(f"Found Default Domain Policy GptTmpl.inf: {path}")
124+
else:
125+
context.log.info(f"Found GptTmpl.inf: {path}")
126+
127+
buf = BytesIO()
128+
connection.conn.getFile("SYSVOL", path, buf.write)
129+
130+
try:
131+
content = buf.getvalue().decode("utf-16le")
132+
except UnicodeDecodeError as e:
133+
context.log.error(f"Failed to decode {path} as UTF-16LE: {e}")
134+
continue
135+
136+
privileges = self.extract_privileges(content)
137+
if privileges:
138+
ldap_connection = None
139+
if not self.no_ldap:
140+
ldap_connection = self.initialize_ldap_connection(context, connection)
141+
142+
context.log.success(f"Privileges extracted from {path}:")
143+
for privilege, sids in privileges.items():
144+
resolved_sids = [
145+
self.resolve_sid(context, sid, ldap_connection) for sid in sids
146+
]
147+
context.log.highlight(f"{privilege}: {', '.join(resolved_sids)}")
148+
149+
if ldap_connection:
150+
ldap_connection.unbind()
151+
152+
def extract_privileges(self, content):
153+
"""Parses the content of GptTmpl.inf to extract privilege rights."""
154+
privileges = {}
155+
in_priv_section = False
156+
157+
for line in content.splitlines():
158+
if line.strip() == "[Privilege Rights]":
159+
in_priv_section = True
160+
continue
161+
if in_priv_section and line.strip() == "":
162+
break
163+
if in_priv_section:
164+
match = re.match(r"^(.*?)\s*=\s*(.*)$", line)
165+
if match:
166+
privilege, sids = match.groups()
167+
privileges[privilege] = [sid.strip("*") for sid in sids.split(",")]
168+
169+
return privileges
170+
171+
def initialize_ldap_connection(self, context, connection):
172+
"""
173+
Initializes an LDAP connection using LDAP3 with LDAPS first, then falls back to plaintext LDAP if LDAPS fails.
174+
Attempts to retrieve the base DN from the Root DSE or derive it from the domain name.
175+
"""
176+
tls = ldap3.Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2, ciphers="ALL:@SECLEVEL=0")
177+
base_dn = None
178+
179+
try:
180+
ldap_server = ldap3.Server(connection.host, use_ssl=True, port=636, tls=tls)
181+
ldap_connection = ldap3.Connection(
182+
ldap_server,
183+
user=f"{connection.domain}\\{connection.username}",
184+
password=connection.password,
185+
authentication=ldap3.NTLM,
186+
raise_exceptions=True,
187+
)
188+
ldap_connection.bind()
189+
context.log.success("Connected to LDAP over SSL (LDAPS).")
190+
191+
try:
192+
ldap_connection.search(
193+
search_base="",
194+
search_filter="(objectClass=*)",
195+
search_scope=ldap3.BASE,
196+
attributes=["defaultNamingContext"],
197+
)
198+
if ldap_connection.entries:
199+
base_dn = ldap_connection.entries[0]["defaultNamingContext"].value
200+
context.log.success(f"Retrieved base DN over LDAPS: {base_dn}")
201+
else:
202+
context.log.warning("defaultNamingContext not found in Root DSE. Falling back to domain name derivation.")
203+
except Exception as e:
204+
context.log.warning(f"Failed to query Root DSE for defaultNamingContext over LDAPS: {e}")
205+
206+
if not base_dn:
207+
domain_parts = connection.domain.split(".")
208+
base_dn = ",".join([f"dc={part}" for part in domain_parts])
209+
context.log.info(f"Derived base DN: {base_dn}")
210+
211+
ldap_connection.base_dn = base_dn
212+
return ldap_connection
213+
214+
except Exception as ldaps_error:
215+
context.log.warning(f"LDAPS connection failed: {ldaps_error}")
216+
context.log.info("Falling back to plain LDAP...")
217+
218+
try:
219+
ldap_server = ldap3.Server(connection.host, use_ssl=False, port=389)
220+
ldap_connection = ldap3.Connection(
221+
ldap_server,
222+
user=f"{connection.domain}\\{connection.username}",
223+
password=connection.password,
224+
authentication=ldap3.NTLM,
225+
raise_exceptions=True,
226+
)
227+
ldap_connection.bind()
228+
context.log.info("Connected to LDAP successfully (plaintext).")
229+
230+
try:
231+
ldap_connection.search(
232+
search_base="",
233+
search_filter="(objectClass=*)",
234+
search_scope=ldap3.BASE,
235+
attributes=["defaultNamingContext"],
236+
)
237+
if ldap_connection.entries:
238+
base_dn = ldap_connection.entries[0]["defaultNamingContext"].value
239+
context.log.success(f"Retrieved base DN over plain LDAP: {base_dn}")
240+
else:
241+
context.log.warning("defaultNamingContext not found in Root DSE. Falling back to domain name derivation.")
242+
except Exception as e:
243+
context.log.warning(f"Failed to query Root DSE for defaultNamingContext over plain LDAP: {e}")
244+
245+
if not base_dn:
246+
domain_parts = connection.domain.split(".")
247+
base_dn = ",".join([f"dc={part}" for part in domain_parts])
248+
context.log.info(f"Derived base DN: {base_dn}")
249+
250+
ldap_connection.base_dn = base_dn
251+
return ldap_connection
252+
253+
except Exception as ldap_error:
254+
context.log.error(f"Failed to connect to LDAP: {ldap_error}")
255+
256+
return None
257+
258+
259+
260+
261+
def resolve_sid(self, context, sid, ldap_connection):
262+
"""
263+
Resolves a SID to a human-readable name using well-known mappings or LDAP queries.
264+
"""
265+
if sid in self.WELL_KNOWN_SIDS:
266+
return self.WELL_KNOWN_SIDS[sid]
267+
268+
if ldap_connection and ldap_connection.bound:
269+
try:
270+
base_dn = getattr(ldap_connection, "base_dn", None)
271+
if not base_dn:
272+
context.log.warning(f"No base DN found for LDAP connection. Cannot resolve SID {sid}.")
273+
return sid
274+
275+
search_filter = f"(objectSid={ldap3.utils.conv.escape_filter_chars(sid)})"
276+
ldap_connection.search(
277+
search_base=base_dn,
278+
search_filter=search_filter,
279+
attributes=["sAMAccountName"],
280+
)
281+
282+
if ldap_connection.entries:
283+
entry = ldap_connection.entries[0]
284+
return f"{entry['sAMAccountName']}"
285+
else:
286+
context.log.warning(f"SID {sid} not found in LDAP. Returning raw SID.")
287+
288+
except Exception as e:
289+
context.log.error(f"Failed to resolve SID {sid} via LDAP: {e}")
290+
else:
291+
context.log.warning(f"LDAP connection not established or unbound. Returning raw SID: {sid}")
292+
293+
return sid
294+
295+

0 commit comments

Comments
 (0)