├── .gitignore ├── GetConfigInfos ├── README.md ├── __init__.py ├── api.py ├── common.py ├── generic.py ├── main.py ├── pbx.py ├── vnetd.py └── vxss.py ├── LICENSE ├── ListUsersInfoFromDB ├── AZDBpwdRetriever.py ├── NBDBDBAPwdRetriever.py ├── NBDBSybaseConnector.py ├── ParseFilesHelper.py ├── README.md ├── __init__.py ├── main.py └── tests │ ├── .yekcnedwssap │ └── vxd.conf ├── PreAuthCartography ├── README.md ├── __init__.py ├── main.py └── plotNetbackup.py ├── README.md ├── __init__.py ├── bin ├── nbudbdump.py ├── nbumap.py └── nbuscan.py ├── docs └── NetBackupForDummies.md ├── img └── carto.png ├── network-analysis ├── BpcdClasses.py ├── BprdClasses.py ├── NbatdClasses.py ├── PbxRegisterClasses.py └── pynet │ └── NBUEndpoints.py ├── requirements.txt ├── setup.py └── utils ├── __init__.py └── network.py /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | *.pyc 3 | __pycache__ 4 | *.egg-info 5 | *.jar 6 | -------------------------------------------------------------------------------- /GetConfigInfos/README.md: -------------------------------------------------------------------------------- 1 | # `GetConfigInfos` module 2 | 3 | `GetConfigInfos` performs an unauthenticated scan of the given list of 4 | NetBackup hosts to determine as much information about their configuration as 5 | possible. It will notably list available `pbx_exchange` extensions, fingerprint 6 | the target's role, display authentication and authorization information, etc. 7 | 8 | This tool can be helpful to get more detailed information about a set of hosts 9 | mapped using the `PreAuthCartography` module. Most notably, it can help identify 10 | misconfigurations or inconsistencies. -------------------------------------------------------------------------------- /GetConfigInfos/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | -------------------------------------------------------------------------------- /GetConfigInfos/api.py: -------------------------------------------------------------------------------- 1 | import json 2 | import asyncio 3 | import logging 4 | import aiohttp 5 | 6 | from .generic import ScanBaseClass 7 | from .common import NbuComponentType 8 | 9 | 10 | class GetApiInfo(ScanBaseClass): 11 | # This scan should only run if the target is a primary server 12 | _SCANNABLE_COMPONENTS = {NbuComponentType.PRIMARY} 13 | 14 | def __init__(self, *args, **kwargs): 15 | super().__init__(*args, **kwargs) 16 | self.api_info = {} 17 | 18 | def name(self): 19 | """ 20 | Full name of this scan 21 | """ 22 | return "NETBACKUP_API" 23 | 24 | def _api_url(self) -> str: 25 | return f"https://{self.host}:1556/netbackup" 26 | 27 | async def _fetch_data(self, client, url) -> bytes: 28 | async with self._semaphore: 29 | try: 30 | logging.debug( 31 | "[GetApiInfo] Attempting to fetch information from API at %s", url 32 | ) 33 | async with client.get( 34 | url, verify_ssl=False, timeout=self.timeout 35 | ) as res: 36 | res.raise_for_status() 37 | data = await res.text() 38 | logging.debug("[GetApiInfo] Got response from %s: %s", url, data) 39 | return data 40 | except TimeoutError: 41 | logging.warning( 42 | "[GetApiInfo] Timed out while connecting API from %s", url 43 | ) 44 | return b"" 45 | except aiohttp.ClientError as e: 46 | logging.warning( 47 | "[GetApiInfo] Failed to get API info from %s: %s", url, e 48 | ) 49 | return b"" 50 | 51 | async def _fetch_json(self, client, url) -> dict: 52 | data = await self._fetch_data(client, url) 53 | if not data: 54 | return {} 55 | 56 | try: 57 | return json.loads(data) 58 | except json.decoder.JSONDecodeError as e: 59 | logging.warning("[GetApiInfo] Failed to parse API data from %s: %s", url, e) 60 | return {} 61 | 62 | async def _get_api_info(self) -> dict: 63 | """ 64 | See https://sort.veritas.com/public/documents/nbu/10.1/windowsandunix/productguides/html/gateway 65 | """ 66 | base_url = self._api_url() 67 | async with aiohttp.ClientSession() as client: 68 | results = await asyncio.gather( 69 | self._fetch_data(client, f"{base_url}/tokenkey"), 70 | # self._fetch_data(client, f"{base_url}/ping"), 71 | ) 72 | return { 73 | "jwt-key": results.pop(0).decode("utf-8"), 74 | # "ping": results.pop(0).decode("utf-8"), 75 | } 76 | 77 | async def _get_security_info(self) -> dict: 78 | """ 79 | See https://sort.veritas.com/public/documents/nbu/10.1/windowsandunix/productguides/html/security 80 | """ 81 | base_url = f"{self._api_url()}/security" 82 | 83 | async with aiohttp.ClientSession() as client: 84 | results = await asyncio.gather( 85 | # self._fetch_json(client, f"{base_url}/cacert"), 86 | # self._fetch_data(client, f"{base_url}/certificates/crl"), 87 | self._fetch_json(client, f"{base_url}/properties"), 88 | # self._fetch_json(client, f"{base_url}/properties/default"), 89 | # self._fetch_data(client, f"{base_url}/ping"), 90 | self._fetch_json(client, f"{base_url}/servertime"), 91 | self._fetch_json(client, f"{base_url}/serverinfo"), 92 | ) 93 | return { 94 | # "cacert": results.pop(0), 95 | # "crl": results.pop(0).hex(), 96 | "properties": results.pop(0), 97 | # "properties-default": results.pop(0), 98 | # "ping": results.pop(0).decode("utf-8"), 99 | "servertime": results.pop(0), 100 | "serverinfo": results.pop(0), 101 | } 102 | 103 | async def _run(self): 104 | """ 105 | Attempt to connect to the Primary Server API and obtain information 106 | """ 107 | # Reset gathered info 108 | self.api_info = {} 109 | 110 | # Poll pre-auth API endpoints for information 111 | results = await asyncio.gather( 112 | # self._get_api_info() 113 | self._get_security_info() 114 | ) 115 | 116 | # self.api_info["api"] = results.pop(0) 117 | self.api_info["security"] = results.pop(0) 118 | 119 | def _text(self) -> str: 120 | """ 121 | Get a textual representation of the results 122 | """ 123 | out_lines = [] 124 | 125 | security_info = self.api_info.get("security", {}) 126 | server_info = security_info.get("serverinfo") 127 | if server_info: 128 | # Extract most important information from the whole dict 129 | out_lines.append(f"Server Name: {server_info.get('serverName', 'Unknown')}") 130 | out_lines.append(f"Host ID: {server_info.get('masterHostId', 'Unknown')}") 131 | out_lines.append( 132 | f"NetBackup version: {server_info.get('nbuVersion', 'Unknown')}" 133 | ) 134 | out_lines.append(f"SSO enabled: {server_info.get('ssoEnabled', 'Unknown')}") 135 | 136 | security_properties = security_info.get("properties") 137 | if security_properties: 138 | # Extract most important information from the whole dict 139 | # First, whether TLS is used 140 | secure_comms = security_properties["allowInsecureBackLevelHost"] 141 | state = "Enabled" if secure_comms else "Disabled" 142 | out_lines.append(f"Secure Communications: {state}") 143 | 144 | # Second, how certificates are deployed 145 | cert_deploy_level = security_properties["certificateAutoDeployLevel"] 146 | if cert_deploy_level == 0: 147 | state = "Very high" 148 | elif cert_deploy_level == 1: 149 | state = "High" 150 | elif cert_deploy_level == 2: 151 | state = "Medium" 152 | else: 153 | state = "Unknown" 154 | out_lines.append(f"Certificate auto-deployment level: {state}") 155 | 156 | return "\n".join(out_lines) 157 | 158 | def _json(self) -> dict: 159 | """ 160 | Get a dictionary representation of the results 161 | """ 162 | return { 163 | "api": self.api_info, 164 | } 165 | -------------------------------------------------------------------------------- /GetConfigInfos/common.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | 4 | class ScanState(enum.Enum): 5 | PENDING = 0 6 | RUNNING = 1 7 | SUCCESS = 2 8 | SKIPPED = 3 9 | ERROR = -1 10 | 11 | 12 | class NbuComponentType(enum.Enum): 13 | OPSCENTER = "OpsCenter" 14 | PRIMARY = "Primary Server" 15 | MEDIA = "Media Server" 16 | CLIENT = "Client" 17 | UNKNOWN = "Unknown" 18 | -------------------------------------------------------------------------------- /GetConfigInfos/generic.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import asyncio 3 | import logging 4 | 5 | from .common import ScanState, NbuComponentType 6 | 7 | 8 | class ScanBaseClass(abc.ABC): 9 | # This can be used to limit on which component a scan should run 10 | _SCANNABLE_COMPONENTS = set(NbuComponentType) 11 | 12 | def __init__(self, host, nb_threads=1, timeout=5, verbose=False, scanners=[]): 13 | self.host = host 14 | self.nb_threads = nb_threads 15 | self.timeout = timeout 16 | self.verbose = verbose 17 | self.scanners = scanners 18 | 19 | self._state = ScanState.PENDING 20 | self._lock = asyncio.Lock() 21 | self._event = asyncio.Event() 22 | self._semaphore = asyncio.Semaphore(value=self.nb_threads) 23 | 24 | ### Helper methods 25 | 26 | async def _component_type(self) -> NbuComponentType: 27 | """ 28 | Return the guessed role of the target of this scanner 29 | This will rely on the ListPbxExtensions scanner. If it was not been run 30 | (yet), it will wait for the result before moving on 31 | """ 32 | # Build a list of scanners able to guess the target's role 33 | scanners = (s for s in self.scanners if hasattr(s, "guess_role")) 34 | 35 | for pbx_scanner in scanners: 36 | # Check whether this scanner has already run 37 | if pbx_scanner.state == ScanState.ERROR: 38 | raise RuntimeError("ListPbxExtensions failed") 39 | if pbx_scanner.state == ScanState.SKIPPED: 40 | raise RuntimeError("ListPbxExtensions was skipped") 41 | elif pbx_scanner.state != ScanState.SUCCESS: 42 | # If it hasn't run, run it or wait for it to be done running 43 | await pbx_scanner.run() 44 | break 45 | else: 46 | raise RuntimeError("No ListPbxExtensions scanner found") 47 | 48 | # Check the guessed role of the target 49 | return pbx_scanner.guess_role() 50 | 51 | ### Wrappers handling state 52 | 53 | @property 54 | def state(self) -> ScanState: 55 | """ 56 | Current state of this scanner 57 | """ 58 | return self._state 59 | 60 | async def run(self): 61 | """ 62 | Run this scanner, updating its state and triggering a finish event 63 | If it is already running, simply wait for it to finish 64 | """ 65 | if self._state == ScanState.RUNNING: 66 | # If the scan is already running, simply wait for it to finish 67 | await self._event.wait() 68 | return 69 | 70 | self._state = ScanState.RUNNING 71 | self._event.clear() 72 | try: 73 | if await self.skip(): 74 | logging.info("Skipping %s scan", self.name()) 75 | self._state = ScanState.SKIPPED 76 | else: 77 | logging.info("Starting %s scan", self.name()) 78 | await self._run() 79 | self._state = ScanState.SUCCESS 80 | except Exception as e: 81 | self._state = ScanState.ERROR 82 | logging.warn("%s scanner failed with error: %s", self.name(), e) 83 | finally: 84 | self._event.set() 85 | 86 | async def skip(self) -> bool: 87 | """ 88 | Check whether this scan should be skipped or not 89 | """ 90 | # If every type of component can be scanned, never skip this 91 | if self._SCANNABLE_COMPONENTS == set(NbuComponentType): 92 | return False 93 | 94 | # If this scan is restricted to only specific components, wait to know 95 | # which component type we are dealing with 96 | try: 97 | component = await self._component_type() 98 | return component not in self._SCANNABLE_COMPONENTS 99 | except RuntimeError as e: 100 | logging.info("Failed to guess target role: %s", e) 101 | return True 102 | 103 | def text(self) -> str: 104 | """ 105 | Get a textual representation of the results 106 | """ 107 | if self._state == ScanState.PENDING: 108 | raise RuntimeError("The run function has not been called") 109 | elif self._state == ScanState.RUNNING: 110 | raise RuntimeError("The scan is not done running") 111 | elif self._state == ScanState.SKIPPED: 112 | return "Skipped" 113 | 114 | return self._text() 115 | 116 | def json(self) -> dict: 117 | """ 118 | Get a JSON representation of the results 119 | """ 120 | if self._state == ScanState.PENDING: 121 | raise RuntimeError("The run function has not been called") 122 | elif self._state == ScanState.RUNNING: 123 | raise RuntimeError("The scan is not done running") 124 | 125 | return self._json() 126 | 127 | ### Methods to override 128 | 129 | @abc.abstractmethod 130 | def name(self) -> str: 131 | """ 132 | Full name of this scan 133 | """ 134 | raise NotImplementedError() 135 | 136 | @abc.abstractmethod 137 | async def _run(self): 138 | """ 139 | Actually run the scan 140 | """ 141 | raise NotImplementedError() 142 | 143 | @abc.abstractmethod 144 | def _text(self, verbose: bool) -> str: 145 | """ 146 | Get a textual representation of the results 147 | """ 148 | raise NotImplementedError() 149 | 150 | @abc.abstractmethod 151 | def _json(self) -> dict: 152 | """ 153 | Get a JSON representation of the results 154 | """ 155 | raise NotImplementedError() 156 | -------------------------------------------------------------------------------- /GetConfigInfos/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import enum 3 | import json 4 | import asyncio 5 | import logging 6 | import argparse 7 | 8 | from .vnetd import GetVnetdInfo 9 | from .vxss import TestVxssConfig 10 | from .pbx import ListPbxExtensions 11 | from .api import GetApiInfo 12 | 13 | 14 | NBU_SCANNERS = [GetVnetdInfo, TestVxssConfig, ListPbxExtensions, GetApiInfo] 15 | 16 | 17 | class OutputFormat(enum.Enum): 18 | PLAIN = "plain" 19 | JSON = "json" 20 | 21 | 22 | async def scan( 23 | target: str, nb_threads: int, timeout: int, verbose: bool, out_format: OutputFormat 24 | ): 25 | out = {} 26 | tasks = {} 27 | scanners = [ 28 | cls(host=target, nb_threads=nb_threads, timeout=timeout, verbose=verbose) 29 | for cls in NBU_SCANNERS 30 | ] 31 | 32 | def callback(task): 33 | scanner = tasks[task] 34 | if out_format == OutputFormat.PLAIN: 35 | print("---", scanner.name(), "Scan Results:") 36 | print(scanner.text()) 37 | 38 | async with asyncio.TaskGroup() as tg: 39 | for scanner in scanners: 40 | scanner.scanners = scanners 41 | task = tg.create_task(scanner.run()) 42 | tasks[task] = scanner 43 | task.add_done_callback(callback) 44 | 45 | if out_format == OutputFormat.JSON: 46 | for scanner in scanners: 47 | out |= scanner.json() 48 | return out 49 | 50 | 51 | def main(): 52 | parser = argparse.ArgumentParser(description="NetBackup scanner tool") 53 | 54 | parser.add_argument("targets", nargs="*", help="Target hosts") 55 | parser.add_argument( 56 | "-j", 57 | "--jobs", 58 | type=int, 59 | default=5, 60 | help="Maximum number of concurrent jobs", 61 | ) 62 | parser.add_argument( 63 | "-v", 64 | "--verbose", 65 | help="Run in verbose mode", 66 | action="count", 67 | default=0, 68 | ) 69 | parser.add_argument( 70 | "-q", 71 | "--quiet", 72 | help="Run in quiet mode", 73 | action="store_true", 74 | default=False, 75 | ) 76 | parser.add_argument( 77 | "-l", 78 | "--log-level", 79 | help="Define the log level", 80 | choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"], 81 | default="WARNING", 82 | ) 83 | parser.add_argument( 84 | "-t", "--timeout", type=float, default=10, help="Timeout for TCP connections" 85 | ) 86 | parser.add_argument( 87 | "-f", 88 | "--format", 89 | help="Output format", 90 | choices=[e.value for e in OutputFormat], 91 | default="plain", 92 | ) 93 | parser.add_argument( 94 | "-i", 95 | "--input", 96 | help="Input file containing the list of hosts to scan", 97 | ) 98 | parser.add_argument( 99 | "-o", 100 | "--output", 101 | help="Output file", 102 | ) 103 | 104 | args = parser.parse_args() 105 | 106 | # Start by setting up logging 107 | if args.verbose >= 2: 108 | logging.getLogger().setLevel(logging.DEBUG) 109 | elif args.verbose == 1: 110 | logging.getLogger().setLevel(logging.INFO) 111 | elif args.quiet: 112 | logging.getLogger().setLevel(logging.ERROR) 113 | else: 114 | logging.getLogger().setLevel(args.log_level) 115 | 116 | # Get targets 117 | targets = [] 118 | if args.input: 119 | with open(args.input, "r") as f: 120 | targets = f.readlines() 121 | 122 | # Cleanup lines 123 | targets = map(str.strip, targets) 124 | targets = filter(lambda l: bool(l), targets) 125 | targets = list(targets) 126 | 127 | targets += args.targets 128 | if not targets: 129 | parser.print_help() 130 | exit(1) 131 | 132 | # Check whether the number of jobs is reasonable 133 | if args.jobs > 20: 134 | logging.warn( 135 | """Running with a high number of concurrent jobs may yield incorrect results! 136 | pbx_exchange is not designed to handle high numbers of clients and could return false positives/negatives""" 137 | ) 138 | 139 | # Check where and how to write the output 140 | out_format = OutputFormat(args.format) 141 | if args.output and out_format == OutputFormat.PLAIN: 142 | logging.error("--output option is not supported with plaintext format") 143 | exit(1) 144 | 145 | # Actually perform the scan 146 | out_dict = {} 147 | for i, target in enumerate(targets): 148 | if out_format == OutputFormat.PLAIN: 149 | if i > 0: 150 | print("\n\n", end="") 151 | print(f"[nbuscan results for {target}]") 152 | 153 | verbose = logging.getLogger().getEffectiveLevel() < logging.WARNING 154 | out_dict[target] = asyncio.run( 155 | scan(target, args.jobs, args.timeout, verbose, out_format) 156 | ) 157 | 158 | # Write the output if it wasn't already done during the scan 159 | if out_format == OutputFormat.JSON: 160 | if args.output: 161 | with open(args.output, "w") as f: 162 | json.dump(out_dict, f) 163 | else: 164 | print(json.dumps(out_dict, indent=2)) 165 | 166 | 167 | if __name__ == "__main__": 168 | main() 169 | -------------------------------------------------------------------------------- /GetConfigInfos/pbx.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | 4 | from utils.network import PBXSocket 5 | from .generic import ScanBaseClass 6 | from .common import ScanState, NbuComponentType 7 | 8 | 9 | class ListPbxExtensions(ScanBaseClass): 10 | OPS_PBX_EXTENSIONS = set( 11 | [ 12 | "CycloneDomainService", 13 | "InSecCycloneDomainService", 14 | "opscenter_agent_pd", 15 | "OPSCENTER_PBXSSLServiceID", 16 | "SclInsecure", 17 | "SclInsecure6x", 18 | "SclSecure6x", 19 | "SclSecureI", 20 | "SclSecureIc", 21 | "SearchBroker", 22 | "SearchService", 23 | ] 24 | ) 25 | PRIMARY_PBX_EXTENSIONS = set( 26 | [ 27 | "HTTPTUNNEL", 28 | "TLSPROXY", 29 | "bpcd", 30 | "bpdbm", 31 | "bpdbm-auth-only", 32 | "bpjobd", 33 | "bprd", 34 | "DiscoveryService", 35 | "DiscoveryService_secsvc", 36 | "EMM", 37 | "nbars", 38 | "nbatd", 39 | "nbaudit", 40 | "nbazd", 41 | "NBDSMFSM", 42 | "nbevtmgr", 43 | "NBFSMCLIENT", 44 | "nbim", 45 | "nbjm", 46 | "nbpem", 47 | "nbrb", 48 | "NBREM", 49 | "nbrmms", 50 | "nbrmms_secsvc", 51 | "nbsl", 52 | "nbsl_secsvc", 53 | "nbstserv", 54 | "nbsvcmon", 55 | "nbsvcmon_secsvc", 56 | "nbvault", 57 | "vmd", 58 | "vnetd", 59 | "vnetd-auth-only", 60 | "vnetd-no-auth", 61 | "vnetd-ssa", 62 | ] 63 | ) 64 | MEDIA_PBX_EXTENSIONS = set( 65 | [ 66 | "HTTPTUNNEL", 67 | "TLSPROXY", 68 | "bpcd", 69 | "DiscoveryService", 70 | "DiscoveryService_secsvc", 71 | "nbrmms", 72 | "nbrmms_secsvc", 73 | "nbsl", 74 | "nbsl_secsvc", 75 | "nbsvcmon", 76 | "nbsvcmon_secsvc", 77 | "vmd", 78 | "vnetd", 79 | "vnetd-auth-only", 80 | "vnetd-no-auth", 81 | "vnetd-ssa", 82 | ] 83 | ) 84 | CLIENT_PBX_EXTENSIONS = set( 85 | [ 86 | "bpcd", 87 | "DiscoveryService", 88 | "DiscoveryService_secsvc", 89 | "vnetd", 90 | "vnetd-auth-only", 91 | "vnetd-no-auth", 92 | "vnetd-ssa", 93 | ] 94 | ) 95 | PBX_EXTENSIONS = sorted( 96 | list( 97 | OPS_PBX_EXTENSIONS 98 | | PRIMARY_PBX_EXTENSIONS 99 | | MEDIA_PBX_EXTENSIONS 100 | | CLIENT_PBX_EXTENSIONS 101 | ), 102 | key=str.casefold, 103 | ) 104 | 105 | def __init__(self, *args, **kwargs): 106 | super().__init__(*args, **kwargs) 107 | self.extension_states = {} 108 | 109 | def name(self): 110 | """ 111 | Full name of this scan 112 | """ 113 | return "PBX_EXCHANGE" 114 | 115 | async def _scan_ext(self, ext: str) -> bool: 116 | """ 117 | Internal method returning whether an extension was found or not 118 | """ 119 | async with self._semaphore: 120 | sock = PBXSocket(self.host, timeout=self.timeout) 121 | try: 122 | await sock.connect() 123 | await sock.handshake(ext) 124 | await sock.close() 125 | logging.info( 126 | "[ListPbxExtensions] Found accessible extension %s for %s", 127 | ext, 128 | self.host, 129 | ) 130 | return True 131 | except RuntimeError: 132 | await sock.close() 133 | return False 134 | 135 | def _guess_role(self) -> NbuComponentType: 136 | # Build a set of available extensions to compare with those specific to 137 | # each component 138 | extensions = set( 139 | [ext for ext in self.extension_states.keys() if self.extension_states[ext]] 140 | ) 141 | 142 | # First, check if it's an OpsCenter since it has very unique extensions 143 | ops_unique_exts = self.OPS_PBX_EXTENSIONS - ( 144 | self.PRIMARY_PBX_EXTENSIONS 145 | | self.MEDIA_PBX_EXTENSIONS 146 | | self.CLIENT_PBX_EXTENSIONS 147 | ) 148 | if extensions & ops_unique_exts: 149 | return NbuComponentType.OPSCENTER 150 | 151 | # Move on to the primary server as it has plenty of unique extensions 152 | primary_unique_exts = self.PRIMARY_PBX_EXTENSIONS - ( 153 | self.MEDIA_PBX_EXTENSIONS | self.CLIENT_PBX_EXTENSIONS 154 | ) 155 | if extensions & primary_unique_exts: 156 | return NbuComponentType.PRIMARY 157 | 158 | # Now, the media server 159 | media_unique_exts = self.MEDIA_PBX_EXTENSIONS - self.CLIENT_PBX_EXTENSIONS 160 | if extensions & media_unique_exts: 161 | return NbuComponentType.MEDIA 162 | 163 | # Finally, at least check some of the client extensions are present 164 | if extensions & self.CLIENT_PBX_EXTENSIONS: 165 | return NbuComponentType.CLIENT 166 | 167 | return NbuComponentType.UNKNOWN 168 | 169 | def guess_role(self) -> NbuComponentType: 170 | """ 171 | Attempt to guess the role of the scanned host based on the discovered 172 | extensions 173 | """ 174 | if self.state == ScanState.PENDING: 175 | raise RuntimeError("The run function has not been called") 176 | elif self.state == ScanState.RUNNING: 177 | raise RuntimeError("The scan is not done running") 178 | return self._guess_role() 179 | 180 | async def _run(self): 181 | """ 182 | Iterate over all know extensions and check whether they are reachable 183 | through pbx_exchange or not 184 | """ 185 | # Reset extension states 186 | self.extension_states = {ext: False for ext in self.PBX_EXTENSIONS} 187 | 188 | # Run one task for each extension 189 | tasks = [self._scan_ext(ext) for ext in self.PBX_EXTENSIONS] 190 | results = await asyncio.gather(*tasks) 191 | 192 | # Populate the dictionary of extensions states 193 | self.extension_states = { 194 | self.PBX_EXTENSIONS[i]: results[i] for i in range(len(self.PBX_EXTENSIONS)) 195 | } 196 | 197 | def _text(self) -> str: 198 | """ 199 | Get a textual representation of the results 200 | """ 201 | COLUMN_SIZES = (max([len(ext) for ext in self.PBX_EXTENSIONS]), len("[ERROR]")) 202 | 203 | def format_line(ext, status): 204 | fmt_string = "| {{:<{}}} | {{:<{}}} |".format(*COLUMN_SIZES) 205 | return fmt_string.format(ext, status) 206 | 207 | out_lines = [ 208 | format_line("EXTENSION", "STATE"), 209 | format_line(*["-" * n for n in COLUMN_SIZES]), 210 | ] 211 | for ext in self.PBX_EXTENSIONS: 212 | if not self.extension_states[ext] and not self.verbose: 213 | continue 214 | status = "[OK]" if self.extension_states[ext] else "[ERROR]" 215 | out_lines.append(format_line(ext, status)) 216 | 217 | out_lines.append("=> Guessed role: " + self.guess_role().value) 218 | return "\n".join(out_lines) 219 | 220 | def _json(self) -> dict: 221 | """ 222 | Get a dictionary representation of the results 223 | """ 224 | return { 225 | "pbx": { 226 | "extensions": self.extension_states, 227 | "guessed_role": self.guess_role().value, 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /GetConfigInfos/vnetd.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from utils.network import VNETDSocket 4 | from .generic import ScanBaseClass 5 | 6 | 7 | class GetVnetdInfo(ScanBaseClass): 8 | def __init__(self, *args, **kwargs): 9 | super().__init__(*args, **kwargs) 10 | self.nbu_version = "Unknown" 11 | self.primary_server = "Unknown" 12 | 13 | def name(self): 14 | """ 15 | Full name of this scan 16 | """ 17 | return "VNETD" 18 | 19 | async def _run(self): 20 | """ 21 | Attempt to connect to vnetd and fetch information 22 | """ 23 | # Reset gathered info 24 | self.nbu_version = "Unknown" 25 | self.primary_server = "Unknown" 26 | 27 | # Open a socket to vnetd using pbx_exchange or, if that fails, through 28 | # the assigned vnetd port 29 | try: 30 | sock = VNETDSocket(self.host, use_pbx=True, timeout=self.timeout) 31 | await sock.connect() 32 | except: 33 | sock = VNETDSocket(self.host, use_pbx=False, timeout=self.timeout) 34 | await sock.connect() 35 | 36 | # Perform the initial handshake with vnetd, which is necessary before 37 | # issuing any command 38 | await sock.handshake() 39 | 40 | # Send the commands to get the information we can 41 | self.nbu_version = await sock.get_version() or self.nbu_version 42 | self.primary_server = await sock.get_primary_server() or self.primary_server 43 | 44 | # Cleanup 45 | await sock.close() 46 | 47 | def _text(self) -> str: 48 | """ 49 | Get a textual representation of the results 50 | """ 51 | return "Running NetBackup version {}\nAssigned Primary Server: {}".format( 52 | self.nbu_version, self.primary_server 53 | ) 54 | 55 | def _json(self) -> dict: 56 | """ 57 | Get a dictionary representation of the results 58 | """ 59 | return { 60 | "vnetd": { 61 | "version": self.nbu_version, 62 | "primary-server": self.primary_server, 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /GetConfigInfos/vxss.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from utils.network import VNETDSocket 4 | from .generic import ScanBaseClass 5 | 6 | 7 | class TestVxssConfig(ScanBaseClass): 8 | def __init__(self, *args, **kwargs): 9 | super().__init__(*args, **kwargs) 10 | self.vxss_info = None 11 | 12 | def name(self): 13 | """ 14 | Full name of this scan 15 | """ 16 | return "VXSS" 17 | 18 | USE_VXSS_MAP = { 19 | 0: "AUTOMATIC", 20 | 1: "REQUIRED", 21 | 2: "PROHIBITED", 22 | } 23 | 24 | AUTHENTICATION_MAP = { 25 | 0: "OFF", 26 | 1: "ON", 27 | } 28 | 29 | AUTHORIZATION_MECHANISM_MAP = { 30 | 0: "NIS", 31 | 1: "NIS+", 32 | 2: "PASSWD", 33 | 3: "VXPD", 34 | 4: "WINDOWS", 35 | } 36 | 37 | def _prettify(self, d, k, m): 38 | v = d.get(k, None) 39 | try: 40 | d[k] = m[int(v)] 41 | except (ValueError, KeyError): 42 | return v 43 | except TypeError: 44 | return "Unknown" 45 | 46 | async def _run(self): 47 | """ 48 | Attempt to connect to vnetd and fetch information about VxSS 49 | """ 50 | # Reset gathered info 51 | self.vxss_info = None 52 | 53 | # Open a socket to vnetd using pbx_exchange or, if that fails, through 54 | # the assigned vnetd port 55 | try: 56 | sock = VNETDSocket(self.host, use_pbx=True, timeout=self.timeout) 57 | await sock.connect() 58 | except: 59 | sock = VNETDSocket(self.host, use_pbx=False, timeout=self.timeout) 60 | await sock.connect() 61 | 62 | # Perform the initial handshake with vnetd, which is necessary before 63 | # issuing any command 64 | await sock.handshake() 65 | 66 | # Send the command to get the information we can 67 | self.vxss_info = await sock.get_security_info() 68 | 69 | if self.vxss_info: 70 | # "Translate" some fields into readable information 71 | self._prettify(self.vxss_info, "USE_VXSS", self.USE_VXSS_MAP) 72 | self._prettify( 73 | self.vxss_info, "USE_AUTHENTICATION", self.AUTHENTICATION_MAP 74 | ) 75 | 76 | for domain in self.vxss_info["AUTHENTICATION_DOMAIN"]: 77 | self._prettify(domain, "mechanism", self.AUTHORIZATION_MECHANISM_MAP) 78 | 79 | for network in self.vxss_info["VXSS_NETWORK"]: 80 | self._prettify(network, "use_vxss", self.USE_VXSS_MAP) 81 | else: 82 | logging.warning("[TestVxssConfig] Failed to get security info from vnetd") 83 | 84 | # Cleanup 85 | await sock.close() 86 | 87 | def _text(self) -> str: 88 | """ 89 | Get a textual representation of the results 90 | """ 91 | if not self.vxss_info: 92 | return "No information" 93 | 94 | # Attempt to reproduce the format of bp.conf 95 | out_lines = [] 96 | out_lines.append("USE_VXSS = {}".format(self.vxss_info["USE_VXSS"])) 97 | for network in self.vxss_info["VXSS_NETWORK"]: 98 | out_lines.append( 99 | "VXSS_NETWORK = {} {}".format( 100 | network["network"], 101 | network["use_vxss"], 102 | ) 103 | ) 104 | 105 | out_lines.append( 106 | "USE_AUTHENTICATION = {}".format(self.vxss_info["USE_AUTHENTICATION"]) 107 | ) 108 | for domain in self.vxss_info["AUTHENTICATION_DOMAIN"]: 109 | out_lines.append( 110 | 'AUTHENTICATION_DOMAIN = {} "{}" {} {} {}'.format( 111 | domain["domain"], 112 | domain["comment"], 113 | domain["mechanism"], 114 | domain["broker"], 115 | domain["port"], 116 | ) 117 | ) 118 | 119 | return "\n".join(out_lines) 120 | 121 | def _json(self) -> dict: 122 | """ 123 | Get a dictionary representation of the results 124 | """ 125 | return {"vxss": self.vxss_info} 126 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The GNU General Public License, Version 2, June 1991 (GPLv2) 2 | ============================================================ 3 | 4 | > Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | > 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA 6 | 7 | Everyone is permitted to copy and distribute verbatim copies of this license 8 | document, but changing it is not allowed. 9 | 10 | 11 | Preamble 12 | -------- 13 | 14 | The licenses for most software are designed to take away your freedom to share 15 | and change it. By contrast, the GNU General Public License is intended to 16 | guarantee your freedom to share and change free software--to make sure the 17 | software is free for all its users. This General Public License applies to most 18 | of the Free Software Foundation's software and to any other program whose 19 | authors commit to using it. (Some other Free Software Foundation software is 20 | covered by the GNU Lesser General Public License instead.) You can apply it to 21 | your programs, too. 22 | 23 | When we speak of free software, we are referring to freedom, not price. Our 24 | General Public Licenses are designed to make sure that you have the freedom to 25 | distribute copies of free software (and charge for this service if you wish), 26 | that you receive source code or can get it if you want it, that you can change 27 | the software or use pieces of it in new free programs; and that you know you can 28 | do these things. 29 | 30 | To protect your rights, we need to make restrictions that forbid anyone to deny 31 | you these rights or to ask you to surrender the rights. These restrictions 32 | translate to certain responsibilities for you if you distribute copies of the 33 | software, or if you modify it. 34 | 35 | For example, if you distribute copies of such a program, whether gratis or for a 36 | fee, you must give the recipients all the rights that you have. You must make 37 | sure that they, too, receive or can get the source code. And you must show them 38 | these terms so they know their rights. 39 | 40 | We protect your rights with two steps: (1) copyright the software, and (2) offer 41 | you this license which gives you legal permission to copy, distribute and/or 42 | modify the software. 43 | 44 | Also, for each author's protection and ours, we want to make certain that 45 | everyone understands that there is no warranty for this free software. If the 46 | software is modified by someone else and passed on, we want its recipients to 47 | know that what they have is not the original, so that any problems introduced by 48 | others will not reflect on the original authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software patents. We wish 51 | to avoid the danger that redistributors of a free program will individually 52 | obtain patent licenses, in effect making the program proprietary. To prevent 53 | this, we have made it clear that any patent must be licensed for everyone's free 54 | use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and modification 57 | follow. 58 | 59 | 60 | Terms And Conditions For Copying, Distribution And Modification 61 | --------------------------------------------------------------- 62 | 63 | **0.** This License applies to any program or other work which contains a notice 64 | placed by the copyright holder saying it may be distributed under the terms of 65 | this General Public License. The "Program", below, refers to any such program or 66 | work, and a "work based on the Program" means either the Program or any 67 | derivative work under copyright law: that is to say, a work containing the 68 | Program or a portion of it, either verbatim or with modifications and/or 69 | translated into another language. (Hereinafter, translation is included without 70 | limitation in the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not covered by 73 | this License; they are outside its scope. The act of running the Program is not 74 | restricted, and the output from the Program is covered only if its contents 75 | constitute a work based on the Program (independent of having been made by 76 | running the Program). Whether that is true depends on what the Program does. 77 | 78 | **1.** You may copy and distribute verbatim copies of the Program's source code 79 | as you receive it, in any medium, provided that you conspicuously and 80 | appropriately publish on each copy an appropriate copyright notice and 81 | disclaimer of warranty; keep intact all the notices that refer to this License 82 | and to the absence of any warranty; and give any other recipients of the Program 83 | a copy of this License along with the Program. 84 | 85 | You may charge a fee for the physical act of transferring a copy, and you may at 86 | your option offer warranty protection in exchange for a fee. 87 | 88 | **2.** You may modify your copy or copies of the Program or any portion of it, 89 | thus forming a work based on the Program, and copy and distribute such 90 | modifications or work under the terms of Section 1 above, provided that you also 91 | meet all of these conditions: 92 | 93 | * **a)** You must cause the modified files to carry prominent notices stating 94 | that you changed the files and the date of any change. 95 | 96 | * **b)** You must cause any work that you distribute or publish, that in whole 97 | or in part contains or is derived from the Program or any part thereof, to 98 | be licensed as a whole at no charge to all third parties under the terms of 99 | this License. 100 | 101 | * **c)** If the modified program normally reads commands interactively when 102 | run, you must cause it, when started running for such interactive use in the 103 | most ordinary way, to print or display an announcement including an 104 | appropriate copyright notice and a notice that there is no warranty (or 105 | else, saying that you provide a warranty) and that users may redistribute 106 | the program under these conditions, and telling the user how to view a copy 107 | of this License. (Exception: if the Program itself is interactive but does 108 | not normally print such an announcement, your work based on the Program is 109 | not required to print an announcement.) 110 | 111 | These requirements apply to the modified work as a whole. If identifiable 112 | sections of that work are not derived from the Program, and can be reasonably 113 | considered independent and separate works in themselves, then this License, and 114 | its terms, do not apply to those sections when you distribute them as separate 115 | works. But when you distribute the same sections as part of a whole which is a 116 | work based on the Program, the distribution of the whole must be on the terms of 117 | this License, whose permissions for other licensees extend to the entire whole, 118 | and thus to each and every part regardless of who wrote it. 119 | 120 | Thus, it is not the intent of this section to claim rights or contest your 121 | rights to work written entirely by you; rather, the intent is to exercise the 122 | right to control the distribution of derivative or collective works based on the 123 | Program. 124 | 125 | In addition, mere aggregation of another work not based on the Program with the 126 | Program (or with a work based on the Program) on a volume of a storage or 127 | distribution medium does not bring the other work under the scope of this 128 | License. 129 | 130 | **3.** You may copy and distribute the Program (or a work based on it, under 131 | Section 2) in object code or executable form under the terms of Sections 1 and 2 132 | above provided that you also do one of the following: 133 | 134 | * **a)** Accompany it with the complete corresponding machine-readable source 135 | code, which must be distributed under the terms of Sections 1 and 2 above on 136 | a medium customarily used for software interchange; or, 137 | 138 | * **b)** Accompany it with a written offer, valid for at least three years, to 139 | give any third party, for a charge no more than your cost of physically 140 | performing source distribution, a complete machine-readable copy of the 141 | corresponding source code, to be distributed under the terms of Sections 1 142 | and 2 above on a medium customarily used for software interchange; or, 143 | 144 | * **c)** Accompany it with the information you received as to the offer to 145 | distribute corresponding source code. (This alternative is allowed only for 146 | noncommercial distribution and only if you received the program in object 147 | code or executable form with such an offer, in accord with Subsection b 148 | above.) 149 | 150 | The source code for a work means the preferred form of the work for making 151 | modifications to it. For an executable work, complete source code means all the 152 | source code for all modules it contains, plus any associated interface 153 | definition files, plus the scripts used to control compilation and installation 154 | of the executable. However, as a special exception, the source code distributed 155 | need not include anything that is normally distributed (in either source or 156 | binary form) with the major components (compiler, kernel, and so on) of the 157 | operating system on which the executable runs, unless that component itself 158 | accompanies the executable. 159 | 160 | If distribution of executable or object code is made by offering access to copy 161 | from a designated place, then offering equivalent access to copy the source code 162 | from the same place counts as distribution of the source code, even though third 163 | parties are not compelled to copy the source along with the object code. 164 | 165 | **4.** You may not copy, modify, sublicense, or distribute the Program except as 166 | expressly provided under this License. Any attempt otherwise to copy, modify, 167 | sublicense or distribute the Program is void, and will automatically terminate 168 | your rights under this License. However, parties who have received copies, or 169 | rights, from you under this License will not have their licenses terminated so 170 | long as such parties remain in full compliance. 171 | 172 | **5.** You are not required to accept this License, since you have not signed 173 | it. However, nothing else grants you permission to modify or distribute the 174 | Program or its derivative works. These actions are prohibited by law if you do 175 | not accept this License. Therefore, by modifying or distributing the Program (or 176 | any work based on the Program), you indicate your acceptance of this License to 177 | do so, and all its terms and conditions for copying, distributing or modifying 178 | the Program or works based on it. 179 | 180 | **6.** Each time you redistribute the Program (or any work based on the 181 | Program), the recipient automatically receives a license from the original 182 | licensor to copy, distribute or modify the Program subject to these terms and 183 | conditions. You may not impose any further restrictions on the recipients' 184 | exercise of the rights granted herein. You are not responsible for enforcing 185 | compliance by third parties to this License. 186 | 187 | **7.** If, as a consequence of a court judgment or allegation of patent 188 | infringement or for any other reason (not limited to patent issues), conditions 189 | are imposed on you (whether by court order, agreement or otherwise) that 190 | contradict the conditions of this License, they do not excuse you from the 191 | conditions of this License. If you cannot distribute so as to satisfy 192 | simultaneously your obligations under this License and any other pertinent 193 | obligations, then as a consequence you may not distribute the Program at all. 194 | For example, if a patent license would not permit royalty-free redistribution of 195 | the Program by all those who receive copies directly or indirectly through you, 196 | then the only way you could satisfy both it and this License would be to refrain 197 | entirely from distribution of the Program. 198 | 199 | If any portion of this section is held invalid or unenforceable under any 200 | particular circumstance, the balance of the section is intended to apply and the 201 | section as a whole is intended to apply in other circumstances. 202 | 203 | It is not the purpose of this section to induce you to infringe any patents or 204 | other property right claims or to contest validity of any such claims; this 205 | section has the sole purpose of protecting the integrity of the free software 206 | distribution system, which is implemented by public license practices. Many 207 | people have made generous contributions to the wide range of software 208 | distributed through that system in reliance on consistent application of that 209 | system; it is up to the author/donor to decide if he or she is willing to 210 | distribute software through any other system and a licensee cannot impose that 211 | choice. 212 | 213 | This section is intended to make thoroughly clear what is believed to be a 214 | consequence of the rest of this License. 215 | 216 | **8.** If the distribution and/or use of the Program is restricted in certain 217 | countries either by patents or by copyrighted interfaces, the original copyright 218 | holder who places the Program under this License may add an explicit 219 | geographical distribution limitation excluding those countries, so that 220 | distribution is permitted only in or among countries not thus excluded. In such 221 | case, this License incorporates the limitation as if written in the body of this 222 | License. 223 | 224 | **9.** The Free Software Foundation may publish revised and/or new versions of 225 | the General Public License from time to time. Such new versions will be similar 226 | in spirit to the present version, but may differ in detail to address new 227 | problems or concerns. 228 | 229 | Each version is given a distinguishing version number. If the Program specifies 230 | a version number of this License which applies to it and "any later version", 231 | you have the option of following the terms and conditions either of that version 232 | or of any later version published by the Free Software Foundation. If the 233 | Program does not specify a version number of this License, you may choose any 234 | version ever published by the Free Software Foundation. 235 | 236 | **10.** If you wish to incorporate parts of the Program into other free programs 237 | whose distribution conditions are different, write to the author to ask for 238 | permission. For software which is copyrighted by the Free Software Foundation, 239 | write to the Free Software Foundation; we sometimes make exceptions for this. 240 | Our decision will be guided by the two goals of preserving the free status of 241 | all derivatives of our free software and of promoting the sharing and reuse of 242 | software generally. 243 | 244 | 245 | No Warranty 246 | ----------- 247 | 248 | **11.** BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR 249 | THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE 250 | STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM 251 | "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, 252 | BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 253 | PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 254 | PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 255 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 256 | 257 | **12.** IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 258 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE 259 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 260 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR 261 | INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA 262 | BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 263 | FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER 264 | OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 265 | -------------------------------------------------------------------------------- /ListUsersInfoFromDB/AZDBpwdRetriever.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import codecs 3 | import base64 4 | import argparse 5 | from Crypto.Cipher import DES3 6 | 7 | 8 | class AZDBpwdRetriever(object): 9 | """AZDBpwdRetriever""" 10 | 11 | def __init__(self): 12 | self.OBINT = codecs.decode( 13 | "17528CC5079303B1F62FB81C5247271BDBD18D9D691D524B3281AA7F00C8DCE6D9CCC1112D37346CEA02974B0EBBB171330915FDDD2387075E89AB6B7C5FECA624DC530000000000000000000000000000000000000000000000000000000000", 14 | "hex", 15 | ) 16 | self.OBSFC = codecs.decode( 17 | "F881897D1424C5D1E6F7BF3AE490F4FC73FB34B5FA4C56A2EAA7E9C0C0CE89E1FA633FB06B3266F1D17BB0008FCA87C2AE98892617C205D2EC08D08CFF000000", 18 | "hex", 19 | ) 20 | self.OFF = 62 21 | self.LEN = 68 22 | 23 | def _transformKey(self, off): 24 | key = [] 25 | for i in range(self.LEN): 26 | tmp = self.OBINT[i] ^ self.OBSFC[off - 2] 27 | key.append(tmp) 28 | key = bytearray(base64.b64encode(bytes(key[:off]))) 29 | key.append(0) 30 | return key 31 | 32 | def decrypt_azdb_dba_password(self, pwd): 33 | # 3DES decryption 34 | ## key 35 | key = self._transformKey(self.OFF) 36 | ## IV size in 3DES: 8 bytes 37 | cipher = DES3.new(key=bytes(key[:24]), IV=self.OBINT[0:8], mode=DES3.MODE_CBC) 38 | ## Decrypt payload 39 | dec = cipher.decrypt(base64.b64decode(pwd)) 40 | if dec: 41 | print( 42 | "AZ_DB_PASSWORD: " 43 | + str(pwd) 44 | + " corresponds to the NBAZDB.db dba password: " 45 | + dec.decode("ascii") 46 | ) 47 | return dec.decode("ascii") 48 | else: 49 | return "" 50 | 51 | 52 | def parse_args(): 53 | parser = argparse.ArgumentParser( 54 | description="De-obfuscate the dba password of NBAZDB.db" 55 | ) 56 | parser.add_argument( 57 | "--pwd", 58 | "-p", 59 | metavar="PASSWORD", 60 | help="The AZ_DB_PASSWORD to de-obfuscate (base64 string. default location: /usr/openv/db/data/vxdbms.conf under AZ_DB_PASSWORD parameter)", 61 | required=True, 62 | ) 63 | return parser.parse_args() 64 | 65 | 66 | def main(): 67 | args = parse_args() 68 | if args.pwd: 69 | pwd_retriever = AZDBpwdRetriever() 70 | dbapwd = pwd_retriever.decrypt_azdb_dba_password(args.pwd) 71 | 72 | 73 | if __name__ == "__main__": 74 | main() 75 | -------------------------------------------------------------------------------- /ListUsersInfoFromDB/NBDBDBAPwdRetriever.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import codecs 3 | import logging 4 | 5 | from Crypto.Cipher import AES 6 | from Crypto.Util import Counter 7 | 8 | 9 | class NBDBDBAPwdRetriever(object): 10 | """NBDBDBAPwdRetriever""" 11 | 12 | def __init__(self, yekkey): # yekkey format: hexa string 13 | self.AES_256_IV_SZ = 32 14 | self.key = codecs.decode(yekkey, "hex") 15 | 16 | def decrypt_nbdb_dba_password(self, vxdbmsnb_password): 17 | logging.debug("Attempting to decrypt DBA password %s", vxdbmsnb_password) 18 | 19 | iv = codecs.decode(vxdbmsnb_password[: self.AES_256_IV_SZ], "hex") 20 | encpwd = codecs.decode(vxdbmsnb_password[self.AES_256_IV_SZ :], "hex") 21 | ctr = Counter.new(128, initial_value=int.from_bytes(iv, "big")) 22 | 23 | aes = AES.new(self.key, AES.MODE_CTR, counter=ctr) 24 | pwd = aes.decrypt(encpwd) 25 | 26 | logging.debug("Decrypted DBA password: %s", pwd) 27 | return pwd 28 | 29 | 30 | def test(): 31 | pwd = "a18a56ec761ad234ba1549712ba53c4ada3228badb5f" 32 | key = "d2a3ee736aafa29bf997f1c355c8b2da279fb00ca879997bc69d31acc2bb9f23" 33 | 34 | pwd_retriever = NBDBDBAPwdRetriever(key) 35 | dbapwd = pwd_retriever.decrypt_nbdb_dba_password(pwd) 36 | print(codecs.decode(dbapwd)) 37 | 38 | 39 | if __name__ == "__main__": 40 | test() 41 | -------------------------------------------------------------------------------- /ListUsersInfoFromDB/NBDBSybaseConnector.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import logging 3 | 4 | import jpype 5 | import jaydebeapi 6 | 7 | 8 | class NBDBSybaseConnector(object): 9 | """SybaseConnector""" 10 | 11 | def __init__(self, password, host, port, jconn4_file_path): 12 | self.con = None 13 | try: 14 | logging.debug( 15 | "Attempting to start JVM with classes from %s", jconn4_file_path 16 | ) 17 | jvm_path = jpype.getDefaultJVMPath() 18 | jpype.addClassPath(jconn4_file_path) 19 | jpype.startJVM(jvm_path) 20 | SybDriver = jpype.JClass("com.sybase.jdbc4.jdbc.SybDriver") 21 | drivers = SybDriver() 22 | logging.debug("Successfully loaded Sybase driver") 23 | except Exception as e: 24 | logging.error("Failed to load Sybase driver: %s", e) 25 | return 26 | 27 | try: 28 | url = f"jdbc:sybase:Tds:{host}:{port}?ServiceName=NBDB" 29 | logging.debug("Attempting to connect to database on %s", url) 30 | self.con = jaydebeapi.connect( 31 | jclassname="com.sybase.jdbc4.jdbc.SybDriver", 32 | url=url, 33 | driver_args={"user": "dba", "password": password}, 34 | ) 35 | 36 | logging.debug( 37 | "Connection to %s with DBA password %s successful", host, password 38 | ) 39 | except Exception as e: 40 | logging.error( 41 | "Failed to authenticate on host %s with DBA password %s: %s", 42 | host, 43 | password, 44 | e, 45 | ) 46 | 47 | def exec_sql(self, query): 48 | if self.con: 49 | curs = self.con.cursor() 50 | curs.execute(query) 51 | try: 52 | return curs.fetchall() 53 | except jaydebeapi.Error: 54 | return None 55 | 56 | def get_username_and_hashedpwd_from_ndbd(self): 57 | if self.con == None: 58 | logging.error("Connection failed") 59 | return None 60 | 61 | query = """SELECT users.user_name, pwd.password 62 | FROM SYS.SYSUSERPASSWORD as pwd 63 | INNER JOIN SYS.SYSUSER as users 64 | ON users.user_id=pwd.user_id""" 65 | 66 | logging.debug( 67 | "Executing SQL query '%s'", query.replace("\n", " ").replace(" ", "") 68 | ) 69 | curs = self.exec_sql(query) 70 | return curs 71 | 72 | 73 | def test(): 74 | nbdbcntr = NBDBSybaseConnector("aaaaaa", "127.0.0.1") 75 | if nbdbcntr.con: 76 | curs = nbdbcntr.get_username_and_hashedpwd_from_ndbd() 77 | 78 | 79 | if __name__ == "__main__": 80 | test() 81 | -------------------------------------------------------------------------------- /ListUsersInfoFromDB/ParseFilesHelper.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import codecs 3 | import logging 4 | 5 | 6 | class ParseFilesHelper(object): 7 | """ParseFilesHelper""" 8 | 9 | def __init__(self, yekcnedwssap_filepath, vxdbms_filepath): 10 | self.RECORD_LEN = 268 11 | self.KEY_OFF = 0x89 12 | self.TAG_OFF = 0x08 13 | self.yekcnedwssap = yekcnedwssap_filepath 14 | self.vxdbmsconf = vxdbms_filepath 15 | 16 | def find_current_encryptionkey(self): 17 | try: 18 | with open(self.yekcnedwssap, "rb") as f: 19 | with open(self.vxdbmsconf, "r") as conf: 20 | yekcnedwssapbytes = f.read() 21 | if len(yekcnedwssapbytes) % self.RECORD_LEN != 0: 22 | logging.error( 23 | "Wrong file format for encryption key (%s): unexpected length", 24 | self.yekcnedwssap, 25 | ) 26 | return None 27 | 28 | confstr = conf.read() 29 | records = {} 30 | for i in range(0, int(len(yekcnedwssapbytes) / self.RECORD_LEN)): 31 | raw = yekcnedwssapbytes[ 32 | i * self.RECORD_LEN : i * self.RECORD_LEN 33 | + self.RECORD_LEN 34 | - 1 35 | ] 36 | tag = codecs.encode( 37 | raw[self.TAG_OFF : self.KEY_OFF - 1].rstrip(b"\x00"), "hex" 38 | ).decode() 39 | key = codecs.encode( 40 | raw[self.KEY_OFF :].rstrip(b"\x00"), "hex" 41 | ).decode() 42 | records[tag] = key 43 | 44 | for i in records: 45 | if i in confstr: 46 | logging.debug("TAG found. corresponding key: %s", records[i]) 47 | return records[i] 48 | 49 | logging.error( 50 | "No TAG found in both configuration file %s and encryption key file %s", 51 | self.vxdbmsconf, 52 | self.yekcnedwssap, 53 | ) 54 | return None 55 | except (FileNotFoundError, UnicodeDecodeError) as e: 56 | logging.error("File not found or with wrong format, parsing aborted: %s", e) 57 | return None 58 | 59 | def find_current_VXDBMS_NB_PASSWORD(self): 60 | try: 61 | with open(self.vxdbmsconf, "r") as f: 62 | vxdbmsconf = f.readlines() 63 | for l in vxdbmsconf: 64 | if "VXDBMS_NB_PASSWORD" in l: 65 | pwd = l.split(":")[1][:-4] 66 | return pwd 67 | return None 68 | except (FileNotFoundError, UnicodeDecodeError): 69 | print("[DEBUG] File not found or with wrong format. Parsing aborted.") 70 | return None 71 | 72 | 73 | def test(yekcnedwssap_path, vxdbmsconf_path): 74 | filehlpr = ParseFilesHelper(yekcnedwssap_path, vxdbmsconf_path) 75 | key = filehlpr.find_current_encryptionkey() 76 | pwd = filehlpr.find_current_VXDBMS_NB_PASSWORD() 77 | if key and pwd: 78 | print("Files have been parsed correctly:") 79 | print("- Pwd: " + pwd) 80 | print("- Key: " + key) 81 | 82 | 83 | if __name__ == "__main__": 84 | test("tests/.yekcnedwssap", "tests/vxd.conf") 85 | test("tests/nofile", "tests/vxd.conf") 86 | test("tests/.yekcnedwssap", "tests/.yekcnedwssap") 87 | test("tests/vxd.conf", "tests/vxd.conf") 88 | -------------------------------------------------------------------------------- /ListUsersInfoFromDB/README.md: -------------------------------------------------------------------------------- 1 | # `ListUsersInfoFromDB` module 2 | 3 | `ListUsersInfoFromDB` dumps users from a Linux NetBackup Primary Server database 4 | using local access (or remotely if the database is configured to allow remote 5 | authenticated access). To do so, it performs the following actions: 6 | 7 | * Parse `NBDB.db` configuration files to retrieve the DBA password, 8 | * Connect to the Sybase database and print usernames and hashes of their 9 | corresponding passwords stored in the database. 10 | 11 | **Note:** this tool has the following pre-requites: 12 | * Access to NetBackup configuration files (in theory, only accessible to the 13 | `root` user), 14 | * Network access to the Sybase database (which only accepts authentication from 15 | `localhost` by default). 16 | -------------------------------------------------------------------------------- /ListUsersInfoFromDB/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | -------------------------------------------------------------------------------- /ListUsersInfoFromDB/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import codecs 3 | import argparse 4 | import logging 5 | 6 | from .NBDBDBAPwdRetriever import NBDBDBAPwdRetriever 7 | from .ParseFilesHelper import ParseFilesHelper 8 | from .NBDBSybaseConnector import NBDBSybaseConnector 9 | 10 | 11 | def parse_args(): 12 | parser = argparse.ArgumentParser( 13 | description="Purpose: Retrieve DBA pwd of NBDB.db and get User Infos" 14 | ) 15 | parser.add_argument( 16 | "-k", 17 | "--yekcnedwssap_file_path", 18 | help=".yekcnedwssap file path (example: /usr/openv/var/global/.yekcnedwssap)", 19 | required=True, 20 | ) 21 | parser.add_argument( 22 | "-p", 23 | "--vxdbmsconf_file_path", 24 | help="vxdbms.conf file path (example: /usr/openv/db/data/vxdbms.conf)", 25 | required=True, 26 | ) 27 | parser.add_argument( 28 | "-j", 29 | "--jconn4_file_path", 30 | help="jconn4 jar file path (example: /usr/openv/netbackup/web/jconn4-16.0.jar)", 31 | required=True, 32 | ) 33 | parser.add_argument( 34 | "--host", 35 | "-H", 36 | help="IP address of the host where the NBDB Sybase Server is running", 37 | required=True, 38 | ) 39 | parser.add_argument( 40 | "--port", 41 | default=13785, 42 | help="Port where the NBDB Sybase Server is running (default: 13785)", 43 | ) 44 | parser.add_argument( 45 | "-v", 46 | "--verbose", 47 | help="Run in verbose mode", 48 | action="store_true", 49 | default=False, 50 | ) 51 | 52 | return parser.parse_args() 53 | 54 | 55 | def main(): 56 | args = parse_args() 57 | 58 | # Start by setting up logging 59 | if args.verbose: 60 | logging.getLogger().setLevel(logging.DEBUG) 61 | else: 62 | logging.getLogger().setLevel(logging.INFO) 63 | 64 | filehlpr = ParseFilesHelper( 65 | args.yekcnedwssap_file_path, 66 | args.vxdbmsconf_file_path, 67 | ) 68 | key = filehlpr.find_current_encryptionkey() 69 | pwd = filehlpr.find_current_VXDBMS_NB_PASSWORD() 70 | 71 | pwd_retriever = NBDBDBAPwdRetriever(key) 72 | dbapwd = pwd_retriever.decrypt_nbdb_dba_password(pwd) 73 | 74 | nbdbcntr = NBDBSybaseConnector( 75 | dbapwd.decode("ascii"), args.host, args.port, args.jconn4_file_path 76 | ) 77 | if nbdbcntr.con: 78 | curs = nbdbcntr.get_username_and_hashedpwd_from_ndbd() 79 | if curs != None: 80 | for u, p in curs: 81 | if p != None: 82 | print( 83 | "Username: " 84 | + str(u).replace(" ", "") 85 | + "\tHash: " 86 | + codecs.encode(p, "hex").decode() 87 | ) 88 | nbdbcntr.con.close() 89 | 90 | 91 | if __name__ == "__main__": 92 | main() 93 | -------------------------------------------------------------------------------- /ListUsersInfoFromDB/tests/.yekcnedwssap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbus-seclab/nbutools/d82fb96d5623e7d3076cc0a1db06a640f63b9552/ListUsersInfoFromDB/tests/.yekcnedwssap -------------------------------------------------------------------------------- /ListUsersInfoFromDB/tests/vxd.conf: -------------------------------------------------------------------------------- 1 | VXDBMS_NB_PASSWORD = AES-256-CTR:a18a56ec761ad234ba1549712ba53c4ada3228badb5f/TAG:65637ceeb8724a6d2256fdfb60e21418b69f5e4c2ea0b9ec2aa58e359a793e8f 2 | -------------------------------------------------------------------------------- /PreAuthCartography/README.md: -------------------------------------------------------------------------------- 1 | # `PreAuthCartography` module 2 | 3 | `PreAuthCartography` performs an unauthenticated remote scan of the given list 4 | of NetBackup hosts to determine their version, role and, if relevant, their 5 | associated primary server. Optionally, it can also categorize relationships 6 | between components and plot them as a graph. 7 | 8 | This tool can be useful to provide additional details regarding open TCP `1556` 9 | ports discovered after scanning a target network range. 10 | -------------------------------------------------------------------------------- /PreAuthCartography/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | -------------------------------------------------------------------------------- /PreAuthCartography/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import asyncio 3 | import argparse 4 | import logging 5 | from io import StringIO 6 | 7 | import requests 8 | from lxml import etree 9 | 10 | from tabulate import tabulate 11 | 12 | from GetConfigInfos.vnetd import GetVnetdInfo 13 | from GetConfigInfos.pbx import ListPbxExtensions 14 | from GetConfigInfos.common import ScanState 15 | from .plotNetbackup import * 16 | 17 | import urllib3 18 | urllib3.disable_warnings() 19 | 20 | 21 | class NetbackupServer(object): 22 | def __init__(self, ip): 23 | self.ip = ip 24 | 25 | def connect(self): 26 | self.vnetd = GetVnetdInfo(self.ip) 27 | asyncio.run(self.vnetd.run()) 28 | return self.vnetd.state 29 | 30 | def get_version(self): 31 | try: 32 | version = self.vnetd.json()["vnetd"]["version"] 33 | logging.info("Found version %s for %s", version, self.ip) 34 | return version 35 | except Exception: 36 | logging.warning("Got no version information for %s", self.ip) 37 | return "Unknown" 38 | 39 | def get_ops_version(self): 40 | page = requests.get("https://" + self.ip + "/opscenter", verify=False) 41 | tree = etree.parse( 42 | StringIO(page.content.decode("utf-8")), parser=etree.HTMLParser() 43 | ) 44 | 45 | version = "" 46 | for i in tree.getroot(): 47 | if i.tag == "body": 48 | for j in i: 49 | # print(j.attrib, j.keys()) 50 | if ("class" in j.keys()) and (j.attrib["class"] == "wrapper"): 51 | for t in j: 52 | if t.attrib["class"] == "productname": 53 | version = ( 54 | t[0].text.split(" ")[1].replace(".", "") + "0000" 55 | ) 56 | break 57 | 58 | logging.info("Found version %s for %s", version, self.ip) 59 | return version 60 | 61 | def get_master(self): 62 | try: 63 | master_server = self.vnetd.json()["vnetd"]["primary-server"] 64 | logging.info("Found primary server %s for %s", master_server, self.ip) 65 | except: 66 | logging.info("Got no primary server information for %s", self.ip) 67 | return "Unknown" 68 | 69 | if master_server: 70 | return master_server 71 | else: 72 | logging.warning("Unable to find master server for %s", self.ip) 73 | return "Unknown" 74 | 75 | def get_type(self): 76 | logging.info("Starting pbx scan for %s", self.ip) 77 | pbx = ListPbxExtensions(self.ip) 78 | asyncio.run(pbx.run()) 79 | return pbx.guess_role() 80 | 81 | def get_info(self): 82 | try: 83 | nature = self.get_type() 84 | except Exception as e: 85 | logging.warning("Failed to guess role for %s: %s", self.ip, e) 86 | nature = NbuComponentType.UNKNOWN 87 | 88 | if nature == NbuComponentType.OPSCENTER: 89 | return [nature, self.get_ops_version(), "-", "-"] 90 | 91 | connect_error = self.connect() 92 | if connect_error != ScanState.SUCCESS: 93 | if not nature: 94 | return [NbuComponentType.UNKNOWN, "Unknown", "Unknown", "down"] 95 | else: 96 | return [nature, "Unknown", "Unknown", "down"] 97 | else: 98 | return [nature, self.get_version(), self.get_master(), "up"] 99 | 100 | 101 | class NetbackupInfra(object): 102 | def __init__(self, servers, output, noout=False, plot=""): 103 | self.to_scan = servers 104 | self.servers_scanned = {} 105 | self.output = output 106 | self.noout = noout 107 | self.plot = plot 108 | 109 | def init(self): 110 | try: 111 | with open(self.output, "r") as f: 112 | logging.warning("Using cached data from %s", self.output) 113 | for line in f.readlines(): 114 | line = line.strip().split(";") 115 | s, v = line[0], [ 116 | NbuComponentType(line[1]), 117 | line[2], 118 | line[3], 119 | line[4], 120 | ] 121 | self.servers_scanned[s] = v 122 | except FileNotFoundError: 123 | pass 124 | 125 | def run(self): 126 | self.init() 127 | while len(self.to_scan) != 0: 128 | server = self.to_scan.pop(0) 129 | if self.need_scan(server): 130 | self.scan(server) 131 | master = self.servers_scanned[server][2] 132 | if master != "Unknown" and master != "-" and self.need_scan(master): 133 | logging.info("Discovered new server to scan: %r", master) 134 | self.to_scan.append(master) 135 | if not (self.noout): 136 | self.print() 137 | if self.plot: 138 | self.plotter() 139 | self.store() 140 | 141 | def need_scan(self, server): 142 | if server: 143 | return not server in self.to_scan and not server in self.servers_scanned 144 | else: 145 | return 0 146 | 147 | def scan(self, server): 148 | logging.info("Starting scan for server %s", server) 149 | nb = NetbackupServer(server) 150 | self.servers_scanned[server] = nb.get_info() 151 | 152 | def result(self): 153 | s = [] 154 | for server, values in self.servers_scanned.items(): 155 | s.append(f"{server};{values[0].value};{values[1]};{values[2]};{values[3]}") 156 | return s 157 | 158 | def store(self): 159 | with open(self.output, "w") as f: 160 | f.write("\n".join(self.result())) 161 | 162 | def print(self): 163 | tab = [["Machines", "Type", "Version", "Master", "Vnetd State"]] 164 | for r in self.result(): 165 | tab.append(r.split(";")) 166 | print(tabulate(tab, headers="firstrow", tablefmt="grid")) 167 | 168 | def plotter(self): 169 | dotgraph = generate_dotgraph(self.result()) 170 | dotgraph.format = "png" 171 | dotgraph.render(self.plot.split(".")[0], cleanup=True) 172 | return 0 173 | 174 | def get_server_result(self, server): 175 | return f"{server};{self.servers_scanned[server][0].value};{self.servers_scanned[server][1]};{self.servers_scanned[server][2]};{self.servers_scanned[server][3]}" 176 | 177 | 178 | def parse_args(): 179 | parser = argparse.ArgumentParser(description="NetBackup infrastructure scanner") 180 | parser.add_argument("targets", nargs="*", help="Target hosts") 181 | parser.add_argument( 182 | "-i", 183 | "--input", 184 | help="Input file containing the list of hosts to scan", 185 | ) 186 | parser.add_argument( 187 | "-v", 188 | "--verbose", 189 | help="Run in verbose mode", 190 | action="count", 191 | default=0, 192 | ) 193 | parser.add_argument( 194 | "-q", 195 | "--quiet", 196 | help="Disable output on stdout", 197 | action="store_true", 198 | default=False, 199 | ) 200 | parser.add_argument( 201 | "-o", 202 | "--output", 203 | default="netbackup_infra.csv", 204 | help="CSV File output", 205 | ) 206 | parser.add_argument( 207 | "--plot", 208 | default="netbackup_infra_map.png", 209 | help="Infrastructure map file output path", 210 | ) 211 | return parser, parser.parse_args() 212 | 213 | 214 | def main(): 215 | parser, args = parse_args() 216 | 217 | # Start by setting up logging 218 | if args.verbose >= 2: 219 | logging.getLogger().setLevel(logging.DEBUG) 220 | elif args.verbose == 1: 221 | logging.getLogger().setLevel(logging.INFO) 222 | elif args.quiet: 223 | logging.getLogger().setLevel(logging.ERROR) 224 | else: 225 | logging.getLogger().setLevel(logging.WARNING) 226 | 227 | # Get targets 228 | targets = [] 229 | if args.input: 230 | with open(args.input, "r") as f: 231 | targets = f.readlines() 232 | 233 | # Cleanup lines 234 | targets = map(str.strip, targets) 235 | targets = filter(lambda l: bool(l), targets) 236 | targets = list(targets) 237 | 238 | targets += args.targets 239 | if not targets: 240 | parser.print_help() 241 | exit(1) 242 | 243 | nb = NetbackupInfra(targets, output=args.output, noout=args.quiet, plot=args.plot) 244 | nb.run() 245 | 246 | return 0 247 | 248 | 249 | if __name__ == "__main__": 250 | main() 251 | -------------------------------------------------------------------------------- /PreAuthCartography/plotNetbackup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | import argparse 6 | import graphviz 7 | from GetConfigInfos.common import NbuComponentType 8 | 9 | OFF_NAME = 0 10 | OFF_TYPE = 1 11 | OFF_VERS = 2 12 | OFF_MAST = 3 13 | OFF_VNET = 4 14 | 15 | type_colors = { # [fill, border/font] 16 | NbuComponentType.OPSCENTER: ["#ffe6ccff", "#d79b00"], 17 | NbuComponentType.PRIMARY: ["#d5e8d4", "#82b366"], 18 | NbuComponentType.MEDIA: ["#f8ceccff", "#b85450"], 19 | NbuComponentType.CLIENT: ["#dae8fcff", "#6c8ebf"], 20 | NbuComponentType.UNKNOWN: ["white", "gray"], 21 | } 22 | 23 | version_colors = { 24 | "750000": "firebrick4", 25 | "760000": "brown3", 26 | "800000": "chocolate3", 27 | "810000": "gold", 28 | "820000": "darkgreen", 29 | "unknown": "gray", 30 | } 31 | 32 | 33 | def parse_args(): 34 | parser = argparse.ArgumentParser(description="Plotter of Netbackup infra") 35 | parser.add_argument("servers", metavar="PATH", nargs="?") 36 | return parser.parse_args() 37 | 38 | 39 | def get_nodes(s): 40 | l = [] 41 | for i in s.body: 42 | l.append(i.split(" ")[0][1:]) 43 | # print(l) 44 | return l 45 | 46 | 47 | def generate_component(s, component): 48 | if '"' + component[OFF_NAME] + '"' not in get_nodes(s): 49 | s.attr( 50 | "node", 51 | shape="Mrecord", 52 | style="filled", 53 | fillcolor=type_colors.get(component[OFF_TYPE])[0], 54 | color=type_colors.get(component[OFF_TYPE])[1], 55 | fontcolor=type_colors.get(component[OFF_TYPE])[1], 56 | ) 57 | s.node( 58 | component[OFF_NAME], 59 | label=component[OFF_NAME] + "|" + component[OFF_TYPE].value, 60 | rankdir="TB", 61 | ) 62 | s.edge( 63 | component[OFF_NAME] + ":s", 64 | component[OFF_NAME] + ":s", 65 | style="dotted", 66 | rankdir="LR", 67 | arrowhead="dot", 68 | arrowsize="1.3", 69 | maxlen=".1", 70 | color="invis", 71 | headlabel=component[OFF_VERS], 72 | fontsize="7", 73 | fontcolor=version_colors.get(component[OFF_VERS]), 74 | labeldistance="2", 75 | fillcolor=version_colors.get(component[OFF_VERS]), 76 | ) 77 | 78 | 79 | def generate_link(s, component): 80 | generate_component( 81 | s, 82 | [ 83 | component[OFF_MAST], 84 | NbuComponentType.UNKNOWN, 85 | "unknown", 86 | "unknown", 87 | "unknown", 88 | ], 89 | ) 90 | s.attr( 91 | "edge", 92 | color=type_colors.get(component[OFF_TYPE])[1], 93 | arrowhead="none", 94 | ) 95 | s.edge(component[OFF_NAME], component[OFF_MAST]) 96 | 97 | 98 | def generate_legend(s): 99 | with s.subgraph(name="legend") as c: 100 | for v in version_colors: 101 | c.attr( 102 | "node", 103 | shape="none", 104 | style="filled", 105 | fillcolor="white", 106 | fontcolor="black", 107 | ) 108 | c.node(v, v) 109 | c.edge(v, version_colors.get(v), style="invis", color="white") 110 | 111 | c.attr( 112 | "node", 113 | shape="point", 114 | style="filled", 115 | width=".15", 116 | color=version_colors.get(v), 117 | fillcolor=version_colors.get(v), 118 | margin="0,0", 119 | height="0.5", 120 | ) 121 | c.node(version_colors.get(v), "") 122 | 123 | c.attr(style="filled", bcolor="lightgrey", mindist="0.1") 124 | 125 | 126 | def generate_dotgraph(servers): 127 | s = graphviz.Digraph("G") 128 | s.attr(rankdir="LR") 129 | 130 | # creation of boxes for each identified component 131 | for line in servers: 132 | line = line.split(";") 133 | line = [line[0], NbuComponentType(line[1]), line[2], line[3], line[4]] 134 | generate_component(s, line) 135 | 136 | # creation of links 137 | for line in servers: 138 | line = line.split(";") 139 | line = [line[0], NbuComponentType(line[1]), line[2], line[3], line[4]] 140 | if line[OFF_MAST] != "Unknown" and line[OFF_TYPE] != NbuComponentType.OPSCENTER: 141 | generate_link(s, line) 142 | 143 | # generate_legend(s) 144 | 145 | return s 146 | 147 | 148 | def main(): 149 | args = parse_args() 150 | with open(args.servers, "r") as f: 151 | servers = list(map(lambda p: p.lower(), f.readlines())) 152 | print(generate_dotgraph(servers)) 153 | 154 | return 0 155 | 156 | 157 | if __name__ == "__main__": 158 | sys.exit(main()) 159 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `nbutools` 2 | 3 | `nbutools` is a Python toolbox that aims to assist security audits and analysis 4 | of [NetBackup](https://www.veritas.com/protection/netbackup) infrastructures. It 5 | provides tools to map out the exposed attack surface, to collect deployment 6 | information (e.g. configuration values, accessible services, etc.) and more. It 7 | also includes a set of utilities to help study NetBackup services relying on 8 | custom protocols and "beginner's guides" to using native NetBackup tools 9 | relevant for offensive activities. 10 | 11 | Though most tools do not require any form of authentication (simple network 12 | access to the targets), others require specific privileges or access to 13 | components of the NetBackup infrastructure. 14 | 15 | **Note:** this toolbox has been designed, used and tested against the following 16 | NetBackup versions: `8.2`, `8.3` and `9.0`. The toolbox may not work properly on 17 | other versions. Moreover, it is not designed to include any form of exploit. 18 | 19 | 20 | ## Installation 21 | 22 | ### Packages required 23 | 24 | `nbutools` relies on Python 3.11 and the modules listed in [`requirements.txt`](requirements.txt). 25 | 26 | ### Basic installation 27 | 28 | `nbutools` can be installed with the following commands (assuming a Debian derivative): 29 | 30 | ```bash 31 | $ sudo apt update 32 | $ sudo apt install python3 python3-pip graphviz 33 | $ pip3 install . 34 | ``` 35 | 36 | In particular, it will install the following new commands: `nbuscan.py`, 37 | `nbumap.py` and `nbudbdump.py`. 38 | 39 | ### Development 40 | 41 | A development environment can be setup using `pip`: 42 | 43 | ``` 44 | $ pip3 install --editable . 45 | ``` 46 | 47 | 48 | ## Usage 49 | 50 | ### Reconnaissance (PreAuthentication) 51 | 52 | #### `nbumap`: infrastructure map builder 53 | 54 | `nbumap.py` is a Python script designed to collect information about NetBackup 55 | hosts, including the software version, the type of NetBackup component (client, 56 | primary, media, opscenter) and the associated primary server when relevant. It 57 | then tries to reconstruct a map of the infrastructure with the links between 58 | these hosts. 59 | 60 | **Context of usage:** 61 | * Can be used after a network scan resulting in a list of IP responding on port 62 | `1556`. 63 | * No authentication needed. 64 | 65 | ```bash 66 | $ nbumap.py -h 67 | usage: nbumap.py [-h] [-i INPUT] [-v] [-q] [-o OUTPUT] [--plot PLOT] [targets ...] 68 | 69 | NetBackup infrastructure scanner 70 | 71 | positional arguments: 72 | targets Target hosts 73 | 74 | options: 75 | -h, --help show this help message and exit 76 | -i INPUT, --input INPUT 77 | Input file containing the list of hosts to scan 78 | -v, --verbose Run in verbose mode 79 | -q, --quiet Disable output on stdout 80 | -o OUTPUT, --output OUTPUT 81 | CSV File output 82 | --plot PLOT Infrastructure map file output path 83 | ``` 84 | 85 | For example, this command builds a png image representing the hosts listed in 86 | `listening_1556_IPlist.txt`: 87 | 88 | ```bash 89 | $ cat listening_1556_IPlist.txt 90 | 172.16.142.49 91 | 172.16.142.50 92 | 172.16.142.51 93 | 172.16.142.52 94 | 172.16.142.53 95 | 172.16.142.60 96 | 97 | $ nbumap.py -i listening_1556_IPlist.txt --plot carto.png 98 | +---------------+----------------+-----------+--------------+---------------+ 99 | | Machines | Type | Version | Master | Vnetd State | 100 | +===============+================+===========+==============+===============+ 101 | | 172.16.142.49 | OpsCenter | 820000 | - | - | 102 | +---------------+----------------+-----------+--------------+---------------+ 103 | | 172.16.142.50 | Primary Server | 820000 | nb-primary-a | up | 104 | +---------------+----------------+-----------+--------------+---------------+ 105 | | 172.16.142.51 | Media Server | 820000 | nb-primary-a | up | 106 | +---------------+----------------+-----------+--------------+---------------+ 107 | | 172.16.142.52 | Client | 820000 | nb-primary-a | up | 108 | +---------------+----------------+-----------+--------------+---------------+ 109 | | 172.16.142.53 | Client | 820000 | nb-primary-a | up | 110 | +---------------+----------------+-----------+--------------+---------------+ 111 | | 172.16.142.60 | Primary Server | 820000 | nb-primary-b | up | 112 | +---------------+----------------+-----------+--------------+---------------+ 113 | | nb-primary-a | Unknown | Unknown | Unknown | DNS | 114 | +---------------+----------------+-----------+--------------+---------------+ 115 | | nb-primary-b | Unknown | Unknown | Unknown | DNS | 116 | +---------------+----------------+-----------+--------------+---------------+ 117 | ``` 118 | 119 | ![](img/carto.png "`carto.png` generated by `nbumap.py`"). 120 | 121 | 122 | #### `nbuscan`: Information collector 123 | 124 | `nbuscan` performs an unauthenticated remote scan of the given list of NetBackup 125 | hosts to determine their version, role and, if relevant, their associated 126 | primary server. It is more exhaustive than `nbumap` and thus serves a 127 | complimentary role. 128 | 129 | Usage: 130 | 131 | ```bash 132 | $ nbuscan.py -h 133 | usage: nbuscan.py [-h] [-j JOBS] [-v] [-q] [-l {DEBUG,INFO,WARNING,ERROR,CRITICAL}] [-t TIMEOUT] [-f {plain,json}] [-i INPUT] [-o OUTPUT] [targets ...] 134 | 135 | NetBackup scanner tool 136 | 137 | positional arguments: 138 | targets Target hosts 139 | 140 | options: 141 | -h, --help show this help message and exit 142 | -j JOBS, --jobs JOBS Maximum number of concurrent jobs 143 | -v, --verbose Run in verbose mode 144 | -q, --quiet Run in quiet mode 145 | -l {DEBUG,INFO,WARNING,ERROR,CRITICAL}, --log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL} 146 | Define the log level 147 | -t TIMEOUT, --timeout TIMEOUT 148 | Timeout for TCP connections 149 | -f {plain,json}, --format {plain,json} 150 | Output format 151 | -i INPUT, --input INPUT 152 | Input file containing the list of hosts to scan 153 | -o OUTPUT, --output OUTPUT 154 | Output file 155 | ``` 156 | 157 | For example, this command scans information on the `172.16.142.50` host and 158 | prints the output in json format: 159 | 160 | ```bash 161 | $ nbuscan.py -f json 172.16.142.50 162 | ``` 163 | 164 | ### Post-exploitation on primary servers (PostAuthentication, root privileges) 165 | 166 | Primary servers are key components of the NetBackup infrastructure, on which 167 | `root` access grants significant post-exploitation capabilities for an attacker. 168 | The following section aims at showcasing how such an access can be leveraged to 169 | perform several tasks, including retrieving files from backups and dumping parts 170 | of a NetBackup database. 171 | 172 | #### Relevant resources 173 | 174 | Plenty of administrative tools are provided with the NetBackup product, and some 175 | of them can be of interest from a security point of view. Some notes about 176 | interesting commands to know were gathered 177 | [here](./docs/NetBackupForDummies.md). 178 | 179 | #### `nbudbdump`: Sybase database hash dumper 180 | 181 | `nbudbdump` dumps user hashes from a Linux NetBackup Primary Server database 182 | using local access (or remotely if the database is configured to allow remote 183 | authenticated access), assuming having access to the following files: 184 | 185 | * `/usr/openv/var/global/.yekcnedwssap` 186 | * `/usr/openv/db/data/vxdbms.conf` 187 | * `jconn4-16.0.jar` 188 | 189 | ```bash 190 | $ nbudbdump.py -h 191 | usage: nbudbdump.py [-h] -k YEKCNEDWSSAP_FILE_PATH -p VXDBMSCONF_FILE_PATH -j JCONN4_FILE_PATH --host HOST [--port PORT] [-v] 192 | 193 | Purpose: Retrieve DBA pwd of NBDB.db and get User Infos 194 | 195 | options: 196 | -h, --help show this help message and exit 197 | -k YEKCNEDWSSAP_FILE_PATH, --yekcnedwssap_file_path YEKCNEDWSSAP_FILE_PATH 198 | .yekcnedwssap file path (example: /usr/openv/var/global/.yekcnedwssap) 199 | -p VXDBMSCONF_FILE_PATH, --vxdbmsconf_file_path VXDBMSCONF_FILE_PATH 200 | vxdbms.conf file path (example: /usr/openv/db/data/vxdbms.conf) 201 | -j JCONN4_FILE_PATH, --jconn4_file_path JCONN4_FILE_PATH 202 | jconn4 jar file path (example: /usr/openv/netbackup/web/jconn4-16.0.jar) 203 | --host HOST, -H HOST IP address of the host where the NBDB Sybase Server is running 204 | --port PORT Port where the NBDB Sybase Server is running (default: 13785) 205 | -v, --verbose Run in verbose mode 206 | ``` 207 | 208 | For example, the following command dumps the hashes of a `NBDB.db` Sybase 209 | database from a remote server using port forwarding: 210 | 211 | ```bash 212 | # Download required files 213 | $ scp -OT root@172.16.142.50:"/usr/openv/var/global/.yekcnedwssap /usr/openv/db/data/vxdbms.conf /usr/openv/netbackup/web/jconn4-16.0.jar" . 214 | # Use port-forwarding to grant access to database port 215 | $ ssh -N -L 127.0.0.1:13785:127.0.0.1:13785 root@172.16.142.50 216 | # Dump hashes 217 | $ nbudbdump.py -k .yekcnedwssap -p vxdbms.conf -h jconn4-16.0.jar -H 127.0.0.1 218 | ``` 219 | 220 | 221 | ## To go further 222 | 223 | Some other NetBackup protocols were analyzed by AirbusSeclab. The 224 | [network-analysis folder](./network-analysis) contains custom 225 | [pynet](https://github.com/ben-64/pynet) plugins and definitions for custom 226 | [Scapy](https://scapy.net) packets based on our understanding of their formats. 227 | 228 | 229 | ## License 230 | 231 | `nbutools` is released under [GPLv2](./LICENSE). 232 | 233 | 234 | ## Associated publications 235 | 236 | * [Hexacon 2022 conference](https://2022.hexacon.fr/conference/speakers/#unavoidable_pain) 237 | * [SSTIC 2023 conference](https://www.sstic.org/2023/presentation/analyse_securite_netbackup) 238 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | all = ["GetConfigInfos"] 3 | -------------------------------------------------------------------------------- /bin/nbudbdump.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from ListUsersInfoFromDB import main 3 | 4 | main.main() 5 | -------------------------------------------------------------------------------- /bin/nbumap.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from PreAuthCartography import main 3 | 4 | main.main() 5 | -------------------------------------------------------------------------------- /bin/nbuscan.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from GetConfigInfos import main 3 | 4 | main.main() 5 | -------------------------------------------------------------------------------- /docs/NetBackupForDummies.md: -------------------------------------------------------------------------------- 1 | # NetBackup for dummies 2 | 3 | The following guide aims at listing helpful commands, resources and steps for 4 | various NetBackup tasks 5 | 6 | ## Environment 7 | 8 | It is assumed that the following variables have been defined with the 9 | appropriate values: 10 | 11 | ```bash 12 | MASTER_SERVER='' 13 | MEDIA_SERVER='' 14 | CLIENT='' 15 | ``` 16 | 17 | The following values should also be added to the `$PATH` variable: 18 | 19 | ```bash 20 | PATH="$PATH:/usr/openv/netbackup/bin/admincmd:/usr/openv/netbackup/bin:/usr/openv/volmgr/bin" 21 | ``` 22 | 23 | ## Existing resources 24 | 25 | The following [cheatsheet](http://www.datadisk.co.uk/html_docs/veritas/veritas_netbackup_cs.htm) 26 | gives a list of interesting examples of commands and tool uses offered with the 27 | NetBackup product. 28 | 29 | Below some other examples interesting from a security point of view. 30 | 31 | ## Definitions 32 | 33 | ## Backup policy 34 | 35 | Definition containing the list of files to backup, the clients targeted, the 36 | frequency of backups, and more 37 | 38 | ### Storage Lifecycle Policies (SLP) 39 | 40 | Storage Plan for a set of backup which determines how the data is stored, 41 | copied, replicated, and retained 42 | 43 | ### Storage Unit 44 | 45 | Location where data is stored 46 | 47 | ### Backup image 48 | 49 | A backup image stores all items backed-up in one job. Each image is related to 50 | a backup policy and a server, and is assigned a backup id 51 | 52 | ### Backup image copy 53 | 54 | An image can have multiple copies 55 | 56 | ### Backup image copy fragment 57 | 58 | A copy can be split in multiple fragments, each fragment located on a media 59 | server 60 | 61 | ## Listing clients 62 | 63 | On the master server: 64 | 65 | ```bash 66 | # List all clients 67 | $ bpplclients -allunique -U 68 | ``` 69 | 70 | ## Listing client backups 71 | 72 | On the master server: 73 | 74 | ```bash 75 | # List backups from the last 48 hours 76 | $ bpimagelist -hoursago 48 -client "$BACKUP" 77 | ``` 78 | 79 | ## Viewing backup policies 80 | 81 | On the master server: 82 | 83 | ```bash 84 | # List all backup policies 85 | $ bppllist -allpolicies -L 86 | 87 | # Display information of a backup policy 88 | $ bppllist -U -L 89 | 90 | # Display all backup policy for a specific client 91 | $ bppllist -byclient "$CLIENT" -U 92 | ``` 93 | 94 | ## Viewing scheduled backups 95 | 96 | On the master server: 97 | 98 | ```bash 99 | $ nbpemreq -predict_all -date 02/10/2021 23:59:59 100 | ``` 101 | 102 | ## Listing files from a backup policy 103 | 104 | On the master server: 105 | 106 | ```bash 107 | # List all backup policy names 108 | $ bppllist -allpolicies -L | grep "Policy Name:" 109 | 110 | # List all explicitely included paths 111 | $ bppllist | grep INCLUDE | cut -d' ' -f2 112 | 113 | # Some more contents 114 | $ bplist -C "$CLIENT" -R "/root" 115 | ``` 116 | 117 | ## Listing Storage Lifecycle Policies 118 | 119 | On the master server: 120 | 121 | ```bash 122 | # Display all STLs 123 | $ nbstlutil list 124 | 125 | # Display information about a backupid 126 | $ nbstlutil list -backupid -U 127 | 128 | # Display information about a client 129 | $ nbstlutil list -client "$CLIENT" -U 130 | ``` 131 | 132 | More details: 133 | 134 | ## Viewing storage units 135 | 136 | On the master server: 137 | 138 | ```bash 139 | # List all storage unit 140 | $ bpstulist -L 141 | 142 | # Display information about a specific storage unit 143 | $ bpstulist -label -U 144 | ``` 145 | 146 | ## Viewing volume pools 147 | 148 | On the master server: 149 | 150 | ```bash 151 | # List volume pools 152 | $ vmpool -listall 153 | 154 | # List media needed for a specific pool 155 | $ vmquery -h "$MEDIA_SERVER" -pn 156 | ``` 157 | 158 | ## Finding client versions 159 | 160 | On the master server: 161 | 162 | ### Linux 163 | 164 | ```bash 165 | while read x; do echo "$x : $(bpgetconfig -g $x | tail -n +3 | head -n 1)" >> /tmp/agent.txt; done < <(bpplclients -allunique -U | tr -s ' ' | tail -n +3 | cut -d' ' -f3) 166 | ``` 167 | 168 | ### Windows 169 | 170 | ```powershell 171 | bpplclients -allunique -U | Select-Object -skip 3 | ForEach-Object {$_.Substring(44) + " : " + "$(bpgetconfig -g $_.Substring(44) | Select-Object -Index 2)"} 172 | ``` 173 | 174 | ## Finding what is backed up for a specific host 175 | 176 | On the master server: 177 | 178 | ```bash 179 | $ bpflist -U -client "$CLIENT" -rl 100 180 | ``` 181 | 182 | ## List all Storage Policies for a client 183 | 184 | On the master server: 185 | 186 | ```bash 187 | $ nbstlutil list -client "$CLIENT" -U 188 | ``` 189 | 190 | ## Restoring a backup 191 | 192 | ### Unique file 193 | 194 | #### Create a rename file 195 | 196 | The rename file contains a mapping between restored files and location to which 197 | to restore the file (do not forget the `\n` at the end) 198 | 199 | ```bash 200 | $ cat rename_file 201 | change /etc/shadow to /tmp/stolen 202 | ``` 203 | 204 | #### Restore files 205 | 206 | ```bash 207 | $ bprestore -C "$CLIENT" -D "$MEDIA_SERVER" -K -R ./rename_file /etc/shadow 208 | ``` 209 | 210 | ### Folder 211 | 212 | ```bash 213 | $ cat rename_file 214 | change /home/user to /tmp/stolen 215 | 216 | $ bprestore -C "$CLIENT" -D "$MEDIA_SERVER" -K -R ./rename_file /home/user 217 | ``` 218 | 219 | ## Creating a backup for a client 220 | 221 | Main source: 222 | 223 | ### Create storage 224 | 225 | 1. Create a storage server (on the master server) 226 | 227 | ```bash 228 | $ nbdevconfig -creatests -storage_server "$MEDIA_SERVER" -stype AdvancedDisk -st 5 -media_server "$MEDIA_SERVER" 229 | Storage server nb-media-a has been successfully created 230 | $ nbdevquery -liststs -u 231 | Storage Server : nb-media-a 232 | Storage Server Type : AdvancedDisk 233 | Storage Type : Formatted Disk, Direct Attached 234 | State : UP 235 | Flag : OpenStorage 236 | Flag : AdminUp 237 | Flag : InternalUp 238 | Flag : SpanImages 239 | Flag : LifeCycle 240 | Flag : CapacityMgmt 241 | Flag : FragmentImages 242 | Flag : Cpr 243 | Flag : RandomWrites 244 | Flag : FT-Transfer 245 | Flag : CapacityManagedRetention 246 | Flag : CapacityManagedJobQueuing 247 | ``` 248 | 2. Create volume (on the media server). Note that the volume needs to be a mount 249 | point, not a folder 250 | 251 | ```bash 252 | $ mkdir /root/volume /mnt/volume 253 | $ mount -o bind /root/volume /mnt/volume 254 | ``` 255 | 3. Create a disk pool (on the master server) 256 | 257 | ```bash 258 | # First list available volumes 259 | $ nbdevconfig -previewdv -storage_server "$MEDIA_SERVER" -stype AdvancedDisk 260 | V_5_ DiskVolume < "/mnt/volume" "/mnt/volume" 53660876800 37310164992 0 0 0 0 0 0 0 > 261 | V_5_ DiskVolume < "/run/user/0" "/run/user/0" 192712704 192712704 0 0 0 0 0 0 0 > 262 | V_5_ DiskVolume < "/home" "/home" 50432839680 50398937088 0 0 0 0 0 0 0 > 263 | ... 264 | 265 | # Select the one you want in a file, here the first one 266 | $ nbdevconfig -previewdv -storage_server "$MEDIA_SERVER" -stype AdvancedDisk | head -n 1 | tee /root/dvlist.txt 267 | V_5_ DiskVolume < "/mnt/volume" "/mnt/volume" 53660876800 37310164992 0 0 0 0 0 0 0 > 268 | 269 | # Create a disk pool with the volumes you want 270 | $ nbdevconfig -createdp -dp airbusseclab_pool -stype AdvancedDisk -storage_servers "$MEDIA_SERVER" -dvlist /root/dvlist.txt 271 | Disk pool airbusseclab_pool has been successfully created with 1 volumes 272 | 273 | # List disk pool created 274 | $ nbdevquery -listdv -stype AdvancedDisk 275 | V_5_ airbusseclab_pool AdvancedDisk /mnt/volume @aaaac 49.98 34.75 30 1 0 1 0 0 14 0 276 | 277 | # List disk volumes available 278 | $ nbdevquery -listdp 279 | V7.5 airbusseclab_pool 1 49.98 49.98 1 98 80 -1 nb-media 280 | ``` 281 | 4. Create a storage unit (on the master server) 282 | 283 | ```bash 284 | $ bpstuadd -label airbusseclab_stu -dt 6 -dp airbusseclab_pool -host "$MEDIA_SERVER" 285 | $ bpstulist 286 | airbusseclab_stu 0 nb-media-a 0 -1 -1 1 0 "*NULL*" 1 1 524288 nb-media-a 0 6 0 0 0 0 airbusseclab_pool nb-media-a 515527 287 | ``` 288 | 5. Create a Storage Lifecycle Policy (on the master server, optional) 289 | 290 | ```bash 291 | $ nbstl airbusseclab_stl -add -residence airbusseclab_stu 292 | $ nbstl -L 293 | Name: airbusseclab_stl 294 | Data Classification: (none specified) 295 | Duplication Job Priority: 0 296 | State: active 297 | Version: 0 298 | Operation 1 Use for: 0 (backup) 299 | Storage: airbusseclab_stu 300 | Volume Pool: (none specified) 301 | Server Group: (none specified) 302 | Retention Type: 0 (Fixed) 303 | Retention Level: 1 (2 weeks) 304 | Alternate Read Server: (none specified) 305 | Preserve Multiplexing: false 306 | Enable Automatic Remote Import: false 307 | State: active 308 | Source: 0 (client) 309 | Operation ID: (none specified) 310 | Operation Index: 1 311 | Window Name: -- 312 | Window Close Option: -- 313 | Deferred Duplication: no 314 | ``` 315 | 316 | ### Create backup 317 | 318 | 1. Create a backup policy (on the master server) 319 | 320 | 1. Create a default backup policy 321 | 322 | ```bash 323 | $ bppolicynew airbusseclab_backup_policy 324 | $ bppllist airbusseclab_backup_policy -U 325 | ------------------------------------------------------------ 326 | 327 | Policy Name: airbusseclab_backup_policy 328 | 329 | Policy Type: Standard 330 | Active: yes 331 | Effective date: 06/02/2023 12:13:07 332 | Client Compress: no 333 | Follow NFS Mounts: no 334 | Cross Mount Points: no 335 | Collect TIR info: no 336 | Block Incremental: no 337 | Mult. Data Streams: no 338 | Client Encrypt: no 339 | Checkpoint: no 340 | Policy Priority: 0 341 | Max Jobs/Policy: Unlimited 342 | Disaster Recovery: 0 343 | Collect BMR info: no 344 | Residence: (specific storage unit not required) 345 | Volume Pool: NetBackup 346 | Server Group: *ANY* 347 | Keyword: (none specified) 348 | Data Classification: - 349 | Residence is Storage Lifecycle Policy: no 350 | Application Discovery: no 351 | Discovery Lifetime: 0 seconds 352 | ASC Application and attributes: (none defined) 353 | 354 | Granular Restore Info: no 355 | Ignore Client Direct: no 356 | Use Accelerator: no 357 | 358 | Clients: (none defined) 359 | 360 | Include: (none defined) 361 | 362 | Schedule: (none defined) 363 | ``` 364 | 2. Update backup policy attributes 365 | 366 | ```bash 367 | $ bpplinfo airbusseclab_backup_policy -modify -residence airbusseclab_stu 368 | $ bppllist airbusseclab_backup_policy -U 369 | ------------------------------------------------------------ 370 | 371 | Policy Name: airbusseclab_backup_policy 372 | 373 | Policy Type: Standard 374 | Active: yes 375 | Effective date: 06/02/2023 12:13:07 376 | Client Compress: no 377 | Follow NFS Mounts: no 378 | Cross Mount Points: no 379 | Collect TIR info: no 380 | Block Incremental: no 381 | Mult. Data Streams: no 382 | Client Encrypt: no 383 | Checkpoint: no 384 | Policy Priority: 0 385 | Max Jobs/Policy: Unlimited 386 | Disaster Recovery: 0 387 | Collect BMR info: no 388 | Residence: airbusseclab_stu 389 | Volume Pool: NetBackup 390 | Server Group: *ANY* 391 | Keyword: (none specified) 392 | Data Classification: - 393 | Residence is Storage Lifecycle Policy: no 394 | Application Discovery: no 395 | Discovery Lifetime: 0 seconds 396 | ASC Application and attributes: (none defined) 397 | 398 | Granular Restore Info: no 399 | Ignore Client Direct: no 400 | Use Accelerator: no 401 | 402 | Clients: (none defined) 403 | 404 | Include: (none defined) 405 | 406 | Schedule: (none defined) 407 | ```` 408 | 3. Add clients to that policy 409 | 410 | ```bash 411 | $ bpplclients -allunique -U 412 | Hardware OS Client 413 | --------------- ------------------------- -------------- 414 | Linux Centos nb-client-a-1 415 | $ bpplclients airbusseclab_backup_policy -add "$CLIENT" 416 | ``` 417 | 4. Add Backup selection 418 | 419 | ```bash 420 | # First create a list of path to backup 421 | $ echo "/etc/shadow" > tobackup.txt 422 | 423 | # Register this path 424 | $ bpplinclude airbusseclab_backup_policy -add -f tobackup.txt 425 | ``` 426 | 427 | 5. Add schedules 428 | 429 | ```bash 430 | $ bpplsched airbusseclab_backup_policy -add airbusseclab_schedule -freq 600 -st FULL 431 | ``` 432 | 2. Launch a backup (on the client or on the master server) 433 | 434 | ```bash 435 | $ bpbackup -i -p airbusseclab_backup_policy -s airbusseclab_schedule -t 0 436 | ``` 437 | 438 | ## Troubleshooting 439 | 440 | Logs for admin commands, used while configuring backups, are located in 441 | `/usr/openv/netbackup/logs/admin`. 442 | 443 | Backup logs are accessible from the web UI on the master server 444 | `https:///webui/jobs` or from the `bpjobd` logs in 445 | `/usr/openv/netbackup/logs/bpjobd`. 446 | 447 | Here are a few known issues and how to fix them. 448 | 449 | ### Clear host cache 450 | 451 | ```bash 452 | /usr/openv/netbackup/bin/bpclntcmd -clear_host_cache 453 | ``` 454 | 455 | ### No matching database entry found 456 | 457 | If creating the storage server fails with the following error: 458 | 459 | ``` 460 | DSM has encountered an EMM error: No matching database entry found for: nb-media 461 | ``` 462 | 463 | Then it probably means that there is an issue with your `bp.conf` file, either 464 | on the master server or on the media server. 465 | 466 | On the master server, ensure you have the following attributes: 467 | 468 | ``` 469 | # Order is important, you only need one if master server = media server 470 | SERVER = 471 | SERVER = 472 | CLIENT_NAME = 473 | MEDIA_SERVER = 474 | ``` 475 | 476 | On the media server, ensure you have the following attributes: 477 | 478 | ``` 479 | # Order is important 480 | SERVER = 481 | SERVER = 482 | CLIENT_NAME = 483 | MEDIA_SERVER = localhost 484 | ``` 485 | 486 | ### Access to the client was not allowed 487 | 488 | If backups fail and the web UI show the following status: 489 | 490 | ``` 491 | Access to the client was not allowed 492 | ``` 493 | 494 | The client's `bp.conf` file is most likely missing either the master server or 495 | the media server, ensure you have the following attributes: 496 | 497 | ``` 498 | SERVER = 499 | SERVER = 500 | CLIENT_NAME = 501 | ``` 502 | 503 | ### Disk volume is down 504 | 505 | If backups fail and the web UI show the following status: 506 | 507 | ``` 508 | Disk volume is down 509 | ``` 510 | 511 | The media server is probably down, or the `/mnt/volume` folder is not mounted. 512 | 513 | Restart the media server, mount the folder, and enter the following command: 514 | ```bash 515 | $ nbdevconfig -changestate -stype server_type -dp disk_pool_name -dv disk_volume_name -state RESET 516 | ``` 517 | -------------------------------------------------------------------------------- /img/carto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbus-seclab/nbutools/d82fb96d5623e7d3076cc0a1db06a640f63b9552/img/carto.png -------------------------------------------------------------------------------- /network-analysis/BpcdClasses.py: -------------------------------------------------------------------------------- 1 | from scapy import * 2 | from scapy.packet import * 3 | from scapy.fields import * 4 | 5 | BPCD_CMD_IDS = { 6 | b"\x00\x01": "BPCD_FORK_CMD_RQST", 7 | b"\x00\x02": "BPCD_IMMED_CMD_RQST", 8 | b"\x00\x03": "BPCD_OPEN_FOR_READ_RQST", 9 | b"\x00\x04": "BPCD_OPEN_FOR_WRITE_RQST", 10 | b"\x00\x05": "BPCD_READ_RQST", 11 | b"\x00\x06": "BPCD_WRITE_RQST", 12 | b"\x00\x07": "BPCD_RM_FILE_RQST", 13 | b"\x00\x08": "BPCD_RM_DIR_RQST", 14 | b"\x00\t": "BPCD_CLOSE_FILE_RQST", 15 | b"\x00\n": "BPCD_BECOME_USER_RQST", 16 | b"\x00\x0b": "BPCD_SEND_MAIL_RQST", 17 | b"\x00\x0c": "BPCD_DISCONNECT_RQST", 18 | b"\x00\r": "BPCD_FORK_CMD_W_LOG_RQST", 19 | b"\x00\x0e": "BPCD_LOG_RQST", 20 | b"\x00\x0f": "BPCD_LOG_RQST_NO_STATUS", 21 | b"\x00\x10": "BPCD_PEERNAME_RQST", 22 | b"\x00\x11": "BPCD_BECOME_USER_GROUP_RQST", 23 | b"\x00\x12": "BPCD_SEND_SIGNAL_TO_PID", 24 | b"\x00\x13": "BPCD_HOSTNAME_RQST", 25 | b"\x00\x14": "BPCD_GET_STDOUT_SOCKET_RQST", 26 | b"\x00\x15": "BPCD_TIME_TO_KEY_RQST", 27 | b"\x00\x16": "BPCD_CLIENTNAME_RQST", 28 | b"\x00\x17": "BPCD_GET_STDIN_SOCKET_RQST", 29 | b"\x00\x18": "BPCD_GET_VERSION_RQST", 30 | b"\x00\x19": "BPCD_GET_LIST_JOBS_RQST", 31 | b"\x00\x1a": "BPCD_GET_JOB_FILE_RQST", 32 | b"\x00\x1b": "BPCD_GET_PLATFORM_RQST", 33 | b"\x00\x1c": "BPCD_GET_CRYPT_CONF_RQST", 34 | b"\x00\x1d": "BPCD_SET_CRYPT_CONF_RQST", 35 | b"\x00\x1e": "BPCD_GET_CRYPT_VERSIONS_RQST", 36 | b"\x00\x1f": "BPCD_RENAME_FILE_RQST", 37 | b"\x00 ": "BPCD_TEMP_FILE_RQST", 38 | b"\x00!": "BPCD_DELETE_JOB_FILE_RQST", 39 | b'\x00"': "BPCD_ADD_CRYPT_PASSPHRASE_RQST", 40 | b"\x00#": "BPCD_OPEN_FOR_WRITE_WITH_MODE_RQST", 41 | b"\x00$": "BPCD_READ_HOST_CONFIG_RQST", 42 | b"\x00%": "BPCD_READ_USERS_CONFIG_RQST", 43 | b"\x00&": "BPCD_UPDATE_HOST_CONFIG_RQST", 44 | b"\x00'": "BPCD_UPDATE_USERS_CONFIG_RQST", 45 | b"\x00-": "BPCD_GET_BE_VERSION_RQST", 46 | b"\x00.": "BPCD_ADD_CRYPT_KEYS_RQST", 47 | b"\x00/": "BPCD_CRYPT_KEYS_RQST", 48 | b"\x000": "BPCD_READ_DIR_RQST", 49 | b"\x001": "BPCD_FORK_CMD_W_STAT_RQST", 50 | b"\x003": "BPCD_READ_TEXT_FILE_RQST", 51 | b"\x004": "BPCD_GET_PROCESS_STATUS_RQST", 52 | b"\x007": "BPCD_READ_DIR_RQST_EX", 53 | b"\x00<": "BPCD_GET_STDIN_HOST_SOCKET_RQST", 54 | b"\x00;": "BPCD_GET_STDOUT_HOST_SOCKET_RQST", 55 | b"\x00=": "BPCD_GET_BPRD_SOCKET_RQST", 56 | b"\x00@": "BPCD_GET_REMOTE_INFO", 57 | b"\x00A": "BPCD_GET_PRIVILEGES_RQST", 58 | b"\x00B": "BPCD_FORK_OTHER_CMD_RQST", 59 | b"\x00C": "BPCD_GET_NB_VERSION_RQST", 60 | b"\x00D": "BPCD_GET_LICENSE_PLATFORM_RQST", 61 | b"\x00E": "BPCD_GET_UNAME_RQST", 62 | b"\x00F": "BPCD_SET_LOCALE_RQST", 63 | b"\x00G": "BPCD_SET_ALL_LOCALE_RQST", 64 | b"\x00H": "BPCD_MK_DIR_RQST", 65 | b"\x00I": "BPCD_GET_FILEINFO_RQST", 66 | b"\x00J": "BPCD_SNAPAPI_GET_SW_STATUS_RQST", 67 | b"\x00K": "BPCD_SNAPAPI_GET_FSLIST_RQST", 68 | b"\x00L": "BPCD_SNAPAPI_GET_VOLLIST_RQST", 69 | b"\x00M": "BPCD_SNAPAPI_GET_PARTLIST_RQST", 70 | b"\x00N": "BPCD_SNAPAPI_GET_FIMINFO_RQST", 71 | b"\x00O": "BPCD_SNAPAPI_GET_CACHESTS_RQST", 72 | b"\x00P": "BPCD_SNAPAPI_PREPARE_MIRROR_VOL", 73 | b"\x00Q": "BPCD_GET_BMR_CONFIG", 74 | b"\x00S": "BPCD_UL_RQST", 75 | b"\x00T": "BPCD_SEEK_RQST", 76 | b"\x00U": "BPCD_TEST_BPCD_RQST", 77 | b"\x00V": "BPCD_READ_LOG_FILES_DATE_RQST", 78 | b"\x00W": "BPCD_GET_EMM_INFO_RQST", 79 | b"\x00X": "BPCD_SNAPAPI_GET_DBLIST_RQST", 80 | b"\x00Y": "BPCD_FORK_W_PID_W_STAT_RQST", 81 | b"\x00Z": "BPCD_GET_TFI_INFO_RQST", 82 | b"\x00[": "BPCD_VOLUME_CLEANUP_RQST", 83 | b"\x00\\": "BPCD_FORK_OTHER_W_PID_RQST", 84 | b"\x00]": "BPCD_SPSV2_START_RECOVERY_ASST_RQST", 85 | b"\x00^": "BPCD_FORK_OUTPUT_RQST", 86 | b"\x00_": "BPCD_VMUTIL_CMD_RQST", 87 | b"\x00`": "BPCD_VSSAT_CMD_RQST", 88 | b"\x00a": "BPCD_VSSAZ_CMD_RQST", 89 | b"\x00b": "BPCD_PATCH_VERSION_RQST", 90 | b"\x00c": "BPCD_START_NBFSD_RQST", 91 | b"\x00d": "BPCD_REMOTE_FILE_COPY", 92 | b"\x00e": "BPCD_START_NBGRE_RQST", 93 | b"\x00f": "BPCD_GET_PROD_OPT_INST_LOG_RQST", 94 | b"\x00g": "BPCD_GET_PROD_OPT_CONF_RQST", 95 | b"\x00h": "BPCD_CHMOD_VLTRUN_RQST", 96 | b"\x00i": "BPCD_SLIBCLEAN_RQST", 97 | b"\x00j": "BPCD_AT_LOGINMACHINE_RQST", 98 | b"\x00k": "BPCD_AT_GETVXSSVER_RQST", 99 | b"\x00l": "BPCD_BPFIS_STATE_XFER_PREPARE_RQST", 100 | b"\x00m": "BPCD_TERMINATE_RQST", 101 | b"\x00n": "BPCD_GET_FILEINFO2_RQST", 102 | b"\x00o": "BPCD_GET_TIER_INFO_RQST", 103 | b"\x00p": "BPCD_GET_EXCLUDE_LIST_RQST", 104 | b"\x00q": "BPCD_SET_EXCLUDE_LIST_RQST", 105 | b"\x00r": "BPCD_GET_INCLUDE_LIST_RQST", 106 | b"\x00s": "BPCD_SET_INCLUDE_LIST_RQST", 107 | b"\x00t": "BPCD_SPS_VALIDATE_CREDENTIALS", 108 | b"\x00u": "BPCD_GET_BROKER_CERT_RQST", 109 | b"\x00v": "BPCD_GET_VXDBMS_CONF_RQST", 110 | b"\x00w": "BPCD_SET_VXDBMS_CONF_RQST", 111 | b"\x00x": "BPCD_GET_ALL_HOSTNAMES_RQST", 112 | b"\x00y": "BPCD_SET_CREDENTIALS_RQST", 113 | b"\x00{": "BPCD_STOP_PROXY_RQST", 114 | b"\x00\x80": "BPCD_GET_EXCH_VERSION_RQST", 115 | b"\x00\x81": "BPCD_GET_CHARSET_RQST", 116 | b"\x00\x82": "BPCD_OPEN_FOR_VM_READ_RQST", 117 | b"\x00\x83": "BPCD_UPDATE_HOST_CONFIG_W_AUDIT_RQST", 118 | b"\x00\x84": "BPCD_MANAGE_BROKER_RQST", 119 | b"\x00\x85": "BPCD_UPDATE_RESILIENCY_RQST", 120 | b"\x00\x86": "BPCD_GET_ACCL_CLIENT_BACKUP_IDS_RQST", 121 | b"\x00\x87": "BPCD_GET_ACCL_SUPPORTED_RQST", 122 | b"\x00\x89": "BPCD_START_PROXY2_RQST", 123 | b"\x00\x8a": "BPCD_GET_VALID_APP_GRT_PLATFORM_RQST", 124 | b"\x00\x8b": "BPCD_TOPOLOGY_TO_CLIENT", 125 | b"\x00\x8c": "BPCD_TOPOLOGY_TO_SERVER", 126 | b"\x00\x8d": "BPCD_GET_ACCL_APP_SUPPORTED_RQST", 127 | b"\x00\x8e": "BPCD_SPS_START_RECOVERY_ASST_RQST", 128 | b"\x00\x8f": "BPCD_GET_VM_CLIENT_ACCL_BACKUP_ID_RQST", 129 | b"\x00\x90": "BPCD_WANT_STATUSMSGS_RQST", 130 | b"\x00\x91": "BPCD_PUT_ACCL_CLIENT_BACKUP_IDS_RQST", 131 | b"\x00\x92": "BPCD_TEST_BPCD_2_RQST", 132 | b"\x00\x93": "BPCD_OPEN_TMPFILE_FOR_WRITE_RQST", 133 | b"\x00\x94": "BPCD_NBORABKUPSETS_RQST", 134 | b"\x00\x95": "BPCD_GET_REMOTE_FILE_STATUS_RQST", 135 | b"\x00\x96": "BPCD_GET_APPLIANCE_MODE_RQST", 136 | b"\x00\x97": "BPCD_GET_ACCL_NDMP_BACKUP_IDS_RQST", 137 | b"\x00\x98": "BPCD_EXPORT_IMAGE_RQST", 138 | b"\x00\x99": "BPCD_READ_LOG_FILES_DATE_USER_RQST", 139 | b"\x00\x9a": "BPCD_UNMOUNT_IMAGE_RQST", 140 | b"\x00\x9b": "BPCD_GET_RMAN_HEADER_RQST", 141 | b"\x00\x9c": "BPCD_REMOTE_FILE_READ_FROM_NB_TMP_DIR", 142 | b"\x00\x9d": "BPCD_REMOTE_FILE_WRITE_TO_NB_TMP_DIR", 143 | b"\x00\x9e": "BPCD_GET_VM_CLIENT_ACCL_BACKUP_ID_ON_MASTER_RQST", 144 | b"\x00\x9f": "BPCD_GET_CGD_TELEMETRY_INFO", 145 | b"\x00\xa0": "BPCD_GET_STDERR_SOCKET_RQST", 146 | b"\x00\xa1": "BPCD_TEST_BPCD_3_RQST", 147 | b"\x00\xa2": "BPCD_GET_STDIN_STDOUT_SOCKET_RQST", 148 | b"\x00\xa3": "BPCD_GET_STDERR_STDOUT_SOCKET_RQST", 149 | b"\x00\xa4": "BPCD_REMOVE_TEMP_FILE_RQST", 150 | b"\x00\xa5": "BPCD_SET_DOMAIN_CTX_RQST", 151 | b"\x00\xa6": "BPCD_GET_DOMAIN_CTX_RQST", 152 | b"\x00\xa7": "BPCD_CERT_MGMT", 153 | b"\x00\xa8": "BPCD_GET_SERVICE_STATUS_RQST", 154 | b"\x00\xa9": "BPCD_GET_INCLUDE_LISTS_RQST", 155 | b"\x00\xaa": "BPCD_GET_EXCLUDE_LISTS_RQST", 156 | b"\x00\xab": "BPCD_SET_INCLUDE_LISTS_RQST", 157 | b"\x00\xac": "BPCD_SET_EXCLUDE_LISTS_RQST", 158 | b"\x00\xad": "BPCD_NBAAPI_CMD_RQST", 159 | b"\x00\xae": "BPCD_DELETE_CONFIGURATION_SETTING_RQST", 160 | } 161 | 162 | 163 | class BpcdCmdField(StrFixedLenEnumField): 164 | def i2repr(self, pkt, v): 165 | r = v.rstrip(b"\0").decode() 166 | rr = repr(r) 167 | if self.enum: 168 | if v in self.enum: 169 | rr = self.enum[v] 170 | elif r in self.enum: 171 | rr = self.enum[r] 172 | return rr 173 | 174 | 175 | class TLSBpcdPacket(Packet): 176 | fields_desc = [ 177 | X3BytesField("TLSmagic", 0x5EC100), 178 | ByteField("client_state", 1), 179 | FieldLenField("tls_msg_size", None, fmt=">L"), 180 | BpcdCmdField( 181 | "data", "", length_from=lambda pkt: pkt.tls_msg_size, enum=BPCD_CMD_IDS 182 | ), 183 | ] 184 | 185 | 186 | def main(): 187 | d = TLSBpcdPacket( 188 | bytes.fromhex("5ec10001000000020005"), 189 | ) 190 | d.show2() 191 | 192 | 193 | if __name__ == "__main__": 194 | main() 195 | -------------------------------------------------------------------------------- /network-analysis/BprdClasses.py: -------------------------------------------------------------------------------- 1 | from scapy import * 2 | from scapy.packet import * 3 | from scapy.fields import * 4 | 5 | BprdSEP = " " 6 | BPRD_CMD_IDS = { 7 | "0": "REREAD_CONFIG", 8 | "1": "ALIVE", 9 | "2": "LOGSTATUS", 10 | "3": "TERMINATE", 11 | "4": "VERBOSE", 12 | "5": "RECYCLE_LOGS", 13 | "6": "MASTER", 14 | "7": "BE_MASTER", 15 | "8": "SUICIDE", 16 | "9": "RECYCLE_JOBS", 17 | "10": "WAKEUP", 18 | "11": "USER_BPBACKUP_SYNC", 19 | "12": "BPBACKUP_SYNC", 20 | "13": "USER_BPARCHIVE_SYNC", 21 | "14": "BPRESTORE_SYNC", 22 | "15": "TIR_RESTORE_SYNC", 23 | "16": "UNUSED_16", 24 | "17": "UNUSED_17", 25 | "18": "UNUSED_18", 26 | "19": "USER_BPBACKUP", 27 | "20": "USER_BPARCHIVE", 28 | "21": "START_SCHED", 29 | "22": "SUNUSED4", 30 | "23": "BPBACKUP", 31 | "24": "BPRESTORE", 32 | "25": "BPLIST", 33 | "26": "IMAGE_LIST", 34 | "27": "LIKELY_DATE", 35 | "28": "USER_BPBACKUP_1_7", 36 | "29": "USER_BPARCHIVE_1_7", 37 | "30": "BPBACKUP_1_7", 38 | "31": "BPRESTORE_1_7", 39 | "32": "BPLIST_1_7", 40 | "33": "IMAGE_LIST_1_7", 41 | "34": "LIKELY_DATE_1_7", 42 | "35": "BPLIST_2_0", 43 | "36": "TIR_LIST", 44 | "37": "TIR_RESTORE", 45 | "38": "BPLIST_2_1", 46 | "39": "BPRESTORE_2_1", 47 | "40": "TIR_LIST_2_1", 48 | "41": "TIR_RESTORE_2_1", 49 | "42": "AUNUSED2", 50 | "43": "REMOTE_HOST_VERSION", 51 | "44": "VERSION", 52 | "45": "CLIENT_ID", 53 | "46": "DEL_IMAGE_BY_FILE", 54 | "47": "MEDIA_LIST_BY_FILE", 55 | "48": "SYNC", 56 | "49": "USER_BPBACKUP_SYNC_3_0", 57 | "50": "USER_BPARCHIVE_SYNC_3_0", 58 | "51": "BPBACKUP_SYNC_3_0", 59 | "52": "BPRESTORE_SYNC_3_0", 60 | "53": "USER_BPBACKUP_3_0", 61 | "54": "USER_BPARCHIVE_3_0", 62 | "55": "BPBACKUP_3_0", 63 | "56": "BPRESTORE_3_0", 64 | "57": "BPLIST_3_0", 65 | "58": "IMAGE_LIST_3_0", 66 | "59": "LIKELY_DATE_3_0", 67 | "60": "SET_DYNAMIC_CLIENT", 68 | "61": "BPRESTORE_SYNC_3_1", 69 | "62": "BPRESTORE_3_1", 70 | "63": "BPBACKUP_DB_3_2", 71 | "64": "BPBACKUP_DB_SYNC_3_2", 72 | "65": "BPRESTORE_SYNC_3_2", 73 | "66": "BPRESTORE_3_2", 74 | "67": "MEDIA_LIST_BY_FILE_3_2", 75 | "68": "GET_KEY_FILE", 76 | "69": "AUTHORIZE_CHECK", 77 | "70": "DONOTUSE", 78 | "71": "REMOTE_WRITE", 79 | "72": "GET_FEATURES", 80 | "73": "READ_HOST_CONFIG", 81 | "74": "READ_USER_CONFIG", 82 | "75": "UPDATE_HOST_CONFIG", 83 | "76": "UPDATE_USER_CONFIG", 84 | "77": "CLLIST", 85 | "78": "OBSOLETE", 86 | "79": "BPRESTORE_SYNC_4_5", 87 | "80": "BPRESTORE_4_5", 88 | "81": "IMAGE_LIST_4_5", 89 | "82": "BPLIST_4_5", 90 | "83": "DBTMPL_LIST", 91 | "84": "DBTMPL_GET", 92 | "85": "DBTMPL_PUT", 93 | "86": "DBTMPL_DELETE", 94 | "87": "DBTMPL_RENAME", 95 | "88": "CLNTLIST", 96 | "89": "DBTMPL_LIST_4_5_1", 97 | "90": "RESTART_RESTORE", 98 | "91": "VAULT_PROFILES", 99 | "92": "REMOTE_BROWSE", 100 | "93": "GET_POLICY_INFO", 101 | "94": "FORK_CMD", 102 | "95": "GET_USR_POLICY", 103 | "96": "CHECK_VXSS_AUTH", 104 | "97": "READ_TEXT_FILE", 105 | "98": "SUSPEND_OR_CANCEL_ALL", 106 | "99": "REMOTE_BPKEYUTIL", 107 | "100": "SNAPAPI_SW_STATUS", 108 | "101": "SNAPAPI_FSLIST", 109 | "102": "SNAPAPI_VOLLIST", 110 | "103": "SNAPAPI_PARTLIST", 111 | "104": "SNAPAPI_GETFIMINFO", 112 | "105": "SNAPAPI_VALCACHE", 113 | "106": "SNAPAPI_GET_DBTMPL", 114 | "107": "SNAPAPI_GET_VFMCONF", 115 | "108": "SNAPAPI_PREPAREMIRROR_VOL", 116 | "109": "SNAPAPI_SELECTMETHOD", 117 | "110": "VAULT_REPORTS", 118 | "111": "BPBACKUP_DB_6_0", 119 | "112": "PFI_ROTATION", 120 | "113": "GET_PREVIEW_MEDIA_IDS", 121 | "114": "GET_HOSTINFO", 122 | "115": "REREAD_AZ_HANDLE_CACHE", 123 | "116": "GET_NBAC_PDR_INFO", 124 | "117": "BPRESTORE_SYNC_6_5", 125 | "118": "BPRESTORE_6_5", 126 | "119": "GET_EMM_SERVER_NAME", 127 | "120": "UNUSED_120", 128 | "121": "SPSRCVASST", 129 | "122": "GET_POLICY_INFO_EX", 130 | "123": "BPRESTORE_SYNC_6_5_2", 131 | "124": "BPRESTORE_6_5_2", 132 | "125": "BPRESTORE_SYNC_6_5_2_2", 133 | "126": "BPRESTORE_6_5_2_2", 134 | "127": "BPLIST_6_5_2", 135 | "128": "BPFIS_STATE_XFER_TO_CLIENT", 136 | "129": "BPFIS_STATE_XFER_TO_SERVER", 137 | "130": "SUSPEND_OR_CANCEL_SELECTIVE", 138 | "131": "SPS_VALIDATE_CREDENTIALS", 139 | "132": "DARS_SEND_XML", 140 | "133": "SET_CLIENT_NBAC_CREDENTIALS", 141 | "134": "BPRESTORE_DAG", 142 | "135": "BPRESTORE_SYNC_DAG", 143 | "136": "BPRD_VM_PROXY_QUERY", 144 | "137": "BPRESTORE_SYNC_7_0", 145 | "138": "BPRESTORE_7_0", 146 | "139": "DARS_RUN_RESTORE", 147 | "140": "GET_VTS_CLIENT_LIST", 148 | "141": "DEL_FILE_IN_IMAGE", 149 | "142": "RESTORE_SFR_7_0_1", 150 | "143": "SET_CLIENT_NUGGET_UNUSED", 151 | "144": "BPFIS_STATE_DELETE_FROM_SERVER", 152 | "145": "CREATE_PARENT_JOB", 153 | "146": "SET_PARENT_JOB_STATUS", 154 | "147": "BPRD_GET_LOG_DAYS", 155 | "148": "LOG_JOB_INFO", 156 | "149": "SET_CLIENT_NBAC_CREDENTIALS_2", 157 | "150": "MANAGE_REMOTE_BROKER", 158 | "151": "GET_JOB_EXISTS", 159 | "152": "BPFIS_GET_SNAP_REPLICA_INFO", 160 | "153": "BPFIS_UPDATE_CATALOG", 161 | "154": "BPFIS_CREATE_BASE_STATE_FILES", 162 | "155": "CREATE_JOB", 163 | "156": "BPBACKUP_SYNC_7_5", 164 | "157": "USER_BPBACKUP_SYNC_7_5", 165 | "158": "USER_BPARCHIVE_SYNC_7_5", 166 | "159": "BPBACKUP_7_5", 167 | "160": "USER_BPBACKUP_7_5", 168 | "161": "USER_BPARCHIVE_7_5", 169 | "162": "GET_PREVIEW_MEDIA_IDS_7_5", 170 | "163": "BPLIST_7_5", 171 | "164": "IMAGE_LIST_7_5", 172 | "165": "CLEANUP_MPX_MSGQ", 173 | "166": "CREATE_JOB_7_6", 174 | "167": "INSTANCE_ADMIN", 175 | "168": "BPRD_VM_INSTANT_RECOVERY", 176 | "169": "BPRESTORE_7_6", 177 | "170": "BPRESTORE_SYNC_7_6", 178 | "171": "UPDATE_HOST_FIPS_CONFIG", 179 | "172": "SPSRCVASST_76", 180 | "173": "BPBACKUP_SYNC_7_6", 181 | "174": "USER_BPBACKUP_SYNC_7_6", 182 | "175": "USER_BPARCHIVE_SYNC_7_6", 183 | "176": "BPBACKUP_7_6", 184 | "177": "USER_BPBACKUP_7_6", 185 | "178": "USER_BPARCHIVE_7_6", 186 | "179": "BPFIS_STATE_TOPOLOGY_TO_CLIENT", 187 | "180": "BPFIS_STATE_TOPOLOGY_TO_SERVER", 188 | "181": "VM_SEARCH", 189 | "182": "BPLIST_7_6", 190 | "183": "IMAGE_LIST_7_6", 191 | "184": "EXCHANGE_VALIDATE_CREDENTIALS", 192 | "185": "READ_TEXT_FILE_7_6", 193 | "186": "TRUSTED_MASTER", 194 | "187": "TRUSTED_MASTER_LOCAL", 195 | "188": "SET_CLIENT_SECURITY_CREDENTIALS", 196 | "189": "ADD_MEDIA_DESCR_TO_IMPORT_ENTRY", 197 | "190": "GET_MEDIAID_SERVERS", 198 | "191": "BPBACKUP_DB_7_6", 199 | "192": "GET_PREVIEW_MEDIA_IDS_7_5_0_7", 200 | "193": "BPLIST_7_6_1", 201 | "194": "GET_TL_LATEST_APP_DATA", 202 | "195": "GET_TL_BACKUP_INFO", 203 | "196": "XFER_TL_TO_SERVER", 204 | "197": "XFER_TL_TO_CLIENT", 205 | "198": "XFER_TL_MSG_TO_SERVER", 206 | "199": "XBSA_PCB_XFER_TO_CLIENT", 207 | "200": "XBSA_PCB_XFER_TO_SERVER", 208 | "201": "USER_BPBACKUP_7_7", 209 | "202": "BPBACKUP_DB_7_7", 210 | "203": "NBORABKUPSETS", 211 | "204": "GET_REMOTE_FILE_STATUS", 212 | "205": "IS_SERVER_AN_APPLIANCE", 213 | "206": "BPRESTORE_7_7", 214 | "207": "BPLIST_7_7", 215 | "208": "BPBACKUP_SYNC_7_7", 216 | "209": "USER_BPBACKUP_SYNC_7_7", 217 | "210": "USER_BPARCHIVE_SYNC_7_7", 218 | "211": "BPBACKUP_7_7", 219 | "212": "USER_BPARCHIVE_7_7", 220 | "213": "GET_ORACLE_RMAN_HEADER_SIZE", 221 | "214": "MEDIA_LIST_BY_FILE_7_7", 222 | "215": "RELEASE_LEVEL", 223 | "216": "BPRESTORE_SYNC_7_7_1", 224 | "217": "BPRESTORE_7_7_1", 225 | "218": "SET_CLIENT_ENHANCE_AUDITING", 226 | "219": "READ_GUI_CONF", 227 | "220": "IS_USER_ACCOUNT_LOCKED", 228 | "221": "AUTO_UNLOCK_USER", 229 | "222": "UPDATE_USER_LOGIN_DETAILS", 230 | "223": "UNLOCK_USER", 231 | "224": "GET_JOB_INFO", 232 | "225": "BPRESTORE_SYNC_7_7_2", 233 | "226": "BPRESTORE_7_7_2", 234 | "227": "GET_LIST_LOCKED_USERS", 235 | "228": "BPRESTORE_SYNC_7_7_3", 236 | "229": "BPRESTORE_7_7_3", 237 | "230": "GET_RESTORE_INFO", 238 | "231": "XFER_TRACKLOG", 239 | "232": "AUDIT_USER_LOGIN_DETAILS", 240 | "233": "ORA_INSTANT_RECOVERY", 241 | "234": "VALIDATE_RESTORE_SPEC", 242 | "235": "BPBACKUP_JSON_8_1", 243 | "236": "XFER_ACC_LICENSE_INFO", 244 | "237": "GET_MASTER_TIMEZONE_OFFSET", 245 | "238": "USER_BPBACKUP_8_1", 246 | "239": "BPRESTORE_8_1_2", 247 | "240": "USER_BPBACKUP_8_1_2_1", 248 | "241": "CLIENT_TEST_REVERSE_CONNECT", 249 | "242": "BPRD_NBCS_REQ", 250 | "243": "BPBACKUP_8_2", 251 | "244": "BPIMPORT_8_2", 252 | "245": "RECOVEREC2_8_2", 253 | "246": "BPRESTORE_8_2", 254 | "247": "LOG_PROGRESS_INFO", 255 | "248": "USE_CONTROL_JOB_8_2", 256 | "249": "DARS_SEND_SQL_TOPOLOGY_JSON", 257 | "250": "GET_PROCESS_STATUS", 258 | "251": "GET_SERVICE_STATUS", 259 | "252": "GET_INCLUDE_LISTS", 260 | "253": "GET_EXCLUDE_LISTS", 261 | "254": "SET_EXCLUDE_LISTS", 262 | "255": "SET_INCLUDE_LISTS", 263 | "256": "BPBACKUP_SYNTHDUP", 264 | "257": "GET_APP_SERVER_DETAILS", 265 | "258": "READ_PROGRESS_INFO", 266 | "259": "SET_JOB_VALUES", 267 | "260": "LOG_JOB_ERROR", 268 | "261": "LIKELY_DATE_8_2", 269 | "262": "IMAGE_LIST_8_2", 270 | "263": "GET_JOB_INFO_JSON", 271 | "264": "COMPOUND_RESTORE", 272 | "265": "CREATE_JOB_COMPOUND_RESTORE", 273 | "266": "USER_PASSWORD_EXPIRATION_INFO", 274 | "267": "BPRESTORE_BIGDATA_APPPROXY_ASYNC", 275 | "268": "IMAGE_LIST_BIGDATA_APPROXY_QUERY", 276 | "269": "DELETE_CONFIGURATION_SETTING", 277 | } 278 | 279 | 280 | class BprdParamPacket(Packet): 281 | fields_desc = [ 282 | StrStopField("value", default=None, stop=b" "), 283 | StrFixedLenField("sep", default=BprdSEP, length=1), 284 | ] 285 | 286 | def extract_padding(self, s): 287 | return b"", s 288 | 289 | def i2repr(self, pkt, v): 290 | return v.value.rstrip(b"\0").decode() 291 | 292 | 293 | class BprdParamsPacketList(PacketListField): 294 | def i2repr(self, pkt, v): 295 | params_repr = [f'"{param.i2repr(pkt, param)}"' for param in v] 296 | return f"[{' '.join(params_repr)}]" 297 | 298 | 299 | class StrStopField(StrField): 300 | __slots__ = ["stop", "enum"] 301 | 302 | def __init__(self, name, default, stop, enum={}): 303 | Field.__init__(self, name, default) 304 | self.stop = stop 305 | self.enum = enum 306 | 307 | def getfield(self, pkt, s): 308 | len_str = s.find(self.stop) 309 | if len_str < 0: 310 | return b"", s 311 | return s[len_str:], s[:len_str] 312 | 313 | def i2repr(self, pkt, v): 314 | r = v.rstrip(b"\0").decode() 315 | rr = repr(r) 316 | if self.enum: 317 | if v in self.enum: 318 | rr = self.enum[v] 319 | elif r in self.enum: 320 | rr = self.enum[r] 321 | return rr 322 | 323 | 324 | class BprdPacket(Packet): 325 | fields_desc = [ 326 | FieldLenField("msg_size", None, fmt=">L"), 327 | StrStopField("magic", "329199", stop=b" "), 328 | StrFixedLenField("sep1", default=BprdSEP, length=1), 329 | StrStopField("cmd", "43", stop=b" ", enum=BPRD_CMD_IDS), 330 | StrFixedLenField("sep2", default=BprdSEP, length=1), 331 | BprdParamsPacketList("param", "nbu-primary-a", BprdParamPacket), 332 | ] 333 | 334 | 335 | class TLSBprdPacket(Packet): 336 | fields_desc = [ 337 | X3BytesField("TLSmagic", 0x5EC100), 338 | ByteField("client_state", 1), 339 | FieldLenField("tls_msg_size", None, fmt=">L"), 340 | FieldLenField("msg_size", None, fmt=">L"), 341 | StrStopField("magic", "329199", stop=b" "), 342 | StrFixedLenField("sep1", default=BprdSEP, length=1), 343 | StrStopField("cmd", "43", stop=b" ", enum=BPRD_CMD_IDS), 344 | StrFixedLenField("sep2", default=BprdSEP, length=1), 345 | BprdParamsPacketList("param", "nbu-primary-a", BprdParamPacket), 346 | ] 347 | 348 | 349 | def main(): 350 | c = BprdPacket(bytes.fromhex("00000015333239313939203433206e62752d7072696d617279")) 351 | c.show2() 352 | 353 | 354 | if __name__ == "__main__": 355 | main() 356 | -------------------------------------------------------------------------------- /network-analysis/NbatdClasses.py: -------------------------------------------------------------------------------- 1 | from scapy import * 2 | from scapy.packet import * 3 | from scapy.fields import * 4 | 5 | NBATD_MSG_TYPES = { 6 | 0x01: "PK_AUTH_OK", 7 | 0x02: "PING_2", 8 | 0x04: "PING_4", 9 | 0x08: "PING_8", 10 | 0x0D: "PING_D", 11 | 0x03: "PK_AUTH_QUERY", 12 | 0x05: "PK_AUTH_DATA", 13 | 0x06: "PK_AUTH_TYPE", 14 | 0x07: "PK_AUTH_PUBLIC_KEY", 15 | 0x0C: "CERTIFICATE_REQUEST", 16 | 0x0E: "PK_AUTH_DATA_PLUGIN_SPECIFIC", 17 | 0x0F: "PK_RENEWAL_DATA", 18 | 0x10: "PK_WEBCREDENTIAL_DATA", 19 | 0x32: "PK_AUTH_CSR", 20 | 0x11: "PK_VALIDATE_PRPLEX", 21 | 0x3E: "VALIDATEPRPL", 22 | 0x13: "PK_VALIDATE_GROUP", 23 | 0x15: "PK_QUIESCE", 24 | 0x16: "PK_GETSET_ADLEVEL", 25 | 0x17: "PK_GETSET_ADLEVEL2", 26 | 0x19: "PK_CLOCKSKEW", 27 | 0x1A: "PK_UUID", 28 | 0x1E: "PK_REGISTER", 29 | 0x28: "PK_PULL_BROKER_ATTRS_REQUEST", 30 | 0x29: "PK_PUSH_BROKER_ATTRS_REQUEST", 31 | 0x2A: "PK_TRUST_REFRESH_REQUEST", 32 | 0x2B: "PK_PULL_BROKER_DOMAIN_INFO", 33 | 0x2C: "PK_CREATE_PRPL", 34 | 0x33: "PK_SET_CERTIFICATE_SAN", 35 | 0x34: "PK_SIGN_CSR_DATA", 36 | 0x35: "PK_AUTH_START_CRL", 37 | 0x36: "PK_CRL_REVOKED", 38 | 0x37: "PK_SIGN_CRL_DATA", 39 | 0x38: "PK_ADD_CERTIFICATE_DISTPOINT", 40 | 0x39: "PK_ADD_LDAP_DOMAIN", 41 | 0x3A: "PK_MODIFY_LDAP_DOMAIN", 42 | 0x3B: "PK_DELETE_LDAP_DOMAIN", 43 | 0x3C: "PK_LIST_LDAP_DOMAIN", 44 | 0x3D: "PK_TEST_LDAP_DOMAIN", 45 | } 46 | 47 | 48 | class NbatdCommand(Packet): 49 | fields_desc = [ 50 | IntField("proto_version", 1), 51 | XIntField("magic", 0xBAADF00D), 52 | ByteEnumField("msg_type", default=1, enum=NBATD_MSG_TYPES), 53 | FieldLenField("msg_size", None, length_of="data", fmt=">L"), 54 | ByteEnumField("unicode_flag", default=1, enum={0: "False", 1: "True"}), 55 | StrLenField("data", "", length_from=lambda pkt: pkt.msg_size), 56 | ] 57 | 58 | 59 | def main(): 60 | p = NbatdCommand() 61 | p.msg_type = 0x05 62 | p.data = '' 63 | p.show2() 64 | 65 | 66 | if __name__ == "__main__": 67 | main() 68 | -------------------------------------------------------------------------------- /network-analysis/PbxRegisterClasses.py: -------------------------------------------------------------------------------- 1 | from scapy import * 2 | from scapy.packet import * 3 | from scapy.fields import * 4 | 5 | 6 | PBX_MSG_TYPES = { 7 | 0x0101: "INIT", 8 | 0x0102: "CON", 9 | 0x0106: "RDV", 10 | 0x0300: "ACK", 11 | } 12 | 13 | 14 | class PbxRegister(Packet): 15 | fields_desc = [ 16 | ByteField("proto_version", 4), 17 | ShortEnumField( 18 | "msg_type", 19 | 0x0101, 20 | PBX_MSG_TYPES, 21 | ), 22 | ByteField("client_state", 4), 23 | FieldLenField("msg_size", None, length_of="data", fmt=">L"), 24 | ByteField("error_code", 0), 25 | XStrFixedLenField("rand", default=0, length=7), 26 | StrLenField("data", "", length_from=lambda pkt: pkt.msg_size), 27 | ] 28 | 29 | 30 | def main(): 31 | b = PbxRegister( 32 | bytes.fromhex( 33 | "0401010400000085000000000000000073737469630000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000007573657200" 34 | ) 35 | ) 36 | b.show2() 37 | 38 | 39 | if __name__ == "__main__": 40 | main() 41 | -------------------------------------------------------------------------------- /network-analysis/pynet/NBUEndpoints.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import ssl 3 | import struct 4 | 5 | from threading import Condition,Lock 6 | 7 | from pynet.endpoint import * 8 | from pynet.endpoints.socket import * 9 | 10 | def get_extension(data): 11 | x = data.split(b"extension=") 12 | return x[1].strip() 13 | 14 | 15 | class NBUService(object): 16 | def __init__(self,sock,certificate,key): 17 | self.sock = sock 18 | self.certificate = certificate 19 | self.key = key 20 | self.ssl_context = ssl.create_default_context() 21 | self.ssl_context.check_hostname = False 22 | self.ssl_context.verify_mode = ssl.CERT_NONE 23 | self.ssl_context.set_ciphers('DEFAULT@SECLEVEL=1') 24 | self.ssl_context.load_cert_chain(self.certificate,self.key) 25 | self.lock = Lock() 26 | self.cond = Condition() 27 | self.wait_for_tls = False 28 | 29 | # Start TLS 30 | self.tls = False 31 | self.next_pkt_to_receive = 0 32 | self.next_pkt_to_send = 0 33 | 34 | # json 35 | self.json_received = False 36 | self.json_sent = False 37 | 38 | def raw_send(self,data): 39 | self.sock.send(data) 40 | self.next_pkt_to_send += 1 41 | 42 | def raw_recv(self): 43 | #print("%r receving %r" % (self,self.next_pkt_to_receive)) 44 | data = self.sock.recv(4096) 45 | self.next_pkt_to_receive += 1 46 | return data 47 | 48 | def send(self,data): 49 | #print("%r pkt : len:%r num:%r" % (self,len(data),self.next_pkt_to_send)) 50 | if self.next_pkt_to_send == self.JSON_PKT_SENT: 51 | self.json_sent = True 52 | self.raw_send(data) 53 | #print("%r json sent %r" % (self,len(data))) 54 | if self.json_received: 55 | with self.lock: 56 | if not self.tls: 57 | self.start_tls(False) 58 | else: 59 | self.raw_send(data) 60 | 61 | def recv(self): 62 | if self.next_pkt_to_receive == self.JSON_PKT_RECEIVED: 63 | data = self.receive_json() 64 | self.json_received = True 65 | self.wait_for_tls = True 66 | #print("%r json received" % (self,)) 67 | else: 68 | self.lock.acquire() 69 | if not self.tls and self.wait_for_tls: 70 | with self.cond: 71 | self.lock.release() 72 | #print("%r block on cond" % self) 73 | self.cond.wait() 74 | #print("%r unblock on cond" % self) 75 | self.wait_for_tls = False 76 | else: 77 | self.lock.release() 78 | data = self.raw_recv() 79 | if self.json_received and self.json_sent: 80 | with self.lock: 81 | if not self.tls: 82 | self.start_tls(True) 83 | return data 84 | 85 | def receive_json(self): 86 | pkt = self.raw_recv() 87 | sz = struct.unpack(">H",pkt[6:8])[0] + 8 88 | while sz != len(pkt): 89 | pkt += self.raw_recv() 90 | return pkt 91 | 92 | def start_tls(self,b): 93 | self.tls = True 94 | print("%r STARTING TLS %r" % (self,b)) 95 | self.ssl_context.verify_mode = ssl.CERT_NONE 96 | self.sock = self.ssl_context.wrap_socket(self.sock,server_side=self.TLS_SERVER_SIDE) 97 | with self.cond: 98 | self.cond.notify() 99 | print("%r STARTED TLS" % (self,)) 100 | 101 | def close(self): 102 | self.sock.close() 103 | 104 | 105 | class ServiceNBUListen(NBUService): 106 | TLS_SERVER_SIDE = True 107 | 108 | 109 | class ServiceNBU(NBUService): 110 | TLS_SERVER_SIDE = False 111 | 112 | 113 | class BPCD(ServiceNBU): 114 | JSON_PKT_SENT = 0 115 | JSON_PKT_RECEIVED = 0 116 | 117 | class BPCDListen(ServiceNBUListen): 118 | JSON_PKT_SENT = 0 119 | JSON_PKT_RECEIVED = 0 120 | 121 | class VNETD(ServiceNBU): 122 | JSON_PKT_SENT = 2 123 | JSON_PKT_RECEIVED = 1 124 | 125 | class VNETDListen(ServiceNBUListen): 126 | JSON_PKT_SENT = 1 127 | JSON_PKT_RECEIVED = 2 128 | 129 | 130 | @Endpoint.register 131 | class PBX(TCP): 132 | SWITCH = {b"bpcd":BPCD,b"vnetd":VNETD} 133 | 134 | @classmethod 135 | def set_cli_arguments(cls,parser): 136 | TCP.set_cli_arguments(parser) 137 | tls = parser.add_argument_group('TLS',"TLS specific options") 138 | tls.add_argument("-c","--certificate",metavar="PATH",help="Certificate file for client authentication") 139 | tls.add_argument("-k","--key",metavar="PATH",help="Key file for client authentication") 140 | 141 | def __init__(self,certificate,key,*args,**kargs): 142 | super().__init__(*args,**kargs) 143 | self.certificate = certificate 144 | self.key = key 145 | self.ext = None 146 | self.next_pkt_sent = 0 147 | self.next_pkt_received = 0 148 | 149 | def handle_send(self,data): 150 | if self.next_pkt_sent == 0: 151 | ext = get_extension(data) 152 | if ext in PBX.SWITCH: 153 | self.ext = PBX.SWITCH[ext](self.sock,self.certificate,self.key) 154 | self.next_pkt_sent += 1 155 | 156 | def send(self,data): 157 | try: 158 | if self.ext: 159 | self.ext.send(data) 160 | else: 161 | super().send(data) 162 | self.handle_send(data) 163 | except (ssl.SSLEOFError,ssl.SSLZeroReturnError): 164 | raise EndpointClose() 165 | 166 | def recv(self): 167 | try: 168 | if self.next_pkt_received > 0 and self.ext: 169 | self.next_pkt_received += 1 170 | return self.ext.recv() 171 | else: 172 | self.next_pkt_received += 1 173 | return super().recv() 174 | except (ssl.SSLEOFError,ssl.SSLZeroReturnError): 175 | raise EndpointClose() 176 | 177 | def close(self): 178 | print("closing") 179 | if self.ext: 180 | self.ext.close() 181 | else: 182 | super().close() 183 | 184 | 185 | @Endpoint.register 186 | class PBXListen(TCP_LISTEN): 187 | SWITCH = {b"bpcd":BPCDListen,b"vnetd":VNETDListen} 188 | 189 | @classmethod 190 | def set_cli_arguments(cls,parser): 191 | TCP_LISTEN.set_cli_arguments(parser) 192 | tls = parser.add_argument_group('TLS',"TLS specific options") 193 | tls.add_argument("-c","--certificate",metavar="PATH",help="Certificate server") 194 | tls.add_argument("-k","--key",metavar="PATH",help="Key") 195 | 196 | def __init__(self,certificate,key,*args,**kargs): 197 | super().__init__(*args,**kargs) 198 | self.certificate = certificate 199 | self.key = key 200 | self.ext = None 201 | self.next_pkt_recv = 0 202 | self.next_pkt_sent = 0 203 | 204 | def get_conf(self): 205 | return {"certificate":self.certificate, "key":self.key} 206 | 207 | def handle_recv(self,data): 208 | if self.next_pkt_recv == 0: 209 | ext = get_extension(data) 210 | if ext in PBXListen.SWITCH: 211 | print("RECEIVED switching to %r" % (ext,)) 212 | self.ext = PBXListen.SWITCH[ext](self.sock,self.certificate,self.key) 213 | self.next_pkt_recv += 1 214 | 215 | def recv(self): 216 | try: 217 | if self.ext: 218 | data = self.ext.recv() 219 | else: 220 | data = super().recv() 221 | self.handle_recv(data) 222 | except (ssl.SSLEOFError,ssl.SSLZeroReturnError): 223 | raise EndpointClose() 224 | return data 225 | 226 | def send(self,data): 227 | try: 228 | if self.next_pkt_sent > 0 and self.ext: 229 | self.ext.send(data) 230 | else: 231 | data = super().send(data) 232 | self.next_pkt_sent += 1 233 | except (ssl.SSLEOFError,ssl.SSLZeroReturnError): 234 | raise EndpointClose() 235 | 236 | def close(self): 237 | if self.ext: 238 | self.ext.close() 239 | else: 240 | super().close() 241 | 242 | 243 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pycryptodome 2 | jpype1 3 | JayDeBeApi 4 | scapy 5 | lxml 6 | requests 7 | aiohttp 8 | aiodns 9 | beautifulsoup4 10 | graphviz 11 | tabulate 12 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | with open("README.md", "r") as f: 4 | long_description = f.read() 5 | 6 | with open("requirements.txt", "r") as f: 7 | requirements = [l for l in f.readlines() if l] 8 | 9 | setup( 10 | name="nbutools", 11 | version="0.1", 12 | description="Utils for auditing a NetBackup infrastructure written in python", 13 | long_description=long_description, 14 | author="AirbusSeclab", 15 | packages=["utils", "GetConfigInfos", "PreAuthCartography", "ListUsersInfoFromDB"], 16 | scripts=["bin/nbuscan.py", "bin/nbumap.py", "bin/nbudbdump.py"], 17 | install_requires=requirements, 18 | ) 19 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/airbus-seclab/nbutools/d82fb96d5623e7d3076cc0a1db06a640f63b9552/utils/__init__.py -------------------------------------------------------------------------------- /utils/network.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | 4 | 5 | class NBUSocket: 6 | """ 7 | Abstract base class for all NetBackup sockets 8 | """ 9 | 10 | def __init__(self, host, port, timeout=5, quiet=False): 11 | self.host = host 12 | self.port = port 13 | self.timeout = timeout 14 | self.quiet = quiet 15 | self._reader = self._writer = None 16 | 17 | @property 18 | def reader(self): 19 | if not self._reader: 20 | raise RuntimeError("Socket is not connected") 21 | return self._reader 22 | 23 | @property 24 | def writer(self): 25 | if not self._writer: 26 | raise RuntimeError("Socket is not connected") 27 | return self._writer 28 | 29 | async def sendall(self, data): 30 | if not self.quiet: 31 | logging.debug("Sending %s to %s:%d", data, self.host, self.port) 32 | 33 | async with asyncio.timeout(self.timeout): 34 | self.writer.write(data) 35 | await self.writer.drain() 36 | 37 | async def send_pkt(self, payload): 38 | await self.sendall(len(payload).to_bytes(4, "big") + payload) 39 | 40 | async def recv(self, length=4096): 41 | async with asyncio.timeout(self.timeout): 42 | data = await self.reader.read(length) 43 | 44 | if not self.quiet: 45 | logging.debug("Read %s from %s:%d", data, self.host, self.port) 46 | return data 47 | 48 | async def recv_pkt(self): 49 | size = await self.recv(4) 50 | size = int.from_bytes(size, "big") 51 | return await self.recv(size) 52 | 53 | async def connect(self): 54 | try: 55 | async with asyncio.timeout(self.timeout): 56 | self._reader, self._writer = await asyncio.open_connection( 57 | self.host, self.port 58 | ) 59 | except TimeoutError: 60 | e = TimeoutError(f"[Errno 60] Operation timed out") 61 | logging.info("Failed to connect to %s:%d: %s", self.host, self.port, e) 62 | raise e 63 | except Exception as e: 64 | logging.info("Failed to connect to %s:%d: %s", self.host, self.port, e) 65 | raise e 66 | 67 | async def handshake(self): 68 | pass 69 | 70 | async def close(self): 71 | self.writer.close() 72 | await self.writer.wait_closed() 73 | self._reader = self._writer = None 74 | 75 | 76 | class PBXSocket(NBUSocket): 77 | """ 78 | Helper class to communicate with the pbx_exchange process 79 | """ 80 | 81 | def __init__(self, host, port=1556, **kwargs): 82 | super().__init__(host, port, **kwargs) 83 | 84 | async def handshake(self, extension): 85 | msg = "ack=1\nextension={ext}\n\n".format(ext=extension) 86 | logging.debug( 87 | "[PBXSocket] Performing handshake with %s:%d for extension %s", 88 | self.host, 89 | self.port, 90 | extension, 91 | ) 92 | await self.sendall(msg.encode()) 93 | if not await self.recv(): 94 | raise RuntimeError( 95 | f"{extension} not available through pbx_exchange for {self.host}:{self.port}" 96 | ) 97 | 98 | 99 | class VNETDSocket(NBUSocket): 100 | """ 101 | Helper class to communicate with the vnetd process 102 | """ 103 | 104 | def __init__(self, host, use_pbx=True, **kwargs): 105 | port = 1556 if use_pbx else 13724 106 | super().__init__(host, port, **kwargs) 107 | self.use_pbx = use_pbx 108 | 109 | @property 110 | def writer(self): 111 | return self.sock.writer 112 | 113 | @property 114 | def reader(self): 115 | return self.sock.reader 116 | 117 | async def connect(self): 118 | # No need to catch errors for logging purposes as it is already handled 119 | # by the underlying sockets 120 | if self.use_pbx: 121 | logging.debug( 122 | "[VNETDSocket] Attempting to connect to vnetd through pbx_exchange on %s:%d", 123 | self.host, 124 | self.port, 125 | ) 126 | self.sock = PBXSocket(self.host, timeout=self.timeout, quiet=True) 127 | await self.sock.connect() 128 | await self.sock.handshake("vnetd") 129 | else: 130 | logging.debug( 131 | "[VNETDSocket] Attempting to connect directly to vnetd on %s:%d", 132 | self.host, 133 | self.port, 134 | ) 135 | self.sock = NBUSocket( 136 | self.host, self.port, timeout=self.timeout, quiet=True 137 | ) 138 | await self.sock.connect() 139 | await self.sock.handshake() 140 | 141 | async def handshake(self): 142 | # Perform vnetd version handshake 143 | await self.sendall(b"4\x00") 144 | ver = await self.recv() 145 | await self.sendall(ver) 146 | 147 | async def read_value(self) -> bytes: 148 | data = await self.sock.recv(1) 149 | while data and data[-1] != 0: 150 | data += await self.sock.recv(1) 151 | logging.debug("Read %s from %s:%d", data, self.host, self.port) 152 | return data 153 | 154 | async def read_string(self) -> str: 155 | val = await self.read_value() 156 | return val.rstrip(b"\x00").decode() 157 | 158 | async def read_int(self) -> int: 159 | try: 160 | return int(await self.read_string()) 161 | except ValueError: 162 | return None 163 | 164 | async def get_version(self) -> str: 165 | logging.debug( 166 | "[VNETDSocket] Sending VN_VERSION_GET to vnetd on %s:%d", 167 | self.host, 168 | self.port, 169 | ) 170 | await self.sendall(b"8\x00") 171 | version = await self.read_string() 172 | ret_code = await self.read_value() 173 | return version 174 | 175 | async def get_security_info(self) -> dict: 176 | logging.debug( 177 | "[VNETDSocket] Sending VN_REQUEST_GET_SECURITY_INFO to vnetd on %s:%d", 178 | self.host, 179 | self.port, 180 | ) 181 | await self.sendall(b"9\x00") 182 | ret_code = await self.read_value() 183 | 184 | out = {} 185 | if ret_code != b"0\x00": 186 | logging.debug( 187 | "[VNETDSocket] Got unexpected response from vnetd %s:%d: %s", 188 | self.host, 189 | self.port, 190 | ret_code, 191 | ) 192 | return out 193 | 194 | # Send this first so we get the "raw" ports from bp.conf instead of the 195 | # "resolved" one (most of the time the raw ones are "0" for "default") 196 | # We do this so the result is as close to the original bp.conf as 197 | # possible 198 | await self.sendall(b"ni_ess_available\x00") 199 | 200 | # See https://www.veritas.com/content/support/en_US/doc/18716246-126559472-0/v40574999-126559472 201 | await self.sendall(b"ni_use_vxss\x00") 202 | out["USE_VXSS"] = await self.read_int() 203 | 204 | # See https://www.veritas.com/content/support/en_US/doc/18716246-126559472-0/v101111836-126559472 205 | await self.sendall(b"ni_use_at\x00") 206 | out["USE_AUTHENTICATION"] = await self.read_int() 207 | 208 | # See https://www.veritas.com/content/support/en_US/doc/18716246-126559472-0/v40554370-126559472 209 | await self.sendall(b"ni_authorization_service\x00") 210 | out["AUTHORIZATION_SERVICE"] = { 211 | "host": await self.read_string(), 212 | "port": await self.read_int(), 213 | } 214 | 215 | # See https://www.veritas.com/content/support/en_US/doc/18716246-126559472-0/v40491531-126559472 216 | await self.sendall(b"ni_authentication_domains\x00") 217 | _ = await self.recv(1) 218 | domains = [] 219 | while True: 220 | broker = await self.read_value() 221 | if broker == b"\x00": 222 | break 223 | domains.append( 224 | { 225 | "broker": broker.rstrip(b"\x00").decode(), 226 | "port": await self.read_int(), 227 | "domain": await self.read_string(), 228 | "comment": await self.read_string(), 229 | "mechanism": await self.read_int(), 230 | } 231 | ) 232 | out["AUTHENTICATION_DOMAIN"] = domains 233 | 234 | # See https://www.veritas.com/content/support/en_US/doc/18716246-126559472-0/v40575005-126559472 235 | await self.sendall(b"ni_vxss_networks\x00") 236 | networks = [] 237 | while True: 238 | host = await self.read_value() 239 | if host == b"\x00": 240 | break 241 | networks.append( 242 | { 243 | "network": host.rstrip(b"\x00").decode(), 244 | "use_vxss": await self.read_int(), 245 | } 246 | ) 247 | out["VXSS_NETWORK"] = networks 248 | 249 | return out 250 | 251 | async def get_primary_server(self) -> str: 252 | logging.debug( 253 | "[VNETDSocket] Sending VN_REQUEST_MASTER_NAME to vnetd on %s:%d", 254 | self.host, 255 | self.port, 256 | ) 257 | await self.sendall(b"14\x00") 258 | server = await self.read_string() 259 | ret_code = await self.read_value() 260 | return server 261 | --------------------------------------------------------------------------------