dissect.target 3.19.dev11__py3-none-any.whl → 3.19.dev13__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/apps/ssh/openssh.py +1 -1
- dissect/target/plugins/apps/ssh/ssh.py +177 -0
- dissect/target/plugins/os/unix/_os.py +14 -3
- dissect/target/plugins/os/unix/shadow.py +47 -31
- {dissect.target-3.19.dev11.dist-info → dissect.target-3.19.dev13.dist-info}/METADATA +1 -1
- {dissect.target-3.19.dev11.dist-info → dissect.target-3.19.dev13.dist-info}/RECORD +11 -12
- dissect/target/helpers/ssh.py +0 -177
- {dissect.target-3.19.dev11.dist-info → dissect.target-3.19.dev13.dist-info}/COPYRIGHT +0 -0
- {dissect.target-3.19.dev11.dist-info → dissect.target-3.19.dev13.dist-info}/LICENSE +0 -0
- {dissect.target-3.19.dev11.dist-info → dissect.target-3.19.dev13.dist-info}/WHEEL +0 -0
- {dissect.target-3.19.dev11.dist-info → dissect.target-3.19.dev13.dist-info}/entry_points.txt +0 -0
- {dissect.target-3.19.dev11.dist-info → dissect.target-3.19.dev13.dist-info}/top_level.txt +0 -0
@@ -7,7 +7,6 @@ from typing import Iterator
|
|
7
7
|
from dissect.target import Target
|
8
8
|
from dissect.target.exceptions import UnsupportedPluginError
|
9
9
|
from dissect.target.helpers.fsutil import TargetPath
|
10
|
-
from dissect.target.helpers.ssh import SSHPrivateKey
|
11
10
|
from dissect.target.plugin import export
|
12
11
|
from dissect.target.plugins.apps.ssh.ssh import (
|
13
12
|
AuthorizedKeysRecord,
|
@@ -15,6 +14,7 @@ from dissect.target.plugins.apps.ssh.ssh import (
|
|
15
14
|
PrivateKeyRecord,
|
16
15
|
PublicKeyRecord,
|
17
16
|
SSHPlugin,
|
17
|
+
SSHPrivateKey,
|
18
18
|
calculate_fingerprints,
|
19
19
|
)
|
20
20
|
|
@@ -1,10 +1,55 @@
|
|
1
1
|
import base64
|
2
|
+
import binascii
|
2
3
|
from hashlib import md5, sha1, sha256
|
3
4
|
|
5
|
+
from dissect.cstruct import cstruct
|
6
|
+
|
4
7
|
from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension
|
5
8
|
from dissect.target.helpers.record import create_extended_descriptor
|
6
9
|
from dissect.target.plugin import NamespacePlugin
|
7
10
|
|
11
|
+
rfc4716_def = """
|
12
|
+
struct ssh_string {
|
13
|
+
uint32 length;
|
14
|
+
char value[length];
|
15
|
+
}
|
16
|
+
|
17
|
+
struct ssh_private_key {
|
18
|
+
char magic[15];
|
19
|
+
|
20
|
+
ssh_string cipher;
|
21
|
+
ssh_string kdf_name;
|
22
|
+
ssh_string kdf_options;
|
23
|
+
|
24
|
+
uint32 number_of_keys;
|
25
|
+
|
26
|
+
ssh_string public;
|
27
|
+
ssh_string private;
|
28
|
+
}
|
29
|
+
"""
|
30
|
+
|
31
|
+
c_rfc4716 = cstruct(endian=">").load(rfc4716_def)
|
32
|
+
|
33
|
+
RFC4716_MARKER_START = b"-----BEGIN OPENSSH PRIVATE KEY-----"
|
34
|
+
RFC4716_MARKER_END = b"-----END OPENSSH PRIVATE KEY-----"
|
35
|
+
RFC4716_MAGIC = b"openssh-key-v1\x00"
|
36
|
+
RFC4716_PADDING = b"\x01\x02\x03\x04\x05\x06\x07"
|
37
|
+
RFC4716_NONE = b"none"
|
38
|
+
|
39
|
+
PKCS8_MARKER_START = b"-----BEGIN PRIVATE KEY-----"
|
40
|
+
PKCS8_MARKER_END = b"-----END PRIVATE KEY-----"
|
41
|
+
PKCS8_MARKER_START_ENCRYPTED = b"-----BEGIN ENCRYPTED PRIVATE KEY-----"
|
42
|
+
PKCS8_MARKER_END_ENCRYPTED = b"-----END ENCRYPTED PRIVATE KEY-----"
|
43
|
+
|
44
|
+
PEM_MARKER_START_RSA = b"-----BEGIN RSA PRIVATE KEY-----"
|
45
|
+
PEM_MARKER_END_RSA = b"-----END RSA PRIVATE KEY-----"
|
46
|
+
PEM_MARKER_START_DSA = b"-----BEGIN DSA PRIVATE KEY-----"
|
47
|
+
PEM_MARKER_END_DSA = b"-----END DSA PRIVATE KEY-----"
|
48
|
+
PEM_MARKER_START_EC = b"-----BEGIN EC PRIVATE KEY-----"
|
49
|
+
PEM_MARKER_END_EC = b"-----END EC PRIVATE KEY-----"
|
50
|
+
PEM_ENCRYPTED = b"ENCRYPTED"
|
51
|
+
|
52
|
+
|
8
53
|
OpenSSHUserRecordDescriptor = create_extended_descriptor([UserRecordDescriptorExtension])
|
9
54
|
|
10
55
|
COMMON_ELLEMENTS = [
|
@@ -96,3 +141,135 @@ def calculate_fingerprints(public_key_decoded: bytes, ssh_keygen_format: bool =
|
|
96
141
|
fingerprint_sha256 = digest_sha256.hex()
|
97
142
|
|
98
143
|
return digest_md5.hex(), fingerprint_sha1, fingerprint_sha256
|
144
|
+
|
145
|
+
|
146
|
+
def is_rfc4716(data: bytes) -> bool:
|
147
|
+
"""Validate data is a valid looking SSH private key in the OpenSSH format."""
|
148
|
+
return data.startswith(RFC4716_MARKER_START) and data.endswith(RFC4716_MARKER_END)
|
149
|
+
|
150
|
+
|
151
|
+
def decode_rfc4716(data: bytes) -> bytes:
|
152
|
+
"""Base64 decode the private key data."""
|
153
|
+
encoded_key_data = data.removeprefix(RFC4716_MARKER_START).removesuffix(RFC4716_MARKER_END)
|
154
|
+
try:
|
155
|
+
return base64.b64decode(encoded_key_data)
|
156
|
+
except binascii.Error:
|
157
|
+
raise ValueError("Error decoding RFC4716 key data")
|
158
|
+
|
159
|
+
|
160
|
+
def is_pkcs8(data: bytes) -> bool:
|
161
|
+
"""Validate data is a valid looking PKCS8 SSH private key."""
|
162
|
+
return (data.startswith(PKCS8_MARKER_START) and data.endswith(PKCS8_MARKER_END)) or (
|
163
|
+
data.startswith(PKCS8_MARKER_START_ENCRYPTED) and data.endswith(PKCS8_MARKER_END_ENCRYPTED)
|
164
|
+
)
|
165
|
+
|
166
|
+
|
167
|
+
def is_pem(data: bytes) -> bool:
|
168
|
+
"""Validate data is a valid looking PEM SSH private key."""
|
169
|
+
return (
|
170
|
+
(data.startswith(PEM_MARKER_START_RSA) and data.endswith(PEM_MARKER_END_RSA))
|
171
|
+
or (data.startswith(PEM_MARKER_START_DSA) and data.endswith(PEM_MARKER_END_DSA))
|
172
|
+
or (data.startswith(PEM_MARKER_START_EC) and data.endswith(PEM_MARKER_END_EC))
|
173
|
+
)
|
174
|
+
|
175
|
+
|
176
|
+
class SSHPrivateKey:
|
177
|
+
"""A class to parse (OpenSSH-supported) SSH private keys.
|
178
|
+
|
179
|
+
OpenSSH supports three types of keys:
|
180
|
+
* RFC4716 (default)
|
181
|
+
* PKCS8
|
182
|
+
* PEM
|
183
|
+
"""
|
184
|
+
|
185
|
+
def __init__(self, data: bytes):
|
186
|
+
self.key_type = None
|
187
|
+
self.public_key = None
|
188
|
+
self.comment = ""
|
189
|
+
|
190
|
+
if is_rfc4716(data):
|
191
|
+
self.format = "RFC4716"
|
192
|
+
self._parse_rfc4716(data)
|
193
|
+
|
194
|
+
elif is_pkcs8(data):
|
195
|
+
self.format = "PKCS8"
|
196
|
+
self.is_encrypted = data.startswith(PKCS8_MARKER_START_ENCRYPTED)
|
197
|
+
|
198
|
+
elif is_pem(data):
|
199
|
+
self.format = "PEM"
|
200
|
+
self._parse_pem(data)
|
201
|
+
|
202
|
+
else:
|
203
|
+
raise ValueError("Unsupported private key format")
|
204
|
+
|
205
|
+
def _parse_rfc4716(self, data: bytes) -> None:
|
206
|
+
"""Parse OpenSSH format SSH private keys.
|
207
|
+
|
208
|
+
The format:
|
209
|
+
"openssh-key-v1"0x00 # NULL-terminated "Auth Magic" string
|
210
|
+
32-bit length, "none" # ciphername length and string
|
211
|
+
32-bit length, "none" # kdfname length and string
|
212
|
+
32-bit length, nil # kdf (0 length, no kdf)
|
213
|
+
32-bit 0x01 # number of keys, hard-coded to 1 (no length)
|
214
|
+
32-bit length, sshpub # public key in ssh format
|
215
|
+
32-bit length, keytype
|
216
|
+
32-bit length, pub0
|
217
|
+
32-bit length, pub1
|
218
|
+
32-bit length for rnd+prv+comment+pad
|
219
|
+
64-bit dummy checksum? # a random 32-bit int, repeated
|
220
|
+
32-bit length, keytype # the private key (including public)
|
221
|
+
32-bit length, pub0 # Public Key parts
|
222
|
+
32-bit length, pub1
|
223
|
+
32-bit length, prv0 # Private Key parts
|
224
|
+
... # (number varies by type)
|
225
|
+
32-bit length, comment # comment string
|
226
|
+
padding bytes 0x010203 # pad to blocksize (see notes below)
|
227
|
+
|
228
|
+
Source: https://coolaj86.com/articles/the-openssh-private-key-format/
|
229
|
+
"""
|
230
|
+
|
231
|
+
key_data = decode_rfc4716(data)
|
232
|
+
private_key = c_rfc4716.ssh_private_key(key_data)
|
233
|
+
|
234
|
+
# RFC4716 only supports 1 key at the moment.
|
235
|
+
if private_key.magic != RFC4716_MAGIC or private_key.number_of_keys != 1:
|
236
|
+
raise ValueError("Unexpected number of keys for RFC4716 format private key")
|
237
|
+
|
238
|
+
self.is_encrypted = private_key.cipher.value != RFC4716_NONE
|
239
|
+
|
240
|
+
self.public_key = base64.b64encode(private_key.public.value)
|
241
|
+
public_key_type = c_rfc4716.ssh_string(private_key.public.value)
|
242
|
+
self.key_type = public_key_type.value
|
243
|
+
|
244
|
+
if not self.is_encrypted:
|
245
|
+
private_key_data = private_key.private.value.rstrip(RFC4716_PADDING)
|
246
|
+
|
247
|
+
# We skip the two dummy uint32s at the start.
|
248
|
+
private_key_index = 8
|
249
|
+
|
250
|
+
private_key_type = c_rfc4716.ssh_string(private_key_data[private_key_index:])
|
251
|
+
private_key_index += 4 + private_key_type.length
|
252
|
+
self.key_type = private_key_type.value
|
253
|
+
|
254
|
+
private_key_fields = []
|
255
|
+
while private_key_index < len(private_key_data):
|
256
|
+
field = c_rfc4716.ssh_string(private_key_data[private_key_index:])
|
257
|
+
private_key_index += 4 + field.length
|
258
|
+
private_key_fields.append(field)
|
259
|
+
|
260
|
+
# There is always a comment present (with a length field of 0 for empty comments).
|
261
|
+
self.comment = private_key_fields[-1].value
|
262
|
+
|
263
|
+
def _parse_pem(self, data: bytes) -> None:
|
264
|
+
"""Detect key type and encryption of PEM keys."""
|
265
|
+
self.is_encrypted = PEM_ENCRYPTED in data
|
266
|
+
|
267
|
+
if data.startswith(PEM_MARKER_START_RSA):
|
268
|
+
self.key_type = "ssh-rsa"
|
269
|
+
|
270
|
+
elif data.startswith(PEM_MARKER_START_DSA):
|
271
|
+
self.key_type = "ssh-dss"
|
272
|
+
|
273
|
+
# This is not a valid SSH key type, but we currently do not detect the specific ecdsa variant.
|
274
|
+
else:
|
275
|
+
self.key_type = "ecdsa"
|
@@ -40,12 +40,18 @@ class UnixPlugin(OSPlugin):
|
|
40
40
|
@export(record=UnixUserRecord)
|
41
41
|
@arg("--sessions", action="store_true", help="Parse syslog for recent user sessions")
|
42
42
|
def users(self, sessions: bool = False) -> Iterator[UnixUserRecord]:
|
43
|
-
"""
|
43
|
+
"""Yield unix user records from passwd files or syslog session logins.
|
44
|
+
|
45
|
+
Resources:
|
46
|
+
- https://manpages.ubuntu.com/manpages/oracular/en/man5/passwd.5.html
|
47
|
+
"""
|
48
|
+
|
49
|
+
PASSWD_FILES = ["/etc/passwd", "/etc/passwd-", "/etc/master.passwd"]
|
44
50
|
|
45
51
|
seen_users = set()
|
46
52
|
|
47
53
|
# Yield users found in passwd files.
|
48
|
-
for passwd_file in
|
54
|
+
for passwd_file in PASSWD_FILES:
|
49
55
|
if (path := self.target.fs.path(passwd_file)).exists():
|
50
56
|
for line in path.open("rt"):
|
51
57
|
line = line.strip()
|
@@ -53,7 +59,12 @@ class UnixPlugin(OSPlugin):
|
|
53
59
|
continue
|
54
60
|
|
55
61
|
pwent = dict(enumerate(line.split(":")))
|
56
|
-
|
62
|
+
|
63
|
+
current_user = (pwent.get(0), pwent.get(5), pwent.get(6))
|
64
|
+
if current_user in seen_users:
|
65
|
+
continue
|
66
|
+
|
67
|
+
seen_users.add(current_user)
|
57
68
|
yield UnixUserRecord(
|
58
69
|
name=pwent.get(0),
|
59
70
|
passwd=pwent.get(1),
|
@@ -29,39 +29,55 @@ class ShadowPlugin(Plugin):
|
|
29
29
|
if not self.target.fs.path("/etc/shadow").exists():
|
30
30
|
raise UnsupportedPluginError("No shadow file found")
|
31
31
|
|
32
|
+
SHADOW_FILES = ["/etc/shadow", "/etc/shadow-"]
|
33
|
+
|
32
34
|
@export(record=UnixShadowRecord)
|
33
35
|
def passwords(self) -> Iterator[UnixShadowRecord]:
|
34
|
-
"""
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
36
|
+
"""Yield shadow records from /etc/shadow files.
|
37
|
+
|
38
|
+
Resources:
|
39
|
+
- https://manpages.ubuntu.com/manpages/oracular/en/man5/passwd.5.html#file:/etc/shadow
|
40
|
+
"""
|
41
|
+
|
42
|
+
seen_hashes = set()
|
43
|
+
|
44
|
+
for shadow_file in self.SHADOW_FILES:
|
45
|
+
if (path := self.target.fs.path(shadow_file)).exists():
|
46
|
+
for line in path.open("rt"):
|
47
|
+
line = line.strip()
|
48
|
+
if line == "" or line.startswith("#"):
|
49
|
+
continue
|
50
|
+
|
51
|
+
shent = dict(enumerate(line.split(":")))
|
52
|
+
crypt = extract_crypt_details(shent)
|
53
|
+
|
54
|
+
# do not return a shadow record if we have no hash
|
55
|
+
if crypt.get("hash") is None or crypt.get("hash") == "":
|
56
|
+
continue
|
57
|
+
|
58
|
+
# prevent duplicate user hashes
|
59
|
+
current_hash = (shent.get(0), crypt.get("hash"))
|
60
|
+
if current_hash in seen_hashes:
|
61
|
+
continue
|
62
|
+
|
63
|
+
seen_hashes.add(current_hash)
|
64
|
+
|
65
|
+
yield UnixShadowRecord(
|
66
|
+
name=shent.get(0),
|
67
|
+
crypt=shent.get(1),
|
68
|
+
algorithm=crypt.get("algo"),
|
69
|
+
crypt_param=crypt.get("param"),
|
70
|
+
salt=crypt.get("salt"),
|
71
|
+
hash=crypt.get("hash"),
|
72
|
+
last_change=shent.get(2),
|
73
|
+
min_age=shent.get(3),
|
74
|
+
max_age=shent.get(4),
|
75
|
+
warning_period=shent.get(5),
|
76
|
+
inactivity_period=shent.get(6),
|
77
|
+
expiration_date=shent.get(7),
|
78
|
+
unused_field=shent.get(8),
|
79
|
+
_target=self.target,
|
80
|
+
)
|
65
81
|
|
66
82
|
|
67
83
|
def extract_crypt_details(shent: dict) -> dict:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: dissect.target
|
3
|
-
Version: 3.19.
|
3
|
+
Version: 3.19.dev13
|
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
|
@@ -65,7 +65,6 @@ dissect/target/helpers/record.py,sha256=lWl7k2Mp9Axllm0tXzPGJx2zj2zONsyY_p5g424T
|
|
65
65
|
dissect/target/helpers/record_modifier.py,sha256=3I_rC5jqvl0TsW3V8OQ6Dltz_D8J4PU1uhhzbJGKm9c,3245
|
66
66
|
dissect/target/helpers/regutil.py,sha256=kX-sSZbW8Qkg29Dn_9zYbaQrwLumrr4Y8zJ1EhHXIAM,27337
|
67
67
|
dissect/target/helpers/shell_folder_ids.py,sha256=Behhb8oh0kMxrEk6YYKYigCDZe8Hw5QS6iK_d2hTs2Y,24978
|
68
|
-
dissect/target/helpers/ssh.py,sha256=obB7sqUH0IoUo78NAmHM8TX0pgA_4GHICZ3TA3TW_0E,6324
|
69
68
|
dissect/target/helpers/targetd.py,sha256=ELhUulzQ4OgXgHsWhsLgM14vut8Wm6btr7qTynlwKaE,1812
|
70
69
|
dissect/target/helpers/utils.py,sha256=r36Bn0UL0E6Z8ajmQrHzC6RyUxTRdwJ1PNsd904Lmzs,4027
|
71
70
|
dissect/target/helpers/compat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -135,10 +134,10 @@ dissect/target/plugins/apps/remoteaccess/teamviewer.py,sha256=SiEH36HM2NvdPuCjfL
|
|
135
134
|
dissect/target/plugins/apps/shell/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
136
135
|
dissect/target/plugins/apps/shell/powershell.py,sha256=biPSMRWxPI6kRqP0-75yMtrw0Ti2Bzfl_xI3xbmmF48,2641
|
137
136
|
dissect/target/plugins/apps/ssh/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
138
|
-
dissect/target/plugins/apps/ssh/openssh.py,sha256=
|
137
|
+
dissect/target/plugins/apps/ssh/openssh.py,sha256=oaJeKmTvVMo4aePo4Ep7t0ludJPNuuokGEW07w4gAvQ,7216
|
139
138
|
dissect/target/plugins/apps/ssh/opensshd.py,sha256=DaXKdgGF3GYHHA4buEvphcm6FF4C8YFjgD96Dv6rRnM,5510
|
140
139
|
dissect/target/plugins/apps/ssh/putty.py,sha256=EmsXr2NbOB13-EWS5AkpEPMUhOkVl6FAy8JGUiaDhxk,10133
|
141
|
-
dissect/target/plugins/apps/ssh/ssh.py,sha256=
|
140
|
+
dissect/target/plugins/apps/ssh/ssh.py,sha256=d3U8PJbtMvOV3K0wV_9KzGt2oRs-mfNQz1_Xd6SNx0Y,9320
|
142
141
|
dissect/target/plugins/apps/vpn/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
143
142
|
dissect/target/plugins/apps/vpn/openvpn.py,sha256=d-DGINTIHP_bvv3T09ZwbezHXGctvCyAhJ482m2_-a0,7654
|
144
143
|
dissect/target/plugins/apps/vpn/wireguard.py,sha256=SoAMED_bwWJQ3nci5qEY-qV4wJKSSDZQ8K7DoJRYq0k,6521
|
@@ -184,7 +183,7 @@ dissect/target/plugins/general/scrape.py,sha256=Fz7BNXflvuxlnVulyyDhLpyU8D_hJdH6
|
|
184
183
|
dissect/target/plugins/general/users.py,sha256=cQXPQ2XbkPjckCPHYTUW4JEhYN0_CT8JI8hJPZn3qSs,3030
|
185
184
|
dissect/target/plugins/os/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
186
185
|
dissect/target/plugins/os/unix/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
187
|
-
dissect/target/plugins/os/unix/_os.py,sha256=
|
186
|
+
dissect/target/plugins/os/unix/_os.py,sha256=GcbP8HbK1XtwYFGbl8x0BdfoLAC2ROv9xieeFGI5dWM,14557
|
188
187
|
dissect/target/plugins/os/unix/cronjobs.py,sha256=2ssj97UVJueyATVl7NMJmqd9uHflQ2tXUqdOCFIEje8,3182
|
189
188
|
dissect/target/plugins/os/unix/datetime.py,sha256=gKfBdPyUirt3qmVYfOJ1oZXRPn8wRzssbZxR_ARrtk8,1518
|
190
189
|
dissect/target/plugins/os/unix/etc.py,sha256=HoPEC1hxqurSnAXQAK-jf_HxdBIDe-1z_qSw_n-ViI4,258
|
@@ -192,7 +191,7 @@ dissect/target/plugins/os/unix/generic.py,sha256=6_MJrV1LbIxNQJwAZR0HEQljoxwF5BP
|
|
192
191
|
dissect/target/plugins/os/unix/history.py,sha256=ptNGHkHOLJ5bE4r1PqtkQFcQHqzS6-qe5ms1tTGOJp8,6620
|
193
192
|
dissect/target/plugins/os/unix/locale.py,sha256=V3R7mEyrH3f-h7SGAucByaYYDA2SIil9Qb-s3dPmDEA,3961
|
194
193
|
dissect/target/plugins/os/unix/packagemanager.py,sha256=Wm2AAJOD_B3FAcZNXgWtSm_YwbvrHBYOP8bPmOXNjG4,2427
|
195
|
-
dissect/target/plugins/os/unix/shadow.py,sha256=
|
194
|
+
dissect/target/plugins/os/unix/shadow.py,sha256=W6W6rMru7IVnuBc6sl5wsRWTOrJdS1s7_2_q7QRf7Is,4148
|
196
195
|
dissect/target/plugins/os/unix/bsd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
197
196
|
dissect/target/plugins/os/unix/bsd/_os.py,sha256=e5rttTOFOmd7e2HqP9ZZFMEiPLBr-8rfH0XH1IIeroQ,1372
|
198
197
|
dissect/target/plugins/os/unix/bsd/citrix/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -345,10 +344,10 @@ dissect/target/volumes/luks.py,sha256=OmCMsw6rCUXG1_plnLVLTpsvE1n_6WtoRUGQbpmu1z
|
|
345
344
|
dissect/target/volumes/lvm.py,sha256=wwQVR9I3G9YzmY6UxFsH2Y4MXGBcKL9aayWGCDTiWMU,2269
|
346
345
|
dissect/target/volumes/md.py,sha256=j1K1iKmspl0C_OJFc7-Q1BMWN2OCC5EVANIgVlJ_fIE,1673
|
347
346
|
dissect/target/volumes/vmfs.py,sha256=-LoUbn9WNwTtLi_4K34uV_-wDw2W5hgaqxZNj4UmqAQ,1730
|
348
|
-
dissect.target-3.19.
|
349
|
-
dissect.target-3.19.
|
350
|
-
dissect.target-3.19.
|
351
|
-
dissect.target-3.19.
|
352
|
-
dissect.target-3.19.
|
353
|
-
dissect.target-3.19.
|
354
|
-
dissect.target-3.19.
|
347
|
+
dissect.target-3.19.dev13.dist-info/COPYRIGHT,sha256=m-9ih2RVhMiXHI2bf_oNSSgHgkeIvaYRVfKTwFbnJPA,301
|
348
|
+
dissect.target-3.19.dev13.dist-info/LICENSE,sha256=DZak_2itbUtvHzD3E7GNUYSRK6jdOJ-GqncQ2weavLA,34523
|
349
|
+
dissect.target-3.19.dev13.dist-info/METADATA,sha256=oFZiiry3QZEqrgYijsGOlPjZn1DfUM3GBMdf8WZaIFc,12719
|
350
|
+
dissect.target-3.19.dev13.dist-info/WHEEL,sha256=Wyh-_nZ0DJYolHNn1_hMa4lM7uDedD_RGVwbmTjyItk,91
|
351
|
+
dissect.target-3.19.dev13.dist-info/entry_points.txt,sha256=tvFPa-Ap-gakjaPwRc6Fl6mxHzxEZ_arAVU-IUYeo_s,447
|
352
|
+
dissect.target-3.19.dev13.dist-info/top_level.txt,sha256=Mn-CQzEYsAbkxrUI0TnplHuXnGVKzxpDw_po_sXpvv4,8
|
353
|
+
dissect.target-3.19.dev13.dist-info/RECORD,,
|
dissect/target/helpers/ssh.py
DELETED
@@ -1,177 +0,0 @@
|
|
1
|
-
import base64
|
2
|
-
import binascii
|
3
|
-
|
4
|
-
from dissect.cstruct import cstruct
|
5
|
-
|
6
|
-
rfc4716_def = """
|
7
|
-
struct ssh_string {
|
8
|
-
uint32 length;
|
9
|
-
char value[length];
|
10
|
-
}
|
11
|
-
|
12
|
-
struct ssh_private_key {
|
13
|
-
char magic[15];
|
14
|
-
|
15
|
-
ssh_string cipher;
|
16
|
-
ssh_string kdf_name;
|
17
|
-
ssh_string kdf_options;
|
18
|
-
|
19
|
-
uint32 number_of_keys;
|
20
|
-
|
21
|
-
ssh_string public;
|
22
|
-
ssh_string private;
|
23
|
-
}
|
24
|
-
"""
|
25
|
-
|
26
|
-
c_rfc4716 = cstruct(endian=">").load(rfc4716_def)
|
27
|
-
|
28
|
-
RFC4716_MARKER_START = b"-----BEGIN OPENSSH PRIVATE KEY-----"
|
29
|
-
RFC4716_MARKER_END = b"-----END OPENSSH PRIVATE KEY-----"
|
30
|
-
RFC4716_MAGIC = b"openssh-key-v1\x00"
|
31
|
-
RFC4716_PADDING = b"\x01\x02\x03\x04\x05\x06\x07"
|
32
|
-
RFC4716_NONE = b"none"
|
33
|
-
|
34
|
-
PKCS8_MARKER_START = b"-----BEGIN PRIVATE KEY-----"
|
35
|
-
PKCS8_MARKER_END = b"-----END PRIVATE KEY-----"
|
36
|
-
PKCS8_MARKER_START_ENCRYPTED = b"-----BEGIN ENCRYPTED PRIVATE KEY-----"
|
37
|
-
PKCS8_MARKER_END_ENCRYPTED = b"-----END ENCRYPTED PRIVATE KEY-----"
|
38
|
-
|
39
|
-
PEM_MARKER_START_RSA = b"-----BEGIN RSA PRIVATE KEY-----"
|
40
|
-
PEM_MARKER_END_RSA = b"-----END RSA PRIVATE KEY-----"
|
41
|
-
PEM_MARKER_START_DSA = b"-----BEGIN DSA PRIVATE KEY-----"
|
42
|
-
PEM_MARKER_END_DSA = b"-----END DSA PRIVATE KEY-----"
|
43
|
-
PEM_MARKER_START_EC = b"-----BEGIN EC PRIVATE KEY-----"
|
44
|
-
PEM_MARKER_END_EC = b"-----END EC PRIVATE KEY-----"
|
45
|
-
PEM_ENCRYPTED = b"ENCRYPTED"
|
46
|
-
|
47
|
-
|
48
|
-
class SSHPrivateKey:
|
49
|
-
"""A class to parse (OpenSSH-supported) SSH private keys.
|
50
|
-
|
51
|
-
OpenSSH supports three types of keys:
|
52
|
-
* RFC4716 (default)
|
53
|
-
* PKCS8
|
54
|
-
* PEM
|
55
|
-
"""
|
56
|
-
|
57
|
-
def __init__(self, data: bytes):
|
58
|
-
self.key_type = None
|
59
|
-
self.public_key = None
|
60
|
-
self.comment = ""
|
61
|
-
|
62
|
-
if is_rfc4716(data):
|
63
|
-
self.format = "RFC4716"
|
64
|
-
self._parse_rfc4716(data)
|
65
|
-
|
66
|
-
elif is_pkcs8(data):
|
67
|
-
self.format = "PKCS8"
|
68
|
-
self.is_encrypted = data.startswith(PKCS8_MARKER_START_ENCRYPTED)
|
69
|
-
|
70
|
-
elif is_pem(data):
|
71
|
-
self.format = "PEM"
|
72
|
-
self._parse_pem(data)
|
73
|
-
|
74
|
-
else:
|
75
|
-
raise ValueError("Unsupported private key format")
|
76
|
-
|
77
|
-
def _parse_rfc4716(self, data: bytes) -> None:
|
78
|
-
"""Parse OpenSSH format SSH private keys.
|
79
|
-
|
80
|
-
The format:
|
81
|
-
"openssh-key-v1"0x00 # NULL-terminated "Auth Magic" string
|
82
|
-
32-bit length, "none" # ciphername length and string
|
83
|
-
32-bit length, "none" # kdfname length and string
|
84
|
-
32-bit length, nil # kdf (0 length, no kdf)
|
85
|
-
32-bit 0x01 # number of keys, hard-coded to 1 (no length)
|
86
|
-
32-bit length, sshpub # public key in ssh format
|
87
|
-
32-bit length, keytype
|
88
|
-
32-bit length, pub0
|
89
|
-
32-bit length, pub1
|
90
|
-
32-bit length for rnd+prv+comment+pad
|
91
|
-
64-bit dummy checksum? # a random 32-bit int, repeated
|
92
|
-
32-bit length, keytype # the private key (including public)
|
93
|
-
32-bit length, pub0 # Public Key parts
|
94
|
-
32-bit length, pub1
|
95
|
-
32-bit length, prv0 # Private Key parts
|
96
|
-
... # (number varies by type)
|
97
|
-
32-bit length, comment # comment string
|
98
|
-
padding bytes 0x010203 # pad to blocksize (see notes below)
|
99
|
-
|
100
|
-
Source: https://coolaj86.com/articles/the-openssh-private-key-format/
|
101
|
-
"""
|
102
|
-
|
103
|
-
key_data = decode_rfc4716(data)
|
104
|
-
private_key = c_rfc4716.ssh_private_key(key_data)
|
105
|
-
|
106
|
-
# RFC4716 only supports 1 key at the moment.
|
107
|
-
if private_key.magic != RFC4716_MAGIC or private_key.number_of_keys != 1:
|
108
|
-
raise ValueError("Unexpected number of keys for RFC4716 format private key")
|
109
|
-
|
110
|
-
self.is_encrypted = private_key.cipher.value != RFC4716_NONE
|
111
|
-
|
112
|
-
self.public_key = base64.b64encode(private_key.public.value)
|
113
|
-
public_key_type = c_rfc4716.ssh_string(private_key.public.value)
|
114
|
-
self.key_type = public_key_type.value
|
115
|
-
|
116
|
-
if not self.is_encrypted:
|
117
|
-
private_key_data = private_key.private.value.rstrip(RFC4716_PADDING)
|
118
|
-
|
119
|
-
# We skip the two dummy uint32s at the start.
|
120
|
-
private_key_index = 8
|
121
|
-
|
122
|
-
private_key_type = c_rfc4716.ssh_string(private_key_data[private_key_index:])
|
123
|
-
private_key_index += 4 + private_key_type.length
|
124
|
-
self.key_type = private_key_type.value
|
125
|
-
|
126
|
-
private_key_fields = []
|
127
|
-
while private_key_index < len(private_key_data):
|
128
|
-
field = c_rfc4716.ssh_string(private_key_data[private_key_index:])
|
129
|
-
private_key_index += 4 + field.length
|
130
|
-
private_key_fields.append(field)
|
131
|
-
|
132
|
-
# There is always a comment present (with a length field of 0 for empty comments).
|
133
|
-
self.comment = private_key_fields[-1].value
|
134
|
-
|
135
|
-
def _parse_pem(self, data: bytes) -> None:
|
136
|
-
"""Detect key type and encryption of PEM keys."""
|
137
|
-
self.is_encrypted = PEM_ENCRYPTED in data
|
138
|
-
|
139
|
-
if data.startswith(PEM_MARKER_START_RSA):
|
140
|
-
self.key_type = "ssh-rsa"
|
141
|
-
|
142
|
-
elif data.startswith(PEM_MARKER_START_DSA):
|
143
|
-
self.key_type = "ssh-dss"
|
144
|
-
|
145
|
-
# This is not a valid SSH key type, but we currently do not detect the specific ecdsa variant.
|
146
|
-
else:
|
147
|
-
self.key_type = "ecdsa"
|
148
|
-
|
149
|
-
|
150
|
-
def is_rfc4716(data: bytes) -> bool:
|
151
|
-
"""Validate data is a valid looking SSH private key in the OpenSSH format."""
|
152
|
-
return data.startswith(RFC4716_MARKER_START) and data.endswith(RFC4716_MARKER_END)
|
153
|
-
|
154
|
-
|
155
|
-
def decode_rfc4716(data: bytes) -> bytes:
|
156
|
-
"""Base64 decode the private key data."""
|
157
|
-
encoded_key_data = data.removeprefix(RFC4716_MARKER_START).removesuffix(RFC4716_MARKER_END)
|
158
|
-
try:
|
159
|
-
return base64.b64decode(encoded_key_data)
|
160
|
-
except binascii.Error:
|
161
|
-
raise ValueError("Error decoding RFC4716 key data")
|
162
|
-
|
163
|
-
|
164
|
-
def is_pkcs8(data: bytes) -> bool:
|
165
|
-
"""Validate data is a valid looking PKCS8 SSH private key."""
|
166
|
-
return (data.startswith(PKCS8_MARKER_START) and data.endswith(PKCS8_MARKER_END)) or (
|
167
|
-
data.startswith(PKCS8_MARKER_START_ENCRYPTED) and data.endswith(PKCS8_MARKER_END_ENCRYPTED)
|
168
|
-
)
|
169
|
-
|
170
|
-
|
171
|
-
def is_pem(data: bytes) -> bool:
|
172
|
-
"""Validate data is a valid looking PEM SSH private key."""
|
173
|
-
return (
|
174
|
-
(data.startswith(PEM_MARKER_START_RSA) and data.endswith(PEM_MARKER_END_RSA))
|
175
|
-
or (data.startswith(PEM_MARKER_START_DSA) and data.endswith(PEM_MARKER_END_DSA))
|
176
|
-
or (data.startswith(PEM_MARKER_START_EC) and data.endswith(PEM_MARKER_END_EC))
|
177
|
-
)
|
File without changes
|
File without changes
|
File without changes
|
{dissect.target-3.19.dev11.dist-info → dissect.target-3.19.dev13.dist-info}/entry_points.txt
RENAMED
File without changes
|
File without changes
|