diff --git a/dissect/target/plugins/os/unix/linux/_os.py b/dissect/target/plugins/os/unix/linux/_os.py index fb2e6367d..545eb2615 100644 --- a/dissect/target/plugins/os/unix/linux/_os.py +++ b/dissect/target/plugins/os/unix/linux/_os.py @@ -8,6 +8,7 @@ from dissect.target.plugins.os.unix.bsd.osx._os import MacPlugin from dissect.target.plugins.os.unix.linux.network_managers import ( LinuxNetworkManager, + parse_unix_dhcp_leases, parse_unix_dhcp_log_messages, ) from dissect.target.plugins.os.windows._os import WindowsPlugin @@ -39,8 +40,11 @@ def ips(self) -> list[str]: for ip_set in self.network_manager.get_config_value("ips"): ips.update(ip_set) - for ip in parse_unix_dhcp_log_messages(self.target, iter_all=False): - ips.add(ip) + if dhcp_lease_ips := parse_unix_dhcp_leases(self.target): + ips.update(dhcp_lease_ips) + + elif dhcp_log_ips := parse_unix_dhcp_log_messages(self.target, iter_all=False): + ips.update(dhcp_log_ips) return list(ips) diff --git a/dissect/target/plugins/os/unix/linux/network_managers.py b/dissect/target/plugins/os/unix/linux/network_managers.py index 4fb4def76..75fb1bbe7 100644 --- a/dissect/target/plugins/os/unix/linux/network_managers.py +++ b/dissect/target/plugins/os/unix/linux/network_managers.py @@ -12,6 +12,7 @@ from defusedxml import ElementTree from dissect.target.exceptions import PluginError +from dissect.target.helpers import configutil if TYPE_CHECKING: from dissect.target.helpers.fsutil import TargetPath @@ -601,6 +602,40 @@ def records_enumerate(iterable: Iterable) -> Iterator[tuple[int, JournalRecord | return ips +def parse_unix_dhcp_leases(target: Target) -> set[str]: + """Parse NetworkManager and dhclient DHCP ``.lease`` files. + + Resources: + - https://linux.die.net/man/5/dhclient.conf + + Args: + target: Target to discover and obtain network information from. + + Returns: + A set of found DHCP IP addresses. + """ + ips = set() + + for lease_file in chain( + target.fs.path("/var/lib/NetworkManager").glob("*.lease*"), + target.fs.path("/var/lib/dhcp").glob("*.lease*"), + target.fs.path("/var/lib/dhclient").glob("*.lease*"), + ): + lease_text = lease_file.read_text() + + if "lease {" in lease_text: + for line in lease_text.split("\n"): + if "fixed-address" in line: + ips.add(line.split(" ")[-1].strip(";")) + + elif "ADDRESS=" in lease_text: + lease = configutil.parse(lease_file, hint="env") + if ip := lease.get("ADDRESS"): + ips.add(ip) + + return ips + + def should_ignore_ip(ip: str) -> bool: for i in IGNORED_IPS: if ip.startswith(i): diff --git a/tests/plugins/os/unix/test_ips.py b/tests/plugins/os/unix/test_ips.py index c86d8505e..ec52a76c5 100644 --- a/tests/plugins/os/unix/test_ips.py +++ b/tests/plugins/os/unix/test_ips.py @@ -246,3 +246,58 @@ def test_regression_ips_unique_strings(target_unix: Target, fs_unix: VirtualFile assert len(target_unix.ips) == 1 assert target_unix.ips == ["1.2.3.4"] + + +def test_ips_dhcp_lease_files(target_unix: Target, fs_unix: VirtualFilesystem) -> None: + """Test if we can detect DHCP lease files from NetworkManager and dhclient.""" + + lease1 = """ + # This is private data. Do not parse. + ADDRESS=1.2.3.4 + """ + + lease2 = """ + lease { + interface "eth0"; + fixed-address 9.0.1.2; + option dhcp-lease-time 13337; + option routers 0.0.0.0; + option host-name "hostname"; + renew 1 2023/12/31 13:37:00; + rebind 2 2023/01/01 01:00:00; + expire 3 2024/01/01 13:37:00; + # real leases contain more key/value pairs + } + lease { + interface "eth0"; + fixed-address 5.6.7.8; + option dhcp-lease-time 13337; + option routers 0.0.0.0; + option host-name "hostname"; + renew 1 2024/12/31 13:37:00; + rebind 2 2024/01/01 01:00:00; + expire 3 2025/01/01 13:37:00; + # real leases contain more key/value pairs + } + """ + + lease3 = """ + some-other "value"; + lease { + interface "eth1"; + fixed-address 3.4.5.6; + } + """ + + fs_unix.map_file_fh("/var/lib/NetworkManager/internal-uuid-eth0.lease", BytesIO(textwrap.dedent(lease1).encode())) + fs_unix.map_file_fh("/var/lib/dhcp/dhclient.leases", BytesIO(textwrap.dedent(lease2).encode())) + fs_unix.map_file_fh("/var/lib/dhclient/dhclient.eth0.leases", BytesIO(textwrap.dedent(lease3).encode())) + + syslog = "Apr 4 13:37:04 localhost dhclient[4]: bound to 1.3.3.7 -- renewal in 1337 seconds." + fs_unix.map_file_fh("/var/log/syslog", BytesIO(textwrap.dedent(syslog).encode())) + + target_unix.add_plugin(LinuxPlugin) + + # tests if we did not call :func:`parse_unix_dhcp_log_messages` since :func:`parse_unix_dhcp_leases` has results. + assert len(target_unix.ips) == 4 + assert sorted(target_unix.ips) == sorted(["1.2.3.4", "5.6.7.8", "9.0.1.2", "3.4.5.6"])