-
Notifications
You must be signed in to change notification settings - Fork 103
/
Copy pathmsrdc.py
245 lines (201 loc) · 12.1 KB
/
msrdc.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
"""
Copyright (c) 2024 Minoru Kobayashi
This file is part of mac_apt (macOS Artifact Parsing Tool).
Usage or distribution of this software/code is subject to the
terms of the MIT License.
msrdc.py
---------------
This plugin parses Microsoft Remote Desktop settings (host names, group names, last connected timestamp, and shared folders) and extracts thumbnails.
TODO: Add support for parsing the following tables: ZGATEWAYENTITY, ZWORKSPACEENTITY (These are probably related to RD Gateway and Workspaces)
"""
from __future__ import annotations
import logging
import os
import sqlite3
import nska_deserialize as nd
from plugins.helpers.common import CommonFunctions
from plugins.helpers.macinfo import DataType, MacInfo, MountedIosInfo, OutputParams, SqliteWrapper
from plugins.helpers.writer import WriteList
__Plugin_Name = "MSRDC" # Cannot have spaces, and must be all caps!
__Plugin_Friendly_Name = "Microsoft Remote Desktop Client"
__Plugin_Version = "1.0"
__Plugin_Description = "Parses Microsoft Remote Desktop settings and extracts thumbnails"
__Plugin_Author = "Minoru Kobayashi"
__Plugin_Author_Email = "unknownbit@gmail.com"
__Plugin_Modes = "MACOS,ARTIFACTONLY" # Valid values are 'MACOS', 'IOS, 'ARTIFACTONLY'
__Plugin_ArtifactOnly_Usage = "Provide folder path(s) that contains XProtect diagnostic files or XProtect Behavior Service diagnostic database files."
log = logging.getLogger("MAIN." + __Plugin_Name) # Do not rename or remove this ! This is the logger object
# ---- Do not change the variable names in above section ----#
class MSRDCItem:
def __init__(self: MSRDCItem, date: str, conn_minutes: str,
hostname: str, host_id: str, friendly_hostname: str, groupname: str,
use_credential: str, friendly_credential_name: str, username: str, nil_passwd: str,
folder_redirection: str, source: str) -> None: # fmt: skip
self.date = date
self.conn_minutes = conn_minutes
self.hostname = hostname
self.host_id = host_id
self.friendly_hostname = friendly_hostname
self.groupname = groupname
self.use_credential = use_credential
self.friendly_credential_name = friendly_credential_name
self.username = username
self.nil_passwd = nil_passwd
self.folder_redirection = folder_redirection
self.source = source
def OpenDb(inputPath: str) -> sqlite3.Connection | None:
log.info("Processing file " + inputPath)
try:
conn = CommonFunctions.open_sqlite_db_readonly(inputPath)
log.debug("Opened database successfully")
except sqlite3.Error:
log.exception("Failed to open database, is it a valid DB?")
return None
else:
return conn
def OpenDbFromImage(mac_info: MacInfo, inputPath: str) -> tuple[sqlite3.Connection | None, SqliteWrapper | None]:
"""Returns tuple of (connection, wrapper_obj)"""
log.info(f"Processing MSRDC database {inputPath}")
try:
sqlite = SqliteWrapper(mac_info)
conn = sqlite.connect(inputPath)
if conn:
log.debug("Opened database successfully")
except sqlite3.Error:
log.exception("Failed to open database, is it a valid DB?")
return None, None
else:
return conn, sqlite
def ParseMSRDCdb(db: sqlite3.Connection, msrdc_artifacts: list[MSRDCItem], msrdc_path: str) -> None:
db.row_factory = sqlite3.Row
tables = CommonFunctions.GetTableNames(db)
if "ZBOOKMARKENTITY" in tables:
query = """SELECT ZLASTCONNECTED,
ZHOSTNAME, ZBOOKMARKENTITY.ZFRIENDLYNAME AS FriendlyPCName,
ZBOOKMARKFOLDERENTITY.ZTITLE,
CASE
WHEN ZCREDENTIAL IS NULL THEN 'Ask when required'
ELSE 'Use User Account setting'
END AS Credential,
ZCREDENTIALENTITY.ZFRIENDLYNAME AS FriendlyAccountName, ZCREDENTIALENTITY.ZUSERNAME,
CASE ZCREDENTIALENTITY.ZNILPASSWORD
WHEN 0 THEN 'False'
WHEN 1 THEN 'True'
END AS NilPassword,
ZFOLDERREDIRECTIONCOLLECTION, ZBOOKMARKENTITY.ZID FROM ZBOOKMARKENTITY
LEFT JOIN ZBOOKMARKFOLDERENTITY ON ZBOOKMARKENTITY.ZBOOKMARKFOLDER = ZBOOKMARKFOLDERENTITY.Z_PK
LEFT JOIN ZCREDENTIALENTITY ON ZBOOKMARKENTITY.ZCREDENTIAL = ZCREDENTIALENTITY.Z_PK"""
cursor = db.execute(query)
for row in cursor:
try:
last_connected = nd.deserialize_plist_from_string(row["ZLASTCONNECTED"])["root"]
except (nd.DeserializeError, nd.biplist.NotBinaryPlistException, nd.ccl_bplist.BplistError, nd.plistlib.InvalidFileException, ValueError, TypeError, OSError, OverflowError):
last_connected = ''
folder_redirection_info = [
f"Path: {folder_redirection['path']}, Name: {folder_redirection['name']}, ReadOnly: {folder_redirection['readOnly']}"
for folder_redirection in nd.deserialize_plist_from_string(row["ZFOLDERREDIRECTIONCOLLECTION"])
]
folder_redirection_collection = "; ".join(folder_redirection_info)
item = MSRDCItem(last_connected, "", row['ZHOSTNAME'], row['ZID'], row['FriendlyPCName'], row['ZTITLE'],
row['Credential'], row['FriendlyAccountName'], row['ZUSERNAME'],
row['NilPassword'], folder_redirection_collection, msrdc_path) # fmt: skip
msrdc_artifacts.append(item)
else:
log.error("There is no ZBOOKMARKENTITY table.")
if "ZCONNECTIONTIMEENTITY" in tables:
query = """SELECT ZSTARTTIME, ZMINUTESCONNECTED,
ZBOOKMARKENTITY.ZHOSTNAME, ZBOOKMARKENTITY.ZFRIENDLYNAME AS FriendlyPCName,
ZBOOKMARKFOLDERENTITY.ZTITLE,
CASE
WHEN ZCREDENTIAL IS NULL THEN 'Ask when required'
ELSE 'Use User Account setting'
END AS Credential,
ZCREDENTIALENTITY.ZFRIENDLYNAME AS FriendlyAccountName, ZCREDENTIALENTITY.ZUSERNAME,
CASE ZCREDENTIALENTITY.ZNILPASSWORD
WHEN 0 THEN 'False'
WHEN 1 THEN 'True'
END AS NilPassword,
ZFOLDERREDIRECTIONCOLLECTION, ZBOOKMARKENTITY.ZID FROM ZCONNECTIONTIMEENTITY
LEFT JOIN ZBOOKMARKENTITY ON ZCONNECTIONTIMEENTITY.Z_OPT = ZBOOKMARKENTITY.Z_PK
LEFT JOIN ZBOOKMARKFOLDERENTITY ON ZBOOKMARKENTITY.ZBOOKMARKFOLDER = ZBOOKMARKFOLDERENTITY.Z_PK
LEFT JOIN ZCREDENTIALENTITY ON ZBOOKMARKENTITY.ZCREDENTIAL = ZCREDENTIALENTITY.Z_PK"""
cursor = db.execute(query)
for row in cursor:
start_time = CommonFunctions.ReadMacAbsoluteTime(row["ZSTARTTIME"]).strftime("%Y-%m-%d %H:%M:%S.%f")
if row["ZFOLDERREDIRECTIONCOLLECTION"] is None:
folder_redirection_info = []
else:
folder_redirection_info = [
f"Path: {folder_redirection['path']}, Name: {folder_redirection['name']}, ReadOnly: {folder_redirection['readOnly']}"
for folder_redirection in nd.deserialize_plist_from_string(row["ZFOLDERREDIRECTIONCOLLECTION"])
]
folder_redirection_collection = "; ".join(folder_redirection_info)
item = MSRDCItem(start_time, row["ZMINUTESCONNECTED"], row['ZHOSTNAME'], row['ZID'], row['FriendlyPCName'], row['ZTITLE'],
row['Credential'], row['FriendlyAccountName'], row['ZUSERNAME'],
row['NilPassword'], folder_redirection_collection, msrdc_path) # fmt: skip
msrdc_artifacts.append(item)
else:
log.error("There is no ZCONNECTIONTIMEENTITY table.")
def ExtractAndReadMSRDC(mac_info: MacInfo, msrdc_artifacts: list[MSRDCItem], username: str, msrdc_path: str) -> None:
db, wrapper = OpenDbFromImage(mac_info, msrdc_path)
if db:
mac_info.ExportFile(msrdc_path, __Plugin_Name, username + "_", overwrite=True)
ParseMSRDCdb(db, msrdc_artifacts, msrdc_path)
db.close()
def OpenAndReadMSRDC(msrdc_artifacts: list[MSRDCItem], msrdc_path: str) -> None:
db = OpenDb(msrdc_path)
if db:
ParseMSRDCdb(db, msrdc_artifacts, msrdc_path)
db.close()
def PrintAll(msrdc_artifacts: list[MSRDCItem], output_params: OutputParams, source_path: str) -> None:
msrdc_info = [('Date', DataType.TEXT), ('Connection_Minutes', DataType.TEXT),
('Hostname', DataType.TEXT), ('Friendly_Hostname', DataType.TEXT), ('Groupname', DataType.TEXT),
('Use_Credential', DataType.TEXT), ('Friendly_Credential_Name', DataType.TEXT), ('Username', DataType.TEXT),
('Nil_Password', DataType.TEXT), ('Folder_Redirection', DataType.TEXT), ('Host_ID', DataType.TEXT), ('Source', DataType.TEXT)] # fmt: skip
log.info(f"{len(msrdc_artifacts)} Microsoft Remote Desktop Client artifact(s) found")
data_list = [[item.date, item.conn_minutes, item.hostname, item.friendly_hostname, item.groupname,
item.use_credential, item.friendly_credential_name, item.username, item.nil_passwd,
item.folder_redirection, item.host_id, item.source] for item in msrdc_artifacts] # fmt: skip
WriteList("MSRDC", "MSRDC", data_list, msrdc_info, output_params, source_path)
def Plugin_Start(mac_info: MacInfo) -> None:
"""Main Entry point function for plugin"""
msrdc_artifacts: list[MSRDCItem] = []
msrdc_db_base_path = "{}/Library/Containers/com.microsoft.rdc.macos/Data/Library/Application Support/com.microsoft.rdc.macos/com.microsoft.rdc.application-data.sqlite"
msrdc_thumbs_base_path = "{}/Library/Containers/com.microsoft.rdc.macos/Data/Library/Application Support/com.microsoft.rdc.macos/SupportingImages/" # fmt: skip
processed_paths: set[str] = set()
for user in mac_info.users:
if user.home_dir in processed_paths:
continue # Avoid processing same folder twice (some users have same folder! (Eg: root & daemon))
processed_paths.add(user.home_dir)
msrdc_db_path = msrdc_db_base_path.format(user.home_dir)
if mac_info.IsValidFilePath(msrdc_db_path):
ExtractAndReadMSRDC(mac_info, msrdc_artifacts, user.user_name, msrdc_db_path)
msrdc_thumbs_path = msrdc_thumbs_base_path.format(user.home_dir)
if mac_info.IsValidFolderPath(msrdc_thumbs_path):
mac_info.ExportFolder(msrdc_thumbs_path, os.path.join(__Plugin_Name, user.user_name), overwrite=True)
if len(msrdc_artifacts) > 0:
PrintAll(msrdc_artifacts, mac_info.output_params, "")
log.info("The filenames of thumbnails are the same as the value of Host_ID column in the MSRDC table, and their format is TIFF.")
else:
log.info("No Microsoft Remote Desktop Client artifacts were found!")
def Plugin_Start_Standalone(input_files_list: list[str], output_params: OutputParams) -> None:
"""Main entry point function when used on single artifacts (mac_apt_singleplugin), not on a full disk image"""
log.info("Module Started as standalone")
log.info("MSRDC plugin in standalone mode does not extract thumbnails.")
msrdc_artifacts: list[MSRDCItem] = []
for input_path in input_files_list:
log.debug("Input file passed was: " + input_path)
if os.path.isfile(input_path) and os.path.getsize(input_path) > 0:
if input_path.endswith("com.microsoft.rdc.application-data.sqlite"):
log.debug(f"Processing {input_path}")
OpenAndReadMSRDC(msrdc_artifacts, input_path)
else:
log.info(f"File {input_path} does not exist or is empty.")
if len(msrdc_artifacts) > 0:
PrintAll(msrdc_artifacts, output_params, input_path)
else:
log.info("No Microsoft Remote Desktop Client artifacts were found!")
def Plugin_Start_Ios(ios_info: MountedIosInfo) -> None:
"""Entry point for ios_apt plugin"""
if __name__ == "__main__":
print("This plugin is a part of a framework and does not run independently on its own!")