Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add wget hsts plugin #868

Merged
merged 6 commits into from
Oct 15, 2024
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
91 changes: 91 additions & 0 deletions dissect/target/plugins/apps/shell/wget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from typing import Iterator

from dissect.target.exceptions import UnsupportedPluginError
from dissect.target.helpers.descriptor_extensions import UserRecordDescriptorExtension
from dissect.target.helpers.fsutil import TargetPath
from dissect.target.helpers.record import create_extended_descriptor
from dissect.target.plugin import Plugin, export
from dissect.target.plugins.general.users import UserDetails
from dissect.target.target import Target

WgetHstsRecord = create_extended_descriptor([UserRecordDescriptorExtension])(
"apps/shell/wget/hsts",
[
("datetime", "ts_created"),
("uri", "host"),
("boolean", "explicit_port"),
("boolean", "include_subdomains"),
("datetime", "max_age"),
("path", "source"),
],
)


class WgetPlugin(Plugin):
"""Wget shell plugin."""

__namespace__ = "wget"

def __init__(self, target: Target):
super().__init__(target)
self.artifacts = list(self._find_artifacts())

def _find_artifacts(self) -> Iterator[tuple[UserDetails, TargetPath]]:
for user_details in self.target.user_details.all_with_home():
if (hsts_file := user_details.home_path.joinpath(".wget-hsts")).exists():
yield hsts_file, user_details

def check_compatible(self) -> None:
if not self.artifacts:
raise UnsupportedPluginError("No .wget-hsts files found on target")

Check warning on line 40 in dissect/target/plugins/apps/shell/wget.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugins/apps/shell/wget.py#L40

Added line #L40 was not covered by tests

@export(record=WgetHstsRecord)
def hsts(self) -> Iterator[WgetHstsRecord]:
"""Yield domain entries found in wget HSTS files.

When using the ``wget`` command-line utility, a file named ``.wget-hsts`` is created in the user's home
directory by default. The ``.wget-hsts`` file records HTTP Strict Transport Security (HSTS) information for the
websites visited by the user via ``wget``.

Resources:
- https://www.gnu.org/software/wget
- https://gitlab.com/gnuwget/wget/-/blob/master/src/hsts.c
- https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security

Yields ``WgetHstsRecord``s with the following fields:

.. code-block:: text

ts_created (datetime): When the host was first added to the HSTS file
host (uri): The host that was accessed over TLS by wget
explicit_port (boolean): If the TCP port for TLS should be checked
include_subdomains (boolean): If subdomains are included in the HSTS check
max_age (datetime): Time to live of the entry in the HSTS file
source (path): Location of the .wget-hsts file
"""
for hsts_file, user_details in self.artifacts:
if not hsts_file.is_file():
continue

Check warning on line 68 in dissect/target/plugins/apps/shell/wget.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugins/apps/shell/wget.py#L68

Added line #L68 was not covered by tests

for line in hsts_file.open("rt").readlines():
if not (line := line.strip()) or line.startswith("#"):
continue

try:
host, port, subdomain_count, created, max_age = line.split("\t")

except ValueError as e:
self.target.log.warning("Unexpected wget hsts line in file: %s", hsts_file)
self.target.log.debug("", exc_info=e)
continue

Check warning on line 80 in dissect/target/plugins/apps/shell/wget.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugins/apps/shell/wget.py#L77-L80

Added lines #L77 - L80 were not covered by tests

yield WgetHstsRecord(
ts_created=int(created),
host=host,
explicit_port=int(port),
include_subdomains=int(subdomain_count),
max_age=int(created) + int(max_age),
source=hsts_file,
_user=user_details.user,
_target=self.target,
)
53 changes: 53 additions & 0 deletions tests/plugins/apps/shell/test_wget.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import textwrap
from datetime import datetime, timezone
from io import BytesIO

from dissect.target.filesystem import VirtualFilesystem
from dissect.target.plugins.apps.shell.wget import WgetPlugin
from dissect.target.plugins.os.unix._os import UnixPlugin
from dissect.target.target import Target


def test_wget_hsts(target_unix_users: Target, fs_unix: VirtualFilesystem) -> None:
"""test if .wget-hsts files are parsed as expected"""
fs_unix.map_file_fh("/etc/hostname", BytesIO(b"example.domain"))

fs_unix.map_file_fh(
"/home/user/.wget-hsts",
BytesIO(
textwrap.dedent(
"""
# HSTS 1.0 Known Hosts database for GNU Wget.
# Edit at your own risk.
# <hostname> <port> <incl. subdomains> <created> <max-age>
mozilla.org 0 0 1717171717 31536000
google.com 0 1 1711711711 31536000
www.python.org 0 1 1713143141 63072000
github.com 0 1 1713371337 31536000
"""
).encode()
),
)

target_unix_users.add_plugin(UnixPlugin)
target_unix_users.add_plugin(WgetPlugin)

results = sorted(list(target_unix_users.wget.hsts()), key=lambda r: r.host)

assert len(results) == 4
assert [r.host for r in results] == [
"github.com",
"google.com",
"mozilla.org",
"www.python.org",
]

assert results[0].hostname == "example"
assert results[0].domain == "domain"
assert results[0].username == "user"
assert results[0].host == "github.com"
assert not results[0].explicit_port
assert results[0].include_subdomains
assert results[0].ts_created == datetime(2024, 4, 17, 16, 28, 57, tzinfo=timezone.utc)
assert results[0].max_age == datetime(2025, 4, 17, 16, 28, 57, tzinfo=timezone.utc)
assert results[0].source == "/home/user/.wget-hsts"
Loading