Added 'client policy' field in policy files to distinguish server from client policies.

This commit is contained in:
Joe Testa 2020-07-14 17:14:47 -04:00
parent b27d768c79
commit 8fb07edafd
2 changed files with 38 additions and 10 deletions

View File

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

View File

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