Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion dissect/target/plugins/apps/vpn/wireguard.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
("string", "name"), # basename of .conf file if unset
("net.ipaddress", "address"),
("string", "private_key"),
("string", "listen_port"),
("varint", "listen_port"),
("string", "fw_mark"),
("string", "dns"),
("varint", "table"),
Expand Down
4 changes: 2 additions & 2 deletions dissect/target/plugins/os/unix/linux/sockets.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@
("string", "protocol"),
("uint32", "rx_queue"),
("uint32", "tx_queue"),
("string", "local_ip"),
("net.ipaddress", "local_ip"),
("uint16", "local_port"),
("string", "remote_ip"),
("net.ipaddress", "remote_ip"),
("uint16", "remote_port"),
("string", "state"),
("string", "owner"),
Expand Down
68 changes: 57 additions & 11 deletions dissect/target/plugins/os/unix/shadow.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from __future__ import annotations

from datetime import datetime, timedelta, timezone
from typing import Iterator

from dissect.target.exceptions import UnsupportedPluginError
Expand All @@ -13,12 +16,12 @@
("string", "hash"),
("string", "algorithm"),
("string", "crypt_param"),
("string", "last_change"),
("varint", "min_age"),
("varint", "max_age"),
("datetime", "last_change"),
("datetime", "min_age"),
("datetime", "max_age"),
("varint", "warning_period"),
("string", "inactivity_period"),
("string", "expiration_date"),
("varint", "inactivity_period"),
("datetime", "expiration_date"),
("string", "unused_field"),
],
)
Expand All @@ -39,6 +42,7 @@

Resources:
- https://manpages.ubuntu.com/manpages/oracular/en/man5/passwd.5.html
- https://linux.die.net/man/5/shadow
"""

seen_hashes = set()
Expand All @@ -64,19 +68,53 @@

seen_hashes.add(current_hash)

# improve readability
last_change = None
min_age = None
max_age = None
expiration_date = None

try:
last_change = int(shent.get(2)) if shent.get(2) else None
except ValueError as e:
self.target.log.warning(
"Unable to parse last_change shadow value in %s: %s ('%s')", shadow_file, e, shent.get(2)
)

try:
min_age = int(shent.get(3)) if shent.get(3) else None
except ValueError as e:
self.target.log.warning(

Check warning on line 87 in dissect/target/plugins/os/unix/shadow.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugins/os/unix/shadow.py#L86-L87

Added lines #L86 - L87 were not covered by tests
"Unable to parse last_change shadow value in %s: %s ('%s')", shadow_file, e, shent.get(3)
)

try:
max_age = int(shent.get(4)) if shent.get(4) else None
except ValueError as e:
self.target.log.warning(

Check warning on line 94 in dissect/target/plugins/os/unix/shadow.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugins/os/unix/shadow.py#L93-L94

Added lines #L93 - L94 were not covered by tests
"Unable to parse last_change shadow value in %s: %s ('%s')", shadow_file, e, shent.get(4)
)

try:
expiration_date = int(shent.get(7)) if shent.get(7) else None
except ValueError as e:
self.target.log.warning(

Check warning on line 101 in dissect/target/plugins/os/unix/shadow.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugins/os/unix/shadow.py#L100-L101

Added lines #L100 - L101 were not covered by tests
"Unable to parse last_change shadow value in %s: %s ('%s')", shadow_file, e, shent.get(7)
)

yield UnixShadowRecord(
name=shent.get(0),
crypt=shent.get(1),
algorithm=crypt.get("algo"),
crypt_param=crypt.get("param"),
salt=crypt.get("salt"),
hash=crypt.get("hash"),
last_change=shent.get(2),
min_age=shent.get(3),
max_age=shent.get(4),
warning_period=shent.get(5),
inactivity_period=shent.get(6),
expiration_date=shent.get(7),
last_change=epoch_days_to_datetime(last_change) if last_change else None,
min_age=epoch_days_to_datetime(last_change + min_age) if last_change and min_age else None,
max_age=epoch_days_to_datetime(last_change + max_age) if last_change and max_age else None,
warning_period=shent.get(5) if shent.get(5) else None,
inactivity_period=shent.get(6) if shent.get(6) else None,
expiration_date=epoch_days_to_datetime(expiration_date) if expiration_date else None,
unused_field=shent.get(8),
_target=self.target,
)
Expand Down Expand Up @@ -128,3 +166,11 @@
crypt["algo"] = algos[crypt["algo"]]

return crypt


def epoch_days_to_datetime(days: int) -> datetime:
"""Convert a number representing the days since 1 January 1970 to a datetime object."""
if not isinstance(days, int):
raise ValueError("days argument should be an integer")

Check warning on line 174 in dissect/target/plugins/os/unix/shadow.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugins/os/unix/shadow.py#L174

Added line #L174 was not covered by tests

return datetime(1970, 1, 1, 0, 0, tzinfo=timezone.utc) + timedelta(days)
6 changes: 3 additions & 3 deletions dissect/target/plugins/os/windows/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
"filesystem/registry/ndis",
[
("datetime", "ts"),
("string", "network"),
("string", "network_name"),
("string", "name"),
("string", "pnpinstanceid"),
],
Expand Down Expand Up @@ -113,7 +113,7 @@
("path", "librarypath"),
("string", "displaystring"),
("bytes", "providerid"),
("string", "enabled"),
("boolean", "enabled"),
("string", "version"),
],
)
Expand Down Expand Up @@ -408,7 +408,7 @@ def ndis(self) -> Iterator[NdisRecord]:

yield NdisRecord(
ts=network.ts,
network=sub.name,
network_name=sub.name,
name=name,
pnpinstanceid=pnpinstanceid,
_target=self.target,
Expand Down
5 changes: 3 additions & 2 deletions dissect/target/plugins/os/windows/log/amcache.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
("string", "pe_image"),
("string", "pe_subsystem"),
("string", "crc_checksum"),
("string", "filesize"),
("filesize", "filesize"),
("wstring", "longname"),
("string", "msi"),
]
Expand All @@ -69,6 +69,7 @@ def create_record(
create: str,
target: Target,
) -> TargetRecordDescriptor:

return description(
start_time=_to_log_timestamp(install_properties.get("starttime")),
stop_time=_to_log_timestamp(install_properties.get("stoptime")),
Expand All @@ -91,7 +92,7 @@ def create_record(
binary_type=install_properties.get("binarytype"),
bin_product_version=install_properties.get("binproductversion"),
bin_file_version=install_properties.get("binfileversion"),
filesize=install_properties.get("filesize"),
filesize=int(install_properties.get("filesize", "0"), 16),
pe_image=install_properties.get("peimagetype"),
product_version=install_properties.get("productversion"),
crc_checksum=install_properties.get("crcchecksum"),
Expand Down
4 changes: 2 additions & 2 deletions dissect/target/plugins/os/windows/sru.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,8 @@
("path", "app"),
("string", "user"),
("varint", "flags"),
("varint", "start_time"),
("varint", "end_time"),
("datetime", "start_time"),
("datetime", "end_time"),
("bytes", "usage"),
],
)
Expand Down
2 changes: 1 addition & 1 deletion tests/plugins/apps/vpn/test_wireguard.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def test_wireguard_plugin_global_log(target_unix_users, fs_unix):
assert record.name == "wg0"
assert str(record.address) == "10.13.37.1"
assert record.private_key == "UHJpdmF0ZUtleQ=="
assert record.listen_port == "12345"
assert record.listen_port == 12345
assert record.source == "etc/wireguard/wg0.conf"
assert record.dns is None

Expand Down
77 changes: 72 additions & 5 deletions tests/plugins/os/unix/test_shadow.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
from datetime import datetime, timezone
from io import BytesIO
from pathlib import Path
from textwrap import dedent

import pytest

from dissect.target.filesystem import VirtualFilesystem
from dissect.target.plugins.os.unix.shadow import ShadowPlugin
Expand All @@ -25,12 +29,12 @@ def test_unix_shadow(target_unix_users: Target, fs_unix: VirtualFilesystem) -> N
) # noqa E501
assert results[0].algorithm == "sha512"
assert results[0].crypt_param is None
assert results[0].last_change == "18963"
assert results[0].min_age == 0
assert results[0].max_age == 99999
assert results[0].last_change == datetime(2021, 12, 2, 0, 0, 0, tzinfo=timezone.utc) # 18963
assert results[0].min_age is None
assert results[0].max_age == datetime(2295, 9, 16, 0, 0, 0, tzinfo=timezone.utc) # 99999
assert results[0].warning_period == 7
assert results[0].inactivity_period == ""
assert results[0].expiration_date == ""
assert results[0].inactivity_period is None
assert results[0].expiration_date is None
assert results[0].unused_field == ""


Expand All @@ -49,3 +53,66 @@ def test_unix_shadow_backup_file(target_unix_users: Target, fs_unix: VirtualFile
assert results[0].name == "test"
assert results[1].name == "other-user"
assert results[0].hash == results[1].hash


def test_unix_shadow_invalid_shent(
caplog: pytest.LogCaptureFixture, target_unix_users: Target, fs_unix: VirtualFilesystem
) -> None:
"""test if we can parse invalid day values in shents."""

shadow_invalid = """
no_last_change:$6$salt$hash1::0:99999:7::123456:
no_max_age:$6$salt$hash2:18963:0::7:::
only_last_change:$6$salt$hash3:18963::::::
no_int_fields:$6$salt$hash4:string::::::
daemon:*:18474:0:99999:7:::
bin:*:18474:0:99999:7:::
nobody:*:18474:0:99999:7:::
regular:$6$salt$hash5:1337:0:99999:7::123456:
"""
fs_unix.map_file_fh("/etc/shadow", BytesIO(dedent(shadow_invalid).encode()))

results = list(target_unix_users.passwords())
assert len(results) == 5

assert [r.name for r in results] == [
"no_last_change",
"no_max_age",
"only_last_change",
"no_int_fields",
"regular",
]

assert results[0].name == "no_last_change"
assert results[0].last_change is None
assert results[0].min_age is None
assert results[0].max_age is None
assert results[0].warning_period == 7
assert results[0].inactivity_period is None
assert results[0].expiration_date == datetime(2308, 1, 6, tzinfo=timezone.utc)

assert results[1].name == "no_max_age"
assert results[1].last_change == datetime(2021, 12, 2, tzinfo=timezone.utc)
assert results[1].max_age is None

assert results[2].name == "only_last_change"
assert results[2].last_change == datetime(2021, 12, 2, tzinfo=timezone.utc)

assert results[3].name == "no_int_fields"
assert results[3].last_change is None
assert (
"Unable to parse last_change shadow value in /etc/shadow: invalid literal for int() with base 10: 'string' ('string')"
in caplog.text
)

# make sure we parsed the last entry even though the other entries are 'broken'
assert results[-1].name == "regular"
assert results[-1].salt == "salt"
assert results[-1].hash == "hash5"
assert results[-1].algorithm == "sha512"
assert results[-1].last_change == datetime(1973, 8, 30, tzinfo=timezone.utc)
assert results[-1].min_age is None
assert results[-1].max_age == datetime(2247, 6, 14, tzinfo=timezone.utc)
assert results[-1].warning_period == 7
assert results[-1].inactivity_period is None
assert results[-1].expiration_date == datetime(2308, 1, 6, tzinfo=timezone.utc)
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,4 @@ def test_amcache_install_entry(target_win: Target):
assert str(entry.create) == create
assert str(entry.path) == r"C:\Users\JohnCena"
assert str(entry.longname) == r"7z2201-x64.exe"
assert entry.filesize == 1575742
Loading