diff --git a/nxc/logger.py b/nxc/logger.py index e3f95fe7..5c0f40d6 100755 --- a/nxc/logger.py +++ b/nxc/logger.py @@ -16,13 +16,11 @@ class NXCAdapter(logging.LoggerAdapter): logging.basicConfig( format="%(message)s", datefmt="[%X]", - handlers=[ - RichHandler( - console=nxc_console, - rich_tracebacks=True, - tracebacks_show_locals=False, - ) - ], + handlers=[RichHandler( + console=nxc_console, + rich_tracebacks=True, + tracebacks_show_locals=False + )], ) self.logger = logging.getLogger("nxc") self.extra = extra @@ -40,30 +38,21 @@ class NXCAdapter(logging.LoggerAdapter): if self.extra is None: return f"{msg}", kwargs - if "module_name" in self.extra and len(self.extra["module_name"]) > 8: + if "module_name" in self.extra and len(self.extra["module_name"]) > 11: self.extra["module_name"] = self.extra["module_name"][:8] + "..." # If the logger is being called when hooking the 'options' module function if len(self.extra) == 1 and ("module_name" in self.extra): - return ( - f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<64} {msg}", - kwargs, - ) + return (f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<64} {msg}", kwargs) # If the logger is being called from nxcServer if len(self.extra) == 2 and ("module_name" in self.extra) and ("host" in self.extra): - return ( - f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<24} {self.extra['host']:<39} {msg}", - kwargs, - ) + return (f"{colored(self.extra['module_name'], 'cyan', attrs=['bold']):<24} {self.extra['host']:<39} {msg}", kwargs) # If the logger is being called from a protocol module_name = colored(self.extra["module_name"], "cyan", attrs=["bold"]) if "module_name" in self.extra else colored(self.extra["protocol"], "blue", attrs=["bold"]) - return ( - f"{module_name:<24} {self.extra['host']:<15} {self.extra['port']:<6} {self.extra['hostname'] if self.extra['hostname'] else 'NONE':<16} {msg}", - kwargs, - ) + return (f"{module_name:<24} {self.extra['host']:<15} {self.extra['port']:<6} {self.extra['hostname'] if self.extra['hostname'] else 'NONE':<16} {msg}", kwargs) def display(self, msg, *args, **kwargs): """Display text to console, formatted for nxc""" @@ -104,17 +93,7 @@ class NXCAdapter(logging.LoggerAdapter): if len(self.logger.handlers): try: for handler in self.logger.handlers: - handler.handle( - LogRecord( - "nxc", - 20, - "", - kwargs, - msg=text, - args=args, - exc_info=None, - ) - ) + handler.handle(LogRecord("nxc", 20, "", kwargs, msg=text, args=args, exc_info=None)) except Exception as e: self.logger.fail(f"Issue while trying to custom print handler: {e}") else: diff --git a/nxc/protocols/smb.py b/nxc/protocols/smb.py index 4db4768f..cc3e5faa 100755 --- a/nxc/protocols/smb.py +++ b/nxc/protocols/smb.py @@ -1000,8 +1000,12 @@ class smb(connection): return groups def users(self): - self.logger.display("Trying to dump local users with SAMRPC protocol") - return UserSamrDump(self).dump() + if len(self.args.users) > 1: + self.logger.display(f"Dumping users: {', '.join(self.args.users)}") + else: + self.logger.info("Trying to dump local users with SAMRPC protocol") + + return UserSamrDump(self).dump(self.args.users) def hosts(self): hosts = [] diff --git a/nxc/protocols/smb/proto_args.py b/nxc/protocols/smb/proto_args.py index 85148472..164c03ca 100644 --- a/nxc/protocols/smb/proto_args.py +++ b/nxc/protocols/smb/proto_args.py @@ -38,7 +38,7 @@ def proto_args(parser, std_parser, module_parser): egroup.add_argument("--disks", action="store_true", help="enumerate disks") egroup.add_argument("--loggedon-users-filter", action="store", help="only search for specific user, works with regex") egroup.add_argument("--loggedon-users", action="store_true", help="enumerate logged on users") - egroup.add_argument("--users", nargs="?", const="", metavar="USER", help="enumerate domain users, if a user is specified than only its information is queried.") + egroup.add_argument("--users", nargs="*", metavar="USER", help="enumerate domain users, if a user is specified than only its information is queried.") egroup.add_argument("--groups", nargs="?", const="", metavar="GROUP", help="enumerate domain groups, if a group is specified than its members are enumerated") egroup.add_argument("--computers", nargs="?", const="", metavar="COMPUTER", help="enumerate computer users") egroup.add_argument("--local-groups", nargs="?", const="", metavar="GROUP", help="enumerate local groups, if a group is specified then its members are enumerated") diff --git a/nxc/protocols/smb/samruser.py b/nxc/protocols/smb/samruser.py index 11cf30a9..1eec1828 100644 --- a/nxc/protocols/smb/samruser.py +++ b/nxc/protocols/smb/samruser.py @@ -4,6 +4,7 @@ from impacket.dcerpc.v5 import transport, samr from impacket.dcerpc.v5.rpcrt import DCERPCException from impacket.dcerpc.v5.rpcrt import DCERPC_v5 from impacket.nt_errors import STATUS_MORE_ENTRIES +from datetime import datetime, timedelta class UserSamrDump: @@ -26,6 +27,8 @@ class UserSamrDump: self.doKerberos = connection.kerberos self.protocols = UserSamrDump.KNOWN_PROTOCOLS.keys() self.users = [] + self.rpc_transport = None + self.dce = None if self.hash is not None: if self.hash.find(":") != -1: @@ -36,46 +39,37 @@ class UserSamrDump: if self.password is None: self.password = "" - def dump(self): + def dump(self, requested_users=None): # Try all requested protocols until one works. for protocol in self.protocols: try: protodef = UserSamrDump.KNOWN_PROTOCOLS[protocol] port = protodef[1] except KeyError: - self.logger.debug(f"Invalid Protocol '{protocol}'") + self.logger.debug(f"Invalid Protocol: {protocol}") + self.logger.debug(f"Trying protocol {protocol}") - rpctransport = transport.SMBTransport( - self.addr, - port, - r"\samr", - self.username, - self.password, - self.domain, - self.lmhash, - self.nthash, - self.aesKey, - doKerberos=self.doKerberos, - ) + self.rpc_transport = transport.SMBTransport(self.addr, port, r"\samr", self.username, self.password, self.domain, self.lmhash, self.nthash, self.aesKey, doKerberos=self.doKerberos) + try: - self.fetchList(rpctransport) + self.fetch_users(requested_users) break except Exception as e: - self.logger.debug(f"Protocol failed: {e}") + self.logger.debug(f"Connection with protocol {protocol} failed: {e}") return self.users - def fetchList(self, rpctransport): - dce = DCERPC_v5(rpctransport) - dce.connect() - dce.bind(samr.MSRPC_UUID_SAMR) + def fetch_users(self, requested_users): + self.dce = DCERPC_v5(self.rpc_transport) + self.dce.connect() + self.dce.bind(samr.MSRPC_UUID_SAMR) # Setup Connection - resp = samr.hSamrConnect2(dce) + resp = samr.hSamrConnect2(self.dce) if resp["ErrorCode"] != 0: raise Exception("Connect error") resp2 = samr.hSamrEnumerateDomainsInSamServer( - dce, + self.dce, serverHandle=resp["ServerHandle"], enumerationContext=0, preferedMaximumLength=500, @@ -84,7 +78,7 @@ class UserSamrDump: raise Exception("Connect error") resp3 = samr.hSamrLookupDomainInSamServer( - dce, + self.dce, serverHandle=resp["ServerHandle"], name=resp2["Buffer"]["Buffer"][0]["Name"], ) @@ -92,7 +86,7 @@ class UserSamrDump: raise Exception("Connect error") resp4 = samr.hSamrOpenDomain( - dce, + self.dce, serverHandle=resp["ServerHandle"], desiredAccess=samr.MAXIMUM_ALLOWED, domainId=resp3["DomainId"], @@ -101,28 +95,84 @@ class UserSamrDump: raise Exception("Connect error") self.__domains = resp2["Buffer"]["Buffer"] - domainHandle = resp4["DomainHandle"] + domain_handle = resp4["DomainHandle"] # End Setup - status = STATUS_MORE_ENTRIES - enumerationContext = 0 - while status == STATUS_MORE_ENTRIES: + if requested_users: + self.logger.debug(f"Looping through users requested and looking up their information: {requested_users}") try: - resp = samr.hSamrEnumerateUsersInDomain(dce, domainHandle, enumerationContext=enumerationContext) + names_lookup_resp = samr.hSamrLookupNamesInDomain(self.dce, domain_handle, requested_users) + rids = [r["Data"] for r in names_lookup_resp["RelativeIds"]["Element"]] + self.logger.debug(f"Specific RIDs retrieved: {rids}") + users = self.get_user_info(domain_handle, rids) except DCERPCException as e: - if str(e).find("STATUS_MORE_ENTRIES") < 0: - self.logger.fail("Error enumerating domain user(s)") - break - resp = e.get_packet() - self.logger.success("Enumerated domain user(s)") - for user in resp["Buffer"]["Buffer"]: - r = samr.hSamrOpenUser(dce, domainHandle, samr.MAXIMUM_ALLOWED, user["RelativeId"]) - info_user = samr.hSamrQueryInformationUser2(dce, r["UserHandle"], samr.USER_INFORMATION_CLASS.UserAllInformation)["Buffer"]["All"]["AdminComment"] - self.logger.highlight(f"{self.domain}\\{user['Name']:<30} {info_user}") - self.users.append(user["Name"]) - samr.hSamrCloseHandle(dce, r["UserHandle"]) + self.logger.debug(f"Exception while requesting users in domain: {e}") + if "STATUS_SOME_NOT_MAPPED" in str(e): + # which user is not translated correctly isn't returned so we can't tell the user which is failing, which is very annoying + self.logger.fail("One of the users requested does not exist in the domain, causing a critical failure during translation, re-check the users and try again") + else: + self.logger.fail(f"Error occurred when looking up users in domain: {e}") + else: + status = STATUS_MORE_ENTRIES + enumerationContext = 0 + while status == STATUS_MORE_ENTRIES: + try: + enumerate_users_resp = samr.hSamrEnumerateUsersInDomain(self.dce, domain_handle, enumerationContext=enumerationContext) + except DCERPCException as e: + if str(e).find("STATUS_MORE_ENTRIES") < 0: + self.logger.fail("Error enumerating domain user(s)") + break + enumerate_users_resp = e.get_packet() - enumerationContext = resp["EnumerationContext"] - status = resp["ErrorCode"] + rids = [r["RelativeId"] for r in enumerate_users_resp["Buffer"]["Buffer"]] + self.logger.debug(f"Full domain RIDs retrieved: {rids}") + users = self.get_user_info(domain_handle, rids) - dce.disconnect() + # set these for the while loop + enumerationContext = enumerate_users_resp["EnumerationContext"] + status = enumerate_users_resp["ErrorCode"] + self.print_user_info(users) + self.dce.disconnect() + + def get_user_info(self, domain_handle, user_ids): + self.logger.debug(f"Getting user info for users: {user_ids}") + users = [] + + for user in user_ids: + self.logger.debug(f"Calling hSamrOpenUser for RID {user}") + open_user_resp = samr.hSamrOpenUser( + self.dce, + domain_handle, + samr.MAXIMUM_ALLOWED, + user + ) + info_user_resp = samr.hSamrQueryInformationUser2( + self.dce, + open_user_resp["UserHandle"], + samr.USER_INFORMATION_CLASS.UserAllInformation + )["Buffer"] + + user_info = info_user_resp["All"] + user_name = user_info["UserName"] + bad_pwd_count = user_info["BadPasswordCount"] + user_description = user_info["AdminComment"] + last_pw_set = old_large_int_to_datetime(user_info["PasswordLastSet"]) + if last_pw_set == "1601-01-01 00:00:00": + last_pw_set = "" + users.append({"name": user_name, "description": user_description, "bad_pwd_count": bad_pwd_count, "last_pw_set": last_pw_set}) + + samr.hSamrCloseHandle(self.dce, open_user_resp["UserHandle"]) + return users + + def print_user_info(self, users): + self.logger.highlight(f"{'-Username-':<30}{'-Last PW Set-':<20}{'-BadPW-':<8}{'-Description-':<60}") + for user in users: + self.logger.debug(f"Full user info: {user}") + self.logger.highlight(f"{user['name']:<30}{user['last_pw_set']:<20}{user['bad_pwd_count']:<8}{user['description']} ") + + +def old_large_int_to_datetime(large_int): + combined = (large_int["HighPart"] << 32) | large_int["LowPart"] + timestamp_seconds = combined / 10**7 + start_date = datetime(1601, 1, 1) + return (start_date + timedelta(seconds=timestamp_seconds)).replace(microsecond=0).strftime("%Y-%m-%d %H:%M:%S")