dissect.target 3.16.dev44__py3-none-any.whl → 3.17__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- 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
|