dissect.target 3.16.dev45__py3-none-any.whl → 3.17__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/container.py +1 -0
- dissect/target/containers/fortifw.py +190 -0
- dissect/target/filesystem.py +192 -67
- dissect/target/filesystems/dir.py +14 -1
- dissect/target/filesystems/overlay.py +103 -0
- dissect/target/helpers/compat/path_common.py +19 -5
- dissect/target/helpers/configutil.py +30 -7
- dissect/target/helpers/network_managers.py +101 -73
- dissect/target/helpers/record_modifier.py +4 -1
- dissect/target/loader.py +3 -1
- dissect/target/loaders/dir.py +23 -5
- dissect/target/loaders/itunes.py +3 -3
- dissect/target/loaders/mqtt.py +309 -0
- dissect/target/loaders/overlay.py +31 -0
- dissect/target/loaders/target.py +12 -9
- dissect/target/loaders/vb.py +2 -2
- dissect/target/loaders/velociraptor.py +5 -4
- dissect/target/plugin.py +1 -1
- dissect/target/plugins/apps/browser/brave.py +10 -0
- dissect/target/plugins/apps/browser/browser.py +43 -0
- dissect/target/plugins/apps/browser/chrome.py +10 -0
- dissect/target/plugins/apps/browser/chromium.py +234 -12
- dissect/target/plugins/apps/browser/edge.py +10 -0
- dissect/target/plugins/apps/browser/firefox.py +512 -19
- dissect/target/plugins/apps/browser/iexplore.py +2 -2
- dissect/target/plugins/apps/container/docker.py +24 -4
- dissect/target/plugins/apps/ssh/openssh.py +4 -0
- dissect/target/plugins/apps/ssh/putty.py +45 -14
- dissect/target/plugins/apps/ssh/ssh.py +40 -0
- dissect/target/plugins/apps/vpn/openvpn.py +115 -93
- dissect/target/plugins/child/docker.py +24 -0
- dissect/target/plugins/filesystem/ntfs/mft.py +1 -1
- dissect/target/plugins/filesystem/walkfs.py +2 -2
- dissect/target/plugins/os/unix/bsd/__init__.py +0 -0
- dissect/target/plugins/os/unix/esxi/_os.py +2 -2
- dissect/target/plugins/os/unix/linux/debian/vyos/_os.py +1 -1
- dissect/target/plugins/os/unix/linux/fortios/_os.py +9 -9
- dissect/target/plugins/os/unix/linux/services.py +1 -0
- dissect/target/plugins/os/unix/linux/sockets.py +2 -2
- dissect/target/plugins/os/unix/log/messages.py +53 -8
- dissect/target/plugins/os/windows/_os.py +10 -1
- dissect/target/plugins/os/windows/catroot.py +178 -63
- dissect/target/plugins/os/windows/credhist.py +210 -0
- dissect/target/plugins/os/windows/dpapi/crypto.py +12 -1
- dissect/target/plugins/os/windows/dpapi/dpapi.py +62 -7
- dissect/target/plugins/os/windows/dpapi/master_key.py +22 -2
- dissect/target/plugins/os/windows/regf/runkeys.py +6 -4
- dissect/target/plugins/os/windows/sam.py +10 -1
- dissect/target/target.py +1 -1
- dissect/target/tools/dump/run.py +23 -28
- dissect/target/tools/dump/state.py +11 -8
- dissect/target/tools/dump/utils.py +5 -4
- dissect/target/tools/query.py +3 -15
- dissect/target/tools/shell.py +48 -8
- dissect/target/tools/utils.py +23 -0
- {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/METADATA +7 -3
- {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/RECORD +62 -55
- {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/WHEEL +1 -1
- {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/COPYRIGHT +0 -0
- {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/LICENSE +0 -0
- {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/entry_points.txt +0 -0
- {dissect.target-3.16.dev45.dist-info → dissect.target-3.17.dist-info}/top_level.txt +0 -0
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
3
3
|
import io
|
4
4
|
import posixpath
|
5
5
|
import stat
|
6
|
+
import sys
|
6
7
|
from typing import IO, TYPE_CHECKING, Iterator, Literal, Optional
|
7
8
|
|
8
9
|
if TYPE_CHECKING:
|
@@ -27,11 +28,24 @@ try:
|
|
27
28
|
self._fs = path._fs
|
28
29
|
self._flavour = path._flavour
|
29
30
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
31
|
+
if sys.version_info >= (3, 10):
|
32
|
+
|
33
|
+
def __getitem__(self, idx: int) -> TargetPath:
|
34
|
+
result = super().__getitem__(idx)
|
35
|
+
result._fs = self._fs
|
36
|
+
result._flavour = self._flavour
|
37
|
+
return result
|
38
|
+
|
39
|
+
else:
|
40
|
+
|
41
|
+
def __getitem__(self, idx: int) -> TargetPath:
|
42
|
+
if idx < 0:
|
43
|
+
idx = len(self) + idx
|
44
|
+
|
45
|
+
result = super().__getitem__(idx)
|
46
|
+
result._fs = self._fs
|
47
|
+
result._flavour = self._flavour
|
48
|
+
return result
|
35
49
|
|
36
50
|
except ImportError:
|
37
51
|
pass
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
3
3
|
import io
|
4
4
|
import json
|
5
5
|
import re
|
6
|
+
import sys
|
6
7
|
from collections import deque
|
7
8
|
from configparser import ConfigParser, MissingSectionHeaderError
|
8
9
|
from dataclasses import dataclass
|
@@ -28,11 +29,22 @@ from dissect.target.filesystem import FilesystemEntry
|
|
28
29
|
from dissect.target.helpers.fsutil import TargetPath
|
29
30
|
|
30
31
|
try:
|
31
|
-
import
|
32
|
+
from ruamel.yaml import YAML
|
32
33
|
|
33
|
-
|
34
|
-
except
|
35
|
-
|
34
|
+
HAS_YAML = True
|
35
|
+
except ImportError:
|
36
|
+
HAS_YAML = False
|
37
|
+
|
38
|
+
try:
|
39
|
+
if sys.version_info < (3, 11):
|
40
|
+
import tomli as toml
|
41
|
+
else:
|
42
|
+
# tomllib is included since python 3.11
|
43
|
+
import tomllib as toml # novermin
|
44
|
+
|
45
|
+
HAS_TOML = True
|
46
|
+
except ImportError:
|
47
|
+
HAS_TOML = False
|
36
48
|
|
37
49
|
|
38
50
|
def _update_dictionary(current: dict[str, Any], key: str, value: Any) -> None:
|
@@ -401,11 +413,21 @@ class Yaml(ConfigurationParser):
|
|
401
413
|
"""Parses a Yaml file."""
|
402
414
|
|
403
415
|
def parse_file(self, fh: TextIO) -> None:
|
404
|
-
if
|
405
|
-
parsed_data =
|
416
|
+
if HAS_YAML:
|
417
|
+
parsed_data = YAML(typ="safe").load(fh)
|
406
418
|
self.parsed_data = ListUnwrapper.unwrap(parsed_data)
|
407
419
|
else:
|
408
|
-
raise ConfigurationParsingError("Failed to parse file, please install
|
420
|
+
raise ConfigurationParsingError("Failed to parse file, please install ruamel.yaml.")
|
421
|
+
|
422
|
+
|
423
|
+
class Toml(ConfigurationParser):
|
424
|
+
"""Parses a Toml file."""
|
425
|
+
|
426
|
+
def parse_file(self, fh: TextIO) -> None:
|
427
|
+
if HAS_TOML:
|
428
|
+
self.parsed_data = toml.loads(fh.read())
|
429
|
+
else:
|
430
|
+
raise ConfigurationParsingError("Failed to parse file, please install tomli.")
|
409
431
|
|
410
432
|
|
411
433
|
class ScopeManager:
|
@@ -696,6 +718,7 @@ CONFIG_MAP: dict[tuple[str, ...], ParserConfig] = {
|
|
696
718
|
"sample": ParserConfig(Txt),
|
697
719
|
"systemd": ParserConfig(SystemD),
|
698
720
|
"template": ParserConfig(Txt),
|
721
|
+
"toml": ParserConfig(Toml),
|
699
722
|
}
|
700
723
|
|
701
724
|
KNOWN_FILES: dict[str, type[ConfigurationParser]] = {
|
@@ -1,9 +1,13 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import logging
|
1
4
|
import re
|
2
5
|
from collections import defaultdict
|
3
6
|
from configparser import ConfigParser, MissingSectionHeaderError
|
4
7
|
from io import StringIO
|
8
|
+
from itertools import chain
|
5
9
|
from re import compile, sub
|
6
|
-
from typing import Any, Callable, Iterable, Match, Optional
|
10
|
+
from typing import Any, Callable, Iterable, Match, Optional
|
7
11
|
|
8
12
|
from defusedxml import ElementTree
|
9
13
|
|
@@ -11,12 +15,14 @@ from dissect.target.exceptions import PluginError
|
|
11
15
|
from dissect.target.helpers.fsutil import TargetPath
|
12
16
|
from dissect.target.target import Target
|
13
17
|
|
18
|
+
log = logging.getLogger(__name__)
|
19
|
+
|
14
20
|
try:
|
15
|
-
import
|
21
|
+
from ruamel.yaml import YAML
|
16
22
|
|
17
|
-
|
23
|
+
HAS_YAML = True
|
18
24
|
except ImportError:
|
19
|
-
|
25
|
+
HAS_YAML = False
|
20
26
|
|
21
27
|
IGNORED_IPS = [
|
22
28
|
"0.0.0.0",
|
@@ -49,7 +55,7 @@ class Template:
|
|
49
55
|
"""Sets the name of the the used parsing template to the name of the discovered network manager."""
|
50
56
|
self.name = name
|
51
57
|
|
52
|
-
def create_config(self, path: TargetPath) ->
|
58
|
+
def create_config(self, path: TargetPath) -> Optional[dict]:
|
53
59
|
"""Create a network config dictionary based on the configured template and supplied path.
|
54
60
|
|
55
61
|
Args:
|
@@ -60,7 +66,7 @@ class Template:
|
|
60
66
|
"""
|
61
67
|
|
62
68
|
if not path.exists() or path.is_dir():
|
63
|
-
|
69
|
+
log.debug("Failed to get config file %s", path)
|
64
70
|
config = None
|
65
71
|
|
66
72
|
if self.name == "netplan":
|
@@ -73,26 +79,26 @@ class Template:
|
|
73
79
|
config = self._parse_configparser_config(path)
|
74
80
|
return config
|
75
81
|
|
76
|
-
def _parse_netplan_config(self,
|
82
|
+
def _parse_netplan_config(self, path: TargetPath) -> Optional[dict]:
|
77
83
|
"""Internal function to parse a netplan YAML based configuration file into a dict.
|
78
84
|
|
79
85
|
Args:
|
80
|
-
fh: A
|
86
|
+
fh: A path to the configuration file to be parsed.
|
81
87
|
|
82
88
|
Returns:
|
83
89
|
Dictionary containing the parsed YAML based configuration file.
|
84
90
|
"""
|
85
|
-
if
|
86
|
-
return self.parser(
|
91
|
+
if HAS_YAML:
|
92
|
+
return self.parser(path.open("rb"))
|
87
93
|
else:
|
88
|
-
|
94
|
+
log.error("Failed to parse %s. Cannot import ruamel.yaml", self.name)
|
89
95
|
return None
|
90
96
|
|
91
|
-
def _parse_wicked_config(self,
|
97
|
+
def _parse_wicked_config(self, path: TargetPath) -> dict:
|
92
98
|
"""Internal function to parse a wicked XML based configuration file into a dict.
|
93
99
|
|
94
100
|
Args:
|
95
|
-
fh: A
|
101
|
+
fh: A path to the configuration file to be parsed.
|
96
102
|
|
97
103
|
Returns:
|
98
104
|
Dictionary containing the parsed xml based Linux network manager based configuration file.
|
@@ -101,44 +107,43 @@ class Template:
|
|
101
107
|
# we have to replace the ":" for this with "___" (three underscores) to make the xml config non-namespaced.
|
102
108
|
pattern = compile(r"(?<=\n)\s+(<.+?>)")
|
103
109
|
replace_match: Callable[[Match]] = lambda match: match.group(1).replace(":", "___")
|
104
|
-
text = sub(pattern, replace_match,
|
110
|
+
text = sub(pattern, replace_match, path.open("rt").read())
|
105
111
|
|
106
112
|
xml = self.parser.parse(StringIO(text))
|
107
113
|
return self._parse_xml_config(xml, self.sections, self.options)
|
108
114
|
|
109
|
-
def _parse_configparser_config(self,
|
115
|
+
def _parse_configparser_config(self, path: TargetPath) -> dict:
|
110
116
|
"""Internal function to parse ConfigParser compatible configuration files into a dict.
|
111
117
|
|
112
118
|
Args:
|
113
|
-
|
119
|
+
path: A path to the configuration file to be parsed.
|
114
120
|
|
115
121
|
Returns:
|
116
122
|
Dictionary containing the parsed ConfigParser compatible configuration file.
|
117
123
|
"""
|
118
124
|
try:
|
119
|
-
self.parser.read_string(
|
125
|
+
self.parser.read_string(path.open("rt").read(), path.name)
|
120
126
|
return self.parser._sections
|
121
127
|
except MissingSectionHeaderError:
|
122
128
|
# configparser does like config files without headers, so we inject a header to make it work.
|
123
|
-
self.parser.read_string(f"[{self.name}]\n" +
|
129
|
+
self.parser.read_string(f"[{self.name}]\n" + path.open("rt").read(), path.name)
|
124
130
|
return self.parser._sections
|
125
131
|
|
126
|
-
def _parse_text_config(self, comments: str, delim: str,
|
132
|
+
def _parse_text_config(self, comments: str, delim: str, path: TargetPath) -> dict:
|
127
133
|
"""Internal function to parse a basic plain text based configuration file into a dict.
|
128
134
|
|
129
135
|
Args:
|
130
136
|
comments: A string value defining the comment style of the configuration file.
|
131
137
|
delim: A string value defining the delimiters used in the configuration file.
|
132
|
-
|
138
|
+
path: A path to the configuration file to be parsed.
|
133
139
|
|
134
140
|
Returns:
|
135
141
|
Dictionary with a parsed plain text based Linux network manager configuration file.
|
136
142
|
"""
|
137
143
|
config = defaultdict(dict)
|
138
144
|
option_dict = {}
|
139
|
-
fh = fh.open("rt")
|
140
145
|
|
141
|
-
for line in
|
146
|
+
for line in path.open("rt"):
|
142
147
|
line = line.strip()
|
143
148
|
|
144
149
|
if not line or line.startswith(comments):
|
@@ -279,7 +284,7 @@ class Parser:
|
|
279
284
|
if option in translation_values and value:
|
280
285
|
return translation_key
|
281
286
|
|
282
|
-
def _get_option(self, config: dict, option: str, section: Optional[str] = None) ->
|
287
|
+
def _get_option(self, config: dict, option: str, section: Optional[str] = None) -> Optional[str | Callable]:
|
283
288
|
"""Internal function to get arbitrary options values from a parsed (non-translated) dictionary.
|
284
289
|
|
285
290
|
Args:
|
@@ -290,8 +295,14 @@ class Parser:
|
|
290
295
|
Returns:
|
291
296
|
Value(s) corrensponding to that network configuration option.
|
292
297
|
"""
|
298
|
+
if not config:
|
299
|
+
log.error("Cannot get option %s: No config to parse", option)
|
300
|
+
return
|
301
|
+
|
293
302
|
if section:
|
294
|
-
|
303
|
+
# account for values of sections which are None
|
304
|
+
config = config.get(section, {}) or {}
|
305
|
+
|
295
306
|
for key, value in config.items():
|
296
307
|
if key == option:
|
297
308
|
return value
|
@@ -365,7 +376,7 @@ class NetworkManager:
|
|
365
376
|
if self.registered:
|
366
377
|
self.config = self.parser.parse()
|
367
378
|
else:
|
368
|
-
|
379
|
+
log.error("Network manager %s is not registered. Cannot parse config.", self.name)
|
369
380
|
|
370
381
|
@property
|
371
382
|
def interface(self) -> set:
|
@@ -499,7 +510,7 @@ class LinuxNetworkManager:
|
|
499
510
|
|
500
511
|
|
501
512
|
def parse_unix_dhcp_log_messages(target) -> list[str]:
|
502
|
-
"""Parse local syslog and cloud init
|
513
|
+
"""Parse local syslog, journal and cloud init-log files for DHCP lease IPs.
|
503
514
|
|
504
515
|
Args:
|
505
516
|
target: Target to discover and obtain network information from.
|
@@ -507,53 +518,68 @@ def parse_unix_dhcp_log_messages(target) -> list[str]:
|
|
507
518
|
Returns:
|
508
519
|
List of DHCP ip addresses.
|
509
520
|
"""
|
510
|
-
ips =
|
511
|
-
|
512
|
-
# Search through parsed syslogs for DHCP leases.
|
513
|
-
try:
|
514
|
-
messages = target.messages()
|
515
|
-
for record in messages:
|
516
|
-
line = record.message
|
517
|
-
|
518
|
-
# Ubuntu DHCP
|
519
|
-
if ("DHCPv4" in line or "DHCPv6" in line) and " address " in line and " via " in line:
|
520
|
-
ip = line.split(" address ")[1].split(" via ")[0].strip().split("/")[0]
|
521
|
-
if ip not in ips:
|
522
|
-
ips.append(ip)
|
523
|
-
|
524
|
-
# Ubuntu DHCP NetworkManager
|
525
|
-
elif "option ip_address" in line and ("dhcp4" in line or "dhcp6" in line) and "=> '" in line:
|
526
|
-
ip = line.split("=> '")[1].replace("'", "").strip()
|
527
|
-
if ip not in ips:
|
528
|
-
ips.append(ip)
|
529
|
-
|
530
|
-
# Debian and CentOS dhclient
|
531
|
-
elif record.daemon == "dhclient" and "bound to" in line:
|
532
|
-
ip = line.split("bound to")[1].split(" ")[1].strip()
|
533
|
-
if ip not in ips:
|
534
|
-
ips.append(ip)
|
535
|
-
|
536
|
-
# CentOS DHCP and general NetworkManager
|
537
|
-
elif " address " in line and ("dhcp4" in line or "dhcp6" in line):
|
538
|
-
ip = line.split(" address ")[1].strip()
|
539
|
-
if ip not in ips:
|
540
|
-
ips.append(ip)
|
541
|
-
|
542
|
-
except PluginError:
|
543
|
-
target.log.debug("Can not search for DHCP leases in syslog files as they does not exist.")
|
544
|
-
|
545
|
-
# A unix system might be provisioned using Ubuntu's cloud-init (https://cloud-init.io/).
|
546
|
-
if (path := target.fs.path("/var/log/cloud-init.log")).exists():
|
547
|
-
for line in path.open("rt"):
|
548
|
-
# We are interested in the following log line:
|
549
|
-
# YYYY-MM-DD HH:MM:SS,000 - dhcp.py[DEBUG]: Received dhcp lease on IFACE for IP/MASK
|
550
|
-
if "Received dhcp lease on" in line:
|
551
|
-
interface, ip, netmask = re.search(r"Received dhcp lease on (\w{0,}) for (\S+)\/(\S+)", line).groups()
|
552
|
-
if ip not in ips:
|
553
|
-
ips.append(ip)
|
521
|
+
ips = set()
|
522
|
+
messages = set()
|
554
523
|
|
555
|
-
|
556
|
-
|
524
|
+
for log_func in ["messages", "journal"]:
|
525
|
+
try:
|
526
|
+
messages = chain(messages, getattr(target, log_func)())
|
527
|
+
except PluginError:
|
528
|
+
target.log.debug(f"Could not search for DHCP leases in {log_func} files.")
|
529
|
+
|
530
|
+
if not messages:
|
531
|
+
target.log.warning(f"Could not search for DHCP leases using {log_func}: No log entries found.")
|
532
|
+
|
533
|
+
for record in messages:
|
534
|
+
line = record.message
|
535
|
+
|
536
|
+
# Ubuntu cloud-init
|
537
|
+
if "Received dhcp lease on" in line:
|
538
|
+
interface, ip, netmask = re.search(r"Received dhcp lease on (\w{0,}) for (\S+)\/(\S+)", line).groups()
|
539
|
+
ips.add(ip)
|
540
|
+
continue
|
541
|
+
|
542
|
+
# Ubuntu DHCP
|
543
|
+
if ("DHCPv4" in line or "DHCPv6" in line) and " address " in line and " via " in line:
|
544
|
+
ip = line.split(" address ")[1].split(" via ")[0].strip().split("/")[0]
|
545
|
+
ips.add(ip)
|
546
|
+
continue
|
547
|
+
|
548
|
+
# Ubuntu DHCP NetworkManager
|
549
|
+
if "option ip_address" in line and ("dhcp4" in line or "dhcp6" in line) and "=> '" in line:
|
550
|
+
ip = line.split("=> '")[1].replace("'", "").strip()
|
551
|
+
ips.add(ip)
|
552
|
+
continue
|
553
|
+
|
554
|
+
# Debian and CentOS dhclient
|
555
|
+
if hasattr(record, "daemon") and record.daemon == "dhclient" and "bound to" in line:
|
556
|
+
ip = line.split("bound to")[1].split(" ")[1].strip()
|
557
|
+
ips.add(ip)
|
558
|
+
continue
|
559
|
+
|
560
|
+
# CentOS DHCP and general NetworkManager
|
561
|
+
if " address " in line and ("dhcp4" in line or "dhcp6" in line):
|
562
|
+
ip = line.split(" address ")[1].strip()
|
563
|
+
ips.add(ip)
|
564
|
+
continue
|
565
|
+
|
566
|
+
# Ubuntu/Debian DHCP networkd (Journal)
|
567
|
+
if (
|
568
|
+
hasattr(record, "code_func")
|
569
|
+
and record.code_func == "dhcp_lease_acquired"
|
570
|
+
and " address " in line
|
571
|
+
and " via " in line
|
572
|
+
):
|
573
|
+
interface, ip, netmask, gateway = re.search(
|
574
|
+
r"^(\S+): DHCPv[4|6] address (\S+)\/(\S+) via (\S+)", line
|
575
|
+
).groups()
|
576
|
+
ips.add(ip)
|
577
|
+
continue
|
578
|
+
|
579
|
+
# Journals and syslogs can be large and slow to iterate,
|
580
|
+
# so we stop if we have some results and have reached the journal plugin.
|
581
|
+
if len(ips) >= 2 and record._desc.name == "linux/log/journal":
|
582
|
+
break
|
557
583
|
|
558
584
|
return ips
|
559
585
|
|
@@ -631,7 +657,9 @@ TEMPLATES = {
|
|
631
657
|
["netctl"],
|
632
658
|
["address", "gateway", "dns", "ip"],
|
633
659
|
),
|
634
|
-
"netplan": Template(
|
660
|
+
"netplan": Template(
|
661
|
+
"netplan", YAML(typ="safe").load if HAS_YAML else None, ["network"], ["addresses", "dhcp4", "gateway4"]
|
662
|
+
),
|
635
663
|
"NetworkManager": Template(
|
636
664
|
"NetworkManager",
|
637
665
|
ConfigParser(delimiters=("="), comment_prefixes="#", dict_type=dict),
|
@@ -62,13 +62,16 @@ MODIFIER_MAPPING = {
|
|
62
62
|
|
63
63
|
def _resolve_path_types(target: Target, record: Record) -> Iterator[tuple[str, TargetPath]]:
|
64
64
|
for field_name, field_type in record._field_types.items():
|
65
|
-
if not issubclass(field_type, fieldtypes.path):
|
65
|
+
if not issubclass(field_type, (fieldtypes.path, fieldtypes.command)):
|
66
66
|
continue
|
67
67
|
|
68
68
|
path = getattr(record, field_name, None)
|
69
69
|
if path is None:
|
70
70
|
continue
|
71
71
|
|
72
|
+
if isinstance(path, fieldtypes.command):
|
73
|
+
path = path.executable
|
74
|
+
|
72
75
|
yield field_name, target.resolve(str(path))
|
73
76
|
|
74
77
|
|
dissect/target/loader.py
CHANGED
@@ -77,7 +77,7 @@ class Loader:
|
|
77
77
|
raise NotImplementedError()
|
78
78
|
|
79
79
|
@staticmethod
|
80
|
-
def find_all(path: Path) -> Iterator[Path]:
|
80
|
+
def find_all(path: Path, **kwargs) -> Iterator[Path]:
|
81
81
|
"""Finds all targets to load from ``path``.
|
82
82
|
|
83
83
|
This can be used to open multiple targets from a target path that doesn't necessarily map to files on a disk.
|
@@ -176,6 +176,7 @@ def open(item: Union[str, Path], *args, **kwargs) -> Loader:
|
|
176
176
|
|
177
177
|
register("local", "LocalLoader")
|
178
178
|
register("remote", "RemoteLoader")
|
179
|
+
register("mqtt", "MQTTLoader")
|
179
180
|
register("targetd", "TargetdLoader")
|
180
181
|
register("asdf", "AsdfLoader")
|
181
182
|
register("tar", "TarLoader")
|
@@ -198,6 +199,7 @@ register("target", "TargetLoader")
|
|
198
199
|
register("log", "LogLoader")
|
199
200
|
# Disabling ResLoader because of DIS-536
|
200
201
|
# register("res", "ResLoader")
|
202
|
+
register("overlay", "Overlay2Loader")
|
201
203
|
register("phobos", "PhobosLoader")
|
202
204
|
register("velociraptor", "VelociraptorLoader")
|
203
205
|
register("smb", "SmbLoader")
|
dissect/target/loaders/dir.py
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import zipfile
|
4
|
+
from collections import defaultdict
|
4
5
|
from pathlib import Path
|
5
6
|
from typing import TYPE_CHECKING
|
6
7
|
|
8
|
+
from dissect.target.filesystem import LayerFilesystem
|
7
9
|
from dissect.target.filesystems.dir import DirectoryFilesystem
|
8
10
|
from dissect.target.filesystems.zip import ZipFilesystem
|
9
11
|
from dissect.target.helpers import loaderutil
|
@@ -48,6 +50,7 @@ def map_dirs(target: Target, dirs: list[Path | tuple[str, Path]], os_type: str,
|
|
48
50
|
alt_separator = "\\"
|
49
51
|
case_sensitive = False
|
50
52
|
|
53
|
+
drive_letter_map = defaultdict(list)
|
51
54
|
for path in dirs:
|
52
55
|
drive_letter = None
|
53
56
|
if isinstance(path, tuple):
|
@@ -59,13 +62,28 @@ def map_dirs(target: Target, dirs: list[Path | tuple[str, Path]], os_type: str,
|
|
59
62
|
dfs = ZipFilesystem(path.root.fp, path.at, alt_separator=alt_separator, case_sensitive=case_sensitive)
|
60
63
|
else:
|
61
64
|
dfs = DirectoryFilesystem(path, alt_separator=alt_separator, case_sensitive=case_sensitive)
|
62
|
-
target.filesystems.add(dfs)
|
63
65
|
|
64
|
-
|
65
|
-
|
66
|
+
drive_letter_map[drive_letter].append(dfs)
|
67
|
+
|
68
|
+
fs_to_add = []
|
69
|
+
for drive_letter, dfs in drive_letter_map.items():
|
70
|
+
if drive_letter is not None:
|
71
|
+
if len(dfs) > 1:
|
72
|
+
vfs = LayerFilesystem()
|
73
|
+
for fs in dfs:
|
74
|
+
vfs.append_fs_layer(fs)
|
75
|
+
else:
|
76
|
+
vfs = dfs[0]
|
66
77
|
|
67
|
-
|
68
|
-
|
78
|
+
fs_to_add.append(vfs)
|
79
|
+
target.fs.mount(drive_letter.lower() + ":", vfs)
|
80
|
+
else:
|
81
|
+
fs_to_add.extend(dfs)
|
82
|
+
|
83
|
+
for fs in fs_to_add:
|
84
|
+
target.filesystems.add(fs)
|
85
|
+
if os_type == OperatingSystem.WINDOWS:
|
86
|
+
loaderutil.add_virtual_ntfs_filesystem(target, fs, **kwargs)
|
69
87
|
|
70
88
|
|
71
89
|
def find_and_map_dirs(target: Target, path: Path, **kwargs) -> None:
|
dissect/target/loaders/itunes.py
CHANGED
@@ -28,9 +28,9 @@ except ImportError:
|
|
28
28
|
try:
|
29
29
|
from Crypto.Cipher import AES
|
30
30
|
|
31
|
-
|
31
|
+
HAS_CRYPTO = True
|
32
32
|
except ImportError:
|
33
|
-
|
33
|
+
HAS_CRYPTO = False
|
34
34
|
|
35
35
|
|
36
36
|
DOMAIN_TRANSLATION = {
|
@@ -383,7 +383,7 @@ def _create_cipher(key: bytes, iv: bytes = b"\x00" * 16, mode: str = "cbc") -> A
|
|
383
383
|
raise ValueError(f"Invalid key size: {key_size}")
|
384
384
|
|
385
385
|
return _pystandalone.cipher(f"aes-{key_size * 8}-{mode}", key, iv)
|
386
|
-
elif
|
386
|
+
elif HAS_CRYPTO:
|
387
387
|
mode_map = {
|
388
388
|
"cbc": (AES.MODE_CBC, True),
|
389
389
|
"ecb": (AES.MODE_ECB, False),
|