Skip to content

Commit cb7b9ec

Browse files
authored
Fix inconsistent duplicate field mappings in various plugins (#990)
1 parent a8a085c commit cb7b9ec

File tree

9 files changed

+141
-27
lines changed

9 files changed

+141
-27
lines changed

dissect/target/plugins/apps/vpn/wireguard.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
("string", "name"), # basename of .conf file if unset
1616
("net.ipaddress", "address"),
1717
("string", "private_key"),
18-
("string", "listen_port"),
18+
("varint", "listen_port"),
1919
("string", "fw_mark"),
2020
("string", "dns"),
2121
("varint", "table"),

dissect/target/plugins/os/unix/linux/sockets.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
("string", "protocol"),
1717
("uint32", "rx_queue"),
1818
("uint32", "tx_queue"),
19-
("string", "local_ip"),
19+
("net.ipaddress", "local_ip"),
2020
("uint16", "local_port"),
21-
("string", "remote_ip"),
21+
("net.ipaddress", "remote_ip"),
2222
("uint16", "remote_port"),
2323
("string", "state"),
2424
("string", "owner"),

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

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from __future__ import annotations
2+
3+
from datetime import datetime, timedelta, timezone
14
from typing import Iterator
25

36
from dissect.target.exceptions import UnsupportedPluginError
@@ -13,12 +16,12 @@
1316
("string", "hash"),
1417
("string", "algorithm"),
1518
("string", "crypt_param"),
16-
("string", "last_change"),
17-
("varint", "min_age"),
18-
("varint", "max_age"),
19+
("datetime", "last_change"),
20+
("datetime", "min_age"),
21+
("datetime", "max_age"),
1922
("varint", "warning_period"),
20-
("string", "inactivity_period"),
21-
("string", "expiration_date"),
23+
("varint", "inactivity_period"),
24+
("datetime", "expiration_date"),
2225
("string", "unused_field"),
2326
],
2427
)
@@ -39,6 +42,7 @@ def passwords(self) -> Iterator[UnixShadowRecord]:
3942
4043
Resources:
4144
- https://manpages.ubuntu.com/manpages/oracular/en/man5/passwd.5.html
45+
- https://linux.die.net/man/5/shadow
4246
"""
4347

4448
seen_hashes = set()
@@ -64,19 +68,53 @@ def passwords(self) -> Iterator[UnixShadowRecord]:
6468

6569
seen_hashes.add(current_hash)
6670

71+
# improve readability
72+
last_change = None
73+
min_age = None
74+
max_age = None
75+
expiration_date = None
76+
77+
try:
78+
last_change = int(shent.get(2)) if shent.get(2) else None
79+
except ValueError as e:
80+
self.target.log.warning(
81+
"Unable to parse last_change shadow value in %s: %s ('%s')", shadow_file, e, shent.get(2)
82+
)
83+
84+
try:
85+
min_age = int(shent.get(3)) if shent.get(3) else None
86+
except ValueError as e:
87+
self.target.log.warning(
88+
"Unable to parse last_change shadow value in %s: %s ('%s')", shadow_file, e, shent.get(3)
89+
)
90+
91+
try:
92+
max_age = int(shent.get(4)) if shent.get(4) else None
93+
except ValueError as e:
94+
self.target.log.warning(
95+
"Unable to parse last_change shadow value in %s: %s ('%s')", shadow_file, e, shent.get(4)
96+
)
97+
98+
try:
99+
expiration_date = int(shent.get(7)) if shent.get(7) else None
100+
except ValueError as e:
101+
self.target.log.warning(
102+
"Unable to parse last_change shadow value in %s: %s ('%s')", shadow_file, e, shent.get(7)
103+
)
104+
67105
yield UnixShadowRecord(
68106
name=shent.get(0),
69107
crypt=shent.get(1),
70108
algorithm=crypt.get("algo"),
71109
crypt_param=crypt.get("param"),
72110
salt=crypt.get("salt"),
73111
hash=crypt.get("hash"),
74-
last_change=shent.get(2),
75-
min_age=shent.get(3),
76-
max_age=shent.get(4),
77-
warning_period=shent.get(5),
78-
inactivity_period=shent.get(6),
79-
expiration_date=shent.get(7),
112+
last_change=epoch_days_to_datetime(last_change) if last_change else None,
113+
min_age=epoch_days_to_datetime(last_change + min_age) if last_change and min_age else None,
114+
max_age=epoch_days_to_datetime(last_change + max_age) if last_change and max_age else None,
115+
warning_period=shent.get(5) if shent.get(5) else None,
116+
inactivity_period=shent.get(6) if shent.get(6) else None,
117+
expiration_date=epoch_days_to_datetime(expiration_date) if expiration_date else None,
80118
unused_field=shent.get(8),
81119
_target=self.target,
82120
)
@@ -128,3 +166,11 @@ def extract_crypt_details(shent: dict) -> dict:
128166
crypt["algo"] = algos[crypt["algo"]]
129167

130168
return crypt
169+
170+
171+
def epoch_days_to_datetime(days: int) -> datetime:
172+
"""Convert a number representing the days since 1 January 1970 to a datetime object."""
173+
if not isinstance(days, int):
174+
raise ValueError("days argument should be an integer")
175+
176+
return datetime(1970, 1, 1, 0, 0, tzinfo=timezone.utc) + timedelta(days)

dissect/target/plugins/os/windows/generic.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
"filesystem/registry/ndis",
6161
[
6262
("datetime", "ts"),
63-
("string", "network"),
63+
("string", "network_name"),
6464
("string", "name"),
6565
("string", "pnpinstanceid"),
6666
],
@@ -113,7 +113,7 @@
113113
("path", "librarypath"),
114114
("string", "displaystring"),
115115
("bytes", "providerid"),
116-
("string", "enabled"),
116+
("boolean", "enabled"),
117117
("string", "version"),
118118
],
119119
)
@@ -408,7 +408,7 @@ def ndis(self) -> Iterator[NdisRecord]:
408408

409409
yield NdisRecord(
410410
ts=network.ts,
411-
network=sub.name,
411+
network_name=sub.name,
412412
name=name,
413413
pnpinstanceid=pnpinstanceid,
414414
_target=self.target,

dissect/target/plugins/os/windows/log/amcache.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
("string", "pe_image"),
4343
("string", "pe_subsystem"),
4444
("string", "crc_checksum"),
45-
("string", "filesize"),
45+
("filesize", "filesize"),
4646
("wstring", "longname"),
4747
("string", "msi"),
4848
]
@@ -91,7 +91,7 @@ def create_record(
9191
binary_type=install_properties.get("binarytype"),
9292
bin_product_version=install_properties.get("binproductversion"),
9393
bin_file_version=install_properties.get("binfileversion"),
94-
filesize=install_properties.get("filesize"),
94+
filesize=int(install_properties.get("filesize", "0"), 16),
9595
pe_image=install_properties.get("peimagetype"),
9696
product_version=install_properties.get("productversion"),
9797
crc_checksum=install_properties.get("crcchecksum"),

dissect/target/plugins/os/windows/sru.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,8 @@
176176
("path", "app"),
177177
("string", "user"),
178178
("varint", "flags"),
179-
("varint", "start_time"),
180-
("varint", "end_time"),
179+
("datetime", "start_time"),
180+
("datetime", "end_time"),
181181
("bytes", "usage"),
182182
],
183183
)

tests/plugins/apps/vpn/test_wireguard.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def test_wireguard_plugin_global_log(target_unix_users, fs_unix):
1515
assert record.name == "wg0"
1616
assert str(record.address) == "10.13.37.1"
1717
assert record.private_key == "UHJpdmF0ZUtleQ=="
18-
assert record.listen_port == "12345"
18+
assert record.listen_port == 12345
1919
assert record.source == "etc/wireguard/wg0.conf"
2020
assert record.dns is None
2121

tests/plugins/os/unix/test_shadow.py

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
from datetime import datetime, timezone
12
from io import BytesIO
23
from pathlib import Path
4+
from textwrap import dedent
5+
6+
import pytest
37

48
from dissect.target.filesystem import VirtualFilesystem
59
from dissect.target.plugins.os.unix.shadow import ShadowPlugin
@@ -25,12 +29,12 @@ def test_unix_shadow(target_unix_users: Target, fs_unix: VirtualFilesystem) -> N
2529
) # noqa E501
2630
assert results[0].algorithm == "sha512"
2731
assert results[0].crypt_param is None
28-
assert results[0].last_change == "18963"
29-
assert results[0].min_age == 0
30-
assert results[0].max_age == 99999
32+
assert results[0].last_change == datetime(2021, 12, 2, 0, 0, 0, tzinfo=timezone.utc) # 18963
33+
assert results[0].min_age is None
34+
assert results[0].max_age == datetime(2295, 9, 16, 0, 0, 0, tzinfo=timezone.utc) # 99999
3135
assert results[0].warning_period == 7
32-
assert results[0].inactivity_period == ""
33-
assert results[0].expiration_date == ""
36+
assert results[0].inactivity_period is None
37+
assert results[0].expiration_date is None
3438
assert results[0].unused_field == ""
3539

3640

@@ -49,3 +53,66 @@ def test_unix_shadow_backup_file(target_unix_users: Target, fs_unix: VirtualFile
4953
assert results[0].name == "test"
5054
assert results[1].name == "other-user"
5155
assert results[0].hash == results[1].hash
56+
57+
58+
def test_unix_shadow_invalid_shent(
59+
caplog: pytest.LogCaptureFixture, target_unix_users: Target, fs_unix: VirtualFilesystem
60+
) -> None:
61+
"""test if we can parse invalid day values in shents."""
62+
63+
shadow_invalid = """
64+
no_last_change:$6$salt$hash1::0:99999:7::123456:
65+
no_max_age:$6$salt$hash2:18963:0::7:::
66+
only_last_change:$6$salt$hash3:18963::::::
67+
no_int_fields:$6$salt$hash4:string::::::
68+
daemon:*:18474:0:99999:7:::
69+
bin:*:18474:0:99999:7:::
70+
nobody:*:18474:0:99999:7:::
71+
regular:$6$salt$hash5:1337:0:99999:7::123456:
72+
"""
73+
fs_unix.map_file_fh("/etc/shadow", BytesIO(dedent(shadow_invalid).encode()))
74+
75+
results = list(target_unix_users.passwords())
76+
assert len(results) == 5
77+
78+
assert [r.name for r in results] == [
79+
"no_last_change",
80+
"no_max_age",
81+
"only_last_change",
82+
"no_int_fields",
83+
"regular",
84+
]
85+
86+
assert results[0].name == "no_last_change"
87+
assert results[0].last_change is None
88+
assert results[0].min_age is None
89+
assert results[0].max_age is None
90+
assert results[0].warning_period == 7
91+
assert results[0].inactivity_period is None
92+
assert results[0].expiration_date == datetime(2308, 1, 6, tzinfo=timezone.utc)
93+
94+
assert results[1].name == "no_max_age"
95+
assert results[1].last_change == datetime(2021, 12, 2, tzinfo=timezone.utc)
96+
assert results[1].max_age is None
97+
98+
assert results[2].name == "only_last_change"
99+
assert results[2].last_change == datetime(2021, 12, 2, tzinfo=timezone.utc)
100+
101+
assert results[3].name == "no_int_fields"
102+
assert results[3].last_change is None
103+
assert (
104+
"Unable to parse last_change shadow value in /etc/shadow: invalid literal for int() with base 10: 'string' ('string')" # noqa:E501
105+
in caplog.text
106+
)
107+
108+
# make sure we parsed the last entry even though the other entries are 'broken'
109+
assert results[-1].name == "regular"
110+
assert results[-1].salt == "salt"
111+
assert results[-1].hash == "hash5"
112+
assert results[-1].algorithm == "sha512"
113+
assert results[-1].last_change == datetime(1973, 8, 30, tzinfo=timezone.utc)
114+
assert results[-1].min_age is None
115+
assert results[-1].max_age == datetime(2247, 6, 14, tzinfo=timezone.utc)
116+
assert results[-1].warning_period == 7
117+
assert results[-1].inactivity_period is None
118+
assert results[-1].expiration_date == datetime(2308, 1, 6, tzinfo=timezone.utc)

tests/plugins/os/windows/test_amcache.py renamed to tests/plugins/os/windows/log/test_amcache.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,4 @@ def test_amcache_install_entry(target_win: Target):
159159
assert str(entry.create) == create
160160
assert str(entry.path) == r"C:\Users\JohnCena"
161161
assert str(entry.longname) == r"7z2201-x64.exe"
162+
assert entry.filesize == 1575742

0 commit comments

Comments
 (0)