5 Commits

Author SHA1 Message Date
Bandit Pingu ca08928fae Merge b2e621cafc into 06ebdbd0fe 2024-08-27 17:24:28 +01:00
Joe Testa 06ebdbd0fe Updated README. 2024-08-26 16:46:34 -04:00
Drew Noel 7752023dc2 Switch connect_ex result checks to use errno lookups (#289)
* Switch connect_ex result checks to errno lookups

* Return errno strings, clean up comment
2024-08-26 16:38:44 -04:00
Joe Testa a6f02ae8e8 Added debugging output for key exchanges. 2024-08-26 16:25:32 -04:00
FlyingFish b2e621cafc Modified OutputBuffer to have an error function to output to stderr. Change .fail with errors to .error 2024-05-01 23:00:25 +01:00
8 changed files with 53 additions and 19 deletions
+1
View File
@@ -220,6 +220,7 @@ For convenience, a web front-end on top of the command-line tool is available at
- Added LANcom LCOS server recognition and Ed448 key extraction; credit [Daniel Lenski](https://github.com/dlenskiSB). - Added LANcom LCOS server recognition and Ed448 key extraction; credit [Daniel Lenski](https://github.com/dlenskiSB).
- Fixed crash when running with `-P` and `-T` options simultaneously. - Fixed crash when running with `-P` and `-T` options simultaneously.
- Fixed host key tests from only reporting a key type at most once despite multiple hosts supporting it; credit [Daniel Lenski](https://github.com/dlenskiSB). - Fixed host key tests from only reporting a key type at most once despite multiple hosts supporting it; credit [Daniel Lenski](https://github.com/dlenskiSB).
- Fixed DHEat connection rate testing on MacOS X and BSD platforms; credit [Drew Noel](https://github.com/drewmnoel) and [Michael Osipov](https://github.com/michael-o).
### v3.2.0 (2024-04-22) ### v3.2.0 (2024-04-22)
- Added implementation of the DHEat denial-of-service attack (see `--dheat` option; [CVE-2002-20001](https://nvd.nist.gov/vuln/detail/CVE-2002-20001)). - Added implementation of the DHEat denial-of-service attack (see `--dheat` option; [CVE-2002-20001](https://nvd.nist.gov/vuln/detail/CVE-2002-20001)).
+3 -2
View File
@@ -21,6 +21,7 @@
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 errno
import multiprocessing import multiprocessing
import os import os
import queue import queue
@@ -442,10 +443,10 @@ class DHEat:
# out.d("Creating socket (%u of %u already exist)..." % (len(socket_dict), concurrent_sockets), write_now=True) # out.d("Creating socket (%u of %u already exist)..." % (len(socket_dict), concurrent_sockets), write_now=True)
ret = s.connect_ex((target_ip_address, aconf.port)) ret = s.connect_ex((target_ip_address, aconf.port))
num_attempted_connections += 1 num_attempted_connections += 1
if ret in [0, 115]: # Check if connection is successful or EINPROGRESS. if ret in [0, errno.EINPROGRESS]:
socket_dict[s] = now socket_dict[s] = now
else: else:
out.d("connect_ex() returned: %d" % ret, write_now=True) out.d("connect_ex() returned: %s (%d)" % (os.strerror(ret), ret), write_now=True)
# out.d("Calling select() on %u sockets..." % len(socket_dict), write_now=True) # out.d("Calling select() on %u sockets..." % len(socket_dict), write_now=True)
socket_list: List[socket.socket] = [*socket_dict] # Get a list of sockets from the dictionary. socket_list: List[socket.socket] = [*socket_dict] # Get a list of sockets from the dictionary.
+1 -1
View File
@@ -110,7 +110,7 @@ class GEXTest:
# before continuing to issue reconnects. # before continuing to issue reconnects.
modulus_size_returned, reconnect_failed = GEXTest._send_init(out, s, kex_group, kex, gex_alg, bits_min, bits_pref, bits_max) modulus_size_returned, reconnect_failed = GEXTest._send_init(out, s, kex_group, kex, gex_alg, bits_min, bits_pref, bits_max)
if reconnect_failed: if reconnect_failed:
out.fail('Reconnect failed.') out.error('Reconnect failed.')
return exitcodes.FAILURE return exitcodes.FAILURE
if modulus_size_returned > 0: if modulus_size_returned > 0:
+8
View File
@@ -54,6 +54,14 @@ class OutputBuffer:
self.__is_color_supported = ('colorama' in sys.modules) or (os.name == 'posix') self.__is_color_supported = ('colorama' in sys.modules) or (os.name == 'posix')
self.line_ended = True self.line_ended = True
def error(self, msg, line_ended=True):
"""
Writes an error message to stderr.
"""
end = '' if line_ended else '\n'
sys.stderr.write(f'{msg}{end}')
sys.stderr.flush()
def _print(self, level: str, s: str = '', line_ended: bool = True) -> None: def _print(self, level: str, s: str = '', line_ended: bool = True) -> None:
'''Saves output to buffer (if in buffered mode), or immediately prints to stdout otherwise.''' '''Saves output to buffer (if in buffered mode), or immediately prints to stdout otherwise.'''
+14 -1
View File
@@ -1,7 +1,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (C) 2017-2020 Joe Testa (jtesta@positronsecurity.com) Copyright (C) 2017-2024 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu) Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -132,3 +132,16 @@ class SSH2_Kex:
srv = SSH2_KexParty(srv_enc, srv_mac, srv_compression, srv_languages) srv = SSH2_KexParty(srv_enc, srv_mac, srv_compression, srv_languages)
kex = cls(outputbuffer, cookie, kex_algs, key_algs, cli, srv, follows, unused) kex = cls(outputbuffer, cookie, kex_algs, key_algs, cli, srv, follows, unused)
return kex return kex
def __str__(self) -> str:
ret = "----\nSSH2_Kex object:"
ret += "\nHost keys: "
ret += ", ".join(self.__key_algs)
ret += "\nKey exchanges: "
ret += ", ".join(self.__kex_algs)
ret += "\nClient SSH2_KexParty:"
ret += "\n" + str(self.__client)
ret += "\nServer SSH2_KexParty:"
ret += "\n" + str(self.__server)
ret += "\n----"
return ret
+8
View File
@@ -1,6 +1,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (C) 2024 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu) Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -48,3 +49,10 @@ class SSH2_KexParty:
@property @property
def languages(self) -> List[str]: def languages(self) -> List[str]:
return self.__languages return self.__languages
def __str__(self) -> str:
ret = "Ciphers: " + ", ".join(self.__enc)
ret += "\nMACs: " + ", ".join(self.__mac)
ret += "\nCompressions: " + ", ".join(self.__compression)
ret += "\nLanguages: " + ", ".join(self.__languages)
return ret
+10 -9
View File
@@ -2,7 +2,7 @@
""" """
The MIT License (MIT) The MIT License (MIT)
Copyright (C) 2017-2023 Joe Testa (jtesta@positronsecurity.com) Copyright (C) 2017-2024 Joe Testa (jtesta@positronsecurity.com)
Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu) Copyright (C) 2017 Andris Raugulis (moo@arthepsy.eu)
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -88,7 +88,7 @@ def usage(uout: OutputBuffer, err: Optional[str] = None) -> None:
p = os.path.basename(sys.argv[0]) p = os.path.basename(sys.argv[0])
uout.head('# {} {}, https://github.com/jtesta/ssh-audit\n'.format(p, VERSION)) uout.head('# {} {}, https://github.com/jtesta/ssh-audit\n'.format(p, VERSION))
if err is not None and len(err) > 0: if err is not None and len(err) > 0:
uout.fail(err + '\n') uout.error(err + '\n')
retval = exitcodes.UNKNOWN_ERROR retval = exitcodes.UNKNOWN_ERROR
uout.info('usage: {0} [options] <host>\n'.format(p)) uout.info('usage: {0} [options] <host>\n'.format(p))
uout.info(' -h, --help print this help') uout.info(' -h, --help print this help')
@@ -835,7 +835,7 @@ def list_policies(out: OutputBuffer, verbose: bool) -> None:
out.sep() out.sep()
if len(server_policy_names) == 0 and len(client_policy_names) == 0: if len(server_policy_names) == 0 and len(client_policy_names) == 0:
out.fail("Error: no built-in policies found!") out.error("Error: no built-in policies found!")
else: else:
out.info("\nHint: Use -P and provide the full name of a policy to run a policy scan with.\n") out.info("\nHint: Use -P and provide the full name of a policy to run a policy scan with.\n")
out.info("Hint: Use -L -v to also see the change log for each policy.\n") out.info("Hint: Use -L -v to also see the change log for each policy.\n")
@@ -1051,19 +1051,19 @@ def process_commandline(out: OutputBuffer, args: List[str], usage_cb: Callable[.
try: try:
aconf.policy = Policy(policy_file=aconf.policy_file, json_output=aconf.json) aconf.policy = Policy(policy_file=aconf.policy_file, json_output=aconf.json)
except Exception as e: except Exception as e:
out.fail("Error while loading policy file: %s: %s" % (str(e), traceback.format_exc())) out.error("Error while loading policy file: %s: %s" % (str(e), traceback.format_exc()))
out.write() out.write()
sys.exit(exitcodes.UNKNOWN_ERROR) sys.exit(exitcodes.UNKNOWN_ERROR)
# If the user wants to do a client audit, but provided a server policy, terminate. # 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(): if aconf.client_audit and aconf.policy.is_server_policy():
out.fail("Error: client audit selected, but server policy provided.") out.error("Error: client audit selected, but server policy provided.")
out.write() out.write()
sys.exit(exitcodes.UNKNOWN_ERROR) sys.exit(exitcodes.UNKNOWN_ERROR)
# If the user wants to do a server audit, but provided a client policy, terminate. # 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: if aconf.client_audit is False and aconf.policy.is_server_policy() is False:
out.fail("Error: server audit selected, but client policy provided.") out.error("Error: server audit selected, but client policy provided.")
out.write() out.write()
sys.exit(exitcodes.UNKNOWN_ERROR) sys.exit(exitcodes.UNKNOWN_ERROR)
@@ -1262,7 +1262,7 @@ def audit(out: OutputBuffer, aconf: AuditConf, sshv: Optional[int] = None, print
err = s.connect() err = s.connect()
if err is not None: if err is not None:
out.fail(err) out.error(err)
# If we're running against multiple targets, return a connection error to the calling worker thread. Otherwise, write the error message to the console and exit. # If we're running against multiple targets, return a connection error to the calling worker thread. Otherwise, write the error message to the console and exit.
if len(aconf.target_list) > 0: if len(aconf.target_list) > 0:
@@ -1310,15 +1310,16 @@ def audit(out: OutputBuffer, aconf: AuditConf, sshv: Optional[int] = None, print
err = fmt.format(err_pair[0], err_pair[1], packet_type) err = fmt.format(err_pair[0], err_pair[1], packet_type)
if err is not None: if err is not None:
output(out, aconf, banner, header) output(out, aconf, banner, header)
out.fail(err) out.error(err)
return exitcodes.CONNECTION_ERROR return exitcodes.CONNECTION_ERROR
if sshv == 1: if sshv == 1:
program_retval = output(out, aconf, banner, header, pkm=SSH1_PublicKeyMessage.parse(payload)) program_retval = output(out, aconf, banner, header, pkm=SSH1_PublicKeyMessage.parse(payload))
elif sshv == 2: elif sshv == 2:
try: try:
kex = SSH2_Kex.parse(out, payload) kex = SSH2_Kex.parse(out, payload)
out.d(str(kex))
except Exception: except Exception:
out.fail("Failed to parse server's kex. Stack trace:\n%s" % str(traceback.format_exc())) out.error("Failed to parse server's kex. Stack trace:\n%s" % str(traceback.format_exc()))
return exitcodes.CONNECTION_ERROR return exitcodes.CONNECTION_ERROR
if aconf.dheat is not None: if aconf.dheat is not None:
+8 -6
View File
@@ -108,7 +108,8 @@ class SSH_Socket(ReadBuf, WriteBuf):
s.listen() s.listen()
self.__sock_map[s.fileno()] = s self.__sock_map[s.fileno()] = s
except Exception as e: except Exception as e:
print("Warning: failed to listen on any IPv4 interfaces: %s" % str(e)) self.__outputbuffer.error("Warning: failed to listen on any IPv4 interfaces: %s" % str(e))
sys.exit(exitcodes.CONNECTION_ERROR)
try: try:
# Socket to listen on all IPv6 addresses. # Socket to listen on all IPv6 addresses.
@@ -119,11 +120,12 @@ class SSH_Socket(ReadBuf, WriteBuf):
s.listen() s.listen()
self.__sock_map[s.fileno()] = s self.__sock_map[s.fileno()] = s
except Exception as e: except Exception as e:
print("Warning: failed to listen on any IPv6 interfaces: %s" % str(e)) self.__outputbuffer.error("Warning: failed to listen on any IPv6 interfaces: %s" % str(e))
sys.exit(exitcodes.CONNECTION_ERROR)
# If we failed to listen on any interfaces, terminate. # If we failed to listen on any interfaces, terminate.
if len(self.__sock_map.keys()) == 0: if len(self.__sock_map.keys()) == 0:
print("Error: failed to listen on any IPv4 and IPv6 interfaces!") self.__outputbuffer.error("Error: failed to listen on any IPv4 and IPv6 interfaces!")
sys.exit(exitcodes.CONNECTION_ERROR) sys.exit(exitcodes.CONNECTION_ERROR)
# Wait for an incoming connection. If a timeout was explicitly # Wait for an incoming connection. If a timeout was explicitly
@@ -141,7 +143,7 @@ class SSH_Socket(ReadBuf, WriteBuf):
break break
if self.__timeout_set and time_elapsed >= self.__timeout: if self.__timeout_set and time_elapsed >= self.__timeout:
print("Timeout elapsed. Terminating...") self.__outputbuffer.error("Timeout elapsed. Terminating...")
sys.exit(exitcodes.CONNECTION_ERROR) sys.exit(exitcodes.CONNECTION_ERROR)
# Accept the connection. # Accept the connection.
@@ -275,7 +277,7 @@ class SSH_Socket(ReadBuf, WriteBuf):
payload_length = packet_length - padding_length - 1 payload_length = packet_length - padding_length - 1
check_size = 4 + 1 + payload_length + padding_length check_size = 4 + 1 + payload_length + padding_length
if check_size % self.__block_size != 0: if check_size % self.__block_size != 0:
self.__outputbuffer.fail('[exception] invalid ssh packet (block size)').write() self.__outputbuffer.error('[exception] invalid ssh packet (block size)').write()
sys.exit(exitcodes.CONNECTION_ERROR) sys.exit(exitcodes.CONNECTION_ERROR)
self.ensure_read(payload_length) self.ensure_read(payload_length)
if sshv == 1: if sshv == 1:
@@ -290,7 +292,7 @@ class SSH_Socket(ReadBuf, WriteBuf):
if sshv == 1: if sshv == 1:
rcrc = SSH1.crc32(padding + payload) rcrc = SSH1.crc32(padding + payload)
if crc != rcrc: if crc != rcrc:
self.__outputbuffer.fail('[exception] packet checksum CRC32 mismatch.').write() self.__outputbuffer.error('[exception] packet checksum CRC32 mismatch.').write()
sys.exit(exitcodes.CONNECTION_ERROR) sys.exit(exitcodes.CONNECTION_ERROR)
else: else:
self.ensure_read(padding_length) self.ensure_read(padding_length)