dissect.target 3.16.dev44__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/general/users.py +6 -0
- 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.dev44.dist-info → dissect.target-3.17.dist-info}/METADATA +7 -3
- {dissect.target-3.16.dev44.dist-info → dissect.target-3.17.dist-info}/RECORD +63 -56
- {dissect.target-3.16.dev44.dist-info → dissect.target-3.17.dist-info}/WHEEL +1 -1
- {dissect.target-3.16.dev44.dist-info → dissect.target-3.17.dist-info}/COPYRIGHT +0 -0
- {dissect.target-3.16.dev44.dist-info → dissect.target-3.17.dist-info}/LICENSE +0 -0
- {dissect.target-3.16.dev44.dist-info → dissect.target-3.17.dist-info}/entry_points.txt +0 -0
- {dissect.target-3.16.dev44.dist-info → dissect.target-3.17.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,4 @@
|
|
1
|
+
import base64
|
1
2
|
import re
|
2
3
|
from itertools import product
|
3
4
|
from pathlib import Path
|
@@ -14,6 +15,7 @@ from dissect.target.plugins.apps.ssh.ssh import (
|
|
14
15
|
PrivateKeyRecord,
|
15
16
|
PublicKeyRecord,
|
16
17
|
SSHPlugin,
|
18
|
+
calculate_fingerprints,
|
17
19
|
)
|
18
20
|
|
19
21
|
|
@@ -143,12 +145,14 @@ class OpenSSHPlugin(SSHPlugin):
|
|
143
145
|
continue
|
144
146
|
|
145
147
|
key_type, public_key, comment = parse_ssh_public_key_file(file_path)
|
148
|
+
fingerprints = calculate_fingerprints(base64.b64decode(public_key))
|
146
149
|
|
147
150
|
yield PublicKeyRecord(
|
148
151
|
mtime_ts=file_path.stat().st_mtime,
|
149
152
|
key_type=key_type,
|
150
153
|
public_key=public_key,
|
151
154
|
comment=comment,
|
155
|
+
fingerprint=fingerprints,
|
152
156
|
path=file_path,
|
153
157
|
_target=self.target,
|
154
158
|
_user=user,
|
@@ -1,9 +1,16 @@
|
|
1
1
|
import logging
|
2
|
+
from base64 import b64decode
|
2
3
|
from datetime import datetime
|
3
4
|
from pathlib import Path
|
4
5
|
from typing import Iterator, Optional, Union
|
5
6
|
|
6
|
-
|
7
|
+
try:
|
8
|
+
from Crypto.PublicKey import ECC, RSA
|
9
|
+
|
10
|
+
HAS_CRYPTO = True
|
11
|
+
except ImportError:
|
12
|
+
HAS_CRYPTO = False
|
13
|
+
|
7
14
|
from flow.record.fieldtypes import posix_path, windows_path
|
8
15
|
|
9
16
|
from dissect.target.exceptions import RegistryKeyNotFoundError, UnsupportedPluginError
|
@@ -12,7 +19,11 @@ from dissect.target.helpers.fsutil import TargetPath, open_decompress
|
|
12
19
|
from dissect.target.helpers.record import create_extended_descriptor
|
13
20
|
from dissect.target.helpers.regutil import RegistryKey
|
14
21
|
from dissect.target.plugin import export
|
15
|
-
from dissect.target.plugins.apps.ssh.ssh import
|
22
|
+
from dissect.target.plugins.apps.ssh.ssh import (
|
23
|
+
KnownHostRecord,
|
24
|
+
SSHPlugin,
|
25
|
+
calculate_fingerprints,
|
26
|
+
)
|
16
27
|
from dissect.target.plugins.general.users import UserDetails
|
17
28
|
|
18
29
|
log = logging.getLogger(__name__)
|
@@ -96,12 +107,15 @@ class PuTTYPlugin(SSHPlugin):
|
|
96
107
|
key_type, host = entry.name.split("@")
|
97
108
|
port, host = host.split(":")
|
98
109
|
|
110
|
+
public_key, fingerprints = construct_public_key(key_type, entry.value)
|
111
|
+
|
99
112
|
yield KnownHostRecord(
|
100
113
|
mtime_ts=ssh_host_keys.ts,
|
101
114
|
host=host,
|
102
115
|
port=port,
|
103
116
|
key_type=key_type,
|
104
|
-
public_key=
|
117
|
+
public_key=public_key,
|
118
|
+
fingerprint=fingerprints,
|
105
119
|
comment="",
|
106
120
|
marker=None,
|
107
121
|
path=windows_path(ssh_host_keys.path),
|
@@ -121,12 +135,15 @@ class PuTTYPlugin(SSHPlugin):
|
|
121
135
|
key_type, host = parts[0].split("@")
|
122
136
|
port, host = host.split(":")
|
123
137
|
|
138
|
+
public_key, fingerprints = construct_public_key(key_type, parts[1])
|
139
|
+
|
124
140
|
yield KnownHostRecord(
|
125
141
|
mtime_ts=ts,
|
126
142
|
host=host,
|
127
143
|
port=port,
|
128
144
|
key_type=key_type,
|
129
|
-
public_key=
|
145
|
+
public_key=public_key,
|
146
|
+
fingerprint=fingerprints,
|
130
147
|
comment="",
|
131
148
|
marker=None,
|
132
149
|
path=posix_path(ssh_host_keys_path),
|
@@ -197,8 +214,8 @@ def parse_host_user(host: str, user: str) -> tuple[str, str]:
|
|
197
214
|
return host, user
|
198
215
|
|
199
216
|
|
200
|
-
def construct_public_key(key_type: str, iv: str) -> str:
|
201
|
-
"""Returns OpenSSH format public key calculated from PuTTY SshHostKeys format.
|
217
|
+
def construct_public_key(key_type: str, iv: str) -> tuple[str, tuple[str, str, str]]:
|
218
|
+
"""Returns OpenSSH format public key calculated from PuTTY SshHostKeys format and set of fingerprints.
|
202
219
|
|
203
220
|
PuTTY stores raw public key components instead of OpenSSH-formatted public keys
|
204
221
|
or fingerprints. With RSA public keys the exponent and modulus are stored.
|
@@ -206,9 +223,7 @@ def construct_public_key(key_type: str, iv: str) -> str:
|
|
206
223
|
|
207
224
|
Currently supports ``ssh-ed25519``, ``ecdsa-sha2-nistp256`` and ``rsa2`` key types.
|
208
225
|
|
209
|
-
NOTE:
|
210
|
-
- Sha256 fingerprints of the reconstructed public keys are currently not generated.
|
211
|
-
- More key types could be supported in the future.
|
226
|
+
NOTE: More key types could be supported in the future.
|
212
227
|
|
213
228
|
Resources:
|
214
229
|
- https://github.com/github/putty/blob/master/contrib/kh2reg.py
|
@@ -216,21 +231,37 @@ def construct_public_key(key_type: str, iv: str) -> str:
|
|
216
231
|
- https://pycryptodome.readthedocs.io/en/latest/src/public_key/ecc.html
|
217
232
|
- https://github.com/mkorthof/reg2kh
|
218
233
|
"""
|
234
|
+
if not HAS_CRYPTO:
|
235
|
+
log.warning("Could not reconstruct public key: missing pycryptodome dependency")
|
236
|
+
return iv
|
237
|
+
|
238
|
+
if not isinstance(key_type, str) or not isinstance(iv, str):
|
239
|
+
raise ValueError("Invalid key_type or iv")
|
240
|
+
|
241
|
+
key = None
|
219
242
|
|
220
243
|
if key_type == "ssh-ed25519":
|
221
244
|
x, y = iv.split(",")
|
222
245
|
key = ECC.construct(curve="ed25519", point_x=int(x, 16), point_y=int(y, 16))
|
223
|
-
return key.public_key().export_key(format="OpenSSH").split()[-1]
|
224
246
|
|
225
247
|
if key_type == "ecdsa-sha2-nistp256":
|
226
248
|
_, x, y = iv.split(",")
|
227
249
|
key = ECC.construct(curve="NIST P-256", point_x=int(x, 16), point_y=int(y, 16))
|
228
|
-
return key.public_key().export_key(format="OpenSSH").split()[-1]
|
229
250
|
|
230
251
|
if key_type == "rsa2":
|
231
252
|
exponent, modulus = iv.split(",")
|
232
253
|
key = RSA.construct((int(modulus, 16), int(exponent, 16)))
|
233
|
-
return key.public_key().export_key(format="OpenSSH").decode("utf-8").split()[-1]
|
234
254
|
|
235
|
-
|
236
|
-
|
255
|
+
if key is None:
|
256
|
+
log.warning("Could not reconstruct public key: type %s not implemented", key_type)
|
257
|
+
return iv, (None, None, None)
|
258
|
+
|
259
|
+
openssh_public_key = key.public_key().export_key(format="OpenSSH")
|
260
|
+
|
261
|
+
if isinstance(openssh_public_key, bytes):
|
262
|
+
# RSA's export_key() returns bytes
|
263
|
+
openssh_public_key = openssh_public_key.decode()
|
264
|
+
|
265
|
+
key_part = openssh_public_key.split()[-1]
|
266
|
+
fingerprints = calculate_fingerprints(b64decode(key_part))
|
267
|
+
return key_part, fingerprints
|
@@ -1,3 +1,6 @@
|
|
1
|
+
import base64
|
2
|
+
from hashlib import md5, sha1, sha256
|
3
|
+
|
1
4
|
from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension
|
2
5
|
from dissect.target.helpers.record import create_extended_descriptor
|
3
6
|
from dissect.target.plugin import NamespacePlugin
|
@@ -29,6 +32,7 @@ KnownHostRecord = OpenSSHUserRecordDescriptor(
|
|
29
32
|
("varint", "port"),
|
30
33
|
("string", "public_key"),
|
31
34
|
("string", "marker"),
|
35
|
+
("digest", "fingerprint"),
|
32
36
|
],
|
33
37
|
)
|
34
38
|
|
@@ -50,9 +54,45 @@ PublicKeyRecord = OpenSSHUserRecordDescriptor(
|
|
50
54
|
("datetime", "mtime_ts"),
|
51
55
|
*COMMON_ELLEMENTS,
|
52
56
|
("string", "public_key"),
|
57
|
+
("digest", "fingerprint"),
|
53
58
|
],
|
54
59
|
)
|
55
60
|
|
56
61
|
|
57
62
|
class SSHPlugin(NamespacePlugin):
|
58
63
|
__namespace__ = "ssh"
|
64
|
+
|
65
|
+
|
66
|
+
def calculate_fingerprints(public_key_decoded: bytes, ssh_keygen_format: bool = False) -> tuple[str, str, str]:
|
67
|
+
"""Calculate the MD5, SHA1 and SHA256 digest of the given decoded public key.
|
68
|
+
|
69
|
+
Adheres as much as possible to the output provided by ssh-keygen when ``ssh_keygen_format``
|
70
|
+
parameter is set to ``True``. When set to ``False`` (default) hexdigests are calculated
|
71
|
+
instead for ``sha1``and ``sha256``.
|
72
|
+
|
73
|
+
Resources:
|
74
|
+
- https://en.wikipedia.org/wiki/Public_key_fingerprint
|
75
|
+
- https://man7.org/linux/man-pages/man1/ssh-keygen.1.html
|
76
|
+
- ``ssh-keygen -l -E <alg> -f key.pub``
|
77
|
+
"""
|
78
|
+
if not public_key_decoded:
|
79
|
+
raise ValueError("No decoded public key provided")
|
80
|
+
|
81
|
+
if not isinstance(public_key_decoded, bytes):
|
82
|
+
raise ValueError("Provided public key should be bytes")
|
83
|
+
|
84
|
+
if public_key_decoded[0:3] != b"\x00\x00\x00":
|
85
|
+
raise ValueError("Provided value does not look like a public key")
|
86
|
+
|
87
|
+
digest_md5 = md5(public_key_decoded).digest()
|
88
|
+
digest_sha1 = sha1(public_key_decoded).digest()
|
89
|
+
digest_sha256 = sha256(public_key_decoded).digest()
|
90
|
+
|
91
|
+
if ssh_keygen_format:
|
92
|
+
fingerprint_sha1 = base64.b64encode(digest_sha1).rstrip(b"=").decode()
|
93
|
+
fingerprint_sha256 = base64.b64encode(digest_sha256).rstrip(b"=").decode()
|
94
|
+
else:
|
95
|
+
fingerprint_sha1 = digest_sha1.hex()
|
96
|
+
fingerprint_sha256 = digest_sha256.hex()
|
97
|
+
|
98
|
+
return digest_md5.hex(), fingerprint_sha1, fingerprint_sha256
|
@@ -1,12 +1,15 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import io
|
1
4
|
import itertools
|
2
|
-
import
|
3
|
-
from
|
4
|
-
from typing import Iterator, Union
|
5
|
+
from itertools import product
|
6
|
+
from typing import Iterable, Iterator, Optional, Union
|
5
7
|
|
6
|
-
from dissect.target.exceptions import UnsupportedPluginError
|
8
|
+
from dissect.target.exceptions import ConfigurationParsingError, UnsupportedPluginError
|
7
9
|
from dissect.target.helpers import fsutil
|
10
|
+
from dissect.target.helpers.configutil import Default, ListUnwrapper, _update_dictionary
|
8
11
|
from dissect.target.helpers.record import TargetRecordDescriptor
|
9
|
-
from dissect.target.plugin import OperatingSystem, Plugin, export
|
12
|
+
from dissect.target.plugin import OperatingSystem, Plugin, arg, export
|
10
13
|
|
11
14
|
COMMON_ELEMENTS = [
|
12
15
|
("string", "name"), # basename of .conf file
|
@@ -15,6 +18,7 @@ COMMON_ELEMENTS = [
|
|
15
18
|
("string", "ca"),
|
16
19
|
("string", "cert"),
|
17
20
|
("string", "key"),
|
21
|
+
("boolean", "redacted_key"),
|
18
22
|
("string", "tls_auth"),
|
19
23
|
("string", "status"),
|
20
24
|
("string", "log"),
|
@@ -46,7 +50,60 @@ OpenVPNClient = TargetRecordDescriptor(
|
|
46
50
|
)
|
47
51
|
|
48
52
|
|
49
|
-
|
53
|
+
class OpenVPNParser(Default):
|
54
|
+
def __init__(self, *args, **kwargs):
|
55
|
+
boolean_fields = OpenVPNServer.getfields("boolean") + OpenVPNClient.getfields("boolean")
|
56
|
+
self.boolean_field_names = set(field.name.replace("_", "-") for field in boolean_fields)
|
57
|
+
|
58
|
+
super().__init__(*args, separator=(r"\s",), collapse=["key", "ca", "cert"], **kwargs)
|
59
|
+
|
60
|
+
def parse_file(self, fh: io.TextIOBase) -> None:
|
61
|
+
root = {}
|
62
|
+
iterator = self.line_reader(fh)
|
63
|
+
for line in iterator:
|
64
|
+
if line.startswith("<"):
|
65
|
+
key = line.strip().strip("<>")
|
66
|
+
value = self._read_blob(iterator)
|
67
|
+
_update_dictionary(root, key, value)
|
68
|
+
continue
|
69
|
+
|
70
|
+
self._parse_line(root, line)
|
71
|
+
|
72
|
+
self.parsed_data = ListUnwrapper.unwrap(root)
|
73
|
+
|
74
|
+
def _read_blob(self, lines: Iterable[str]) -> str | list[dict]:
|
75
|
+
"""Read the whole section between <data></data> sections"""
|
76
|
+
output = ""
|
77
|
+
with io.StringIO() as buffer:
|
78
|
+
for line in lines:
|
79
|
+
if "</" in line:
|
80
|
+
break
|
81
|
+
|
82
|
+
buffer.write(line)
|
83
|
+
output = buffer.getvalue()
|
84
|
+
|
85
|
+
# Check for connection profile blocks
|
86
|
+
if not output.startswith("-----"):
|
87
|
+
profile_dict = dict()
|
88
|
+
for line in output.splitlines():
|
89
|
+
self._parse_line(profile_dict, line)
|
90
|
+
|
91
|
+
# We put it as a list as _update_dictionary appends data in a list.
|
92
|
+
output = [profile_dict]
|
93
|
+
|
94
|
+
return output
|
95
|
+
|
96
|
+
def _parse_line(self, root: dict, line: str) -> None:
|
97
|
+
key, *value = self.SEPARATOR.split(line, 1)
|
98
|
+
# Unquote data
|
99
|
+
value = value[0].strip() if value else ""
|
100
|
+
|
101
|
+
value = value.strip("'\"")
|
102
|
+
|
103
|
+
if key in self.boolean_field_names:
|
104
|
+
value = True
|
105
|
+
|
106
|
+
_update_dictionary(root, key, value)
|
50
107
|
|
51
108
|
|
52
109
|
class OpenVPNPlugin(Plugin):
|
@@ -61,133 +118,98 @@ class OpenVPNPlugin(Plugin):
|
|
61
118
|
config_globs = [
|
62
119
|
# This catches openvpn@, openvpn-client@, and openvpn-server@ systemd configurations
|
63
120
|
# Linux
|
64
|
-
"/etc/openvpn
|
65
|
-
"/etc/openvpn/server/*.conf",
|
66
|
-
"/etc/openvpn/client/*.conf",
|
121
|
+
"/etc/openvpn/",
|
67
122
|
# Windows
|
68
|
-
"sysvol/Program Files/OpenVPN/config
|
123
|
+
"sysvol/Program Files/OpenVPN/config/",
|
69
124
|
]
|
70
125
|
|
71
126
|
user_config_paths = {
|
72
|
-
OperatingSystem.WINDOWS.value: ["OpenVPN/config
|
73
|
-
OperatingSystem.OSX.value: ["Library/Application Support/OpenVPN Connect/profiles
|
127
|
+
OperatingSystem.WINDOWS.value: ["OpenVPN/config/"],
|
128
|
+
OperatingSystem.OSX.value: ["Library/Application Support/OpenVPN Connect/profiles/"],
|
74
129
|
}
|
75
130
|
|
76
131
|
def __init__(self, target) -> None:
|
77
132
|
super().__init__(target)
|
78
133
|
self.configs: list[fsutil.TargetPath] = []
|
79
|
-
for
|
80
|
-
self.configs.extend(self.target.fs.path().glob
|
134
|
+
for base, glob in product(self.config_globs, ["*.conf", "*.ovpn"]):
|
135
|
+
self.configs.extend(self.target.fs.path(base).rglob(glob))
|
81
136
|
|
82
137
|
user_paths = self.user_config_paths.get(target.os, [])
|
83
|
-
for path, user_details in itertools.product(
|
84
|
-
self.
|
138
|
+
for path, glob, user_details in itertools.product(
|
139
|
+
user_paths, ["*.conf", "*.ovpn"], self.target.user_details.all_with_home()
|
140
|
+
):
|
141
|
+
self.configs.extend(user_details.home_path.joinpath(path).rglob(glob))
|
85
142
|
|
86
143
|
def check_compatible(self) -> None:
|
87
144
|
if not self.configs:
|
88
145
|
raise UnsupportedPluginError("No OpenVPN configuration files found")
|
89
146
|
|
147
|
+
def _load_config(self, parser: OpenVPNParser, config_path: fsutil.TargetPath) -> Optional[dict]:
|
148
|
+
with config_path.open("rt") as file:
|
149
|
+
try:
|
150
|
+
parser.parse_file(file)
|
151
|
+
except ConfigurationParsingError as e:
|
152
|
+
# Couldn't parse file, continue
|
153
|
+
self.target.log.info("An issue occurred during parsing of %s, continuing", config_path)
|
154
|
+
self.target.log.debug("", exc_info=e)
|
155
|
+
return None
|
156
|
+
|
157
|
+
return parser.parsed_data
|
158
|
+
|
90
159
|
@export(record=[OpenVPNServer, OpenVPNClient])
|
91
|
-
|
160
|
+
@arg("--export-key", action="store_true")
|
161
|
+
def config(self, export_key: bool = False) -> Iterator[Union[OpenVPNServer, OpenVPNClient]]:
|
92
162
|
"""Parses config files from openvpn interfaces."""
|
163
|
+
# We define the parser here so we can reuse it
|
164
|
+
parser = OpenVPNParser()
|
93
165
|
|
94
166
|
for config_path in self.configs:
|
95
|
-
config =
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
167
|
+
config = self._load_config(parser, config_path)
|
168
|
+
|
169
|
+
common_elements = {
|
170
|
+
"name": config_path.stem,
|
171
|
+
"proto": config.get("proto", "udp"), # Default is UDP
|
172
|
+
"dev": config.get("dev"),
|
173
|
+
"ca": config.get("ca"),
|
174
|
+
"cert": config.get("cert"),
|
175
|
+
"key": config.get("key"),
|
176
|
+
"status": config.get("status"),
|
177
|
+
"log": config.get("log"),
|
178
|
+
"source": config_path,
|
179
|
+
"_target": self.target,
|
180
|
+
}
|
181
|
+
|
182
|
+
if not export_key and "PRIVATE KEY" in common_elements.get("key"):
|
183
|
+
common_elements.update({"key": None})
|
184
|
+
common_elements.update({"redacted_key": True})
|
185
|
+
|
103
186
|
tls_auth = config.get("tls-auth", "")
|
104
187
|
# The format of tls-auth is 'tls-auth ta.key <NUM>'.
|
105
188
|
# NUM is either 0 or 1 depending on whether the configuration
|
106
189
|
# is for the client or server, and that does not interest us
|
107
190
|
# This gets rid of the number at the end, while still supporting spaces
|
108
|
-
tls_auth =
|
109
|
-
|
110
|
-
|
191
|
+
tls_auth = " ".join(tls_auth.split(" ")[:-1]).strip("'\"")
|
192
|
+
|
193
|
+
common_elements.update({"tls_auth": tls_auth})
|
111
194
|
|
112
195
|
if "client" in config:
|
113
196
|
remote = config.get("remote", [])
|
114
|
-
# In cases when there is only a single remote,
|
115
|
-
# we want to return it as its own list
|
116
|
-
if isinstance(remote, str):
|
117
|
-
remote = [remote]
|
118
197
|
|
119
198
|
yield OpenVPNClient(
|
120
|
-
|
121
|
-
proto=proto,
|
122
|
-
dev=dev,
|
123
|
-
ca=ca,
|
124
|
-
cert=cert,
|
125
|
-
key=key,
|
126
|
-
tls_auth=tls_auth,
|
127
|
-
status=status,
|
128
|
-
log=log,
|
199
|
+
**common_elements,
|
129
200
|
remote=remote,
|
130
|
-
source=config_path,
|
131
|
-
_target=self.target,
|
132
201
|
)
|
133
202
|
else:
|
134
|
-
pushed_options = config.get("push", [])
|
135
|
-
# In cases when there is only a single push,
|
136
|
-
# we want to return it as its own list
|
137
|
-
if isinstance(pushed_options, str):
|
138
|
-
pushed_options = [pushed_options]
|
139
|
-
pushed_options = [_unquote(opt) for opt in pushed_options]
|
140
203
|
# Defaults here are taken from `man (8) openvpn`
|
141
204
|
yield OpenVPNServer(
|
142
|
-
|
143
|
-
proto=proto,
|
144
|
-
dev=dev,
|
145
|
-
ca=ca,
|
146
|
-
cert=cert,
|
147
|
-
key=key,
|
148
|
-
tls_auth=tls_auth,
|
149
|
-
status=status,
|
150
|
-
log=log,
|
205
|
+
**common_elements,
|
151
206
|
local=config.get("local", "0.0.0.0"),
|
152
207
|
port=int(config.get("port", "1194")),
|
153
|
-
dh=
|
208
|
+
dh=config.get("dh"),
|
154
209
|
topology=config.get("topology"),
|
155
210
|
server=config.get("server"),
|
156
211
|
ifconfig_pool_persist=config.get("ifconfig-pool-persist"),
|
157
|
-
pushed_options=
|
212
|
+
pushed_options=config.get("push", []),
|
158
213
|
client_to_client=config.get("client-to-client", False),
|
159
214
|
duplicate_cn=config.get("duplicate-cn", False),
|
160
|
-
source=config_path,
|
161
|
-
_target=self.target,
|
162
215
|
)
|
163
|
-
|
164
|
-
|
165
|
-
def _parse_config(content: str) -> dict[str, Union[str, list[str]]]:
|
166
|
-
"""Parses Openvpn config files"""
|
167
|
-
lines = content.splitlines()
|
168
|
-
res = {}
|
169
|
-
boolean_fields = OpenVPNServer.getfields("boolean") + OpenVPNClient.getfields("boolean")
|
170
|
-
boolean_field_names = set(field.name for field in boolean_fields)
|
171
|
-
|
172
|
-
for line in lines:
|
173
|
-
# As per man (8) openvpn, lines starting with ; or # are comments
|
174
|
-
if line and not line.startswith((";", "#")):
|
175
|
-
key, *value = line.split(" ", 1)
|
176
|
-
value = value[0] if value else ""
|
177
|
-
# This removes all text after the first comment
|
178
|
-
value = CONFIG_COMMENT_SPLIT_REGEX.split(value, 1)[0].strip()
|
179
|
-
|
180
|
-
if key in boolean_field_names and value == "":
|
181
|
-
value = True
|
182
|
-
|
183
|
-
if old_value := res.get(key):
|
184
|
-
if not isinstance(old_value, list):
|
185
|
-
old_value = [old_value]
|
186
|
-
res[key] = old_value + [value]
|
187
|
-
else:
|
188
|
-
res[key] = value
|
189
|
-
return res
|
190
|
-
|
191
|
-
|
192
|
-
def _unquote(content: str) -> str:
|
193
|
-
return content.strip("\"'")
|
@@ -0,0 +1,24 @@
|
|
1
|
+
from typing import Iterator
|
2
|
+
|
3
|
+
from dissect.target.exceptions import UnsupportedPluginError
|
4
|
+
from dissect.target.helpers.record import ChildTargetRecord
|
5
|
+
from dissect.target.plugin import ChildTargetPlugin
|
6
|
+
|
7
|
+
|
8
|
+
class DockerChildTargetPlugin(ChildTargetPlugin):
|
9
|
+
"""Child target plugin that yields from Docker overlay2fs containers."""
|
10
|
+
|
11
|
+
__type__ = "docker"
|
12
|
+
|
13
|
+
def check_compatible(self) -> None:
|
14
|
+
if not self.target.has_function("docker"):
|
15
|
+
raise UnsupportedPluginError("No Docker data root folder(s) found!")
|
16
|
+
|
17
|
+
def list_children(self) -> Iterator[ChildTargetRecord]:
|
18
|
+
for container in self.target.docker.containers():
|
19
|
+
if container.mount_path:
|
20
|
+
yield ChildTargetRecord(
|
21
|
+
type=self.__type__,
|
22
|
+
path=container.mount_path,
|
23
|
+
_target=self.target,
|
24
|
+
)
|
@@ -3,7 +3,7 @@ from typing import Iterable
|
|
3
3
|
from dissect.util.ts import from_unix
|
4
4
|
|
5
5
|
from dissect.target.exceptions import FileNotFoundError, UnsupportedPluginError
|
6
|
-
from dissect.target.filesystem import
|
6
|
+
from dissect.target.filesystem import LayerFilesystemEntry
|
7
7
|
from dissect.target.helpers.fsutil import TargetPath
|
8
8
|
from dissect.target.helpers.record import TargetRecordDescriptor
|
9
9
|
from dissect.target.plugin import Plugin, export
|
@@ -50,7 +50,7 @@ def generate_record(target: Target, path: TargetPath) -> FilesystemRecord:
|
|
50
50
|
stat = path.lstat()
|
51
51
|
btime = from_unix(stat.st_birthtime) if stat.st_birthtime else None
|
52
52
|
entry = path.get()
|
53
|
-
if isinstance(entry,
|
53
|
+
if isinstance(entry, LayerFilesystemEntry):
|
54
54
|
fs_types = [sub_entry.fs.__type__ for sub_entry in entry.entries]
|
55
55
|
else:
|
56
56
|
fs_types = [entry.fs.__type__]
|
@@ -1,5 +1,7 @@
|
|
1
|
+
from functools import lru_cache
|
1
2
|
from typing import Generator, NamedTuple, Optional, Union
|
2
3
|
|
4
|
+
from dissect.target import Target
|
3
5
|
from dissect.target.exceptions import UnsupportedPluginError
|
4
6
|
from dissect.target.helpers.fsutil import TargetPath
|
5
7
|
from dissect.target.helpers.record import UnixUserRecord, WindowsUserRecord
|
@@ -16,6 +18,10 @@ class UsersPlugin(InternalPlugin):
|
|
16
18
|
|
17
19
|
__namespace__ = "user_details"
|
18
20
|
|
21
|
+
def __init__(self, target: Target):
|
22
|
+
super().__init__(target)
|
23
|
+
self.find = lru_cache(32)(self.find)
|
24
|
+
|
19
25
|
def check_compatible(self) -> None:
|
20
26
|
if not hasattr(self.target, "users"):
|
21
27
|
raise UnsupportedPluginError("Unsupported Plugin")
|
File without changes
|
@@ -97,7 +97,7 @@ class ESXiPlugin(UnixPlugin):
|
|
97
97
|
|
98
98
|
# Create a root layer for the "local state" filesystem
|
99
99
|
# This stores persistent configuration data
|
100
|
-
local_layer = target.fs.
|
100
|
+
local_layer = target.fs.append_layer()
|
101
101
|
|
102
102
|
# Mount all the visor tars in individual filesystem layers
|
103
103
|
_mount_modules(target, sysvol, cfg)
|
@@ -209,7 +209,7 @@ def _mount_modules(target: Target, sysvol: Filesystem, cfg: dict[str, str]):
|
|
209
209
|
tfs = tar.TarFilesystem(cfile, tarinfo=vmtar.VisorTarInfo)
|
210
210
|
|
211
211
|
if tfs:
|
212
|
-
target.fs.
|
212
|
+
target.fs.append_layer().mount("/", tfs)
|
213
213
|
|
214
214
|
|
215
215
|
def _mount_local(target: Target, local_layer: VirtualFilesystem):
|
@@ -24,7 +24,7 @@ class VyosPlugin(LinuxPlugin):
|
|
24
24
|
self._version, rootpath = latest
|
25
25
|
|
26
26
|
# VyOS does some additional magic with base system files
|
27
|
-
layer = target.fs.
|
27
|
+
layer = target.fs.append_layer()
|
28
28
|
layer.map_file_entry("/", target.fs.root.get(f"/boot/{self._version}/{rootpath}"))
|
29
29
|
super().__init__(target)
|
30
30
|
|
@@ -23,9 +23,9 @@ from dissect.target.target import Target
|
|
23
23
|
try:
|
24
24
|
from Crypto.Cipher import AES, ChaCha20
|
25
25
|
|
26
|
-
|
26
|
+
HAS_CRYPTO = True
|
27
27
|
except ImportError:
|
28
|
-
|
28
|
+
HAS_CRYPTO = False
|
29
29
|
|
30
30
|
FortiOSUserRecord = TargetRecordDescriptor(
|
31
31
|
"fortios/user",
|
@@ -113,7 +113,7 @@ class FortiOSPlugin(LinuxPlugin):
|
|
113
113
|
|
114
114
|
# FortiGate
|
115
115
|
if (datafs_tar := sysvol.path("/datafs.tar.gz")).exists():
|
116
|
-
target.fs.
|
116
|
+
target.fs.append_layer().mount("/data", TarFilesystem(datafs_tar.open("rb")))
|
117
117
|
|
118
118
|
# Additional FortiGate or FortiManager tars with corrupt XZ streams
|
119
119
|
target.log.warning("Attempting to load XZ files, this can take a while.")
|
@@ -127,11 +127,11 @@ class FortiOSPlugin(LinuxPlugin):
|
|
127
127
|
):
|
128
128
|
if (tar := target.fs.path(path)).exists() or (tar := sysvol.path(path)).exists():
|
129
129
|
fh = xz.repair_checksum(tar.open("rb"))
|
130
|
-
target.fs.
|
130
|
+
target.fs.append_layer().mount("/", TarFilesystem(fh))
|
131
131
|
|
132
132
|
# FortiAnalyzer and FortiManager
|
133
133
|
if (rootfs_ext_tar := sysvol.path("rootfs-ext.tar.xz")).exists():
|
134
|
-
target.fs.
|
134
|
+
target.fs.append_layer().mount("/", TarFilesystem(rootfs_ext_tar.open("rb")))
|
135
135
|
|
136
136
|
# Filesystem mounts can be discovered in the FortiCare debug report
|
137
137
|
# or using ``fnsysctl ls`` and ``fnsysctl df`` in the cli.
|
@@ -442,8 +442,8 @@ def decrypt_password(input: str) -> str:
|
|
442
442
|
- https://www.fortiguard.com/psirt/FG-IR-19-007
|
443
443
|
"""
|
444
444
|
|
445
|
-
if not
|
446
|
-
raise RuntimeError("
|
445
|
+
if not HAS_CRYPTO:
|
446
|
+
raise RuntimeError("Missing pycryptodome dependency")
|
447
447
|
|
448
448
|
if input[:3] in ["SH2", "AK1"]:
|
449
449
|
raise ValueError("Password is a hash (SHA-256 or SHA-1) and cannot be decrypted.")
|
@@ -511,8 +511,8 @@ def decrypt_rootfs(fh: BinaryIO, key: bytes, iv: bytes) -> BinaryIO:
|
|
511
511
|
RuntimeError: When PyCryptodome is not available.
|
512
512
|
"""
|
513
513
|
|
514
|
-
if not
|
515
|
-
raise RuntimeError("
|
514
|
+
if not HAS_CRYPTO:
|
515
|
+
raise RuntimeError("Missing pycryptodome dependency")
|
516
516
|
|
517
517
|
# First 8 bytes = counter, last 8 bytes = nonce
|
518
518
|
# PyCryptodome interally divides this seek by 64 to get a (position, offset) tuple
|