mirror of
				https://github.com/jtesta/ssh-audit.git
				synced 2025-10-30 21:15:27 +01:00 
			
		
		
		
	Added 'client policy' field in policy files to distinguish server from client policies.
This commit is contained in:
		
							
								
								
									
										44
									
								
								ssh-audit.py
									
									
									
									
									
								
							
							
						
						
									
										44
									
								
								ssh-audit.py
									
									
									
									
									
								
							| @@ -105,6 +105,7 @@ class Policy: | |||||||
|         self._hostkey_sizes = None  # type: Optional[Dict[str, int]] |         self._hostkey_sizes = None  # type: Optional[Dict[str, int]] | ||||||
|         self._cakey_sizes = None  # type: Optional[Dict[str, int]] |         self._cakey_sizes = None  # type: Optional[Dict[str, int]] | ||||||
|         self._dh_modulus_sizes = None  # type: Optional[Dict[str, int]] |         self._dh_modulus_sizes = None  # type: Optional[Dict[str, int]] | ||||||
|  |         self._server_policy = True | ||||||
|  |  | ||||||
|         if (policy_file is None) and (policy_data is None): |         if (policy_file is None) and (policy_data is None): | ||||||
|             raise RuntimeError('policy_file and policy_data must not both be None.') |             raise RuntimeError('policy_file and policy_data must not both be None.') | ||||||
| @@ -134,7 +135,7 @@ class Policy: | |||||||
|             key = key.strip() |             key = key.strip() | ||||||
|             val = val.strip() |             val = val.strip() | ||||||
|  |  | ||||||
|             if key not in ['name', 'version', 'banner', 'compressions', 'host keys', 'key exchanges', 'ciphers', 'macs'] and not key.startswith('hostkey_size_') and not key.startswith('cakey_size_') and not key.startswith('dh_modulus_size_'): |             if key not in ['name', 'version', 'banner', 'compressions', 'host keys', 'key exchanges', 'ciphers', 'macs', 'client policy'] and not key.startswith('hostkey_size_') and not key.startswith('cakey_size_') and not key.startswith('dh_modulus_size_'): | ||||||
|                 raise ValueError("invalid field found in policy: %s" % line) |                 raise ValueError("invalid field found in policy: %s" % line) | ||||||
|  |  | ||||||
|             if key in ['name', 'banner']: |             if key in ['name', 'banner']: | ||||||
| @@ -190,6 +191,8 @@ class Policy: | |||||||
|                 if self._dh_modulus_sizes is None: |                 if self._dh_modulus_sizes is None: | ||||||
|                     self._dh_modulus_sizes = {} |                     self._dh_modulus_sizes = {} | ||||||
|                 self._dh_modulus_sizes[dh_modulus_type] = int(val) |                 self._dh_modulus_sizes[dh_modulus_type] = int(val) | ||||||
|  |             elif key.startswith('client policy') and val.lower() == 'true': | ||||||
|  |                 self._server_policy = False | ||||||
|  |  | ||||||
|  |  | ||||||
|         if self._name is None: |         if self._name is None: | ||||||
| @@ -199,7 +202,7 @@ class Policy: | |||||||
|  |  | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def create(host: str, banner: Optional['SSH.Banner'], kex: Optional['SSH2.Kex']) -> str: |     def create(source: Optional[str], banner: Optional['SSH.Banner'], kex: Optional['SSH2.Kex'], client_audit: bool) -> str: | ||||||
|         '''Creates a policy based on a server configuration.  Returns a string.''' |         '''Creates a policy based on a server configuration.  Returns a string.''' | ||||||
|  |  | ||||||
|         today = date.today().strftime('%Y/%m/%d') |         today = date.today().strftime('%Y/%m/%d') | ||||||
| @@ -211,6 +214,10 @@ class Policy: | |||||||
|         rsa_hostkey_sizes_str = '' |         rsa_hostkey_sizes_str = '' | ||||||
|         rsa_cakey_sizes_str = '' |         rsa_cakey_sizes_str = '' | ||||||
|         dh_modulus_sizes_str = '' |         dh_modulus_sizes_str = '' | ||||||
|  |         client_policy_str = '' | ||||||
|  |  | ||||||
|  |         if client_audit: | ||||||
|  |             client_policy_str = "\n# Set to true to signify this is a policy for clients, not servers.\nclient policy = true\n" | ||||||
|  |  | ||||||
|         if kex is not None: |         if kex is not None: | ||||||
|             if kex.server.compression is not None: |             if kex.server.compression is not None: | ||||||
| @@ -248,7 +255,7 @@ class Policy: | |||||||
|         policy_data = '''# |         policy_data = '''# | ||||||
| # Custom policy based on %s (created on %s) | # Custom policy based on %s (created on %s) | ||||||
| # | # | ||||||
|  | %s | ||||||
| # The name of this policy (displayed in the output during scans).  Must be in quotes. | # The name of this policy (displayed in the output during scans).  Must be in quotes. | ||||||
| name = "Custom Policy (based on %s on %s)" | name = "Custom Policy (based on %s on %s)" | ||||||
|  |  | ||||||
| @@ -272,7 +279,7 @@ ciphers = %s | |||||||
|  |  | ||||||
| # The MACs that must match exactly (order matters). | # The MACs that must match exactly (order matters). | ||||||
| macs = %s | macs = %s | ||||||
| ''' % (host, today, host, today, banner, compressions, rsa_hostkey_sizes_str, rsa_cakey_sizes_str, dh_modulus_sizes_str, host_keys, kex_algs, ciphers, macs) | ''' % (source, today, client_policy_str, source, today, banner, compressions, rsa_hostkey_sizes_str, rsa_cakey_sizes_str, dh_modulus_sizes_str, host_keys, kex_algs, ciphers, macs) | ||||||
|  |  | ||||||
|         return policy_data |         return policy_data | ||||||
|  |  | ||||||
| @@ -353,6 +360,11 @@ macs = %s | |||||||
|         return '%s v%s' % (self._name, self._version) |         return '%s v%s' % (self._name, self._version) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     def is_server_policy(self) -> bool: | ||||||
|  |         '''Returns True if this is a server policy, or False if this is a client policy.''' | ||||||
|  |         return self._server_policy | ||||||
|  |  | ||||||
|  |  | ||||||
|     def __str__(self) -> str: |     def __str__(self) -> str: | ||||||
|         undefined = '{undefined}' |         undefined = '{undefined}' | ||||||
|  |  | ||||||
| @@ -552,6 +564,16 @@ class AuditConf: | |||||||
|                 print("Error while loading policy file: %s: %s" % (str(e), traceback.format_exc())) |                 print("Error while loading policy file: %s: %s" % (str(e), traceback.format_exc())) | ||||||
|                 sys.exit(-1) |                 sys.exit(-1) | ||||||
|  |  | ||||||
|  |             # If the user wants to do a client audit, but provided a server policy, terminate. | ||||||
|  |             if aconf.client_audit and aconf.policy.is_server_policy(): | ||||||
|  |                 print("Error: client audit selected, but server policy provided.") | ||||||
|  |                 sys.exit(-1) | ||||||
|  |  | ||||||
|  |             # If the user wants to do a server audit, but provided a client policy, terminate. | ||||||
|  |             if aconf.client_audit is False and aconf.policy.is_server_policy() is False: | ||||||
|  |                 print("Error: server audit selected, but client policy provided.") | ||||||
|  |                 sys.exit(-1) | ||||||
|  |  | ||||||
|         return aconf |         return aconf | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -2270,7 +2292,7 @@ class SSH:  # pylint: disable=too-few-public-methods | |||||||
|                 self.__ipvo = () |                 self.__ipvo = () | ||||||
|             self.__timeout = timeout |             self.__timeout = timeout | ||||||
|             self.__timeout_set = timeout_set |             self.__timeout_set = timeout_set | ||||||
|             self.client_host = None |             self.client_host = None  # type: Optional[str] | ||||||
|             self.client_port = None |             self.client_port = None | ||||||
|  |  | ||||||
|         def _resolve(self, ipvo: Sequence[int]) -> Iterable[Tuple[int, Tuple[Any, ...]]]: |         def _resolve(self, ipvo: Sequence[int]) -> Iterable[Tuple[int, Tuple[Any, ...]]]: | ||||||
| @@ -3260,8 +3282,14 @@ def evaluate_policy(aconf: AuditConf, banner: Optional['SSH.Banner'], client_hos | |||||||
|     return passed |     return passed | ||||||
|  |  | ||||||
|  |  | ||||||
| def make_policy(aconf: AuditConf, banner: Optional['SSH.Banner'], kex: Optional['SSH2.Kex']) -> None: | def make_policy(aconf: AuditConf, banner: Optional['SSH.Banner'], kex: Optional['SSH2.Kex'], client_host: Optional[str]) -> None: | ||||||
|     policy_data = Policy.create(aconf.host, banner, kex) |  | ||||||
|  |     # Set the source of this policy to the server host if this is a server audit, otherwise set it to the client address. | ||||||
|  |     source = aconf.host  # type: Optional[str] | ||||||
|  |     if aconf.client_audit: | ||||||
|  |         source = client_host | ||||||
|  |  | ||||||
|  |     policy_data = Policy.create(source, banner, kex, aconf.client_audit) | ||||||
|  |  | ||||||
|     if aconf.policy_file is None: |     if aconf.policy_file is None: | ||||||
|         raise RuntimeError('Internal error: cannot write policy file since filename is None!') |         raise RuntimeError('Internal error: cannot write policy file since filename is None!') | ||||||
| @@ -3563,7 +3591,7 @@ def audit(aconf: AuditConf, sshv: Optional[int] = None, print_target: bool = Fal | |||||||
|  |  | ||||||
|         # A new policy should be made from this scan. |         # A new policy should be made from this scan. | ||||||
|         elif (aconf.policy is None) and (aconf.make_policy is True): |         elif (aconf.policy is None) and (aconf.make_policy is True): | ||||||
|             make_policy(aconf, banner, kex=kex) |             make_policy(aconf, banner, kex, s.client_host) | ||||||
|  |  | ||||||
|         else: |         else: | ||||||
|             raise RuntimeError('Internal error while handling output: %r %r' % (aconf.policy is None, aconf.make_policy)) |             raise RuntimeError('Internal error while handling output: %r %r' % (aconf.policy is None, aconf.make_policy)) | ||||||
|   | |||||||
| @@ -186,7 +186,7 @@ macs = mac_alg1, mac_alg2, mac_alg3''' | |||||||
|         '''Creates a policy from a kex and ensures it is generated exactly as expected.''' |         '''Creates a policy from a kex and ensures it is generated exactly as expected.''' | ||||||
|  |  | ||||||
|         kex = self._get_kex() |         kex = self._get_kex() | ||||||
|         pol_data = self.Policy.create('www.l0l.com', 'bannerX', kex) |         pol_data = self.Policy.create('www.l0l.com', 'bannerX', kex, False) | ||||||
|  |  | ||||||
|         # Today's date is embedded in the policy, so filter it out to get repeatable results. |         # Today's date is embedded in the policy, so filter it out to get repeatable results. | ||||||
|         pol_data = pol_data.replace(date.today().strftime('%Y/%m/%d'), '[todays date]') |         pol_data = pol_data.replace(date.today().strftime('%Y/%m/%d'), '[todays date]') | ||||||
| @@ -199,7 +199,7 @@ macs = mac_alg1, mac_alg2, mac_alg3''' | |||||||
|         '''Creates a policy and evaluates it against the same server''' |         '''Creates a policy and evaluates it against the same server''' | ||||||
|  |  | ||||||
|         kex = self._get_kex() |         kex = self._get_kex() | ||||||
|         policy_data = self.Policy.create('www.l0l.com', None, kex) |         policy_data = self.Policy.create('www.l0l.com', None, kex, False) | ||||||
|         policy = self.Policy(policy_data=policy_data) |         policy = self.Policy(policy_data=policy_data) | ||||||
|  |  | ||||||
|         ret, errors = policy.evaluate('SSH Server 1.0', kex) |         ret, errors = policy.evaluate('SSH Server 1.0', kex) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Joe Testa
					Joe Testa