Fixed pylint errors, consolidated error checking for granular GEX tests, renamed functions for better readability.

This commit is contained in:
Joe Testa 2022-03-13 20:58:22 -04:00
parent 19f192d21f
commit 11905ed44a
4 changed files with 51 additions and 65 deletions

View File

@ -172,6 +172,7 @@ For convenience, a web front-end on top of the command-line tool is available at
## ChangeLog ## ChangeLog
### v2.6.0-dev ### v2.6.0-dev
- Added `-g` and `--gex-test` for granular GEX modulus size tests; credit [Adam Russell](https://github.com/thecliguy).
- Snap packages now print more user-friendly error messages when permission errors are encountered. - Snap packages now print more user-friendly error messages when permission errors are encountered.
- JSON 'target' field now always includes port number; credit [tomatohater1337](https://github.com/tomatohater1337). - JSON 'target' field now always includes port number; credit [tomatohater1337](https://github.com/tomatohater1337).
- Added 24 new key exchanges: `ecdh-sha2-1.3.132.0.1`, `ecdh-sha2-1.2.840.10045.3.1.1`, `ecdh-sha2-1.3.132.0.33`, `ecdh-sha2-1.3.132.0.26`, `ecdh-sha2-1.3.132.0.27`, `ecdh-sha2-1.2.840.10045.3.1.7`, `ecdh-sha2-1.3.132.0.16`, `ecdh-sha2-1.3.132.0.34`, `ecdh-sha2-1.3.132.0.36`, `ecdh-sha2-1.3.132.0.37`, `ecdh-sha2-1.3.132.0.35`, `ecdh-sha2-1.3.132.0.38`, `ecdh-sha2-4MHB+NBt3AlaSRQ7MnB4cg==`, `ecdh-sha2-5pPrSUQtIaTjUSt5VZNBjg==`, `ecdh-sha2-VqBg4QRPjxx1EXZdV0GdWQ==`, `ecdh-sha2-zD/b3hu/71952ArpUG4OjQ==`, `ecdh-sha2-qCbG5Cn/jjsZ7nBeR7EnOA==`, `ecdh-sha2-9UzNcgwTlEnSCECZa7V1mw==`, `ecdh-sha2-wiRIU8TKjMZ418sMqlqtvQ==`, `ecdh-sha2-qcFQaMAMGhTziMT0z+Tuzw==`, `ecdh-sha2-m/FtSAmrV4j/Wy6RVUaK7A==`, `ecdh-sha2-D3FefCjYoJ/kfXgAyLddYA==`, `ecdh-sha2-h/SsxnLCtRBh7I9ATyeB3A==`, `ecdh-sha2-mNVwCXAoS1HGmHpLvBC94w==`. - Added 24 new key exchanges: `ecdh-sha2-1.3.132.0.1`, `ecdh-sha2-1.2.840.10045.3.1.1`, `ecdh-sha2-1.3.132.0.33`, `ecdh-sha2-1.3.132.0.26`, `ecdh-sha2-1.3.132.0.27`, `ecdh-sha2-1.2.840.10045.3.1.7`, `ecdh-sha2-1.3.132.0.16`, `ecdh-sha2-1.3.132.0.34`, `ecdh-sha2-1.3.132.0.36`, `ecdh-sha2-1.3.132.0.37`, `ecdh-sha2-1.3.132.0.35`, `ecdh-sha2-1.3.132.0.38`, `ecdh-sha2-4MHB+NBt3AlaSRQ7MnB4cg==`, `ecdh-sha2-5pPrSUQtIaTjUSt5VZNBjg==`, `ecdh-sha2-VqBg4QRPjxx1EXZdV0GdWQ==`, `ecdh-sha2-zD/b3hu/71952ArpUG4OjQ==`, `ecdh-sha2-qCbG5Cn/jjsZ7nBeR7EnOA==`, `ecdh-sha2-9UzNcgwTlEnSCECZa7V1mw==`, `ecdh-sha2-wiRIU8TKjMZ418sMqlqtvQ==`, `ecdh-sha2-qcFQaMAMGhTziMT0z+Tuzw==`, `ecdh-sha2-m/FtSAmrV4j/Wy6RVUaK7A==`, `ecdh-sha2-D3FefCjYoJ/kfXgAyLddYA==`, `ecdh-sha2-h/SsxnLCtRBh7I9ATyeB3A==`, `ecdh-sha2-mNVwCXAoS1HGmHpLvBC94w==`.

View File

@ -21,13 +21,12 @@
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE. THE SOFTWARE.
""" """
import traceback
# pylint: disable=unused-import # pylint: disable=unused-import
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
import traceback
from ssh_audit.kexdh import KexGroupExchange_SHA1, KexGroupExchange_SHA256 from ssh_audit.kexdh import KexGroupExchange_SHA1, KexGroupExchange_SHA256
from ssh_audit.ssh2_kexdb import SSH2_KexDB from ssh_audit.ssh2_kexdb import SSH2_KexDB
from ssh_audit.ssh2_kex import SSH2_Kex from ssh_audit.ssh2_kex import SSH2_Kex
@ -71,11 +70,10 @@ class GEXTest:
return True return True
# Tests for modulus size in bits against the specified target.
@staticmethod @staticmethod
def modulus_size_test(out: 'OutputBuffer', s: 'SSH_Socket', kex: 'SSH2_Kex', bits_min: int, bits_pref: int, bits_max: int, modulus_dict: Dict[str, List[int]]) -> int: def granular_modulus_size_test(out: 'OutputBuffer', s: 'SSH_Socket', kex: 'SSH2_Kex', bits_min: int, bits_pref: int, bits_max: int, modulus_dict: Dict[str, List[int]]) -> int:
''' '''
Tests for modulus size in bits against the target target. Tests for granular modulus sizes.
Builds a dictionary, where a key represents a DH algorithm name and the Builds a dictionary, where a key represents a DH algorithm name and the
values are the modulus sizes (in bits) that have been returned by the values are the modulus sizes (in bits) that have been returned by the
target server. target server.
@ -89,10 +87,6 @@ class GEXTest:
out.d("Bits Pref: " + str(bits_pref)) out.d("Bits Pref: " + str(bits_pref))
out.d("Bits Max: " + str(bits_max)) out.d("Bits Max: " + str(bits_max))
if all(x < 0 for x in (bits_min, bits_pref, bits_max)):
out.fail("min, pref and max values cannot be negative.")
return exitcodes.FAILURE
GEX_ALGS = { GEX_ALGS = {
'diffie-hellman-group-exchange-sha1': KexGroupExchange_SHA1, 'diffie-hellman-group-exchange-sha1': KexGroupExchange_SHA1,
'diffie-hellman-group-exchange-sha256': KexGroupExchange_SHA256, 'diffie-hellman-group-exchange-sha256': KexGroupExchange_SHA256,
@ -100,11 +94,11 @@ class GEXTest:
# Check if the server supports any of the group-exchange # Check if the server supports any of the group-exchange
# algorithms. If so, test each one. # algorithms. If so, test each one.
for gex_alg in GEX_ALGS: for gex_alg, kex_group_class in GEX_ALGS.items():
if gex_alg not in kex.kex_algorithms: if gex_alg not in kex.kex_algorithms:
out.d('Server does not support the algorithm "' + gex_alg + '".', write_now=True) out.d('Server does not support the algorithm "' + gex_alg + '".', write_now=True)
else: else:
kex_group = GEX_ALGS[gex_alg]() kex_group = kex_group_class()
out.d('Preparing to perform DH group exchange using ' + gex_alg + ' with min, pref and max modulus sizes of ' + str(bits_min) + ' bits, ' + str(bits_pref) + ' bits and ' + str(bits_max) + ' bits...', write_now=True) out.d('Preparing to perform DH group exchange using ' + gex_alg + ' with min, pref and max modulus sizes of ' + str(bits_min) + ' bits, ' + str(bits_pref) + ' bits and ' + str(bits_max) + ' bits...', write_now=True)
# It has been observed that reconnecting to some SSH servers # It has been observed that reconnecting to some SSH servers
@ -121,11 +115,8 @@ class GEXTest:
kex_group.recv_reply(s, False) kex_group.recv_reply(s, False)
modulus_size_returned = kex_group.get_dh_modulus_size() modulus_size_returned = kex_group.get_dh_modulus_size()
out.d('Modulus size returned by server: ' + str(modulus_size_returned) + ' bits', write_now=True) out.d('Modulus size returned by server: ' + str(modulus_size_returned) + ' bits', write_now=True)
except Exception as e: except Exception:
out.d('[exception] ' + str(e), write_now=True) out.d('[exception] ' + str(traceback.format_exc()), write_now=True)
# import traceback
# print(traceback.format_exc())
pass
finally: finally:
# The server is in a state that is not re-testable, # The server is in a state that is not re-testable,
# so there's nothing else to do with this open # so there's nothing else to do with this open
@ -159,8 +150,7 @@ class GEXTest:
# algorithms. If so, test each one. # algorithms. If so, test each one.
for gex_alg, kex_group_class in GEX_ALGS.items(): for gex_alg, kex_group_class in GEX_ALGS.items():
if gex_alg in kex.kex_algorithms: if gex_alg in kex.kex_algorithms:
weak_sizes = 512, 1024, 1536 out.d('Preparing to perform DH group exchange using ' + gex_alg + ' with min, pref and max modulus sizes of 512 bits, 1024 bits and 1536 bits...', write_now=True)
out.d('Preparing to perform DH group exchange using ' + gex_alg + ' with min, pref and max modulus sizes of ' + str(weak_sizes[0]) + ' bits, ' + str(weak_sizes[1]) + ' bits and ' + str(weak_sizes[2]) + ' bits...', write_now=True)
if GEXTest.reconnect(out, s, kex, gex_alg) is False: if GEXTest.reconnect(out, s, kex, gex_alg) is False:
break break
@ -170,7 +160,7 @@ class GEXTest:
# First try a range of weak sizes. # First try a range of weak sizes.
try: try:
kex_group.send_init_gex(s, weak_sizes[0], weak_sizes[1], weak_sizes[2]) kex_group.send_init_gex(s, 512, 1024, 1536)
kex_group.recv_reply(s, False) kex_group.recv_reply(s, False)
# Its been observed that servers will return a group # Its been observed that servers will return a group
@ -179,9 +169,8 @@ class GEXTest:
smallest_modulus = kex_group.get_dh_modulus_size() smallest_modulus = kex_group.get_dh_modulus_size()
out.d('Modulus size returned by server: ' + str(smallest_modulus) + ' bits', write_now=True) out.d('Modulus size returned by server: ' + str(smallest_modulus) + ' bits', write_now=True)
except Exception as e: except Exception:
out.d('[exception] ' + str(e), write_now=True) out.d('[exception] ' + str(traceback.format_exc()), write_now=True)
pass
finally: finally:
s.close() s.close()
@ -205,11 +194,8 @@ class GEXTest:
kex_group.recv_reply(s, False) kex_group.recv_reply(s, False)
smallest_modulus = kex_group.get_dh_modulus_size() smallest_modulus = kex_group.get_dh_modulus_size()
out.d('Modulus size returned by server: ' + str(smallest_modulus) + ' bits', write_now=True) out.d('Modulus size returned by server: ' + str(smallest_modulus) + ' bits', write_now=True)
except Exception as e: except Exception:
out.d('[exception] ' + str(e), write_now=True) out.d('[exception] ' + str(traceback.format_exc()), write_now=True)
# import traceback
# print(traceback.format_exc())
pass
finally: finally:
# The server is in a state that is not re-testable, # The server is in a state that is not re-testable,
# so there's nothing else to do with this open # so there's nothing else to do with this open

View File

@ -87,7 +87,7 @@ def usage(err: Optional[str] = None) -> None:
uout.info(' -b, --batch batch output') uout.info(' -b, --batch batch output')
uout.info(' -c, --client-audit starts a server on port 2222 to audit client\n software config (use -p to change port;\n use -t to change timeout)') uout.info(' -c, --client-audit starts a server on port 2222 to audit client\n software config (use -p to change port;\n use -t to change timeout)')
uout.info(' -d, --debug debug output') uout.info(' -d, --debug debug output')
uout.info(' -g, --gex-test=<n[,n,...] | min:pref:max[,min:pref:max,...] | n-n[:step]> dh gex modulus size test') uout.info(' -g, --gex-test=<x[,y,...] | min:pref:max[,min:pref:max,...] | x-y[:step]> dh gex modulus size test')
uout.info(' -j, --json JSON output (use -jj to enable indents)') uout.info(' -j, --json JSON output (use -jj to enable indents)')
uout.info(' -l, --level=<level> minimum output level (info|warn|fail)') uout.info(' -l, --level=<level> minimum output level (info|warn|fail)')
uout.info(' -L, --list-policies list all the official, built-in policies') uout.info(' -L, --list-policies list all the official, built-in policies')
@ -654,10 +654,29 @@ def process_commandline(out: OutputBuffer, args: List[str], usage_cb: Callable[.
aconf.debug = True aconf.debug = True
out.debug = True out.debug = True
elif o in ('-g', '--gex-test'): elif o in ('-g', '--gex-test'):
if not((any(re.search(regex_str, a) for regex_str in get_permitted_syntax_for_gex_test().values()))): permitted_syntax = get_permitted_syntax_for_gex_test()
if not any(re.search(regex_str, a) for regex_str in permitted_syntax.values()):
usage_cb('{} {} is not valid'.format(o, a)) usage_cb('{} {} is not valid'.format(o, a))
if re.search(permitted_syntax['RANGE'], a):
extracted_digits = re.findall(r'\d+', a)
bits_left_bound = int(extracted_digits[0])
bits_right_bound = int(extracted_digits[1])
bits_step = 1
if (len(extracted_digits)) == 3:
bits_step = int(extracted_digits[2])
if bits_step <= 0:
usage_cb('{} {} is not valid'.format(o, bits_step))
if all(x < 0 for x in (bits_left_bound, bits_right_bound)):
usage_cb('{} {} {} is not valid'.format(o, bits_left_bound, bits_right_bound))
aconf.gex_test = a aconf.gex_test = a
if len(args) == 0 and aconf.client_audit is False and aconf.target_file is None and aconf.list_policies is False and aconf.lookup == '' and aconf.manual is False: if len(args) == 0 and aconf.client_audit is False and aconf.target_file is None and aconf.list_policies is False and aconf.lookup == '' and aconf.manual is False:
usage_cb() usage_cb()
@ -928,8 +947,7 @@ def audit(out: OutputBuffer, aconf: AuditConf, sshv: Optional[int] = None, print
if aconf.client_audit is False: if aconf.client_audit is False:
HostKeyTest.run(out, s, kex) HostKeyTest.run(out, s, kex)
if aconf.gex_test != '': if aconf.gex_test != '':
program_retval = invoke_modulus_size_test(out, s, kex, aconf) return run_gex_granular_modulus_size_test(out, s, kex, aconf)
return program_retval
else: else:
GEXTest.run(out, s, kex) GEXTest.run(out, s, kex)
@ -1079,15 +1097,11 @@ def get_permitted_syntax_for_gex_test() -> Dict[str, str]:
return syntax return syntax
def invoke_modulus_size_test(out: OutputBuffer, s: 'SSH_Socket', kex: 'SSH2_Kex', aconf: AuditConf) -> int: def run_gex_granular_modulus_size_test(out: OutputBuffer, s: 'SSH_Socket', kex: 'SSH2_Kex', aconf: AuditConf) -> int:
'''Extracts the user specified modulus sizes and submits them for testing against the target server. Returns an exitcodes.* flag.''' '''Extracts the user specified modulus sizes and submits them for testing against the target server. Returns an exitcodes.* flag.'''
permitted_syntax = get_permitted_syntax_for_gex_test() permitted_syntax = get_permitted_syntax_for_gex_test()
if not((any(re.search(regex_str, aconf.gex_test) for regex_str in permitted_syntax.values()))):
out.fail("Invalid syntax.")
return exitcodes.FAILURE
mod_dict: Dict[str, List[int]] = {} mod_dict: Dict[str, List[int]] = {}
# Range syntax. # Range syntax.
@ -1096,18 +1110,9 @@ def invoke_modulus_size_test(out: OutputBuffer, s: 'SSH_Socket', kex: 'SSH2_Kex'
bits_left_bound = int(extracted_digits[0]) bits_left_bound = int(extracted_digits[0])
bits_right_bound = int(extracted_digits[1]) bits_right_bound = int(extracted_digits[1])
bits_step = 1
if (len(extracted_digits)) == 3: if (len(extracted_digits)) == 3:
bits_step = int(extracted_digits[2]) bits_step = int(extracted_digits[2])
else:
bits_step = 1
if bits_step <= 0:
out.fail("Step value must be greater than zero.")
return exitcodes.FAILURE
if all(x < 0 for x in (bits_left_bound, bits_right_bound)):
out.fail("Start and end values cannot be negative.")
return exitcodes.FAILURE
# If the left value is greater than the right value, then the sequence # If the left value is greater than the right value, then the sequence
# operates from right to left. # operates from right to left.
@ -1119,7 +1124,7 @@ def invoke_modulus_size_test(out: OutputBuffer, s: 'SSH_Socket', kex: 'SSH2_Kex'
out.v("A separate test will be performed against each of the following modulus sizes: " + ", ".join([str(x) for x in bits_in_range_to_test]) + ".", write_now=True) out.v("A separate test will be performed against each of the following modulus sizes: " + ", ".join([str(x) for x in bits_in_range_to_test]) + ".", write_now=True)
for i_bits in bits_in_range_to_test: for i_bits in bits_in_range_to_test:
program_retval = GEXTest.modulus_size_test(out, s, kex, i_bits, i_bits, i_bits, mod_dict) program_retval = GEXTest.granular_modulus_size_test(out, s, kex, i_bits, i_bits, i_bits, mod_dict)
if program_retval != exitcodes.GOOD: if program_retval != exitcodes.GOOD:
return program_retval return program_retval
@ -1128,15 +1133,16 @@ def invoke_modulus_size_test(out: OutputBuffer, s: 'SSH_Socket', kex: 'SSH2_Kex'
bits_in_list_to_test = aconf.gex_test.split(',') bits_in_list_to_test = aconf.gex_test.split(',')
out.v("A separate test will be performed against each of the following modulus sizes: " + ", ".join([str(x) for x in bits_in_list_to_test]) + ".", write_now=True) out.v("A separate test will be performed against each of the following modulus sizes: " + ", ".join([str(x) for x in bits_in_list_to_test]) + ".", write_now=True)
for s_bits in bits_in_list_to_test: for s_bits in bits_in_list_to_test:
program_retval = GEXTest.modulus_size_test(out, s, kex, int(s_bits), int(s_bits), int(s_bits), mod_dict) program_retval = GEXTest.granular_modulus_size_test(out, s, kex, int(s_bits), int(s_bits), int(s_bits), mod_dict)
if program_retval != exitcodes.GOOD: if program_retval != exitcodes.GOOD:
return program_retval return program_retval
if re.search(permitted_syntax['LIST_WITH_MIN_PREF_MAX'], aconf.gex_test): if re.search(permitted_syntax['LIST_WITH_MIN_PREF_MAX'], aconf.gex_test):
sets_of_min_pref_max = aconf.gex_test.split(',') sets_of_min_pref_max = aconf.gex_test.split(',')
out.v("A separate test will be performed against each of the following sets of 'min:pref:max' modulus sizes: " + ', '.join(sets_of_min_pref_max), write_now=True) out.v("A separate test will be performed against each of the following sets of 'min:pref:max' modulus sizes: " + ', '.join(sets_of_min_pref_max), write_now=True)
for set_of_min_pref_max in sets_of_min_pref_max: for set_of_min_pref_max in sets_of_min_pref_max:
bits_in_list_to_test = set_of_min_pref_max.split(':') bits_in_list_to_test = set_of_min_pref_max.split(':')
program_retval = GEXTest.modulus_size_test(out, s, kex, int(bits_in_list_to_test[0]), int(bits_in_list_to_test[1]), int(bits_in_list_to_test[2]), mod_dict) program_retval = GEXTest.granular_modulus_size_test(out, s, kex, int(bits_in_list_to_test[0]), int(bits_in_list_to_test[1]), int(bits_in_list_to_test[2]), mod_dict)
if program_retval != exitcodes.GOOD: if program_retval != exitcodes.GOOD:
return program_retval return program_retval
@ -1150,7 +1156,7 @@ def invoke_modulus_size_test(out: OutputBuffer, s: 'SSH_Socket', kex: 'SSH2_Kex'
for key, value in mod_dict.items(): for key, value in mod_dict.items():
padding = (max_key_len - len(key)) + 1 padding = (max_key_len - len(key)) + 1
out.info(key + " " * padding + '--> ' + ', '.join(map(str, value))) out.info(key + " " * padding + '--> ' + ', '.join([str(i) for i in value]))
return program_retval return program_retval

View File

@ -1,4 +1,4 @@
.TH SSH-AUDIT 1 "February 13, 2022" .TH SSH-AUDIT 1 "March 13, 2022"
.SH NAME .SH NAME
\fBssh-audit\fP \- SSH server & client configuration auditor \fBssh-audit\fP \- SSH server & client configuration auditor
.SH SYNOPSIS .SH SYNOPSIS
@ -52,19 +52,13 @@ Starts a server on port 2222 to audit client software configuration. Use -p/--p
Enable debug output. Enable debug output.
.TP .TP
.B -g, \-\-gex-test=<n[,n,...] | min:pref:max[,min:pref:max,...] | n-n[:step]> .B -g, \-\-gex-test=<x[,y,...] | min1:pref1:max1[,min2:pref2:max2,...] | x-y[:step]>
.br .br
Runs a Diffie-Hellman Group Exchange modulus size test against a server. Runs a Diffie-Hellman Group Exchange modulus size test against a server.
Diffie-Hellman requires the client and server to agree on a generator value and Diffie-Hellman requires the client and server to agree on a generator value and a modulus value. In the "Group Exchange" implementation of Diffie-Hellman, the client specifies the size of the modulus in bits by providing the server with minimum, preferred and maximum values. The server then finds a group that best matches the client's request, returning the corresponding generator and modulus. For a full explanation of this process see RFC 4419 and its successors.
a modulus value. In the "Group Exchange" implementation of Diffie-Hellman, the
client specifies the size of the modulus in bits by providing the server with
minimum, preferred and maximum values. The server then finds a group that best
matches the client's request, returning the corresponding generator and modulus.
For a full explanation of this process see RFC 4419 and its successors.
This test acts as a client by providing an SSH server with the size of a modulus This test acts as a client by providing an SSH server with the size of a modulus and then obtains the size of the modulus returned by the server.
and then obtains the size of the modulus returned by the server.
Three types of syntax are supported: Three types of syntax are supported:
@ -73,14 +67,13 @@ Three types of syntax are supported:
A comma delimited list of modulus sizes. A comma delimited list of modulus sizes.
A test is performed against each value in the list where it acts as the minimum, preferred and maximum modulus size. A test is performed against each value in the list where it acts as the minimum, preferred and maximum modulus size.
2. <min:pref:max[,min:pref:max,...]> 2. <min:pref:max[,min:pref:max,...]>
A set of three colon delimited values denoting minimum, preferred and maximum modulus size. A set of three colon delimited values denoting minimum, preferred and maximum modulus size.
A test is performed against each set. A test is performed against each set.
Multiple sets can specified as a comma separated list. Multiple sets can specified as a comma separated list.
3. <n-n[:step]> 3. <x-y[:step]>
A range of modulus sizes with an optional step value. Step defaults to 1 if omitted. A range of modulus sizes with an optional step value. Step defaults to 1 if omitted.
If the left value is greater than the right value, then the sequence operates from right to left. If the left value is greater than the right value, then the sequence operates from right to left.