Skip to content

Commit aec6448

Browse files
authored
Merge pull request Pennyw0rth#493 from Yeeb1/module_ggp_privs
Add new SMB module to extract GPO deployed privilege assignments
2 parents 5da5bf1 + 25fee91 commit aec6448

1 file changed

Lines changed: 259 additions & 0 deletions

File tree

nxc/modules/gpp_privileges.py

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
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+
def options(self, context, module_options):
97+
"""NO_LDAP If set to True, disables LDAP queries for resolving SIDs."""
98+
self.no_ldap = module_options.get("NO_LDAP", False)
99+
100+
def on_login(self, context, connection):
101+
try:
102+
connection.conn.listPath("SYSVOL", "*")
103+
except Exception as e:
104+
context.log.fail(f"Failed to list shares: {e}")
105+
return
106+
107+
context.log.display("Searching for GptTmpl.inf files")
108+
paths = connection.spider("SYSVOL", pattern=["GptTmpl.inf"])
109+
110+
if not paths:
111+
context.log.warning("No GptTmpl.inf files found in SYSVOL.")
112+
return
113+
114+
for path in paths:
115+
if "6AC1786C-016F-11D2-945F-00C04fB984F9" in path: # Default Domain Policy
116+
context.log.success(f"Found Default Domain Policy GptTmpl.inf: {path}")
117+
else:
118+
context.log.info(f"Found GptTmpl.inf: {path}")
119+
120+
buf = BytesIO()
121+
connection.conn.getFile("SYSVOL", path, buf.write)
122+
123+
try:
124+
content = buf.getvalue().decode("utf-16le")
125+
except UnicodeDecodeError as e:
126+
context.log.error(f"Failed to decode {path} as UTF-16LE: {e}")
127+
continue
128+
129+
privileges = self.extract_privileges(content)
130+
if privileges:
131+
ldap_connection = None
132+
if not self.no_ldap:
133+
ldap_connection = self.initialize_ldap_connection(context, connection)
134+
135+
context.log.success(f"Privileges extracted from {path}:")
136+
for privilege, sids in privileges.items():
137+
resolved_sids = [self.resolve_sid(context, sid, ldap_connection) for sid in sids]
138+
context.log.highlight(f"{privilege}: {', '.join(resolved_sids)}")
139+
140+
if ldap_connection:
141+
ldap_connection.unbind()
142+
143+
def extract_privileges(self, content):
144+
"""Parses the content of GptTmpl.inf to extract privilege rights."""
145+
privileges = {}
146+
in_priv_section = False
147+
148+
for line in content.splitlines():
149+
if line.strip() == "[Privilege Rights]":
150+
in_priv_section = True
151+
continue
152+
if in_priv_section and line.strip() == "":
153+
break
154+
if in_priv_section:
155+
match = re.match(r"^(.*?)\s*=\s*(.*)$", line)
156+
if match:
157+
privilege, sids = match.groups()
158+
privileges[privilege] = [sid.strip("*") for sid in sids.split(",")]
159+
160+
return privileges
161+
162+
def initialize_ldap_connection(self, context, connection):
163+
"""
164+
Initializes an LDAP connection using LDAP3 with LDAPS first, then falls back to plaintext LDAP if LDAPS fails.
165+
Attempts to retrieve the base DN from the Root DSE or derive it from the domain name.
166+
"""
167+
tls = ldap3.Tls(validate=ssl.CERT_NONE, version=ssl.PROTOCOL_TLSv1_2, ciphers="ALL:@SECLEVEL=0")
168+
base_dn = None
169+
170+
try:
171+
ldap_server = ldap3.Server(connection.host, use_ssl=True, port=636, tls=tls, get_info=ldap3.ALL)
172+
ldap_connection = ldap3.Connection(
173+
ldap_server,
174+
user=f"{connection.domain}\\{connection.username}",
175+
password=connection.password,
176+
authentication=ldap3.NTLM,
177+
raise_exceptions=True,
178+
)
179+
ldap_connection.bind()
180+
context.log.success("Connected to LDAP over SSL (LDAPS).")
181+
182+
try:
183+
base_dn = ldap_server.info.other.get("defaultNamingContext", [None])[0]
184+
except Exception as e:
185+
context.log.warning(f"Failed to query Root DSE for defaultNamingContext over plaintext LDAP: {e}")
186+
187+
if not base_dn:
188+
domain_parts = connection.domain.split(".")
189+
base_dn = ",".join([f"dc={part}" for part in domain_parts])
190+
context.log.info(f"Derived base DN: {base_dn}")
191+
192+
ldap_connection.base_dn = base_dn
193+
return ldap_connection
194+
195+
except Exception as ldaps_error:
196+
context.log.warning(f"LDAPS connection failed: {ldaps_error}")
197+
context.log.info("Falling back to plain LDAP...")
198+
199+
try:
200+
ldap_server = ldap3.Server(connection.host, use_ssl=False, port=389, get_info=ldap3.ALL)
201+
ldap_connection = ldap3.Connection(
202+
ldap_server,
203+
user=f"{connection.domain}\\{connection.username}",
204+
password=connection.password,
205+
authentication=ldap3.NTLM,
206+
raise_exceptions=True,
207+
)
208+
ldap_connection.bind()
209+
context.log.info("Connected to LDAP successfully (plaintext).")
210+
211+
try:
212+
base_dn = ldap_server.info.other.get("defaultNamingContext", [None])[0]
213+
except Exception as e:
214+
context.log.warning(f"Failed to query Root DSE for defaultNamingContext over plaintext LDAP: {e}")
215+
216+
if not base_dn:
217+
domain_parts = connection.domain.split(".")
218+
base_dn = ",".join([f"dc={part}" for part in domain_parts])
219+
context.log.info(f"Derived base DN: {base_dn}")
220+
221+
ldap_connection.base_dn = base_dn
222+
return ldap_connection
223+
224+
except Exception as ldap_error:
225+
context.log.error(f"Failed to connect to LDAP: {ldap_error}")
226+
227+
return None
228+
229+
def resolve_sid(self, context, sid, ldap_connection):
230+
"""Resolves a SID to a human-readable name using well-known mappings or LDAP queries."""
231+
if sid in self.WELL_KNOWN_SIDS:
232+
return self.WELL_KNOWN_SIDS[sid]
233+
234+
if ldap_connection and ldap_connection.bound:
235+
try:
236+
base_dn = getattr(ldap_connection, "base_dn", None)
237+
if not base_dn:
238+
context.log.warning(f"No base DN found for LDAP connection. Cannot resolve SID {sid}.")
239+
return sid
240+
241+
search_filter = f"(objectSid={ldap3.utils.conv.escape_filter_chars(sid)})"
242+
ldap_connection.search(
243+
search_base=base_dn,
244+
search_filter=search_filter,
245+
attributes=["sAMAccountName"],
246+
)
247+
248+
if ldap_connection.entries:
249+
entry = ldap_connection.entries[0]
250+
return f"{entry['sAMAccountName']}"
251+
else:
252+
context.log.warning(f"SID {sid} not found in LDAP. Returning raw SID.")
253+
254+
except Exception as e:
255+
context.log.error(f"Failed to resolve SID {sid} via LDAP: {e}")
256+
else:
257+
context.log.warning(f"LDAP connection not established or unbound. Returning raw SID: {sid}")
258+
259+
return sid

0 commit comments

Comments
 (0)