From b685de05055bf478f41df4033a2c35ce230946fc Mon Sep 17 00:00:00 2001 From: R3dP1ll Date: Mon, 6 May 2024 22:52:24 +0200 Subject: [PATCH 1/2] Add files via upload --- .../plugins/apps/remoteaccess/remoteaccess.py | 19 +++- .../plugins/apps/remoteaccess/teamviewer.py | 103 +++++++++++++++++- 2 files changed, 120 insertions(+), 2 deletions(-) diff --git a/dissect/target/plugins/apps/remoteaccess/remoteaccess.py b/dissect/target/plugins/apps/remoteaccess/remoteaccess.py index 99cb3691a8..6d53bcb835 100644 --- a/dissect/target/plugins/apps/remoteaccess/remoteaccess.py +++ b/dissect/target/plugins/apps/remoteaccess/remoteaccess.py @@ -3,6 +3,7 @@ from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension from dissect.target.helpers.record import create_extended_descriptor from dissect.target.plugin import NamespacePlugin +from dissect.target.helpers.record import TargetRecordDescriptor GENERIC_LOG_RECORD_FIELDS = [ ("datetime", "ts"), @@ -24,7 +25,23 @@ RemoteAccessFileTransferRecord = create_extended_descriptor([UserRecordDescriptorExtension])( "remoteaccess/filetransfer", GENERIC_FILE_TRANSFER_RECORD_FIELDS ) - +RemoteAccessIncomingConnectionRecord = TargetRecordDescriptor( + "application/log/remoteaccess", + [ + + ("string", "tool"), + ("path", "logfile"), + ("string", "remote_tvid"), + ("string", "tv_user_host"), + ("string", "tv_user_host"), + ("datetime", "start_time"), + #("string","host"), + ("datetime", "end_time"), + ("string", "user_context"), + ("string", "connection_type"), + ("string", "connection_guid"), + ], +) class RemoteAccessPlugin(NamespacePlugin): """General Remote Access plugin. diff --git a/dissect/target/plugins/apps/remoteaccess/teamviewer.py b/dissect/target/plugins/apps/remoteaccess/teamviewer.py index 1790bca977..59e6ccb8fe 100644 --- a/dissect/target/plugins/apps/remoteaccess/teamviewer.py +++ b/dissect/target/plugins/apps/remoteaccess/teamviewer.py @@ -66,6 +66,11 @@ class TeamViewerPlugin(RemoteAccessPlugin): "/var/log/teamviewer*/*.log", ) + SYSTEM_INCOMING_GLOBS = [ + "sysvol/Program Files/TeamViewer/*_incoming.txt", + "sysvol/Program Files (x86)/TeamViewer/*_incoming.txt", + ] + USER_GLOBS = ( "AppData/Roaming/TeamViewer/teamviewer*_logfile.log", "Library/Logs/TeamViewer/teamviewer*_logfile*.log", @@ -79,12 +84,18 @@ def __init__(self, target: Target): super().__init__(target) self.logfiles: set[tuple[str, UserDetails | None]] = set() + self.incoming_logfiles: set[str] = set() # Find system service log files. for log_glob in self.SYSTEM_GLOBS: for logfile in self.target.fs.glob(log_glob): self.logfiles.add((logfile, None)) + # Find system incoming connection log files. + for log_glob in self.SYSTEM_INCOMING_GLOBS: + for logfile in self.target.fs.glob(log_glob): + self.incoming_logfiles.add(logfile) + # Find user log files. for user_details in self.target.user_details.all_with_home(): for log_glob in self.USER_GLOBS: @@ -92,7 +103,7 @@ def __init__(self, target: Target): self.logfiles.add((logfile, user_details)) def check_compatible(self) -> None: - if not len(self.logfiles): + if not len(self.logfiles) and not len(self.incoming_logfiles): raise UnsupportedPluginError("No Teamviewer logs found on target") @export(record=RemoteAccessLogRecord) @@ -169,6 +180,96 @@ def logs(self) -> Iterator[RemoteAccessLogRecord]: _user=user_details.user if user_details else None, ) + @export(record=RemoteAccessIncomingConnectionRecord) + def incoming_connections(self): + """Return the content of the TeamViewer incoming connections logs. + + TeamViewer is a commercial remote desktop application. An adversary may use it to gain persistence on a + system. + + References: + - https://www.teamviewer.com/nl/ + """ + hostname = str(self.target).split("Collection-")[1].split("-")[0] + for logfile in self.incoming_logfiles: + logfile = self.target.fs.path(logfile) + + with logfile.open("rt", encoding="latin-1") as file: + next(file) + while True: + try: + line = file.readline() + + except UnicodeDecodeError: + continue + + # End of file, quit while loop + if not line: + break + + line = line.strip() + + # Skip empty lines + if not line: + continue + + fields = line.split("\t") + if len(fields) < 7: + print("Line does not contain enough fields:", line) + continue + remote_teamviewer_id = fields[0] + username_or_hostname = fields[1] + # print(username_or_hostname) + starttime = datetime.strptime(fields[2], "%d-%m-%Y %H:%M:%S") # .strftime('%Y-%m-%d %H:%M:%S') + endtime = datetime.strptime(fields[3], "%d-%m-%Y %H:%M:%S") # .strftime('%Y/%m/%d %H:%M:%S') + connected_user = fields[4] + connection_type = fields[5] + connection_guid = fields[6].strip() # Remove any trailing whitespace + """ + # Older logs first mention the start time and then leave out the year + if line.startswith("Start:"): + start_date = datetime.strptime(line.split()[1], "%Y/%m/%d") + + # Sometimes there are weird, mult-line/pretty print log messages. + # We only parse the start line which starts with year (%Y/) or month (%m/) + if not re.match(START_PATTERN, line): + continue + + ts_day, ts_time, description = line.split(" ", 2) + ts_time = ts_time.split(".")[0] + + # Correct for use of : as millisecond separator + if ts_time.count(":") > 2: + ts_time = ":".join(ts_time.split(":")[:3]) + # Correct for missing year in date + if ts_day.count("/") == 1: + if not start_date: + self.target.log.debug("Missing year in log line, skipping line.") + continue + ts_day = f"{start_date.year}/{ts_day}" + # Correct for year if short notation for 2000 is used + if ts_day.count("/") == 2 and len(ts_day.split("/")[0]) == 2: + ts_day = "20" + ts_day + + timestamp = datetime.strptime(f"{ts_day} {ts_time}", "%Y/%m/%d %H:%M:%S") + + + """ + # print(starttime) + # print(endtime) + yield RemoteAccessIncomingConnectionRecord( + tool="teamviewer", + logfile=str(logfile), + remote_tvid=remote_teamviewer_id, + tv_user_host=username_or_hostname, + start_time=starttime, + end_time=endtime, + user_context=connected_user, + connection_type=connection_type, + connection_guid=connection_guid, + _target=self.target, + ) + def parse_start(line: str) -> datetime | None: """TeamViewer ``Start`` messages can be formatted in different ways From ba104b41c4346114030b770a5bc46fe58fa1c1a3 Mon Sep 17 00:00:00 2001 From: Schamper <1254028+Schamper@users.noreply.github.com> Date: Fri, 9 Jan 2026 13:36:09 +0100 Subject: [PATCH 2/2] Changes --- .../plugins/apps/remoteaccess/remoteaccess.py | 19 +-- .../plugins/apps/remoteaccess/teamviewer.py | 139 +++++++----------- .../apps/remoteaccess/test_teamviewer.py | 30 ++++ 3 files changed, 85 insertions(+), 103 deletions(-) diff --git a/dissect/target/plugins/apps/remoteaccess/remoteaccess.py b/dissect/target/plugins/apps/remoteaccess/remoteaccess.py index 6d53bcb835..99cb3691a8 100644 --- a/dissect/target/plugins/apps/remoteaccess/remoteaccess.py +++ b/dissect/target/plugins/apps/remoteaccess/remoteaccess.py @@ -3,7 +3,6 @@ from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension from dissect.target.helpers.record import create_extended_descriptor from dissect.target.plugin import NamespacePlugin -from dissect.target.helpers.record import TargetRecordDescriptor GENERIC_LOG_RECORD_FIELDS = [ ("datetime", "ts"), @@ -25,23 +24,7 @@ RemoteAccessFileTransferRecord = create_extended_descriptor([UserRecordDescriptorExtension])( "remoteaccess/filetransfer", GENERIC_FILE_TRANSFER_RECORD_FIELDS ) -RemoteAccessIncomingConnectionRecord = TargetRecordDescriptor( - "application/log/remoteaccess", - [ - - ("string", "tool"), - ("path", "logfile"), - ("string", "remote_tvid"), - ("string", "tv_user_host"), - ("string", "tv_user_host"), - ("datetime", "start_time"), - #("string","host"), - ("datetime", "end_time"), - ("string", "user_context"), - ("string", "connection_type"), - ("string", "connection_guid"), - ], -) + class RemoteAccessPlugin(NamespacePlugin): """General Remote Access plugin. diff --git a/dissect/target/plugins/apps/remoteaccess/teamviewer.py b/dissect/target/plugins/apps/remoteaccess/teamviewer.py index 59e6ccb8fe..0a85d1c51e 100644 --- a/dissect/target/plugins/apps/remoteaccess/teamviewer.py +++ b/dissect/target/plugins/apps/remoteaccess/teamviewer.py @@ -1,12 +1,12 @@ from __future__ import annotations import re -from datetime import datetime +from datetime import datetime, timezone from typing import TYPE_CHECKING from dissect.target.exceptions import UnsupportedPluginError from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension -from dissect.target.helpers.record import create_extended_descriptor +from dissect.target.helpers.record import TargetRecordDescriptor, create_extended_descriptor from dissect.target.plugin import export from dissect.target.plugins.apps.remoteaccess.remoteaccess import ( GENERIC_LOG_RECORD_FIELDS, @@ -49,6 +49,20 @@ ) +TeamviewerIncomingRecord = TargetRecordDescriptor( + "remoteaccess/teamviewer/incoming", + [ + ("datetime", "ts"), + ("datetime", "end"), + ("string", "remote_id"), + ("string", "name"), + ("string", "user"), + ("string", "connection_type"), + ("string", "connection_id"), + ], +) + + class TeamViewerPlugin(RemoteAccessPlugin): """TeamViewer client plugin. @@ -66,10 +80,10 @@ class TeamViewerPlugin(RemoteAccessPlugin): "/var/log/teamviewer*/*.log", ) - SYSTEM_INCOMING_GLOBS = [ + SYSTEM_INCOMING_GLOBS = ( "sysvol/Program Files/TeamViewer/*_incoming.txt", "sysvol/Program Files (x86)/TeamViewer/*_incoming.txt", - ] + ) USER_GLOBS = ( "AppData/Roaming/TeamViewer/teamviewer*_logfile.log", @@ -180,95 +194,50 @@ def logs(self) -> Iterator[RemoteAccessLogRecord]: _user=user_details.user if user_details else None, ) - @export(record=RemoteAccessIncomingConnectionRecord) - def incoming_connections(self): - """Return the content of the TeamViewer incoming connections logs. + @export(record=TeamviewerIncomingRecord) + def incoming(self) -> Iterator[TeamviewerIncomingRecord]: + """Yield TeamViewer incoming connection logs. - TeamViewer is a commercial remote desktop application. An adversary may use it to gain persistence on a - system. - - References: - - https://www.teamviewer.com/nl/ + TeamViewer is a commercial remote desktop application. An adversary may use it to gain persistence on a system. """ - hostname = str(self.target).split("Collection-")[1].split("-")[0] for logfile in self.incoming_logfiles: logfile = self.target.fs.path(logfile) - with logfile.open("rt", encoding="latin-1") as file: - next(file) - while True: - try: - line = file.readline() - - except UnicodeDecodeError: - continue - - # End of file, quit while loop - if not line: - break - - line = line.strip() - - # Skip empty lines - if not line: - continue + for line in logfile.open("rt", errors="replace"): + if not (line := line.strip()) or line.startswith("# "): + continue - fields = line.split("\t") - if len(fields) < 7: - print("Line does not contain enough fields:", line) - continue - remote_teamviewer_id = fields[0] - username_or_hostname = fields[1] - # print(username_or_hostname) - starttime = datetime.strptime(fields[2], "%d-%m-%Y %H:%M:%S") # .strftime('%Y-%m-%d %H:%M:%S') - endtime = datetime.strptime(fields[3], "%d-%m-%Y %H:%M:%S") # .strftime('%Y/%m/%d %H:%M:%S') - connected_user = fields[4] - connection_type = fields[5] - connection_guid = fields[6].strip() # Remove any trailing whitespace - """ - # Older logs first mention the start time and then leave out the year - if line.startswith("Start:"): - start_date = datetime.strptime(line.split()[1], "%Y/%m/%d") - - # Sometimes there are weird, mult-line/pretty print log messages. - # We only parse the start line which starts with year (%Y/) or month (%m/) - if not re.match(START_PATTERN, line): - continue + fields = line.split("\t") + if len(fields) < 7: + self.target.log.warning("Skipping TeamViewer incoming connection log line %r in %s", line, logfile) + continue - ts_day, ts_time, description = line.split(" ", 2) - ts_time = ts_time.split(".")[0] - - # Correct for use of : as millisecond separator - if ts_time.count(":") > 2: - ts_time = ":".join(ts_time.split(":")[:3]) - # Correct for missing year in date - if ts_day.count("/") == 1: - if not start_date: - self.target.log.debug("Missing year in log line, skipping line.") - continue - ts_day = f"{start_date.year}/{ts_day}" - # Correct for year if short notation for 2000 is used - if ts_day.count("/") == 2 and len(ts_day.split("/")[0]) == 2: - ts_day = "20" + ts_day - - timestamp = datetime.strptime(f"{ts_day} {ts_time}", "%Y/%m/%d %H:%M:%S") - - - """ - # print(starttime) - # print(endtime) - yield RemoteAccessIncomingConnectionRecord( - tool="teamviewer", - logfile=str(logfile), - remote_tvid=remote_teamviewer_id, - tv_user_host=username_or_hostname, - start_time=starttime, - end_time=endtime, - user_context=connected_user, - connection_type=connection_type, - connection_guid=connection_guid, - _target=self.target, + try: + start = datetime.strptime(fields[2], "%d-%m-%Y %H:%M:%S").replace(tzinfo=timezone.utc) + end = datetime.strptime(fields[3], "%d-%m-%Y %H:%M:%S").replace(tzinfo=timezone.utc) + except Exception as e: + self.target.log.warning( + "Unable to parse timestamps in TeamViewer incoming connection log line %r in %s", line, logfile ) + self.target.log.debug("", exc_info=e) + continue + + remote_id = fields[0] + name = fields[1] + user = fields[4] + connection_type = fields[5] + connection_id = fields[6] + + yield TeamviewerIncomingRecord( + ts=start, + end=end, + remote_id=remote_id, + name=name, + user=user, + connection_type=connection_type, + connection_id=connection_id, + _target=self.target, + ) def parse_start(line: str) -> datetime | None: diff --git a/tests/plugins/apps/remoteaccess/test_teamviewer.py b/tests/plugins/apps/remoteaccess/test_teamviewer.py index 26631094eb..ab38144a4e 100644 --- a/tests/plugins/apps/remoteaccess/test_teamviewer.py +++ b/tests/plugins/apps/remoteaccess/test_teamviewer.py @@ -106,3 +106,33 @@ def test_teamviewer_timezone(target_win_users: Target, fs_win: VirtualFilesystem assert records[0].message == "1234 5678 G1 LanguageControl: device language is 'enUS'" assert records[0].source == "C:\\Users\\John\\AppData\\Roaming\\TeamViewer\\TeamViewer1337_Logfile.log" assert records[0].username == "John" + + +def test_teamviewer_incoming(target_win_users: Target, fs_win: VirtualFilesystem) -> None: + """Test TeamViewer incoming connection log parsing.""" + log = """ + 1031857653 DESKTOP-CAK7OMO 11-09-2022 14:44:03 11-09-2022 15:27:53 SERVER TV RemoteControl {C2CC2F16-D1F4-4547-9928-EE63891D4CC0} + 1031857653 DESKTOP-CAK7OMO 22-12-2022 19:25:22 22-12-2022 19:49:28 Server RemoteControl {4BF22BA7-32BA-4F64-8755-97E6E45F9883} + """ # noqa: E501 + fs_win.map_file_fh("Program Files/TeamViewer/Connections_incoming.txt", BytesIO(dedent(log).encode())) + + target_win_users.add_plugin(TeamViewerPlugin) + + records = list(target_win_users.teamviewer.incoming()) + assert len(records) == 2 + + assert records[0].ts == datetime(2022, 9, 11, 14, 44, 3, tzinfo=timezone.utc) + assert records[0].end == datetime(2022, 9, 11, 15, 27, 53, tzinfo=timezone.utc) + assert records[0].remote_id == "1031857653" + assert records[0].name == "DESKTOP-CAK7OMO" + assert records[0].user == "SERVER TV" + assert records[0].connection_type == "RemoteControl" + assert records[0].connection_id == "{C2CC2F16-D1F4-4547-9928-EE63891D4CC0}" + + assert records[1].ts == datetime(2022, 12, 22, 19, 25, 22, tzinfo=timezone.utc) + assert records[1].end == datetime(2022, 12, 22, 19, 49, 28, tzinfo=timezone.utc) + assert records[1].remote_id == "1031857653" + assert records[1].name == "DESKTOP-CAK7OMO" + assert records[1].user == "Server" + assert records[1].connection_type == "RemoteControl" + assert records[1].connection_id == "{4BF22BA7-32BA-4F64-8755-97E6E45F9883}"