diff --git a/dissect/target/plugin.py b/dissect/target/plugin.py index bc9e18bb1..6eec6ec31 100644 --- a/dissect/target/plugin.py +++ b/dissect/target/plugin.py @@ -63,6 +63,7 @@ class OperatingSystem(enum.Enum): VYOS = "vyos" IOS = "ios" FORTIGATE = "fortigate" + CITRIX = "citrix-netscaler" def export(*args, **kwargs) -> Callable: diff --git a/dissect/target/plugins/os/unix/bsd/citrix/__init__.py b/dissect/target/plugins/os/unix/bsd/citrix/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/dissect/target/plugins/os/unix/bsd/citrix/_os.py b/dissect/target/plugins/os/unix/bsd/citrix/_os.py new file mode 100644 index 000000000..10e561496 --- /dev/null +++ b/dissect/target/plugins/os/unix/bsd/citrix/_os.py @@ -0,0 +1,119 @@ +from __future__ import annotations + +import re +from typing import Iterator, Optional + +from dissect.target.filesystem import Filesystem, VirtualFilesystem +from dissect.target.helpers.record import UnixUserRecord +from dissect.target.plugin import OperatingSystem, export +from dissect.target.plugins.os.unix.bsd._os import BsdPlugin +from dissect.target.target import Target + +RE_CONFIG_IP = re.compile(r"-IPAddress (?P[^ ]+) ") +RE_CONFIG_HOSTNAME = re.compile(r"set ns hostName (?P[^\n]+)\n") +RE_CONFIG_TIMEZONE = re.compile( + r'set ns param -timezone "GMT\+(?P[0-9]+):(?P[0-9]+)-.*-(?P.+)"' +) +RE_CONFIG_USER = re.compile(r"bind system user (?P[^ ]+) ") +RE_LOADER_CONFIG_KERNEL_VERSION = re.compile(r'kernel="/(?P.*)"') + + +class CitrixBsdPlugin(BsdPlugin): + def __init__(self, target: Target): + super().__init__(target) + self._ips = [] + self._hostname = None + self.config_usernames = [] + self._parse_netscaler_configs() + + def _parse_netscaler_configs(self) -> None: + ips = set() + usernames = set() + for config_path in self.target.fs.path("/flash/nsconfig/").glob("ns.conf*"): + with config_path.open("rt") as config_file: + config = config_file.read() + for match in RE_CONFIG_IP.finditer(config): + ips.add(match.groupdict()["ip"]) + for match in RE_CONFIG_USER.finditer(config): + usernames.add(match.groupdict()["user"]) + if config_path.name == "ns.conf": + # Current configuration of the netscaler + if hostname_match := RE_CONFIG_HOSTNAME.search(config): + self._hostname = hostname_match.groupdict()["hostname"] + if timezone_match := RE_CONFIG_TIMEZONE.search(config): + tzinfo = timezone_match.groupdict() + self.target.timezone = tzinfo["zone_name"] + + self._config_usernames = list(usernames) + self._ips = list(ips) + + @classmethod + def detect(cls, target: Target) -> Optional[Filesystem]: + newfilesystem = VirtualFilesystem() + is_citrix = False + for fs in target.filesystems: + if fs.exists("/bin/freebsd-version"): + newfilesystem.map_fs("/", fs) + break + for fs in target.filesystems: + if fs.exists("/nsconfig") and fs.exists("/boot"): + newfilesystem.map_fs("/flash", fs) + is_citrix = True + elif fs.exists("/netscaler"): + newfilesystem.map_fs("/var", fs) + is_citrix = True + if is_citrix: + return newfilesystem + return None + + @export(property=True) + def hostname(self) -> Optional[str]: + return self._hostname + + @export(property=True) + def version(self) -> Optional[str]: + version_path = self.target.fs.path("/flash/.version") + version = version_path.read_text().strip() + loader_conf = self.target.fs.path("/flash/boot/loader.conf").read_text() + if match := RE_LOADER_CONFIG_KERNEL_VERSION.search(loader_conf): + kernel_version = match.groupdict()["version"] + return f"{version} ({kernel_version})" + self.target.log.warn("Could not determine kernel version") + return version + + @export(property=True) + def ips(self) -> list[str]: + return self._ips + + @export(record=UnixUserRecord) + def users(self) -> Iterator[UnixUserRecord]: + nstmp_users = set() + nstmp_path = "/var/nstmp/" + + nstmp_user_path = nstmp_path + "{username}" + + for entry in self.target.fs.scandir(nstmp_path): + if entry.is_dir() and entry.name != "#nsinternal#": + nstmp_users.add(entry.name) + for username in self._config_usernames: + nstmp_home = nstmp_user_path.format(username=username) + user_home = nstmp_home if self.target.fs.exists(nstmp_home) else None + + if user_home: + # After this loop we will yield all users who are not in the config, but are listed in /var/nstmp/ + # To prevent double records, we remove entries from the set that we are already yielding here. + nstmp_users.remove(username) + + if username == "root" and self.target.fs.exists("/root"): + # If we got here, 'root' is present both in /var/nstmp and in /root. In such cases, we yield + # the 'root' user as having '/root' as a home, not in /var/nstmp. + user_home = "/root" + + yield UnixUserRecord(name=username, home=user_home) + + for username in nstmp_users: + yield UnixUserRecord(name=username, home=nstmp_user_path.format(username=username)) + + @export(property=True) + def os(self) -> str: + return OperatingSystem.CITRIX.value diff --git a/tests/conftest.py b/tests/conftest.py index 0cb1ae439..0c80f9272 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -68,6 +68,13 @@ def fs_osx(): yield fs +@pytest.fixture +def fs_bsd(): + fs = VirtualFilesystem() + fs.map_file("/bin/freebsd-version", absolute_path("data/plugins/os/unix/bsd/freebsd/freebsd-freebsd-version")) + yield fs + + @pytest.fixture def hive_hklm(): hive = VirtualHive() @@ -141,6 +148,23 @@ def target_osx(fs_osx): yield mock_target +@pytest.fixture +def target_citrix(fs_bsd): + mock_target = next(make_mock_target()) + mock_target.filesystems.add(fs_bsd) + + var_filesystem = VirtualFilesystem() + var_filesystem.makedirs("/netscaler") + mock_target.filesystems.add(var_filesystem) + + flash_filesystem = VirtualFilesystem() + flash_filesystem.map_dir("/", absolute_path("data/plugins/os/unix/bsd/citrix/flash")) + mock_target.filesystems.add(flash_filesystem) + + mock_target.apply() + yield mock_target + + @pytest.fixture def target_win_users(hive_hklm, hive_hku, target_win): profile_list_key_name = "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList" diff --git a/tests/data/plugins/os/unix/bsd/citrix/flash/.version b/tests/data/plugins/os/unix/bsd/citrix/flash/.version new file mode 100644 index 000000000..46e7b1a90 --- /dev/null +++ b/tests/data/plugins/os/unix/bsd/citrix/flash/.version @@ -0,0 +1 @@ +NetScaler 13.1 build 30 diff --git a/tests/data/plugins/os/unix/bsd/citrix/flash/boot/loader.conf b/tests/data/plugins/os/unix/bsd/citrix/flash/boot/loader.conf new file mode 100644 index 000000000..d37e5c0a2 --- /dev/null +++ b/tests/data/plugins/os/unix/bsd/citrix/flash/boot/loader.conf @@ -0,0 +1,5 @@ +autoboot_delay=3 +boot_verbose=1 +kernel="/ns-13.1-30.52" +vfs.root.mountfrom="ufs:/dev/md0c" +console="vidconsole,comconsole" diff --git a/tests/data/plugins/os/unix/bsd/citrix/flash/nsconfig/ns.conf b/tests/data/plugins/os/unix/bsd/citrix/flash/nsconfig/ns.conf new file mode 100644 index 000000000..6fb0e71c2 --- /dev/null +++ b/tests/data/plugins/os/unix/bsd/citrix/flash/nsconfig/ns.conf @@ -0,0 +1,23 @@ +#NS13.1 Build 30.52 +# Modified version of netscaler configuration, to use as test data +set ns config -IPAddress 10.0.0.69 -netmask 255.255.255.240 +set ns config -nsvlan 10 -ifnum 0/1 -tagged NO +enable ns feature LB SSL SSLVPN CH +enable ns mode MBF USNIP PMTUD +set system parameter -forcePasswordChange ENABLED +set system user nsroot 3bb5d2fcab40405ab6d31cf1b33c19955b5a8b6ad8fa1e4f41e39cdcaf7e39d08ad19a614d2e03c98d18870ed89cb2b2c2646239ae3dfde7ecff14e4ba28abbc0 -encrypted +add system user batman a30909560528fe97220cdbc22f22731d76dc4f937554819cbd6467c22d70cbd17ae595aabc90bf5b512f02ddaa400f76896bad8fdf8148cde4fa9548dbf9e5904 -encrypted +add system user root 4514b74ab6ab3a0656ba32b3ad52c2bc80370436daf6fd322c42e7f0abc224494aeb18abf33856d5fac50dde9c8a3e576bdec4b39b45a5d8d2e42bd6c729cb2ec -encrypted +add system user robin 9252ac8551824127fdf5d6c7d24ec86e533cc1299553dca3264a50021e74ee7dee833b9ade76fdc0b9e37cf209a82602b5d064274e1381842a872370233cf8162 -encrypted +set rsskeytype -rsstype ASYMMETRIC +add ns ip 10.164.69.69 255.255.255.128 -vServer DISABLED +set ns encryptionParams -method AES256 -keyValue bea2ce77be21320a17a2ea0fe9f9c3bf5c1d1d9de3e081a4cce4ac3ce8279499a158c19ab34e37a0925ac8b6253a8e402d27510e2fa3c55f71e0366d51736b69612d492e85f15581260532a8df58a9bf -encrypted -encryptmethod ENCMTHD_3 -kek -suffix 2022_12_13_13_07_37 +set cmp parameter -externalCache YES +add server 169.254.169.254 169.254.169.254 +add route 0.0.0.0 0.0.0.0 10.164.0.1 +bind system user batman superuser 0 +bind system user root superuser 0 +bind system user robin superuser 0 +set ns hostName mynetscaler +set ns param -timezone "GMT+02:00-CEST-Europe/Amsterdam" +set videooptimization parameter -RandomSamplingPercentage 0.00e+00 diff --git a/tests/data/plugins/os/unix/bsd/citrix/flash/nsconfig/ns.conf.0 b/tests/data/plugins/os/unix/bsd/citrix/flash/nsconfig/ns.conf.0 new file mode 100644 index 000000000..c23a99faf --- /dev/null +++ b/tests/data/plugins/os/unix/bsd/citrix/flash/nsconfig/ns.conf.0 @@ -0,0 +1,24 @@ +#NS13.1 Build 30.52 +# Modified version of netscaler configuration, to use as test data (backup file) +set ns config -IPAddress 10.0.0.68 -netmask 255.255.255.240 +set ns config -nsvlan 10 -ifnum 0/1 -tagged NO +enable ns feature LB SSL SSLVPN CH +enable ns mode MBF USNIP PMTUD +set system parameter -forcePasswordChange ENABLED +set system user nsroot 3bb5d2fcab40405ab6d31cf1b33c19955b5a8b6ad8fa1e4f41e39cdcaf7e39d08ad19a614d2e03c98d18870ed89cb2b2c2646239ae3dfde7ecff14e4ba28abbc0 -encrypted +add system user batman a30909560528fe97220cdbc22f22731d76dc4f937554819cbd6467c22d70cbd17ae595aabc90bf5b512f02ddaa400f76896bad8fdf8148cde4fa9548dbf9e5904 -encrypted +add system user root 4514b74ab6ab3a0656ba32b3ad52c2bc80370436daf6fd322c42e7f0abc224494aeb18abf33856d5fac50dde9c8a3e576bdec4b39b45a5d8d2e42bd6c729cb2ec -encrypted +add system user robin 9252ac8551824127fdf5d6c7d24ec86e533cc1299553dca3264a50021e74ee7dee833b9ade76fdc0b9e37cf209a82602b5d064274e1381842a872370233cf8162 -encrypted +add system user jasontodd a9de86d10286a181796e1d95235dc0c9fe3a96fcc331cc959fa9dd7660e5612023b5b0c9d2c099223cf353265f44cf098f0beeca05ebe21af7559a4d6b2dd36d5 -encrypted +set rsskeytype -rsstype ASYMMETRIC +add ns ip 10.164.69.69 255.255.255.128 -vServer DISABLED +set ns encryptionParams -method AES256 -keyValue bea2ce77be21320a17a2ea0fe9f9c3bf5c1d1d9de3e081a4cce4ac3ce8279499a158c19ab34e37a0925ac8b6253a8e402d27510e2fa3c55f71e0366d51736b69612d492e85f15581260532a8df58a9bf -encrypted -encryptmethod ENCMTHD_3 -kek -suffix 2022_12_13_13_07_37 +set cmp parameter -externalCache YES +add server 169.254.169.254 169.254.169.254 +add route 0.0.0.0 0.0.0.0 10.164.0.1 +bind system user batman superuser 0 +bind system user root superuser 0 +bind system user robin superuser 0 +bind system user jasontodd superuser 0 +set ns hostName mynetscaler +set videooptimization parameter -RandomSamplingPercentage 0.00e+00 diff --git a/tests/test_plugins_os_unix_bsd_citrix.py b/tests/test_plugins_os_unix_bsd_citrix.py new file mode 100644 index 000000000..92b6d9bbe --- /dev/null +++ b/tests/test_plugins_os_unix_bsd_citrix.py @@ -0,0 +1,43 @@ +from io import BytesIO + +from dissect.target.plugins.os.unix.bsd.citrix._os import CitrixBsdPlugin + + +def test_unix_bsd_citrix_os(target_citrix): + target_citrix.add_plugin(CitrixBsdPlugin) + + assert target_citrix.os == "citrix-netscaler" + + target_citrix.fs.mounts["/"].map_file_fh("/root/.cli_history", BytesIO(b'echo "hello world"')) + target_citrix.fs.mounts["/"].map_file_fh("/var/nstmp/robin/.cli_history", BytesIO(b'echo "hello world"')) + target_citrix.fs.mounts["/"].map_file_fh("/var/nstmp/alfred/.cli_history", BytesIO(b'echo "bye world"')) + + hostname = target_citrix.hostname + version = target_citrix.version + users = sorted(list(target_citrix.users()), key=lambda user: (user.name, user.home if user.home else "")) + ips = target_citrix.ips + ips.sort() + + assert hostname == "mynetscaler" + assert version == "NetScaler 13.1 build 30 (ns-13.1-30.52)" + + assert ips == ["10.0.0.68", "10.0.0.69"] + + assert target_citrix.timezone == "Europe/Amsterdam" + + assert len(users) == 5 + + assert users[0].name == "alfred" # Only listed in /var/nstmp + assert users[0].home == "/var/nstmp/alfred" + + assert users[1].name == "batman" # Only listed in config + assert users[1].home is None + + assert users[2].name == "jasontodd" # Only listed in config backup + assert users[2].home is None + + assert users[3].name == "robin" # Listed in config and /var/nstmp + assert users[3].home == "/var/nstmp/robin" + + assert users[4].name == "root" # User entry for /root + assert users[4].home == "/root"