Skip to content

Commit af45411

Browse files
authored
Merge pull request Pennyw0rth#688 from termanix/add-group
New Module modify-group
2 parents 8347d0a + 0c14d05 commit af45411

File tree

3 files changed

+202
-3
lines changed

3 files changed

+202
-3
lines changed

nxc/modules/modify-group.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import contextlib
2+
import sys
3+
from impacket.ldap.ldap import MODIFY_ADD, MODIFY_DELETE
4+
from impacket.dcerpc.v5 import samr, epm, transport
5+
from impacket.dcerpc.v5.rpcrt import DCERPCException
6+
from nxc.parsers.ldap_results import parse_result_attributes
7+
from nxc.helpers.misc import CATEGORY
8+
9+
10+
class NXCModule:
11+
"""
12+
Module for adding/removing users to/from groups
13+
Module by @termanix
14+
"""
15+
16+
name = "modify-group"
17+
description = "Modify the group membership of users and computers"
18+
supported_protocols = ["smb", "ldap"]
19+
category = CATEGORY.PRIVILEGE_ESCALATION
20+
21+
def options(self, context, module_options):
22+
"""
23+
Required (at least one of):
24+
GROUP Name of the group to add/remove the user to/from
25+
USER Username of the account to modify
26+
27+
Optional:
28+
REMOVE Set to 'True' to remove the user from the specified group instead of adding (default: False)
29+
30+
Examples
31+
--------
32+
Adding a user to a group:
33+
netexec smb <DC_IP> -u adminuser -p password -M modify-group -o USER='targetuser' GROUP='Domain Admins'
34+
netexec ldap <DC_IP> -u adminuser -p password -M modify-group -o USER='targetuser' GROUP='Enterprise Admins'
35+
36+
Removing a user from a group:
37+
netexec smb <DC_IP> -u adminuser -p password -M modify-group -o USER='targetuser' GROUP='Domain Admins' REMOVE=True
38+
netexec ldap <DC_IP> -u adminuser -p password -M modify-group -o USER='targetuser' GROUP='Enterprise Admins' REMOVE=True
39+
40+
SMB/SAMR KNOWN LIMITATIONS:
41+
- SAMR only supports modification of Global security groups.
42+
Domain Local and Universal groups require the LDAP protocol.
43+
- Cross-domain groups (e.g. Enterprise Admins) cannot be modified via SAMR.
44+
Use LDAP instead.
45+
46+
"""
47+
self.group = module_options.get("GROUP")
48+
self.target_user = module_options.get("USER")
49+
self.remove = module_options.get("REMOVE", "False").lower() == "true"
50+
51+
if not (self.target_user and self.group):
52+
self.context.log.fail("USER and GROUP parameters are required!")
53+
sys.exit(1)
54+
55+
def on_login(self, context, connection):
56+
self.context = context
57+
self.connection = connection
58+
59+
if context.protocol == "smb":
60+
self._modify_group_smb()
61+
elif context.protocol == "ldap" and self.group:
62+
self._modify_group_ldap()
63+
64+
def _authenticate_dce(self, protocol="ncacn_np", interface=samr.MSRPC_UUID_SAMR):
65+
"""Authenticate to the target using DCE/RPC"""
66+
try:
67+
# Map to the endpoint on the target
68+
string_binding = epm.hept_map(self.connection.host, interface, protocol=protocol)
69+
rpctransport = transport.DCERPCTransportFactory(string_binding)
70+
rpctransport.setRemoteHost(self.connection.host)
71+
rpctransport.set_credentials(
72+
self.connection.username,
73+
self.connection.password,
74+
self.connection.domain,
75+
self.connection.lmhash,
76+
self.connection.nthash,
77+
aesKey=self.connection.aesKey,
78+
)
79+
self.context.log.info(f"Connecting as {self.connection.domain}\\{self.connection.username}")
80+
81+
# Connect to the DCE/RPC endpoint and bind to the service
82+
dce = rpctransport.get_dce_rpc()
83+
dce.connect()
84+
self.context.log.info("Successfully connected to DCE/RPC")
85+
dce.bind(interface)
86+
self.context.log.info(f"Successfully bound to {interface}")
87+
return dce
88+
except DCERPCException as e:
89+
self.context.log.fail(f"DCE/RPC Exception: {e!s}")
90+
91+
def _modify_group_smb(self):
92+
"""Modify group membership using SMB/SAMR protocol"""
93+
dce = self._authenticate_dce()
94+
if not dce:
95+
return
96+
97+
# Get domain handle
98+
try:
99+
server_handle = samr.hSamrConnect(dce, self.connection.host + "\x00")["ServerHandle"]
100+
domain_sid = samr.hSamrLookupDomainInSamServer(dce, server_handle, self.connection.domain)["DomainId"]
101+
domain_handle = samr.hSamrOpenDomain(dce, server_handle, domainId=domain_sid)["DomainHandle"]
102+
except Exception as e:
103+
self.context.log.fail(f"Failed to connect to SAMR service: {e}")
104+
return
105+
106+
# Find the user RID
107+
try:
108+
user_rid = samr.hSamrLookupNamesInDomain(dce, domain_handle, (self.target_user,))["RelativeIds"]["Element"][0]
109+
except Exception as e:
110+
if "STATUS_NONE_MAPPED" in str(e):
111+
self.context.log.fail(f"Target user not found: {self.target_user}")
112+
else:
113+
self.context.log.fail(f"Failed to find user RID: {e}")
114+
return
115+
116+
# Find the group RID and open the group
117+
try:
118+
group_rid = samr.hSamrLookupNamesInDomain(dce, domain_handle, (self.group,))["RelativeIds"]["Element"][0]
119+
group_handle = samr.hSamrOpenGroup(dce, domain_handle, groupId=group_rid)["GroupHandle"]
120+
except Exception as e:
121+
if "STATUS_NONE_MAPPED" in str(e):
122+
self.context.log.fail(f"Target group not found: {self.group}")
123+
else:
124+
self.context.log.fail(f"Failed to find group: {e}")
125+
return
126+
127+
# Modify group membership
128+
if self.remove:
129+
try:
130+
samr.hSamrRemoveMemberFromGroup(dce, group_handle, user_rid)
131+
self.context.log.success(f"Successfully removed {self.target_user} from group {self.group}")
132+
except Exception as e:
133+
if "STATUS_MEMBER_NOT_IN_GROUP" in str(e):
134+
self.context.log.fail(f"User {self.target_user} is not a member of group {self.group}")
135+
else:
136+
self.context.log.fail(f"Failed to remove user from group via SMB: {e}")
137+
else:
138+
try:
139+
samr.hSamrAddMemberToGroup(dce, group_handle, user_rid, 0x7)
140+
self.context.log.success(f"Successfully added {self.target_user} to group {self.group}")
141+
except Exception as e:
142+
if "STATUS_MEMBER_IN_GROUP" in str(e):
143+
self.context.log.fail(f"User {self.target_user} is already a member of group {self.group}")
144+
else:
145+
self.context.log.fail(f"Failed to add user to group via SMB: {e}")
146+
147+
# Disconnect from DCE/RPC
148+
with contextlib.suppress(Exception):
149+
dce.disconnect()
150+
151+
def _modify_group_ldap(self):
152+
"""Modify group membership using LDAP protocol"""
153+
# Get the DN of the target user
154+
resp = self._find_object_dn(self.target_user)
155+
if not resp:
156+
self.context.log.fail(f"Target user not found: {self.target_user}")
157+
return
158+
else:
159+
target_user_dn = resp[0]["distinguishedName"]
160+
161+
# Get the DN of the target group
162+
resp = self._find_object_dn(self.group)
163+
if not resp:
164+
self.context.log.fail(f"Target group not found: {self.group}")
165+
return
166+
else:
167+
group_dn = resp[0]["distinguishedName"]
168+
169+
# Modify group membership
170+
if self.remove:
171+
try:
172+
self.connection.ldap_connection.modify(group_dn, {"member": [(MODIFY_DELETE, [target_user_dn])]})
173+
self.context.log.success(f"Successfully removed {self.target_user} from group {self.group}")
174+
except Exception as e:
175+
if "unwillingToPerform" in str(e):
176+
self.context.log.fail(f"User {self.target_user} is not a member of group {self.group}")
177+
else:
178+
self.context.log.fail(f"Failed to remove user from group via LDAP: {e}")
179+
else:
180+
try:
181+
self.connection.ldap_connection.modify(group_dn, {"member": [(MODIFY_ADD, [target_user_dn])]})
182+
self.context.log.success(f"Successfully added {self.target_user} to group {self.group}")
183+
except Exception as e:
184+
if "entryAlreadyExists" in str(e):
185+
self.context.log.fail(f"User {self.target_user} is already a member of group {self.group}")
186+
else:
187+
self.context.log.fail(f"Failed to add user to group via LDAP: {e}")
188+
189+
def _find_object_dn(self, value):
190+
"""Find the distinguished name (DN) of an object by sAMAccountName"""
191+
resp = self.connection.ldap_connection.search(
192+
searchFilter=f"(sAMAccountName={value})",
193+
attributes=["distinguishedName"]
194+
)
195+
return parse_result_attributes(resp)

poetry.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/e2e_commands.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M lsassy
114114
#netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M masky -o CA="host.domain.tld\domain-host-CA"
115115
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M met_inject -o SRVHOST=127.0.0.1 SRVPORT=4443 RAND=12345
116116
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mobaxterm
117+
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M modify-group -o USER=USERNAME GROUP="Domain Admins"
118+
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M modify-group -o USER=USERNAME GROUP="Domain Admins" REMOVE=True
117119
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M mremoteng
118120
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ms17-010
119121
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M msol
@@ -229,6 +231,8 @@ netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get-net
229231
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M groupmembership -o USER=LOGIN_USERNAME
230232
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M laps
231233
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M maq
234+
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M modify-group -o USER=USERNAME GROUP="Domain Admins"
235+
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M modify-group -o USER=USERNAME GROUP="Domain Admins" REMOVE=True
232236
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M pre2k
233237
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M pre2k -o ALL=true
234238
netexec ldap TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M subnets

0 commit comments

Comments
 (0)