Skip to content

Commit

Permalink
Add Citrix Netscaler OS Plugin (#357)
Browse files Browse the repository at this point in the history
  • Loading branch information
MaxGroot authored Aug 14, 2023
1 parent 58c0db0 commit 8820f69
Show file tree
Hide file tree
Showing 9 changed files with 240 additions and 0 deletions.
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

@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"

0 comments on commit 8820f69

Please sign in to comment.