diff --git a/nxc/modules/example_module.py b/nxc/modules/example_module.py index 263b5a5f..ae25b4b8 100644 --- a/nxc/modules/example_module.py +++ b/nxc/modules/example_module.py @@ -20,6 +20,7 @@ class NXCModule: """Required. Module options get parsed here. Additionally, put the modules usage here as well """ + # Put "No options available" in the docstring if there are no options for the module def on_login(self, context, connection): """Concurrent. diff --git a/nxc/modules/lockscreendoors.py b/nxc/modules/lockscreendoors.py new file mode 100644 index 00000000..f4005b3c --- /dev/null +++ b/nxc/modules/lockscreendoors.py @@ -0,0 +1,87 @@ +from io import BytesIO +import pefile + + +class NXCModule: + """ + Module for detecting Windows lock screen backdoors + Module by @E1A + """ + + name = "lockscreendoors" + description = "Detect Windows lock screen backdoors by checking FileDescriptions of accessibility binaries." + supported_protocols = ["smb"] + + def __init__(self): + # List of exe names with expected descriptions + self.expected_descriptions = { + "utilman.exe": ["Utility Manager"], + "narrator.exe": ["Screen Reader", "Narrator"], + "sethc.exe": ["Accessibility shortcut keys"], + "osk.exe": ["Accessibility On-Screen Keyboard"], + "magnify.exe": ["Microsoft Screen Magnifier"], + "EaseOfAccessDialog.exe": ["Ease of Access Dialog Host"], + "voiceaccess.exe": ["Voice access"], # Only on Windows 11 / Server 2025+ + "displayswitch.exe": ["Display Switch"], + "atbroker.exe": ["Windows Assistive Technology Manager", "Transitions Accessible technologies between desktops"], + } + + # If description matches one of these it's almost certainly backdoored + self.backdoor_descriptions = [ + "Windows Command Processor", + "Windows PowerShell" + ] + + def options(self, context, module_options): + """No options available""" + + def get_description(self, binary_data): + # Extract the file description from version info + try: + pe = pefile.PE(data=binary_data, fast_load=True) + pe.parse_data_directories(directories=[pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_RESOURCE"]]) + for fileinfo in pe.FileInfo: + for entry in fileinfo: + if entry.Key.decode() == "StringFileInfo": + for st in entry.StringTable: + desc = st.entries.get(b"FileDescription") + if desc: + return desc.decode().strip() + except Exception as e: + self.context.log.debug(f"Failed to extract PE info: {e}") + return None + + def on_admin_login(self, context, connection): + target_path = "\\Windows\\System32" + tampered = False + + for exe, expected_descs in self.expected_descriptions.items(): + try: + # Grab the binary from the share + buf = BytesIO() + connection.conn.getFile("C$", f"{target_path}\\{exe}", buf.write) + binary = buf.getvalue() + + # Extract and normalize the file description + file_desc = self.get_description(binary) + if not file_desc: + context.log.fail(f"{exe}: could not extract FileDescription") + continue + + # Check if the description is as expected + if file_desc not in expected_descs: + tampered = True + if file_desc in self.backdoor_descriptions: + context.log.highlight(f"BACKDOOR DETECTED: {exe} has FileDescription '{file_desc}'") + else: + if len(expected_descs) == 1: + expected_str = f"'{expected_descs[0]}'" + else: + expected_str = ", ".join(f"'{d}'" for d in expected_descs) + expected_str = f"one of: {expected_str}" + context.log.highlight(f"SUSPICIOUS: {exe} has unexpected FileDescription '{file_desc}' (expected {expected_str})") + except Exception as e: + context.log.debug(f"Failed to process {exe}: {e}") + + if not tampered: + context.log.display("All lock screen executable descriptions are consistent with the expected values") diff --git a/nxc/modules/masky.py b/nxc/modules/masky.py index 1acb509a..711ab1ea 100644 --- a/nxc/modules/masky.py +++ b/nxc/modules/masky.py @@ -79,8 +79,8 @@ class NXCModule: pwned_users = 0 for user in rslts.users: - if user.nthash: - context.log.highlight(f"{user.domain}\\{user.name} {user.nthash}") + if user.nt_hash: + context.log.highlight(f"{user.domain}\\{user.name} {user.nt_hash}") self.process_credentials(connection, context, user) pwned_users += 1 @@ -96,7 +96,7 @@ class NXCModule: "hash", user.domain, user.name, - user.nthash, + user.nt_hash, pillaged_from=host, ) add_user_bh(user.name, user.domain, context.log, connection.config) diff --git a/poetry.lock b/poetry.lock index 59646b80..75c01e8f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1252,14 +1252,14 @@ files = [ [[package]] name = "masky" -version = "0.2.0" +version = "0.2.1" description = "Python library with CLI allowing to remotely dump domain user credentials via an ADCS" optional = false python-versions = ">=3.6" groups = ["main"] files = [ - {file = "masky-0.2.0-py3-none-any.whl", hash = "sha256:04f29988e659bd265bf393c833ee473cfd16bf8a32ffdeaacfbefe8f466f53ab"}, - {file = "masky-0.2.0.tar.gz", hash = "sha256:fc0a99086da54e1cf91bb5e9c809aa311ea1519f10a3b6faf6d8e0a47c471ec9"}, + {file = "masky-0.2.1-py3-none-any.whl", hash = "sha256:bcf545f5e2b762fc49df2f29aa934498bb5dd37ba0a28e0c1683acee2e5ef14b"}, + {file = "masky-0.2.1.tar.gz", hash = "sha256:973615bebbd9455a1c2a81a6cec626a2032ec2209f242e3e69a6226e4b60b6fc"}, ] [package.dependencies] @@ -1436,6 +1436,18 @@ all = ["gssapi (>=1.4.1) ; platform_system != \"Windows\"", "invoke (>=2.0)", "p gssapi = ["gssapi (>=1.4.1) ; platform_system != \"Windows\"", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8) ; platform_system == \"Windows\""] invoke = ["invoke (>=2.0)"] +[[package]] +name = "pefile" +version = "2024.8.26" +description = "Python PE parsing module" +optional = false +python-versions = ">=3.6.0" +groups = ["main"] +files = [ + {file = "pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f"}, + {file = "pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632"}, +] + [[package]] name = "pillow" version = "11.1.0" @@ -2463,4 +2475,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "e02f61c9bb3bccd22fc51f90a9e09552afe50cb309155959dbb3ff06fea4d736" +content-hash = "d5a080dff08c7835da466af3c6dbe210650500a127b732f92fb1a417fee228f2" diff --git a/pyproject.toml b/pyproject.toml index cdbad433..fa70c076 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,10 +26,11 @@ dependencies = [ "dsinternals>=1.2.4", "jwt>=1.3.1", "lsassy>=3.1.11", - "masky>=0.2.0", + "masky>=0.2.1", "minikerberos>=0.4.1", "neo4j>=5.0.0", "paramiko>=3.3.1", + "pefile (>=2024.8.26,<2025.0.0)", "pyasn1-modules>=0.3.0", "pylnk3>=0.4.3", "pypsrp>=0.8.1", diff --git a/tests/e2e_commands.txt b/tests/e2e_commands.txt index 0c5cdfe3..5426522d 100644 --- a/tests/e2e_commands.txt +++ b/tests/e2e_commands.txt @@ -168,6 +168,7 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get_gpos netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get_gpos -o NAME="Default" -o FUZZY=True -o DOWNLOAD=False netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get_gpos -o NAME="Default" -o FUZZY=True -o ALL_PROPS=True -o DOWNLOAD=False netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M get_gpos -o ALL_PROPS=True -o DOWNLOAD=False +netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M lockscreendoors # test for multiple modules at once netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M spooler -M petitpotam -M zerologon -M nopac -M enum_av -M enum_dns -M gpp_autologin -M gpp_password -M lsassy -M impersonate -M install_elevated -M ioxidresolver -M ms17-010 -M ntlmv1 -M runasppl -M uac -M webdav -M wifi -M coerce_plus ##### SMB Anonymous Auth