|
| 1 | +import re |
| 2 | +from impacket.dcerpc.v5 import samr, tsch, transport |
| 3 | +from impacket.dcerpc.v5 import tsts as TSTS |
| 4 | +from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_GSS_NEGOTIATE, RPC_C_AUTHN_LEVEL_PKT_PRIVACY |
| 5 | +from contextlib import suppress |
| 6 | +import traceback |
| 7 | + |
| 8 | + |
| 9 | +class NXCModule: |
| 10 | + """ |
| 11 | + Module to find Domain and Enterprise Admin presence on target systems over SMB. |
| 12 | + Made by @crosscutsaw, @NeffIsBack |
| 13 | + """ |
| 14 | + |
| 15 | + name = "presence" |
| 16 | + description = "Traces Domain and Enterprise Admin presence in the target over SMB" |
| 17 | + supported_protocols = ["smb"] |
| 18 | + opsec_safe = True |
| 19 | + multiple_hosts = True |
| 20 | + |
| 21 | + def options(self, context, module_options): |
| 22 | + """There are no module options.""" |
| 23 | + |
| 24 | + def on_admin_login(self, context, connection): |
| 25 | + try: |
| 26 | + admin_users = self.enumerate_admin_users(context, connection) |
| 27 | + if not admin_users: |
| 28 | + context.log.fail("No admin users found.") |
| 29 | + return |
| 30 | + |
| 31 | + # Update user objects to check if they are in tasklist, users directory or in scheduled tasks |
| 32 | + self.check_users_directory(context, connection, admin_users) |
| 33 | + self.check_tasklist(context, connection, admin_users) |
| 34 | + self.check_scheduled_tasks(context, connection, admin_users) |
| 35 | + |
| 36 | + # print grouped/logged results nicely |
| 37 | + self.print_grouped_results(context, admin_users) |
| 38 | + except Exception as e: |
| 39 | + context.log.fail(str(e)) |
| 40 | + context.log.debug(traceback.format_exc()) |
| 41 | + |
| 42 | + def get_dce_rpc(self, target, string_binding, dce_binding, connection): |
| 43 | + # Create a DCE/RPC transport object with the specified string binding |
| 44 | + rpctransport = transport.DCERPCTransportFactory(string_binding) |
| 45 | + rpctransport.setRemoteHost(target) |
| 46 | + rpctransport.set_credentials( |
| 47 | + connection.username, |
| 48 | + connection.password, |
| 49 | + connection.domain, |
| 50 | + connection.lmhash, |
| 51 | + connection.nthash, |
| 52 | + aesKey=connection.aesKey, |
| 53 | + ) |
| 54 | + rpctransport.set_kerberos(connection.kerberos, connection.kdcHost) |
| 55 | + |
| 56 | + # Connect to the DCE/RPC endpoint |
| 57 | + dce = rpctransport.get_dce_rpc() |
| 58 | + if connection.kerberos: |
| 59 | + dce.set_auth_type(RPC_C_AUTHN_GSS_NEGOTIATE) |
| 60 | + dce.set_credentials(*rpctransport.get_credentials()) |
| 61 | + dce.connect() |
| 62 | + dce.set_auth_level(RPC_C_AUTHN_LEVEL_PKT_PRIVACY) |
| 63 | + dce.bind(dce_binding) |
| 64 | + return dce |
| 65 | + |
| 66 | + def enumerate_admin_users(self, context, connection): |
| 67 | + admin_users = [] |
| 68 | + |
| 69 | + try: |
| 70 | + string_binding = fr"ncacn_np:{connection.kdcHost}[\pipe\samr]" |
| 71 | + dce = self.get_dce_rpc(connection.kdcHost, string_binding, samr.MSRPC_UUID_SAMR, connection) |
| 72 | + except Exception as e: |
| 73 | + context.log.fail(f"Failed to connect to SAMR: {e}") |
| 74 | + context.log.debug(traceback.format_exc()) |
| 75 | + return admin_users |
| 76 | + |
| 77 | + try: |
| 78 | + server_handle = samr.hSamrConnect2(dce)["ServerHandle"] |
| 79 | + domain = samr.hSamrEnumerateDomainsInSamServer(dce, server_handle)["Buffer"]["Buffer"][0]["Name"] |
| 80 | + resp = samr.hSamrLookupDomainInSamServer(dce, server_handle, domain) |
| 81 | + self.domain_sid = resp["DomainId"].formatCanonical() |
| 82 | + domain_handle = samr.hSamrOpenDomain(dce, server_handle, samr.DOMAIN_LOOKUP | samr.DOMAIN_LIST_ACCOUNTS, resp["DomainId"])["DomainHandle"] |
| 83 | + context.log.debug(f"Resolved domain SID for {domain}: {self.domain_sid}") |
| 84 | + except Exception as e: |
| 85 | + context.log.fail(f"Failed to open domain {domain}: {e!s}") |
| 86 | + context.log.debug(traceback.format_exc()) |
| 87 | + return admin_users |
| 88 | + |
| 89 | + admin_rids = { |
| 90 | + "Domain Admins": 512, |
| 91 | + "Enterprise Admins": 519, |
| 92 | + } |
| 93 | + |
| 94 | + # Enumerate admin groups and their members |
| 95 | + for group_name, group_rid in admin_rids.items(): |
| 96 | + context.log.debug(f"Looking up group: {group_name} with RID {group_rid}") |
| 97 | + |
| 98 | + try: |
| 99 | + group_handle = samr.hSamrOpenGroup(dce, domain_handle, samr.GROUP_LIST_MEMBERS, group_rid)["GroupHandle"] |
| 100 | + resp = samr.hSamrGetMembersInGroup(dce, group_handle) |
| 101 | + for member in resp["Members"]["Members"]: |
| 102 | + rid = int.from_bytes(member.getData(), byteorder="little") |
| 103 | + try: |
| 104 | + user_handle = samr.hSamrOpenUser(dce, domain_handle, samr.MAXIMUM_ALLOWED, rid)["UserHandle"] |
| 105 | + username = samr.hSamrQueryInformationUser2(dce, user_handle, samr.USER_INFORMATION_CLASS.UserAllInformation)["Buffer"]["All"]["UserName"] |
| 106 | + |
| 107 | + # If user already exists, append group name |
| 108 | + if any(u["sid"] == f"{self.domain_sid}-{rid}" for u in admin_users): |
| 109 | + user = next(u for u in admin_users if u["sid"] == f"{self.domain_sid}-{rid}") |
| 110 | + user["group"].append(group_name) |
| 111 | + else: |
| 112 | + admin_users.append({"username": username, "sid": f"{self.domain_sid}-{rid}", "domain": domain, "group": [group_name], "in_tasks": False, "in_directory": False, "in_scheduled_tasks": False}) |
| 113 | + context.log.debug(f"Found user: {username} with RID {rid} in group {group_name}") |
| 114 | + except Exception as e: |
| 115 | + context.log.debug(f"Failed to get user info for RID {rid}: {e!s}") |
| 116 | + finally: |
| 117 | + with suppress(Exception): |
| 118 | + samr.hSamrCloseHandle(dce, user_handle) |
| 119 | + except Exception as e: |
| 120 | + context.log.debug(f"Failed to get members of group {group_name}: {e!s}") |
| 121 | + finally: |
| 122 | + with suppress(Exception): |
| 123 | + samr.hSamrCloseHandle(dce, group_handle) |
| 124 | + |
| 125 | + return admin_users |
| 126 | + |
| 127 | + def check_users_directory(self, context, connection, admin_users): |
| 128 | + dirs_found = set() |
| 129 | + |
| 130 | + # try C$\Users first |
| 131 | + try: |
| 132 | + files = connection.conn.listPath("C$", "\\Users\\*") |
| 133 | + except Exception as e: |
| 134 | + context.log.debug(f"C$\\Users unavailable: {e}, trying Documents and Settings") |
| 135 | + try: |
| 136 | + files = connection.conn.listPath("C$", "\\Documents and Settings\\*") |
| 137 | + except Exception as e: |
| 138 | + context.log.fail(f"Error listing fallback directory: {e}") |
| 139 | + return |
| 140 | + else: |
| 141 | + context.log.debug("Successfully listed C$\\Users") |
| 142 | + |
| 143 | + # collect folder names (lowercase) ignoring "." and ".." |
| 144 | + dirs_found.update([f.get_shortname().lower() for f in files if f.get_shortname().lower() not in [".", "..", "administrator"]]) |
| 145 | + |
| 146 | + # for admin users, check for folder presence |
| 147 | + for user in admin_users: |
| 148 | + # Look for administrator.domain to check if SID 500 Administrator is present (second check) |
| 149 | + if user["username"].lower() in dirs_found or \ |
| 150 | + (user["username"].lower() == "administrator" and f"{user['username'].lower()}.{user['domain']}" in dirs_found): |
| 151 | + user["in_directory"] = True |
| 152 | + context.log.info(f"Found user {user['username']} in directories") |
| 153 | + |
| 154 | + def check_tasklist(self, context, connection, admin_users): |
| 155 | + """Checks tasklist over rpc.""" |
| 156 | + try: |
| 157 | + with TSTS.LegacyAPI(connection.conn, connection.remoteName, kerberos=connection.kerberos) as legacy: |
| 158 | + handle = legacy.hRpcWinStationOpenServer() |
| 159 | + processes = legacy.hRpcWinStationGetAllProcesses(handle) |
| 160 | + except Exception as e: |
| 161 | + context.log.fail(f"Error in check_tasklist RPC method: {e}") |
| 162 | + return [] |
| 163 | + |
| 164 | + context.log.debug(f"Enumerated {len(processes)} processes on {connection.host}") |
| 165 | + |
| 166 | + for process in processes: |
| 167 | + context.log.debug(f"ImageName: {process['ImageName']}, UniqueProcessId: {process['SessionId']}, pSid: {process['pSid']}") |
| 168 | + # Check if process SID matches any admin user SID |
| 169 | + for user in admin_users: |
| 170 | + if process["pSid"] == user["sid"]: |
| 171 | + user["in_tasks"] = True |
| 172 | + context.log.info(f"Matched process {process['ImageName']} with user {user['username']}") |
| 173 | + |
| 174 | + def check_scheduled_tasks(self, context, connection, admin_users): |
| 175 | + """Checks scheduled tasks over rpc.""" |
| 176 | + try: |
| 177 | + target = connection.remoteName if connection.kerberos else connection.host |
| 178 | + stringbinding = f"ncacn_np:{target}[\\pipe\\atsvc]" |
| 179 | + dce = self.get_dce_rpc(target, stringbinding, tsch.MSRPC_UUID_TSCHS, connection) |
| 180 | + |
| 181 | + # Also extract non admins where we can get the password |
| 182 | + self.non_admins = [] |
| 183 | + non_admin_sids = set() |
| 184 | + |
| 185 | + tasks = tsch.hSchRpcEnumTasks(dce, "\\")["pNames"] |
| 186 | + for task in tasks: |
| 187 | + xml = tsch.hSchRpcRetrieveTask(dce, task["Data"])["pXml"] |
| 188 | + # Extract SID and LogonType from the XML, if LogonType is "Password" we should be able to extract the password |
| 189 | + sid = re.search(fr"<UserId>({self.domain_sid}-\d{{3,}})</UserId>", xml) |
| 190 | + logon_type = re.search(r"<LogonType>(\w+)</LogonType>", xml) |
| 191 | + |
| 192 | + # Check if SID and LogonType are found, then check if SID matches any admin user |
| 193 | + if sid and logon_type and logon_type.group(1) == "Password": |
| 194 | + context.log.debug(f"Found scheduled task '{task['Data']}' with SID {sid} and LogonType {logon_type.group(1)}") |
| 195 | + if any(user["sid"] == sid.group(1) for user in admin_users): |
| 196 | + user = next(user for user in admin_users if user["sid"] == sid.group(1)) |
| 197 | + user["in_scheduled_tasks"] = True |
| 198 | + else: |
| 199 | + # If not an admin user, add to non_admin_sids for further processing |
| 200 | + non_admin_sids.add(sid.group(1)) |
| 201 | + |
| 202 | + if non_admin_sids: |
| 203 | + string_binding = fr"ncacn_np:{connection.kdcHost}[\pipe\samr]" |
| 204 | + dce = self.get_dce_rpc(connection.kdcHost, string_binding, samr.MSRPC_UUID_SAMR, connection) |
| 205 | + |
| 206 | + # Get Domain Handle |
| 207 | + server_handle = samr.hSamrConnect2(dce)["ServerHandle"] |
| 208 | + domain = samr.hSamrEnumerateDomainsInSamServer(dce, server_handle)["Buffer"]["Buffer"][0]["Name"] |
| 209 | + domain_sid = samr.hSamrLookupDomainInSamServer(dce, server_handle, domain)["DomainId"] |
| 210 | + domain_handle = samr.hSamrOpenDomain(dce, server_handle, samr.DOMAIN_LOOKUP | samr.DOMAIN_LIST_ACCOUNTS, domain_sid)["DomainHandle"] |
| 211 | + |
| 212 | + for sid in non_admin_sids: |
| 213 | + user_handle = samr.hSamrOpenUser(dce, domain_handle, samr.MAXIMUM_ALLOWED, int(sid.split("-")[-1]))["UserHandle"] |
| 214 | + username = samr.hSamrQueryInformationUser2(dce, user_handle, samr.USER_INFORMATION_CLASS.UserAllInformation)["Buffer"]["All"]["UserName"] |
| 215 | + self.non_admins.append(username) |
| 216 | + |
| 217 | + except Exception as e: |
| 218 | + context.log.fail(f"Failed to enumerate scheduled tasks: {e}") |
| 219 | + context.log.debug(traceback.format_exc()) |
| 220 | + |
| 221 | + def print_grouped_results(self, context, admin_users): |
| 222 | + """Logs all results grouped per host in order""" |
| 223 | + # Make less verbose for scanning large ranges |
| 224 | + context.log.info(f"Identified Admin Users: {', '.join([user['username'] for user in admin_users])}") |
| 225 | + |
| 226 | + # Print directory users |
| 227 | + dir_users = [user for user in admin_users if user["in_directory"]] |
| 228 | + if dir_users: |
| 229 | + context.log.success("Found admins in directories:") |
| 230 | + for user in dir_users: |
| 231 | + context.log.highlight(f"{user['username']} ({', '.join(user['group'])})") |
| 232 | + |
| 233 | + # Print tasklist users |
| 234 | + tasklist_users = [user for user in admin_users if user["in_tasks"]] |
| 235 | + if tasklist_users: |
| 236 | + context.log.success("Found admins in tasklist:") |
| 237 | + for user in tasklist_users: |
| 238 | + context.log.highlight(f"{user['username']} ({', '.join(user['group'])})") |
| 239 | + |
| 240 | + # Print scheduled tasks users |
| 241 | + scheduled_tasks_users = [user for user in admin_users if user["in_scheduled_tasks"]] |
| 242 | + if scheduled_tasks_users: |
| 243 | + context.log.success("Found admins in scheduled tasks:") |
| 244 | + for user in scheduled_tasks_users: |
| 245 | + context.log.highlight(f"{user['username']} ({', '.join(user['group'])})") |
| 246 | + if self.non_admins: |
| 247 | + context.log.info(f"Found {len(self.non_admins)} non-admin scheduled tasks:") |
| 248 | + for sid in self.non_admins: |
| 249 | + context.log.info(sid) |
| 250 | + |
| 251 | + # Making this less verbose to better scan large ranges |
| 252 | + if not dir_users and not tasklist_users: |
| 253 | + context.log.info("No matches found in users directory, tasklist or scheduled tasks.") |
0 commit comments