mirror of
				https://github.com/jtesta/ssh-audit.git
				synced 2025-10-30 21:15:27 +01:00 
			
		
		
		
	Results from concurrent scans against multiple hosts are no longer improperly combined (#190).
This commit is contained in:
		| @@ -179,6 +179,7 @@ For convenience, a web front-end on top of the command-line tool is available at | |||||||
| ## ChangeLog | ## ChangeLog | ||||||
|  |  | ||||||
| ### v3.0.0-dev (2023-??-??) | ### v3.0.0-dev (2023-??-??) | ||||||
|  |  - Results from concurrent scans against multiple hosts are no longer improperly combined; bug discovered by [Adam Russell](https://github.com/thecliguy). | ||||||
|  - Added 1 new key exchange: `curve448-sha512@libssh.org`. |  - Added 1 new key exchange: `curve448-sha512@libssh.org`. | ||||||
|  |  | ||||||
| ### v2.9.0 (2023-04-29) | ### v2.9.0 (2023-04-29) | ||||||
|   | |||||||
| @@ -54,7 +54,7 @@ class Algorithms: | |||||||
|     def ssh1(self) -> Optional['Algorithms.Item']: |     def ssh1(self) -> Optional['Algorithms.Item']: | ||||||
|         if self.ssh1kex is None: |         if self.ssh1kex is None: | ||||||
|             return None |             return None | ||||||
|         item = Algorithms.Item(1, SSH1_KexDB.ALGORITHMS) |         item = Algorithms.Item(1, SSH1_KexDB.get_db()) | ||||||
|         item.add('key', ['ssh-rsa1']) |         item.add('key', ['ssh-rsa1']) | ||||||
|         item.add('enc', self.ssh1kex.supported_ciphers) |         item.add('enc', self.ssh1kex.supported_ciphers) | ||||||
|         item.add('aut', self.ssh1kex.supported_authentications) |         item.add('aut', self.ssh1kex.supported_authentications) | ||||||
| @@ -64,7 +64,7 @@ class Algorithms: | |||||||
|     def ssh2(self) -> Optional['Algorithms.Item']: |     def ssh2(self) -> Optional['Algorithms.Item']: | ||||||
|         if self.ssh2kex is None: |         if self.ssh2kex is None: | ||||||
|             return None |             return None | ||||||
|         item = Algorithms.Item(2, SSH2_KexDB.ALGORITHMS) |         item = Algorithms.Item(2, SSH2_KexDB.get_db()) | ||||||
|         item.add('kex', self.ssh2kex.kex_algorithms) |         item.add('kex', self.ssh2kex.kex_algorithms) | ||||||
|         item.add('key', self.ssh2kex.key_algorithms) |         item.add('key', self.ssh2kex.key_algorithms) | ||||||
|         item.add('enc', self.ssh2kex.server.encryption) |         item.add('enc', self.ssh2kex.server.encryption) | ||||||
|   | |||||||
| @@ -208,7 +208,7 @@ class GEXTest: | |||||||
|                     # We flag moduli smaller than 2048 as a failure. |                     # We flag moduli smaller than 2048 as a failure. | ||||||
|                     if smallest_modulus < 2048: |                     if smallest_modulus < 2048: | ||||||
|                         text = 'using small %d-bit modulus' % smallest_modulus |                         text = 'using small %d-bit modulus' % smallest_modulus | ||||||
|                         lst = SSH2_KexDB.ALGORITHMS['kex'][gex_alg] |                         lst = SSH2_KexDB.get_db()['kex'][gex_alg] | ||||||
|                         # For 'diffie-hellman-group-exchange-sha256', add |                         # For 'diffie-hellman-group-exchange-sha256', add | ||||||
|                         # a failure reason. |                         # a failure reason. | ||||||
|                         if len(lst) == 1: |                         if len(lst) == 1: | ||||||
| @@ -222,7 +222,7 @@ class GEXTest: | |||||||
|  |  | ||||||
|                     # Moduli smaller than 3072 get flagged as a warning. |                     # Moduli smaller than 3072 get flagged as a warning. | ||||||
|                     elif smallest_modulus < 3072: |                     elif smallest_modulus < 3072: | ||||||
|                         lst = SSH2_KexDB.ALGORITHMS['kex'][gex_alg] |                         lst = SSH2_KexDB.get_db()['kex'][gex_alg] | ||||||
|  |  | ||||||
|                         # Ensure that a warning list exists for us to append to, below. |                         # Ensure that a warning list exists for us to append to, below. | ||||||
|                         while len(lst) < 3: |                         while len(lst) < 3: | ||||||
|   | |||||||
| @@ -216,16 +216,18 @@ class HostKeyTest: | |||||||
|  |  | ||||||
|                         # If the current key is a member of the RSA family, then populate all RSA family members with the same |                         # If the current key is a member of the RSA family, then populate all RSA family members with the same | ||||||
|                         # failure and/or warning comments. |                         # failure and/or warning comments. | ||||||
|                         while len(SSH2_KexDB.ALGORITHMS['key'][rsa_type]) < 3: |                         db = SSH2_KexDB.get_db() | ||||||
|                             SSH2_KexDB.ALGORITHMS['key'][rsa_type].append([]) |                         while len(db['key'][rsa_type]) < 3: | ||||||
|  |                             db['key'][rsa_type].append([]) | ||||||
|  |  | ||||||
|                         SSH2_KexDB.ALGORITHMS['key'][rsa_type][1].extend(key_fail_comments) |                         db['key'][rsa_type][1].extend(key_fail_comments) | ||||||
|                         SSH2_KexDB.ALGORITHMS['key'][rsa_type][2].extend(key_warn_comments) |                         db['key'][rsa_type][2].extend(key_warn_comments) | ||||||
|  |  | ||||||
|                 else: |                 else: | ||||||
|                     host_key_types[host_key_type]['parsed'] = True |                     host_key_types[host_key_type]['parsed'] = True | ||||||
|                     while len(SSH2_KexDB.ALGORITHMS['key'][host_key_type]) < 3: |                     db = SSH2_KexDB.get_db() | ||||||
|                         SSH2_KexDB.ALGORITHMS['key'][host_key_type].append([]) |                     while len(db['key'][host_key_type]) < 3: | ||||||
|  |                         db['key'][host_key_type].append([]) | ||||||
|  |  | ||||||
|                     SSH2_KexDB.ALGORITHMS['key'][host_key_type][1].extend(key_fail_comments) |                     db['key'][host_key_type][1].extend(key_fail_comments) | ||||||
|                     SSH2_KexDB.ALGORITHMS['key'][host_key_type][2].extend(key_warn_comments) |                     db['key'][host_key_type][2].extend(key_warn_comments) | ||||||
|   | |||||||
| @@ -22,6 +22,9 @@ | |||||||
|    THE SOFTWARE. |    THE SOFTWARE. | ||||||
| """ | """ | ||||||
| # pylint: disable=unused-import | # pylint: disable=unused-import | ||||||
|  | import copy | ||||||
|  | import threading | ||||||
|  |  | ||||||
| from typing import Dict, List, Set, Sequence, Tuple, Iterable  # noqa: F401 | from typing import Dict, List, Set, Sequence, Tuple, Iterable  # noqa: F401 | ||||||
| from typing import Callable, Optional, Union, Any  # noqa: F401 | from typing import Callable, Optional, Union, Any  # noqa: F401 | ||||||
|  |  | ||||||
| @@ -34,7 +37,9 @@ class SSH1_KexDB:  # pylint: disable=too-few-public-methods | |||||||
|     FAIL_NA_UNSAFE = 'not implemented in OpenSSH (server), unsafe algorithm' |     FAIL_NA_UNSAFE = 'not implemented in OpenSSH (server), unsafe algorithm' | ||||||
|     TEXT_CIPHER_IDEA = 'cipher used by commercial SSH' |     TEXT_CIPHER_IDEA = 'cipher used by commercial SSH' | ||||||
|  |  | ||||||
|     ALGORITHMS: Dict[str, Dict[str, List[List[Optional[str]]]]] = { |     DB_PER_THREAD: Dict[int, Dict[str, Dict[str, List[List[Optional[str]]]]]] = {} | ||||||
|  |  | ||||||
|  |     MASTER_DB: Dict[str, Dict[str, List[List[Optional[str]]]]] = { | ||||||
|         'key': { |         'key': { | ||||||
|             'ssh-rsa1': [['1.2.2']], |             'ssh-rsa1': [['1.2.2']], | ||||||
|         }, |         }, | ||||||
| @@ -56,3 +61,24 @@ class SSH1_KexDB:  # pylint: disable=too-few-public-methods | |||||||
|             'kerberos': [['1.2.2', '3.6'], [FAIL_OPENSSH37_REMOVE]], |             'kerberos': [['1.2.2', '3.6'], [FAIL_OPENSSH37_REMOVE]], | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def get_db() -> Dict[str, Dict[str, List[List[Optional[str]]]]]: | ||||||
|  |         '''Returns a copy of the MASTER_DB that is private to the calling thread.  This prevents multiple threads from polluting the results of other threads.''' | ||||||
|  |         calling_thread_id = threading.get_ident() | ||||||
|  |  | ||||||
|  |         if calling_thread_id not in SSH1_KexDB.DB_PER_THREAD: | ||||||
|  |             SSH1_KexDB.DB_PER_THREAD[calling_thread_id] = copy.deepcopy(SSH1_KexDB.MASTER_DB) | ||||||
|  |  | ||||||
|  |         return SSH1_KexDB.DB_PER_THREAD[calling_thread_id] | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def thread_exit() -> None: | ||||||
|  |         '''Deletes the calling thread's copy of the MASTER_DB.  This is needed because, in rare circumstances, a terminated thread's ID can be re-used by new threads.''' | ||||||
|  |  | ||||||
|  |         calling_thread_id = threading.get_ident() | ||||||
|  |  | ||||||
|  |         if calling_thread_id in SSH1_KexDB.DB_PER_THREAD: | ||||||
|  |             del SSH1_KexDB.DB_PER_THREAD[calling_thread_id] | ||||||
|   | |||||||
| @@ -23,6 +23,9 @@ | |||||||
|    THE SOFTWARE. |    THE SOFTWARE. | ||||||
| """ | """ | ||||||
| # pylint: disable=unused-import | # pylint: disable=unused-import | ||||||
|  | import copy | ||||||
|  | import threading | ||||||
|  |  | ||||||
| from typing import Dict, List, Set, Sequence, Tuple, Iterable  # noqa: F401 | from typing import Dict, List, Set, Sequence, Tuple, Iterable  # noqa: F401 | ||||||
| from typing import Callable, Optional, Union, Any  # noqa: F401 | from typing import Callable, Optional, Union, Any  # noqa: F401 | ||||||
|  |  | ||||||
| @@ -69,8 +72,10 @@ class SSH2_KexDB:  # pylint: disable=too-few-public-methods | |||||||
|     INFO_REMOVED_IN_OPENSSH70 = 'removed in OpenSSH 7.0: https://www.openssh.com/txt/release-7.0' |     INFO_REMOVED_IN_OPENSSH70 = 'removed in OpenSSH 7.0: https://www.openssh.com/txt/release-7.0' | ||||||
|     INFO_WITHDRAWN_PQ_ALG = 'the sntrup4591761 algorithm was withdrawn, as it may not provide strong post-quantum security' |     INFO_WITHDRAWN_PQ_ALG = 'the sntrup4591761 algorithm was withdrawn, as it may not provide strong post-quantum security' | ||||||
|  |  | ||||||
|  |     # Maintains a dictionary per calling thread that yields its own copy of MASTER_DB.  This prevents results from one thread polluting the results of another thread. | ||||||
|  |     DB_PER_THREAD: Dict[int, Dict[str, Dict[str, List[List[Optional[str]]]]]] = {} | ||||||
|  |  | ||||||
|     ALGORITHMS: Dict[str, Dict[str, List[List[Optional[str]]]]] = { |     MASTER_DB: Dict[str, Dict[str, List[List[Optional[str]]]]] = { | ||||||
|         # Format: 'algorithm_name': [['version_first_appeared_in'], [reason_for_failure1, reason_for_failure2, ...], [warning1, warning2, ...], [info1, info2, ...]] |         # Format: 'algorithm_name': [['version_first_appeared_in'], [reason_for_failure1, reason_for_failure2, ...], [warning1, warning2, ...], [info1, info2, ...]] | ||||||
|         'kex': { |         'kex': { | ||||||
|             'Curve25519SHA256': [[]], |             'Curve25519SHA256': [[]], | ||||||
| @@ -390,3 +395,24 @@ class SSH2_KexDB:  # pylint: disable=too-few-public-methods | |||||||
|             'umac-96@openssh.com': [[], [], [WARN_ENCRYPT_AND_MAC], [INFO_NEVER_IMPLEMENTED_IN_OPENSSH]], |             'umac-96@openssh.com': [[], [], [WARN_ENCRYPT_AND_MAC], [INFO_NEVER_IMPLEMENTED_IN_OPENSSH]], | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def get_db() -> Dict[str, Dict[str, List[List[Optional[str]]]]]: | ||||||
|  |         '''Returns a copy of the MASTER_DB that is private to the calling thread.  This prevents multiple threads from polluting the results of other threads.''' | ||||||
|  |         calling_thread_id = threading.get_ident() | ||||||
|  |  | ||||||
|  |         if calling_thread_id not in SSH2_KexDB.DB_PER_THREAD: | ||||||
|  |             SSH2_KexDB.DB_PER_THREAD[calling_thread_id] = copy.deepcopy(SSH2_KexDB.MASTER_DB) | ||||||
|  |  | ||||||
|  |         return SSH2_KexDB.DB_PER_THREAD[calling_thread_id] | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def thread_exit() -> None: | ||||||
|  |         '''Deletes the calling thread's copy of the MASTER_DB.  This is needed because, in rare circumstances, a terminated thread's ID can be re-used by new threads.''' | ||||||
|  |  | ||||||
|  |         calling_thread_id = threading.get_ident() | ||||||
|  |  | ||||||
|  |         if calling_thread_id in SSH2_KexDB.DB_PER_THREAD: | ||||||
|  |             del SSH2_KexDB.DB_PER_THREAD[calling_thread_id] | ||||||
|   | |||||||
| @@ -446,10 +446,11 @@ def post_process_findings(banner: Optional[Banner], algs: Algorithms) -> List[st | |||||||
|     if (algs.ssh2kex is not None and 'diffie-hellman-group-exchange-sha256' in algs.ssh2kex.kex_algorithms and 'diffie-hellman-group-exchange-sha256' in algs.ssh2kex.dh_modulus_sizes() and algs.ssh2kex.dh_modulus_sizes()['diffie-hellman-group-exchange-sha256'] == 2048) and (banner is not None and banner.software is not None and banner.software.find('OpenSSH') != -1): |     if (algs.ssh2kex is not None and 'diffie-hellman-group-exchange-sha256' in algs.ssh2kex.kex_algorithms and 'diffie-hellman-group-exchange-sha256' in algs.ssh2kex.dh_modulus_sizes() and algs.ssh2kex.dh_modulus_sizes()['diffie-hellman-group-exchange-sha256'] == 2048) and (banner is not None and banner.software is not None and banner.software.find('OpenSSH') != -1): | ||||||
|  |  | ||||||
|         # Ensure a list for notes exists. |         # Ensure a list for notes exists. | ||||||
|         while len(SSH2_KexDB.ALGORITHMS['kex']['diffie-hellman-group-exchange-sha256']) < 4: |         db = SSH2_KexDB.get_db() | ||||||
|             SSH2_KexDB.ALGORITHMS['kex']['diffie-hellman-group-exchange-sha256'].append([]) |         while len(db['kex']['diffie-hellman-group-exchange-sha256']) < 4: | ||||||
|  |             db['kex']['diffie-hellman-group-exchange-sha256'].append([]) | ||||||
|  |  | ||||||
|         SSH2_KexDB.ALGORITHMS['kex']['diffie-hellman-group-exchange-sha256'][3].append("A bug in OpenSSH causes it to fall back to a 2048-bit modulus regardless of server configuration (https://bugzilla.mindrot.org/show_bug.cgi?id=2793)") |         db['kex']['diffie-hellman-group-exchange-sha256'][3].append("A bug in OpenSSH causes it to fall back to a 2048-bit modulus regardless of server configuration (https://bugzilla.mindrot.org/show_bug.cgi?id=2793)") | ||||||
|  |  | ||||||
|         # Ensure that this algorithm doesn't appear in the recommendations section since the user cannot control this OpenSSH bug. |         # Ensure that this algorithm doesn't appear in the recommendations section since the user cannot control this OpenSSH bug. | ||||||
|         algorithm_recommendation_suppress_list.append('diffie-hellman-group-exchange-sha256') |         algorithm_recommendation_suppress_list.append('diffie-hellman-group-exchange-sha256') | ||||||
| @@ -522,7 +523,7 @@ def output(out: OutputBuffer, aconf: AuditConf, banner: Optional[Banner], header | |||||||
|  |  | ||||||
|     # SSHv1 |     # SSHv1 | ||||||
|     if pkm is not None: |     if pkm is not None: | ||||||
|         adb = SSH1_KexDB.ALGORITHMS |         adb = SSH1_KexDB.get_db() | ||||||
|         ciphers = pkm.supported_ciphers |         ciphers = pkm.supported_ciphers | ||||||
|         auths = pkm.supported_authentications |         auths = pkm.supported_authentications | ||||||
|         title, atype = 'SSH1 host-key algorithms', 'key' |         title, atype = 'SSH1 host-key algorithms', 'key' | ||||||
| @@ -534,7 +535,7 @@ def output(out: OutputBuffer, aconf: AuditConf, banner: Optional[Banner], header | |||||||
|  |  | ||||||
|     # SSHv2 |     # SSHv2 | ||||||
|     if kex is not None: |     if kex is not None: | ||||||
|         adb = SSH2_KexDB.ALGORITHMS |         adb = SSH2_KexDB.get_db() | ||||||
|         title, atype = 'key exchange algorithms', 'kex' |         title, atype = 'key exchange algorithms', 'kex' | ||||||
|         program_retval = output_algorithms(out, title, adb, atype, kex.kex_algorithms, unknown_algorithms, aconf.json, program_retval, maxlen, dh_modulus_sizes=kex.dh_modulus_sizes()) |         program_retval = output_algorithms(out, title, adb, atype, kex.kex_algorithms, unknown_algorithms, aconf.json, program_retval, maxlen, dh_modulus_sizes=kex.dh_modulus_sizes()) | ||||||
|         title, atype = 'host-key algorithms', 'key' |         title, atype = 'host-key algorithms', 'key' | ||||||
| @@ -1124,7 +1125,7 @@ def algorithm_lookup(out: OutputBuffer, alg_names: str) -> int: | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     algorithm_names = alg_names.split(",") |     algorithm_names = alg_names.split(",") | ||||||
|     adb = SSH2_KexDB.ALGORITHMS |     adb = SSH2_KexDB.get_db() | ||||||
|  |  | ||||||
|     # Use nested dictionary comprehension to iterate an outer dictionary where |     # Use nested dictionary comprehension to iterate an outer dictionary where | ||||||
|     # each key is an alg type that consists of a value (which is itself a |     # each key is an alg type that consists of a value (which is itself a | ||||||
| @@ -1376,6 +1377,10 @@ def main() -> int: | |||||||
|         if aconf.json: |         if aconf.json: | ||||||
|             print(']') |             print(']') | ||||||
|  |  | ||||||
|  |         # Send notification that this thread is exiting.  This deletes the thread's local copy of the algorithm databases. | ||||||
|  |         SSH1_KexDB.thread_exit() | ||||||
|  |         SSH2_KexDB.thread_exit() | ||||||
|  |  | ||||||
|     else:  # Just a scan against a single target. |     else:  # Just a scan against a single target. | ||||||
|         ret = audit(out, aconf) |         ret = audit(out, aconf) | ||||||
|         out.write() |         out.write() | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ class Test_SSH2_KexDB: | |||||||
|  |  | ||||||
|     @pytest.fixture(autouse=True) |     @pytest.fixture(autouse=True) | ||||||
|     def init(self): |     def init(self): | ||||||
|         self.db = SSH2_KexDB.ALGORITHMS |         self.db = SSH2_KexDB.get_db() | ||||||
|  |  | ||||||
|     def test_ssh2_kexdb(self): |     def test_ssh2_kexdb(self): | ||||||
|         '''Ensures that the SSH2_KexDB.ALGORITHMS dictionary is in the right format.''' |         '''Ensures that the SSH2_KexDB.ALGORITHMS dictionary is in the right format.''' | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Joe Testa
					Joe Testa