Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions dissect/target/plugins/apps/ssh/openssh.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import base64
import re
from itertools import product
from pathlib import Path
Expand All @@ -14,6 +15,7 @@
PrivateKeyRecord,
PublicKeyRecord,
SSHPlugin,
calculate_fingerprints,
)


Expand Down Expand Up @@ -143,12 +145,14 @@ def public_keys(self) -> Iterator[PublicKeyRecord]:
continue

key_type, public_key, comment = parse_ssh_public_key_file(file_path)
fingerprints = calculate_fingerprints(base64.b64decode(public_key))

yield PublicKeyRecord(
mtime_ts=file_path.stat().st_mtime,
key_type=key_type,
public_key=public_key,
comment=comment,
fingerprint=fingerprints,
path=file_path,
_target=self.target,
_user=user,
Expand Down
48 changes: 35 additions & 13 deletions dissect/target/plugins/apps/ssh/putty.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from base64 import b64decode
from datetime import datetime
from pathlib import Path
from typing import Iterator, Optional, Union
Expand All @@ -12,7 +13,11 @@
from dissect.target.helpers.record import create_extended_descriptor
from dissect.target.helpers.regutil import RegistryKey
from dissect.target.plugin import export
from dissect.target.plugins.apps.ssh.ssh import KnownHostRecord, SSHPlugin
from dissect.target.plugins.apps.ssh.ssh import (
KnownHostRecord,
SSHPlugin,
calculate_fingerprints,
)
from dissect.target.plugins.general.users import UserDetails

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -96,12 +101,15 @@ def _regf_known_hosts(self, putty_key: RegistryKey, user_details: UserDetails) -
key_type, host = entry.name.split("@")
port, host = host.split(":")

public_key, fingerprints = construct_public_key(key_type, entry.value)

yield KnownHostRecord(
mtime_ts=ssh_host_keys.ts,
host=host,
port=port,
key_type=key_type,
public_key=construct_public_key(key_type, entry.value),
public_key=public_key,
fingerprint=fingerprints,
comment="",
marker=None,
path=windows_path(ssh_host_keys.path),
Expand All @@ -121,12 +129,15 @@ def _path_known_hosts(self, putty_path: TargetPath, user_details: UserDetails) -
key_type, host = parts[0].split("@")
port, host = host.split(":")

public_key, fingerprints = construct_public_key(key_type, parts[1])

yield KnownHostRecord(
mtime_ts=ts,
host=host,
port=port,
key_type=key_type,
public_key=construct_public_key(key_type, parts[1]),
public_key=public_key,
fingerprint=fingerprints,
comment="",
marker=None,
path=posix_path(ssh_host_keys_path),
Expand Down Expand Up @@ -197,18 +208,16 @@ def parse_host_user(host: str, user: str) -> tuple[str, str]:
return host, user


def construct_public_key(key_type: str, iv: str) -> str:
"""Returns OpenSSH format public key calculated from PuTTY SshHostKeys format.
def construct_public_key(key_type: str, iv: str) -> tuple[str, tuple[str, str, str]]:
"""Returns OpenSSH format public key calculated from PuTTY SshHostKeys format and set of fingerprints.

PuTTY stores raw public key components instead of OpenSSH-formatted public keys
or fingerprints. With RSA public keys the exponent and modulus are stored.
With ECC keys the x and y prime coordinates are stored together with the curve type.

Currently supports ``ssh-ed25519``, ``ecdsa-sha2-nistp256`` and ``rsa2`` key types.

NOTE:
- Sha256 fingerprints of the reconstructed public keys are currently not generated.
- More key types could be supported in the future.
NOTE: More key types could be supported in the future.

Resources:
- https://github.com/github/putty/blob/master/contrib/kh2reg.py
Expand All @@ -217,20 +226,33 @@ def construct_public_key(key_type: str, iv: str) -> str:
- https://github.com/mkorthof/reg2kh
"""

if not isinstance(key_type, str) or not isinstance(iv, str):
raise ValueError("Invalid key_type or iv")

key = None

if key_type == "ssh-ed25519":
x, y = iv.split(",")
key = ECC.construct(curve="ed25519", point_x=int(x, 16), point_y=int(y, 16))
return key.public_key().export_key(format="OpenSSH").split()[-1]

if key_type == "ecdsa-sha2-nistp256":
_, x, y = iv.split(",")
key = ECC.construct(curve="NIST P-256", point_x=int(x, 16), point_y=int(y, 16))
return key.public_key().export_key(format="OpenSSH").split()[-1]

if key_type == "rsa2":
exponent, modulus = iv.split(",")
key = RSA.construct((int(modulus, 16), int(exponent, 16)))
return key.public_key().export_key(format="OpenSSH").decode("utf-8").split()[-1]

log.warning("Could not reconstruct public key: type %s not implemented.", key_type)
return iv
if key is None:
log.warning("Could not reconstruct public key: type %s not implemented", key_type)
return iv, (None, None, None)

openssh_public_key = key.public_key().export_key(format="OpenSSH")

if isinstance(openssh_public_key, bytes):
# RSA's export_key() returns bytes
openssh_public_key = openssh_public_key.decode()

key_part = openssh_public_key.split()[-1]
fingerprints = calculate_fingerprints(b64decode(key_part))
return key_part, fingerprints
40 changes: 40 additions & 0 deletions dissect/target/plugins/apps/ssh/ssh.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import base64
from hashlib import md5, sha1, sha256

from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension
from dissect.target.helpers.record import create_extended_descriptor
from dissect.target.plugin import NamespacePlugin
Expand Down Expand Up @@ -29,6 +32,7 @@
("varint", "port"),
("string", "public_key"),
("string", "marker"),
("digest", "fingerprint"),
],
)

Expand All @@ -50,9 +54,45 @@
("datetime", "mtime_ts"),
*COMMON_ELLEMENTS,
("string", "public_key"),
("digest", "fingerprint"),
],
)


class SSHPlugin(NamespacePlugin):
__namespace__ = "ssh"


def calculate_fingerprints(public_key_decoded: bytes, ssh_keygen_format: bool = False) -> tuple[str, str, str]:
"""Calculate the MD5, SHA1 and SHA256 digest of the given decoded public key.

Adheres as much as possible to the output provided by ssh-keygen when ``ssh_keygen_format``
parameter is set to ``True``. When set to ``False`` (default) hexdigests are calculated
instead for ``sha1``and ``sha256``.

Resources:
- https://en.wikipedia.org/wiki/Public_key_fingerprint
- https://man7.org/linux/man-pages/man1/ssh-keygen.1.html
- ``ssh-keygen -l -E <alg> -f key.pub``
"""
if not public_key_decoded:
raise ValueError("No decoded public key provided")

if not isinstance(public_key_decoded, bytes):
raise ValueError("Provided public key should be bytes")

if public_key_decoded[0:3] != b"\x00\x00\x00":
raise ValueError("Provided value does not look like a public key")

digest_md5 = md5(public_key_decoded).digest()
digest_sha1 = sha1(public_key_decoded).digest()
digest_sha256 = sha256(public_key_decoded).digest()

if ssh_keygen_format:
fingerprint_sha1 = base64.b64encode(digest_sha1).rstrip(b"=").decode()
fingerprint_sha256 = base64.b64encode(digest_sha256).rstrip(b"=").decode()
else:
fingerprint_sha1 = digest_sha1.hex()
fingerprint_sha256 = digest_sha256.hex()

return digest_md5.hex(), fingerprint_sha1, fingerprint_sha256
28 changes: 27 additions & 1 deletion tests/plugins/apps/ssh/test_openssh.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import base64
import textwrap
from enum import Enum, auto
from io import BytesIO
Expand All @@ -8,7 +9,10 @@

from dissect.target import Target
from dissect.target.filesystem import VirtualFilesystem
from dissect.target.plugins.apps.ssh.openssh import OpenSSHPlugin
from dissect.target.plugins.apps.ssh.openssh import (
OpenSSHPlugin,
calculate_fingerprints,
)


@pytest.fixture(
Expand Down Expand Up @@ -393,13 +397,35 @@ def test_public_keys_plugin(target_and_filesystem: tuple[Target, VirtualFilesyst
assert user_public_key.key_type == user_public_key_data.split(" ", 2)[0]
assert user_public_key.public_key == user_public_key_data.split(" ", 2)[1]
assert user_public_key.comment == user_public_key_data.split(" ", 2)[2]
assert user_public_key.fingerprint.md5 == "1f3d475966231eeb5455c8485dd030e4"
assert user_public_key.fingerprint.sha1 == "e39242ca1d74bea99285b212e908e18cc67e4dec"
assert user_public_key.fingerprint.sha256 == "7b77007b0b51a86ced6b5fe25639092484c4c39cf76b283ef65fdf49a00f44d2"
assert str(user_public_key.path).replace("\\", "/") == target_system.filesystem_path(
".ssh/id_ed25519.pub", TargetDir.HOME
).replace("\\", "/")

assert host_public_key.key_type == host_public_key_data.split(" ", 2)[0]
assert host_public_key.public_key == host_public_key_data.split(" ", 2)[1]
assert host_public_key.comment == host_public_key_data.split(" ", 2)[2]
assert host_public_key.fingerprint.md5 == "a3f2ebfa8d16efd321015e1618fd281b"
assert host_public_key.fingerprint.sha1 == "f6656cc642fb08f53a1df77d0acff9852a649989"
assert host_public_key.fingerprint.sha256 == "8c7023d563c763fcf5104332d7cf51c978c5ba1dd9f5cbd341edd32dfcbef3ef"
assert str(host_public_key.path).replace("\\", "/") == target_system.filesystem_path(
"ssh_host_rsa_key.pub", TargetDir.SSHD
).replace(target_system.label, "\\sysvol\\").replace("\\", "/")


def test_calculate_fingerprints() -> None:
ed25519_pub = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINz6oq+IweAoQFMzQ0aJLYXJFkLn3tXMbVZ550wvUKOw long comment here"

assert calculate_fingerprints(base64.b64decode(ed25519_pub.split(" ")[1])) == (
"1f3d475966231eeb5455c8485dd030e4",
"e39242ca1d74bea99285b212e908e18cc67e4dec",
"7b77007b0b51a86ced6b5fe25639092484c4c39cf76b283ef65fdf49a00f44d2",
)

assert calculate_fingerprints(base64.b64decode(ed25519_pub.split(" ")[1]), ssh_keygen_format=True) == (
"1f3d475966231eeb5455c8485dd030e4",
"45JCyh10vqmShbIS6QjhjMZ+Tew",
"e3cAewtRqGzta1/iVjkJJITEw5z3ayg+9l/fSaAPRNI",
)
28 changes: 27 additions & 1 deletion tests/plugins/apps/ssh/test_putty.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
from datetime import datetime, timezone
from os import stat

import pytest
from flow.record.fieldtypes import path

from dissect.target import Target
from dissect.target.filesystem import VirtualFilesystem
from dissect.target.helpers.regutil import VirtualHive, VirtualKey, VirtualValue
from dissect.target.plugins.apps.ssh.putty import PuTTYPlugin
from dissect.target.plugins.apps.ssh.putty import PuTTYPlugin, construct_public_key
from tests._utils import absolute_path


Expand All @@ -22,30 +23,45 @@ def test_putty_plugin_ssh_host_keys_unix(target_unix_users: Target, fs_unix: Vir
assert records[0].mtime_ts is not None
assert records[0].host == "192.168.123.130"
assert records[0].port == 22

assert records[0].key_type == "ssh-ed25519"
assert records[0].public_key == "AAAAC3NzaC1lZDI1NTE5AAAAIHUl23i/4p/7xcZnNPDK+Dr+A539zpEEXutrm/tESFYq"
assert records[0].fingerprint.md5 == "16da68cfc0e1b7954c84147a385be2b6"
assert records[0].fingerprint.sha1 == "11e647366ef9b74feb55da7f8507a7180123eed0"
assert records[0].fingerprint.sha256 == "0ecaca9db7d166fd55f84ad775074bd6c743ae2d26e6dac10e60e88efbfcc01d"

assert records[0].path == path.from_posix("/root/.putty/sshhostkeys")
assert records[0].username == "root"

assert records[1].mtime_ts is not None
assert records[1].host == "example.com"
assert records[1].port == 22

assert records[1].key_type == "ecdsa-sha2-nistp256"
assert (
records[1].public_key
== "AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBNhJOmUcnvcVIizc67M3bS0/2Gz43YPHYCRaogqAgHvgVUartyfsl5nZBoFR3FiUc9kTJJUPybWbqXaGLEthjfM=" # noqa E501
)
assert records[1].fingerprint.md5 == "e23736ef80f12b672cf217c390a1acfb"
assert records[1].fingerprint.sha1 == "93a03a809a02091aab194b04c836c37298c0602a"
assert records[1].fingerprint.sha256 == "53d7097c310ce329d554dfb533c81f81d955facae8f46e9d7cfa4db58304c674"

assert records[1].path == path.from_posix("/root/.putty/sshhostkeys")
assert records[1].username == "root"

assert records[2].mtime_ts is not None
assert records[2].host == "1.2.3.4"
assert records[2].port == 1234

assert records[2].key_type == "rsa2"
assert (
records[2].public_key
== "AAAAB3NzaC1yc2EAAAADAQABAAABgQCmYvCKueiA3lyhZuW0OAObOZvf0g957H0wGhGi3BruZSDpa9UQhsVNtgPZfuyx/yYWUIhQ4VSz+qw00IhZJt9myRNrXCxIABM/qubTghrdjRU+ydb0J9uTBIRz+ys/0dr0dg2Gc7C5w9+E3EpRit6VCZfTi8mNYZi+/GC12VYIDsqY9/4D4xloHJP+fs1mCNnY12VtJuIWw281fKaTCxm3H95NOBbAE6sAwr8H5lMLU34D4DcA5ZIG5F48yDmYUkILtN4qLAZMl4ZlQ6KpcxMYCN6DPj2NwR8JHzr+mkwjmGqbs8tjYD/KRYL2sYhK6Fdx7wvw5djQeVTEnhjxoeiCYNFAGkzB9BfeP4N0K2rp6F3NktFU4WIc6nLsX5G1LEWuAvAxwg+v6y4YzmpXHP+WrRHKyhS+B64aLpmD7AAJzPwSyqtt5+8SY3z7EMCpYBGz8uhdYzaY0U+qWFHtIfI5GLrl2akYBzH9t/YPAF6wdTuSh4jxVe2IVrd1x7g23Zs=" # noqa E501
)
assert records[2].fingerprint.md5 == "539539a9dec6afa0c8d9cdbb9357ed5e"
assert records[2].fingerprint.sha1 == "c0f0c0a51ed7ae4c2b271f0426554f837f9508d0"
assert records[2].fingerprint.sha256 == "c2d4bd813cdf1eec38ea8c7bbf0c56b129f9db3e78627d5b7ebfae8db60c5eb6"

assert records[2].path == path.from_posix("/root/.putty/sshhostkeys")
assert records[2].username == "root"

Expand Down Expand Up @@ -129,3 +145,13 @@ def test_putty_plugin_saved_sessions_windows(
assert records[0].port_forward == ""
assert records[0].manual_ssh_host_keys == ""
assert records[0].path == path.from_windows("Software\\SimonTatham\\PuTTY\\Sessions\\example-saved-session")


def test_construct_public_key() -> None:
with pytest.raises(ValueError):
construct_public_key(None, None)

assert construct_public_key("unsupported", "some-iv") == (
"some-iv",
(None, None, None),
)