dissect.target 3.15.dev33__py3-none-any.whl → 3.15.dev38__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import gzip
4
+ import hashlib
4
5
  from base64 import b64decode
5
6
  from datetime import datetime
7
+ from io import BytesIO
6
8
  from tarfile import ReadError
7
- from typing import Iterator, Optional, TextIO, Union
9
+ from typing import BinaryIO, Iterator, Optional, TextIO, Union
8
10
 
9
- from Crypto.Cipher import AES
10
11
  from dissect.util import cpio
11
12
  from dissect.util.compression import xz
12
13
 
@@ -18,6 +19,13 @@ from dissect.target.plugin import OperatingSystem, export
18
19
  from dissect.target.plugins.os.unix.linux._os import LinuxPlugin
19
20
  from dissect.target.target import Target
20
21
 
22
+ try:
23
+ from Crypto.Cipher import AES, ChaCha20
24
+
25
+ HAS_PYCRYPTODOME = True
26
+ except ImportError:
27
+ HAS_PYCRYPTODOME = False
28
+
21
29
  FortiOSUserRecord = TargetRecordDescriptor(
22
30
  "fortios/user",
23
31
  [
@@ -39,7 +47,7 @@ class FortiOSPlugin(LinuxPlugin):
39
47
 
40
48
  def _load_config(self) -> dict:
41
49
  CONFIG_FILES = {
42
- "/data/system.conf": None,
50
+ "/data/system.conf": "global-config", # FortiManager
43
51
  "/data/config/daemon.conf.gz": "daemon", # FortiOS 4.x
44
52
  "/data/config/sys_global.conf.gz": "global-config", # Seen in FortiOS 5.x - 7.x
45
53
  "/data/config/sys_vd_root.conf.gz": "root-config", # FortiOS 4.x
@@ -55,7 +63,7 @@ class FortiOSPlugin(LinuxPlugin):
55
63
  else:
56
64
  fh = conf_path.open("rt")
57
65
 
58
- if not self._version and section in [None, "global-config", "root-config"]:
66
+ if not self._version and section in ["global-config", "root-config"]:
59
67
  self._version = fh.readline().split("=", 1)[1]
60
68
 
61
69
  parsed = FortiOSConfig.from_fh(fh)
@@ -72,20 +80,31 @@ class FortiOSPlugin(LinuxPlugin):
72
80
 
73
81
  @classmethod
74
82
  def create(cls, target: Target, sysvol: Filesystem) -> FortiOSPlugin:
83
+ target.log.warning("Attempting to load rootfs.gz, this can take a while.")
75
84
  rootfs = sysvol.path("/rootfs.gz")
85
+ vfs = None
76
86
 
77
87
  try:
78
- target.log.warning("Attempting to load compressed rootfs.gz, this can take a while.")
79
- rfs_fh = open_decompress(rootfs)
80
- if rfs_fh.read(4) == b"07" * 2:
88
+ if open_decompress(rootfs).read(4) == b"0707":
81
89
  vfs = TarFilesystem(rootfs.open(), tarinfo=cpio.CpioInfo)
82
90
  else:
83
91
  vfs = TarFilesystem(rootfs.open())
92
+ except ReadError:
93
+ # The rootfs.gz file could be encrypted.
94
+ try:
95
+ rfs_fh = decrypt_rootfs(rootfs.open(), get_kernel_hash(sysvol))
96
+ vfs = TarFilesystem(rfs_fh, tarinfo=cpio.CpioInfo)
97
+ except RuntimeError:
98
+ target.log.warning("Could not decrypt rootfs.gz. Missing `pycryptodome` dependency.")
99
+ except ValueError as e:
100
+ target.log.warning("Could not decrypt rootfs.gz. Unsupported kernel version.")
101
+ target.log.debug("", exc_info=e)
102
+ except ReadError as e:
103
+ target.log.warning("Could not mount rootfs.gz. It could be corrupt.")
104
+ target.log.debug("", exc_info=e)
105
+
106
+ if vfs:
84
107
  target.fs.mount("/", vfs)
85
- except ReadError as e:
86
- # Since FortiOS version ~7.4.1 the rootfs.gz file is encrypted.
87
- target.log.warning("Could not mount FortiOS `/rootfs.gz`. It could be encrypted or corrupt.")
88
- target.log.debug("", exc_info=e)
89
108
 
90
109
  target.fs.mount("/data", sysvol)
91
110
 
@@ -93,13 +112,21 @@ class FortiOSPlugin(LinuxPlugin):
93
112
  if (datafs_tar := sysvol.path("/datafs.tar.gz")).exists():
94
113
  target.fs.add_layer().mount("/data", TarFilesystem(datafs_tar.open("rb")))
95
114
 
96
- # Additional FortiGate tars with corrupt XZ streams
97
- for path in ("bin.tar.xz", "usr.tar.xz", "migadmin.tar.xz", "node-scripts.tar.xz"):
98
- if (tar := target.fs.path(path)).exists():
115
+ # Additional FortiGate or FortiManager tars with corrupt XZ streams
116
+ target.log.warning("Attempting to load XZ files, this can take a while.")
117
+ for path in (
118
+ "bin.tar.xz",
119
+ "usr.tar.xz",
120
+ "migadmin.tar.xz",
121
+ "node-scripts.tar.xz",
122
+ "docker.tar.xz",
123
+ "syntax.tar.xz",
124
+ ):
125
+ if (tar := target.fs.path(path)).exists() or (tar := sysvol.path(path)).exists():
99
126
  fh = xz.repair_checksum(tar.open("rb"))
100
127
  target.fs.add_layer().mount("/", TarFilesystem(fh))
101
128
 
102
- # FortiAnalyzer
129
+ # FortiAnalyzer and FortiManager
103
130
  if (rootfs_ext_tar := sysvol.path("rootfs-ext.tar.xz")).exists():
104
131
  target.fs.add_layer().mount("/", TarFilesystem(rootfs_ext_tar.open("rb")))
105
132
 
@@ -117,9 +144,18 @@ class FortiOSPlugin(LinuxPlugin):
117
144
  target.fs.mount("/boot", fs)
118
145
 
119
146
  # data2 partition
120
- if fs.__type__ == "ext" and fs.path("/new_alert_msg").exists() and fs.path("/template").exists():
147
+ if fs.__type__ == "ext" and (
148
+ (fs.path("/new_alert_msg").exists() and fs.path("/template").exists()) # FortiGate
149
+ or (fs.path("/swapfile").exists() and fs.path("/old_fmversion").exists()) # FortiManager
150
+ ):
121
151
  target.fs.mount("/data2", fs)
122
152
 
153
+ # Symlink unix-like paths
154
+ unix_paths = [("/data/passwd", "/etc/passwd")]
155
+ for src, dst in unix_paths:
156
+ if target.fs.path(src).exists() and not target.fs.path(dst).exists():
157
+ target.fs.symlink(src, dst)
158
+
123
159
  return cls(target)
124
160
 
125
161
  @export(property=True)
@@ -158,8 +194,11 @@ class FortiOSPlugin(LinuxPlugin):
158
194
  def dns(self) -> list[str]:
159
195
  """Return configured WAN DNS servers."""
160
196
  entries = []
161
- for _, entry in self._config["global-config"]["system"]["dns"].items():
162
- entries.append(entry[0])
197
+ try:
198
+ for entry in self._config["global-config"]["system"]["dns"].values():
199
+ entries.append(entry[0])
200
+ except KeyError:
201
+ pass
163
202
  return entries
164
203
 
165
204
  @export(property=True)
@@ -176,7 +215,7 @@ class FortiOSPlugin(LinuxPlugin):
176
215
  # Possible unix-like users
177
216
  yield from super().users()
178
217
 
179
- # Administrative users
218
+ # FortiGate administrative users
180
219
  try:
181
220
  for username, entry in self._config["global-config"]["system"]["admin"].items():
182
221
  yield FortiOSUserRecord(
@@ -190,13 +229,27 @@ class FortiOSPlugin(LinuxPlugin):
190
229
  self.target.log.warning("Exception while parsing FortiOS admin users")
191
230
  self.target.log.debug("", exc_info=e)
192
231
 
232
+ # FortiManager administrative users
233
+ try:
234
+ for username, entry in self._config["global-config"]["system"]["admin"]["user"].items():
235
+ yield FortiOSUserRecord(
236
+ name=username,
237
+ password=":".join(entry.get("password", [])),
238
+ groups=[entry["profileid"][0]],
239
+ home="/root",
240
+ _target=self.target,
241
+ )
242
+ except KeyError as e:
243
+ self.target.log.warning("Exception while parsing FortiManager admin users")
244
+ self.target.log.debug("", exc_info=e)
245
+
193
246
  # Local users
194
247
  try:
195
248
  local_groups = local_groups_to_users(self._config["root-config"]["user"]["group"])
196
249
  for username, entry in self._config["root-config"]["user"].get("local", {}).items():
197
250
  try:
198
251
  password = decrypt_password(entry["passwd"][-1])
199
- except ValueError:
252
+ except (ValueError, RuntimeError):
200
253
  password = ":".join(entry.get("passwd", []))
201
254
 
202
255
  yield FortiOSUserRecord(
@@ -215,7 +268,7 @@ class FortiOSPlugin(LinuxPlugin):
215
268
  for _, entry in self._config["root-config"]["user"]["group"].get("guestgroup", {}).get("guest", {}).items():
216
269
  try:
217
270
  password = decrypt_password(entry.get("password")[-1])
218
- except ValueError:
271
+ except (ValueError, RuntimeError):
219
272
  password = ":".join(entry.get("password"))
220
273
 
221
274
  yield FortiOSUserRecord(
@@ -236,7 +289,10 @@ class FortiOSPlugin(LinuxPlugin):
236
289
  @export(property=True)
237
290
  def architecture(self) -> Optional[str]:
238
291
  """Return architecture FortiOS runs on."""
239
- return self._get_architecture(path="/lib/libav.so")
292
+ paths = ["/lib/libav.so", "/bin/ctr"]
293
+ for path in paths:
294
+ if self.target.fs.path(path).exists():
295
+ return self._get_architecture(path=path)
240
296
 
241
297
 
242
298
  class ConfigNode(dict):
@@ -344,7 +400,7 @@ def parse_version(input: str) -> str:
344
400
  }
345
401
 
346
402
  try:
347
- version_str = input.split(":", 1)[0]
403
+ version_str = input.split(":", 1)[0].strip()
348
404
  type, version, _, build_num, build_date = version_str.rsplit("-", 4)
349
405
 
350
406
  build_num = build_num.replace("build", "build ", 1)
@@ -368,15 +424,111 @@ def local_groups_to_users(config_groups: dict) -> dict:
368
424
  return user_groups
369
425
 
370
426
 
371
- def decrypt_password(ciphertext: str) -> str:
372
- """Decrypt FortiOS version 6 and 7 encrypted secrets."""
427
+ def decrypt_password(input: str) -> str:
428
+ """Decrypt FortiOS encrypted secrets.
429
+
430
+ Works for FortiGate 5.x, 6.x and 7.x (CVE-2019-6693).
431
+
432
+ NOTE:
433
+ - FortiManager uses a 16-byte IV and is not supported (CVE-2020-9289).
434
+ - FortiGate 4.x uses DES and a static 8-byte key and is not supported.
435
+
436
+ Returns decoded plaintext or original input ciphertext when decryption failed.
437
+
438
+ Resources:
439
+ - https://www.fortiguard.com/psirt/FG-IR-19-007
440
+ """
441
+
442
+ if not HAS_PYCRYPTODOME:
443
+ raise RuntimeError("PyCryptodome module not available")
373
444
 
374
- if ciphertext[:3] in ["SH2", "AK1"]:
445
+ if input[:3] in ["SH2", "AK1"]:
375
446
  raise ValueError("Password is a hash (SHA-256 or SHA-1) and cannot be decrypted.")
376
447
 
377
- ciphertext = b64decode(ciphertext)
448
+ ciphertext = b64decode(input)
378
449
  iv = ciphertext[:4] + b"\x00" * 12
379
450
  key = b"Mary had a littl"
380
451
  cipher = AES.new(key, iv=iv, mode=AES.MODE_CBC)
381
452
  plaintext = cipher.decrypt(ciphertext[4:])
382
- return plaintext.split(b"\x00", 1)[0].decode()
453
+
454
+ try:
455
+ return plaintext.split(b"\x00", 1)[0].decode()
456
+ except UnicodeDecodeError:
457
+ return "ENC:" + input
458
+
459
+
460
+ def decrypt_rootfs(fh: BinaryIO, kernel_hash: str) -> BinaryIO:
461
+ """Attempt to decrypt an encrypted ``rootfs.gz`` file.
462
+
463
+ FortiOS releases as of 7.4.1 / 2023-08-31, have ChaCha20 encrypted ``rootfs.gz`` files.
464
+ This function attempts to decrypt a ``rootfs.gz`` file using a static key and IV
465
+ which can be found in the kernel.
466
+
467
+ Currently supported versions (each release has a new key):
468
+ - FortiGate VM 7.0.13
469
+ - FortiGate VM 7.4.1
470
+ - FortiGate VM 7.4.2
471
+
472
+ Resources:
473
+ - https://docs.fortinet.com/document/fortimanager/7.4.2/release-notes/519207/special-notices
474
+ - Reversing kernel (fgt_verifier_iv, fgt_verifier_decrypt, fgt_verifier_initrd)
475
+ """
476
+
477
+ if not HAS_PYCRYPTODOME:
478
+ raise RuntimeError("PyCryptodome module not available")
479
+
480
+ # SHA256 hashes of kernel files
481
+ KERNEL_KEY_MAP = {
482
+ # FortiGate VM 7.0.13
483
+ "25cb2c8a419cde1f42d38fc6cbc95cf8b53db41096d0648015674d8220eba6bf": (
484
+ bytes.fromhex("c87e13e1f7d21c1aca81dc13329c3a948d6e420d3a859f3958bd098747873d08"),
485
+ bytes.fromhex("87486a24637e9a66f09ec182eee25594"),
486
+ ),
487
+ # FortiGate VM 7.4.1
488
+ "a008b47327293e48502a121ee8709f243ad5da4e63d6f663c253db27bd01ea28": _kdf_7_4_x(
489
+ "366486c0f2c6322ec23e4f33a98caa1b19d41c74bb4f25f6e8e2087b0655b30f"
490
+ ),
491
+ # FortiGate VM 7.4.2
492
+ "c392cf83ab484e0b2419b2711b02cdc88a73db35634c10340037243394a586eb": _kdf_7_4_x(
493
+ "480767be539de28ee773497fa731dd6368adc9946df61da8e1253fa402ba0302"
494
+ ),
495
+ }
496
+
497
+ if not (key_data := KERNEL_KEY_MAP.get(kernel_hash)):
498
+ raise ValueError("Failed to decrypt: Unknown kernel hash.")
499
+
500
+ key, iv = key_data
501
+ # First 8 bytes = counter, last 8 bytes = nonce
502
+ # PyCryptodome interally divides this seek by 64 to get a (position, offset) tuple
503
+ # We're interested in updating the position in the ChaCha20 internal state, so to make
504
+ # PyCryptodome "OpenSSL-compatible" we have to multiply the counter by 64
505
+ cipher = ChaCha20.new(key=key, nonce=iv[8:])
506
+ cipher.seek(int.from_bytes(iv[:8], "little") * 64)
507
+ result = cipher.decrypt(fh.read())
508
+
509
+ if result[0:2] != b"\x1f\x8b":
510
+ raise ValueError("Failed to decrypt: No gzip magic header found.")
511
+
512
+ return BytesIO(result)
513
+
514
+
515
+ def _kdf_7_4_x(key_data: Union[str, bytes]) -> tuple[bytes, bytes]:
516
+ """Derive 32 byte key and 16 byte IV from 32 byte seed.
517
+
518
+ As the IV needs to be 16 bytes, we return the first 16 bytes of the sha256 hash.
519
+ """
520
+
521
+ if isinstance(key_data, str):
522
+ key_data = bytes.fromhex(key_data)
523
+
524
+ key = hashlib.sha256(key_data[4:32] + key_data[:4]).digest()
525
+ iv = hashlib.sha256(key_data[5:32] + key_data[:5]).digest()[:16]
526
+ return key, iv
527
+
528
+
529
+ def get_kernel_hash(sysvol: Filesystem) -> Optional[str]:
530
+ """Return the SHA256 hash of the (compressed) kernel."""
531
+ kernel_files = ["flatkc", "vmlinuz", "vmlinux"]
532
+ for k in kernel_files:
533
+ if sysvol.path(k).exists():
534
+ return sysvol.sha256(k)
@@ -16,13 +16,15 @@ class GenericPlugin(Plugin):
16
16
  @export(property=True)
17
17
  def install_date(self) -> Optional[datetime]:
18
18
  """Return the likely install date of FortiOS."""
19
- if (init_log := self.target.fs.path("/data/etc/cloudinit.log")).exists():
20
- return ts.from_unix(init_log.stat().st_mtime)
19
+ files = ["/data/etc/cloudinit.log", "/data/.vm_provisioned", "/data/etc/ssh/ssh_host_dsa_key"]
20
+ for file in files:
21
+ if (fp := self.target.fs.path(file)).exists():
22
+ return ts.from_unix(fp.stat().st_mtime)
21
23
 
22
24
  @export(property=True)
23
25
  def activity(self) -> Optional[datetime]:
24
26
  """Return last seen activity based on filesystem timestamps."""
25
- log_dirs = ["/var/log/log/root", "/var/log/root"]
27
+ log_dirs = ["/var/log/log/root", "/var/log/root", "/data"]
26
28
  for log_dir in log_dirs:
27
29
  if (var_log := self.target.fs.path(log_dir)).exists():
28
30
  return calculate_last_activity(var_log)
@@ -1,3 +1,5 @@
1
+ from typing import Optional
2
+
1
3
  from dissect.target.exceptions import UnsupportedPluginError
2
4
  from dissect.target.plugin import Plugin, export
3
5
 
@@ -8,13 +10,16 @@ class LocalePlugin(Plugin):
8
10
  raise UnsupportedPluginError("FortiOS specific plugin loaded on non-FortiOS target")
9
11
 
10
12
  @export(property=True)
11
- def timezone(self) -> str:
13
+ def timezone(self) -> Optional[str]:
12
14
  """Return configured UI/system timezone."""
13
- timezone_num = self.target._os._config["global-config"]["system"]["global"]["timezone"][0]
14
- return translate_timezone(timezone_num)
15
+ try:
16
+ timezone_num = self.target._os._config["global-config"]["system"]["global"]["timezone"][0]
17
+ return translate_timezone(timezone_num)
18
+ except KeyError:
19
+ pass
15
20
 
16
21
  @export(property=True)
17
- def language(self) -> str:
22
+ def language(self) -> Optional[str]:
18
23
  """Return configured UI language."""
19
24
  LANG_MAP = {
20
25
  "english": "en_US",
@@ -26,8 +31,11 @@ class LocalePlugin(Plugin):
26
31
  "simch": "zh_CN",
27
32
  "korean": "ko_KR",
28
33
  }
29
- lang_str = self.target._os._config["global-config"]["system"]["global"].get("language", ["english"])[0]
30
- return LANG_MAP.get(lang_str, lang_str)
34
+ try:
35
+ lang_str = self.target._os._config["global-config"]["system"]["global"].get("language", ["english"])[0]
36
+ return LANG_MAP.get(lang_str, lang_str)
37
+ except KeyError:
38
+ pass
31
39
 
32
40
 
33
41
  def translate_timezone(timezone_num: str) -> str:
@@ -1,16 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import re
1
5
  import warnings
2
- from typing import Iterator, Union
6
+ from dataclasses import dataclass
7
+ from datetime import datetime
8
+ from typing import Iterator, Optional, Union
3
9
 
4
10
  from flow.record import GroupedRecord
5
11
 
12
+ from dissect.target import Target
6
13
  from dissect.target.exceptions import UnsupportedPluginError
7
14
  from dissect.target.helpers.record import DynamicDescriptor, TargetRecordDescriptor
8
15
  from dissect.target.plugin import Plugin, export
9
16
  from dissect.target.plugins.os.windows.task_helpers.tasks_job import AtTask
10
17
  from dissect.target.plugins.os.windows.task_helpers.tasks_xml import ScheduledTasks
11
- from dissect.target.target import Target
12
18
 
13
19
  warnings.simplefilter(action="ignore", category=FutureWarning)
20
+ log = logging.getLogger(__name__)
14
21
 
15
22
  TaskRecord = TargetRecordDescriptor(
16
23
  "filesystem/windows/task",
@@ -71,6 +78,92 @@ TaskRecord = TargetRecordDescriptor(
71
78
  ],
72
79
  )
73
80
 
81
+ SchedLgURecord = TargetRecordDescriptor(
82
+ "windows/tasks/log/schedlgu",
83
+ [
84
+ ("datetime", "ts"),
85
+ ("string", "job"),
86
+ ("string", "command"),
87
+ ("string", "status"),
88
+ ("uint32", "exit_code"),
89
+ ("string", "version"),
90
+ ],
91
+ )
92
+
93
+ JOB_REGEX_PATTERN = re.compile(r"\"(.*?)\" \((.*?)\)")
94
+ SCHEDLGU_REGEX_PATTERN = re.compile(r"\".+\n.+\n\s{4}.+\n|\".+\n.+", re.MULTILINE)
95
+
96
+
97
+ @dataclass(order=True)
98
+ class SchedLgU:
99
+ ts: datetime = None
100
+ job: str = None
101
+ status: str = None
102
+ command: str = None
103
+ exit_code: int = None
104
+ version: str = None
105
+
106
+ @staticmethod
107
+ def _sanitize_ts(ts: str) -> datetime:
108
+ # sometimes "at" exists before the timestamp
109
+ ts = ts.strip("at ")
110
+ try:
111
+ ts = datetime.strptime(ts, "%m/%d/%Y %I:%M:%S %p")
112
+ except ValueError:
113
+ ts = datetime.strptime(ts, "%d-%m-%Y %H:%M:%S")
114
+
115
+ return ts
116
+
117
+ @staticmethod
118
+ def _parse_job(line: str) -> tuple[str, Optional[str]]:
119
+ matches = JOB_REGEX_PATTERN.match(line)
120
+ if matches:
121
+ return matches.groups()
122
+
123
+ log.warning("SchedLgU failed to parse job and command from line: '%s'. Returning line.", line)
124
+ return line, None
125
+
126
+ @classmethod
127
+ def from_line(cls, line: str) -> SchedLgU:
128
+ """Parse a group of SchedLgU.txt lines."""
129
+ event = cls()
130
+ lines = line.splitlines()
131
+
132
+ # Events can have 2 or 3 lines as a group in total. An example of a complete task job event is:
133
+ # "Symantec NetDetect.job" (NDETECT.EXE)
134
+ # Finished 14-9-2003 13:21:01
135
+ # Result: The task completed with an exit code of (65).
136
+ if len(lines) == 3:
137
+ event.job, event.command = cls._parse_job(lines[0])
138
+ event.status, event.ts = lines[1].split(maxsplit=1)
139
+ event.exit_code = int(lines[2].split("(")[1].rstrip(")."))
140
+
141
+ # Events that have 2 lines as a group can be started task job event or the Task Scheduler Service. Examples:
142
+ # "Symantec NetDetect.job" (NDETECT.EXE)
143
+ # Started at 14-9-2003 13:26:00
144
+ elif len(lines) == 2 and ".job" in lines[0]:
145
+ event.job, event.command = cls._parse_job(lines[0])
146
+ event.status, event.ts = lines[1].split(maxsplit=1)
147
+
148
+ # Events without a task job event are the Task Scheduler Service events. Which can look like this:
149
+ # "Task Scheduler Service"
150
+ # Exited at 14-9-2003 13:40:24
151
+ # OR
152
+ # "Task Scheduler Service"
153
+ # 6.0.6000.16386 (vista_rtm.061101-2205)
154
+ elif len(lines) == 2:
155
+ event.job = lines[0].strip('"')
156
+
157
+ if lines[1].startswith("\t") or lines[1].startswith(" "):
158
+ event.status, event.ts = lines[1].split(maxsplit=1)
159
+ else:
160
+ event.version = lines[1]
161
+
162
+ if event.ts:
163
+ event.ts = cls._sanitize_ts(event.ts)
164
+
165
+ return event
166
+
74
167
 
75
168
  class TasksPlugin(Plugin):
76
169
  """Plugin for retrieving scheduled tasks on a Windows system.
@@ -149,3 +242,56 @@ class TasksPlugin(Plugin):
149
242
  for trigger in task_object.get_triggers():
150
243
  grouped = GroupedRecord("filesystem/windows/task/grouped", [record, trigger])
151
244
  yield grouped
245
+
246
+
247
+ class SchedLgUPlugin(Plugin):
248
+ """Plugin for parsing the Task Scheduler Service transaction log file (SchedLgU.txt)."""
249
+
250
+ PATHS = {
251
+ "sysvol/SchedLgU.txt",
252
+ "sysvol/windows/SchedLgU.txt",
253
+ "sysvol/windows/tasks/SchedLgU.txt",
254
+ "sysvol/winnt/tasks/SchedLgU.txt",
255
+ }
256
+
257
+ def __init__(self, target: Target) -> None:
258
+ self.target = target
259
+ self.paths = [self.target.fs.path(path) for path in self.PATHS if self.target.fs.path(path).exists()]
260
+
261
+ def check_compatible(self) -> None:
262
+ if len(self.paths) == 0:
263
+ raise UnsupportedPluginError("No SchedLgU.txt file found.")
264
+
265
+ @export(record=SchedLgURecord)
266
+ def schedlgu(self) -> Iterator[SchedLgURecord]:
267
+ """Return all events in the Task Scheduler Service transaction log file (SchedLgU.txt).
268
+
269
+ Older Windows systems may log ``.job`` tasks that get started remotely in the SchedLgU.txt file.
270
+ In addition, this log file records when the Task Scheduler service starts and stops.
271
+
272
+ Adversaries may use malicious ``.job`` files to gain persistence on a system.
273
+
274
+ Yield:
275
+ ts (datetime): The timestamp of the event.
276
+ job (str): The name of the ``.job`` file.
277
+ command (str): The command executed.
278
+ status (str): The status of the event (finished, completed, exited, stopped).
279
+ exit_code (int): The exit code of the event.
280
+ version (str): The version of the Task Scheduler service.
281
+ """
282
+
283
+ for path in self.paths:
284
+ content = path.read_text(encoding="UTF-16", errors="surrogateescape")
285
+
286
+ for match in re.findall(SCHEDLGU_REGEX_PATTERN, content):
287
+ event = SchedLgU.from_line(match)
288
+
289
+ yield SchedLgURecord(
290
+ ts=event.ts,
291
+ job=event.job,
292
+ command=event.command,
293
+ status=event.status,
294
+ exit_code=event.exit_code,
295
+ version=event.version,
296
+ _target=self.target,
297
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dissect.target
3
- Version: 3.15.dev33
3
+ Version: 3.15.dev38
4
4
  Summary: This module ties all other Dissect modules together, it provides a programming API and command line tools which allow easy access to various data sources inside disk images or file collections (a.k.a. targets)
5
5
  Author-email: Dissect Team <dissect@fox-it.com>
6
6
  License: Affero General Public License v3
@@ -218,9 +218,9 @@ dissect/target/plugins/os/unix/linux/debian/dpkg.py,sha256=DPBLQiHAF7ZS8IorRsGAi
218
218
  dissect/target/plugins/os/unix/linux/debian/vyos/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
219
219
  dissect/target/plugins/os/unix/linux/debian/vyos/_os.py,sha256=q8qG2FLJhUbpjfwlNCmWAhFdTWMzSWUh7s7H8m4x7Fw,1741
220
220
  dissect/target/plugins/os/unix/linux/fortios/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
221
- dissect/target/plugins/os/unix/linux/fortios/_os.py,sha256=U6LKdFxsRBYvx5I-tJLZTixBFhfQ8OZBt1J1S2eSY4M,13388
222
- dissect/target/plugins/os/unix/linux/fortios/generic.py,sha256=jWs1KEBV68h8TWhBw9gjjGoVbvb8NcMqx82lFqqqnPQ,1116
223
- dissect/target/plugins/os/unix/linux/fortios/locale.py,sha256=hd71u6-fdY8_GaWggjauFOIUrPuZWzpEuWcyC365ffo,5034
221
+ dissect/target/plugins/os/unix/linux/fortios/_os.py,sha256=mYwmGAeY1GQdPdFbGxwNhlRuMD2hTuL1nlEAaXhao4o,19091
222
+ dissect/target/plugins/os/unix/linux/fortios/generic.py,sha256=tT4-lE0Z_DeDIN3zHrQbE8JB3cRJop1_TiEst-Au0bs,1230
223
+ dissect/target/plugins/os/unix/linux/fortios/locale.py,sha256=VDdk60sqe2JTfftssO05C667-_BpI3kcqKOTVzO3ueU,5209
224
224
  dissect/target/plugins/os/unix/linux/redhat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
225
225
  dissect/target/plugins/os/unix/linux/redhat/_os.py,sha256=l_SygO1WMBTvaLvAvhe08yPHLBpUZ9wizW28a9_JhJE,578
226
226
  dissect/target/plugins/os/unix/linux/redhat/yum.py,sha256=kEvB-C2CNoqxSbgGRZiuo6CMPBo_hMWy2KQIE4SNkdQ,2134
@@ -258,7 +258,7 @@ dissect/target/plugins/os/windows/services.py,sha256=_6YkuoZD8LUxk72R3n1p1bOBab3
258
258
  dissect/target/plugins/os/windows/sru.py,sha256=sOM7CyMkW8XIXzI75GL69WoqUrSK2X99TFIfdQR2D64,17767
259
259
  dissect/target/plugins/os/windows/startupinfo.py,sha256=kl8Y7M4nVfmJ71I33VCegtbHj-ZOeEsYAdlNbgwtUOA,3406
260
260
  dissect/target/plugins/os/windows/syscache.py,sha256=WBDx6rixaVnCRsJHLLN_9YWoTDbzkKGbTnk3XmHSSUM,3443
261
- dissect/target/plugins/os/windows/tasks.py,sha256=H0a5Q1DhobaBFtOwqAyJils16hxRRlhtecXSce88fFc,5638
261
+ dissect/target/plugins/os/windows/tasks.py,sha256=2BKxxdd-xn9aAtCIYHqGVcBqeyRbDW8cbuc4N0w1R5g,10828
262
262
  dissect/target/plugins/os/windows/thumbcache.py,sha256=23YjOjTNoE7BYITmg8s9Zs8Wih2e73BkJJEaKlfotcI,4133
263
263
  dissect/target/plugins/os/windows/ual.py,sha256=TYF-R46klEa_HHb86UJd6mPrXwHlAMOUTzC0pZ8uiq0,9787
264
264
  dissect/target/plugins/os/windows/wer.py,sha256=OId9gnqU-z2D_Xl51J9THWTIegre06QsftWnGz7IQb4,7563
@@ -321,10 +321,10 @@ dissect/target/volumes/luks.py,sha256=OmCMsw6rCUXG1_plnLVLTpsvE1n_6WtoRUGQbpmu1z
321
321
  dissect/target/volumes/lvm.py,sha256=wwQVR9I3G9YzmY6UxFsH2Y4MXGBcKL9aayWGCDTiWMU,2269
322
322
  dissect/target/volumes/md.py,sha256=j1K1iKmspl0C_OJFc7-Q1BMWN2OCC5EVANIgVlJ_fIE,1673
323
323
  dissect/target/volumes/vmfs.py,sha256=-LoUbn9WNwTtLi_4K34uV_-wDw2W5hgaqxZNj4UmqAQ,1730
324
- dissect.target-3.15.dev33.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
325
- dissect.target-3.15.dev33.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
326
- dissect.target-3.15.dev33.dist-info/METADATA,sha256=AJDGFmbzNedNH8s5bhC5Z03GZUpVtBAKgNSYHx63_UY,11113
327
- dissect.target-3.15.dev33.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
328
- dissect.target-3.15.dev33.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
329
- dissect.target-3.15.dev33.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
330
- dissect.target-3.15.dev33.dist-info/RECORD,,
324
+ dissect.target-3.15.dev38.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
325
+ dissect.target-3.15.dev38.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
326
+ dissect.target-3.15.dev38.dist-info/METADATA,sha256=hXL7MrjO-icCXVn6ZBQgh0xFBZsV4sePxr5u-eeICUo,11113
327
+ dissect.target-3.15.dev38.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
328
+ dissect.target-3.15.dev38.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
329
+ dissect.target-3.15.dev38.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
330
+ dissect.target-3.15.dev38.dist-info/RECORD,,