Merge branch 'main' into marshall-get-gpos

Signed-off-by: Marshall Hallenbeck <Marshall.Hallenbeck@gmail.com>
This commit is contained in:
Marshall Hallenbeck
2025-08-17 14:59:31 -04:00
committed by GitHub
6 changed files with 110 additions and 8 deletions
+1
View File
@@ -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.
+87
View File
@@ -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")
+3 -3
View File
@@ -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)
Generated
+16 -4
View File
@@ -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"
+2 -1
View File
@@ -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",
+1
View File
@@ -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