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 Citrix Netscaler OS Plugin #357

Merged
merged 4 commits into from
Aug 14, 2023
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
1 change: 1 addition & 0 deletions dissect/target/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class OperatingSystem(enum.Enum):
VYOS = "vyos"
IOS = "ios"
FORTIGATE = "fortigate"
CITRIX = "citrix-netscaler"


def export(*args, **kwargs) -> Callable:
Expand Down
Empty file.
119 changes: 119 additions & 0 deletions dissect/target/plugins/os/unix/bsd/citrix/_os.py
Original file line number Diff line number Diff line change
@@ -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<ip>[^ ]+) ")
RE_CONFIG_HOSTNAME = re.compile(r"set ns hostName (?P<hostname>[^\n]+)\n")
RE_CONFIG_TIMEZONE = re.compile(
r'set ns param -timezone "GMT\+(?P<hours>[0-9]+):(?P<minutes>[0-9]+)-.*-(?P<zone_name>.+)"'
)
RE_CONFIG_USER = re.compile(r"bind system user (?P<user>[^ ]+) ")
RE_LOADER_CONFIG_KERNEL_VERSION = re.compile(r'kernel="/(?P<version>.*)"')


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

Check warning on line 82 in dissect/target/plugins/os/unix/bsd/citrix/_os.py

View check run for this annotation

Codecov / codecov/patch

dissect/target/plugins/os/unix/bsd/citrix/_os.py#L81-L82

Added lines #L81 - L82 were not covered by tests

@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
24 changes: 24 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions tests/data/plugins/os/unix/bsd/citrix/flash/.version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
NetScaler 13.1 build 30
5 changes: 5 additions & 0 deletions tests/data/plugins/os/unix/bsd/citrix/flash/boot/loader.conf
Original file line number Diff line number Diff line change
@@ -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"
23 changes: 23 additions & 0 deletions tests/data/plugins/os/unix/bsd/citrix/flash/nsconfig/ns.conf
Original file line number Diff line number Diff line change
@@ -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
24 changes: 24 additions & 0 deletions tests/data/plugins/os/unix/bsd/citrix/flash/nsconfig/ns.conf.0
Original file line number Diff line number Diff line change
@@ -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
43 changes: 43 additions & 0 deletions tests/test_plugins_os_unix_bsd_citrix.py
Original file line number Diff line number Diff line change
@@ -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"