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.
- dissect/target/plugins/os/unix/linux/fortios/_os.py +180 -28
 - dissect/target/plugins/os/unix/linux/fortios/generic.py +5 -3
 - dissect/target/plugins/os/unix/linux/fortios/locale.py +14 -6
 - dissect/target/plugins/os/windows/tasks.py +148 -2
 - {dissect.target-3.15.dev33.dist-info → dissect.target-3.15.dev38.dist-info}/METADATA +1 -1
 - {dissect.target-3.15.dev33.dist-info → dissect.target-3.15.dev38.dist-info}/RECORD +11 -11
 - {dissect.target-3.15.dev33.dist-info → dissect.target-3.15.dev38.dist-info}/COPYRIGHT +0 -0
 - {dissect.target-3.15.dev33.dist-info → dissect.target-3.15.dev38.dist-info}/LICENSE +0 -0
 - {dissect.target-3.15.dev33.dist-info → dissect.target-3.15.dev38.dist-info}/WHEEL +0 -0
 - {dissect.target-3.15.dev33.dist-info → dissect.target-3.15.dev38.dist-info}/entry_points.txt +0 -0
 - {dissect.target-3.15.dev33.dist-info → dissect.target-3.15.dev38.dist-info}/top_level.txt +0 -0
 
| 
         @@ -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":  
     | 
| 
      
 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 [ 
     | 
| 
      
 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 
     | 
    
         
            -
                         
     | 
| 
       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 
     | 
    
         
            -
                     
     | 
| 
       98 
     | 
    
         
            -
             
     | 
| 
      
 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  
     | 
| 
      
 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 
     | 
    
         
            -
                     
     | 
| 
       162 
     | 
    
         
            -
                         
     | 
| 
      
 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 
     | 
    
         
            -
                    #  
     | 
| 
      
 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 
     | 
    
         
            -
                     
     | 
| 
      
 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( 
     | 
| 
       372 
     | 
    
         
            -
                """Decrypt FortiOS  
     | 
| 
      
 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  
     | 
| 
      
 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( 
     | 
| 
      
 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 
     | 
    
         
            -
             
     | 
| 
      
 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 
     | 
    
         
            -
                     
     | 
| 
       20 
     | 
    
         
            -
             
     | 
| 
      
 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 
     | 
    
         
            -
                     
     | 
| 
       14 
     | 
    
         
            -
             
     | 
| 
      
 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 
     | 
    
         
            -
                     
     | 
| 
       30 
     | 
    
         
            -
             
     | 
| 
      
 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  
     | 
| 
      
 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. 
     | 
| 
      
 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= 
     | 
| 
       222 
     | 
    
         
            -
            dissect/target/plugins/os/unix/linux/fortios/generic.py,sha256= 
     | 
| 
       223 
     | 
    
         
            -
            dissect/target/plugins/os/unix/linux/fortios/locale.py,sha256= 
     | 
| 
      
 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= 
     | 
| 
      
 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. 
     | 
| 
       325 
     | 
    
         
            -
            dissect.target-3.15. 
     | 
| 
       326 
     | 
    
         
            -
            dissect.target-3.15. 
     | 
| 
       327 
     | 
    
         
            -
            dissect.target-3.15. 
     | 
| 
       328 
     | 
    
         
            -
            dissect.target-3.15. 
     | 
| 
       329 
     | 
    
         
            -
            dissect.target-3.15. 
     | 
| 
       330 
     | 
    
         
            -
            dissect.target-3.15. 
     | 
| 
      
 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,,
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     | 
    
        {dissect.target-3.15.dev33.dist-info → dissect.target-3.15.dev38.dist-info}/entry_points.txt
    RENAMED
    
    | 
         
            File without changes
         
     | 
| 
         
            File without changes
         
     |