mirror of
https://github.com/jtesta/ssh-audit.git
synced 2026-05-25 23:41:22 +02:00
Compare commits
5 Commits
5978bb2394
...
ca08928fae
| Author | SHA1 | Date | |
|---|---|---|---|
| ca08928fae | |||
| 06ebdbd0fe | |||
| 7752023dc2 | |||
| a6f02ae8e8 | |||
| b2e621cafc |
@@ -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)).
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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.'''
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user