diff --git a/dissect/target/plugins/apps/remoteaccess/teamviewer.py b/dissect/target/plugins/apps/remoteaccess/teamviewer.py index 1790bca977..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,6 +80,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 +98,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 +117,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 +194,51 @@ def logs(self) -> Iterator[RemoteAccessLogRecord]: _user=user_details.user if user_details else None, ) + @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. + """ + for logfile in self.incoming_logfiles: + logfile = self.target.fs.path(logfile) + + 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: + self.target.log.warning("Skipping TeamViewer incoming connection log line %r in %s", line, logfile) + continue + + 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: """TeamViewer ``Start`` messages can be formatted in different ways 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}"