mirror of
https://github.com/jtesta/ssh-audit.git
synced 2026-07-24 11:35:38 +02:00
Compare commits
5
Commits
1eb11050b3
...
v3.9.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbf8b69633 | ||
|
|
13fd7102d9 | ||
|
|
419df1b788 | ||
|
|
859cb7012e | ||
|
|
0f8bbdbfd9 |
@@ -252,7 +252,7 @@ For convenience, a web front-end on top of the command-line tool is available at
|
|||||||
|
|
||||||
## ChangeLog
|
## ChangeLog
|
||||||
|
|
||||||
### v3.4.0-dev
|
### v3.9.0 (2026-07-04)
|
||||||
- BIG THANKS to [realmiwi](https://github.com/realmiwi) for being the project's *very first sponsor!!*
|
- BIG THANKS to [realmiwi](https://github.com/realmiwi) for being the project's *very first sponsor!!*
|
||||||
- Added support for Python 3.14.
|
- Added support for Python 3.14.
|
||||||
- Dropped support for Python 3.8, as it reached end-of-life in October 2024.
|
- Dropped support for Python 3.8, as it reached end-of-life in October 2024.
|
||||||
@@ -267,12 +267,12 @@ For convenience, a web front-end on top of the command-line tool is available at
|
|||||||
- Added UNIX server socket scanning (specify the target with `unix:///path/to/socket`).
|
- Added UNIX server socket scanning (specify the target with `unix:///path/to/socket`).
|
||||||
- Added SOCKS5 proxy support (specify the proxy with `--socks5 host:port`); partial credit [Michał Majchrowicz](https://github.com/sectroyer).
|
- Added SOCKS5 proxy support (specify the proxy with `--socks5 host:port`); partial credit [Michał Majchrowicz](https://github.com/sectroyer).
|
||||||
- Updated built-in policy for Debian 12.
|
- Updated built-in policy for Debian 12.
|
||||||
- Added built-in policies for OpenSSH 10.0, 10.1, 10.2, and 10.3.
|
- Added built-in policies for OpenSSH 10.0, 10.1, 10.2, 10.3, and 10.4.
|
||||||
- Added hardening guides and policies for Debian 13.
|
- Added hardening guides and policies for Debian 13.
|
||||||
- Added hardening guides and policies for Rocky Linux 10.
|
- Added hardening guides and policies for Rocky Linux 10.
|
||||||
- Added hardening guides and policies for Ubuntu 26.04.
|
- Added hardening guides and policies for Ubuntu 26.04.
|
||||||
- Added 2 new key exchanges: `mlkem768nistp256-sha256`, `mlkem1024nistp384-sha384`.
|
- Added 2 new key exchanges: `mlkem768nistp256-sha256`, `mlkem1024nistp384-sha384`.
|
||||||
- Added 10 new host keys: `webauthn-sk-ecdsa-sha2-nistp256-cert-v01@openssh.com`, `mldsa-44`, `mldsa-65`, `mldsa-87`, `ssh-mldsa-44`, `ssh-mldsa-65`, `ssh-mldsa-87`, `ssh-mldsa44`, `ssh-mldsa65`, `ssh-mldsa87`.
|
- Added 11 new host keys: `webauthn-sk-ecdsa-sha2-nistp256-cert-v01@openssh.com`, `mldsa-44`, `ssh-mldsa44-ed25519@openssh.com`, `mldsa-65`, `mldsa-87`, `ssh-mldsa-44`, `ssh-mldsa-65`, `ssh-mldsa-87`, `ssh-mldsa44`, `ssh-mldsa65`, `ssh-mldsa87`.
|
||||||
- Added 2 new ciphers: `AEAD_CAMELLIA_128_GCM`, `AEAD_CAMELLIA_256_GCM`.
|
- Added 2 new ciphers: `AEAD_CAMELLIA_128_GCM`, `AEAD_CAMELLIA_256_GCM`.
|
||||||
|
|
||||||
### v3.3.0 (2024-10-15)
|
### v3.3.0 (2024-10-15)
|
||||||
|
|||||||
@@ -117,8 +117,6 @@ class Algorithms:
|
|||||||
sshv, alg_db = alg_pair.sshv, alg_pair.db
|
sshv, alg_db = alg_pair.sshv, alg_pair.db
|
||||||
rec[sshv] = {}
|
rec[sshv] = {}
|
||||||
for alg_type, alg_list in alg_pair.items():
|
for alg_type, alg_list in alg_pair.items():
|
||||||
if alg_type == 'aut':
|
|
||||||
continue
|
|
||||||
rec[sshv][alg_type] = {'add': {}, 'del': {}, 'chg': {}}
|
rec[sshv][alg_type] = {'add': {}, 'del': {}, 'chg': {}}
|
||||||
for n, alg_desc in alg_db[alg_type].items():
|
for n, alg_desc in alg_db[alg_type].items():
|
||||||
versions = alg_desc[0]
|
versions = alg_desc[0]
|
||||||
|
|||||||
@@ -125,6 +125,10 @@ BUILTIN_POLICIES: Dict[str, Dict[str, Union[Optional[str], Optional[List[str]],
|
|||||||
|
|
||||||
'Hardened OpenSSH Server v10.3 (version 1)': {'version': '1', 'changelog': 'Initial version.', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['mlkem768x25519-sha256', 'sntrup761x25519-sha512', 'sntrup761x25519-sha512@openssh.com', 'ext-info-s', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-gcm@openssh.com', 'aes128-ctr'], 'macs': ['hmac-sha2-512-etm@openssh.com', 'hmac-sha2-256-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {}, 'server_policy': True},
|
'Hardened OpenSSH Server v10.3 (version 1)': {'version': '1', 'changelog': 'Initial version.', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['mlkem768x25519-sha256', 'sntrup761x25519-sha512', 'sntrup761x25519-sha512@openssh.com', 'ext-info-s', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-gcm@openssh.com', 'aes128-ctr'], 'macs': ['hmac-sha2-512-etm@openssh.com', 'hmac-sha2-256-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {}, 'server_policy': True},
|
||||||
|
|
||||||
|
# Note that the "ssh-mldsa44-ed25519@openssh.com" host key was added in this release as an experimental non-default option. Once it is no longer considered experimental by the OpenSSH developers, it will become a requirement.
|
||||||
|
'Hardened OpenSSH Server v10.4 (version 1)': {'version': '1', 'changelog': 'Initial version.', 'banner': None, 'compressions': None, 'host_keys': ['rsa-sha2-512', 'rsa-sha2-256', 'ssh-ed25519'], 'optional_host_keys': ['sk-ssh-ed25519@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'sk-ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com'], 'kex': ['mlkem768x25519-sha256', 'sntrup761x25519-sha512', 'sntrup761x25519-sha512@openssh.com', 'ext-info-s', 'kex-strict-s-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-gcm@openssh.com', 'aes128-ctr'], 'macs': ['hmac-sha2-512-etm@openssh.com', 'hmac-sha2-256-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': {"rsa-sha2-256": {"hostkey_size": 4096}, "rsa-sha2-256-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "rsa-sha2-512": {"hostkey_size": 4096}, "rsa-sha2-512-cert-v01@openssh.com": {"ca_key_size": 4096, "ca_key_type": "ssh-rsa", "hostkey_size": 4096}, "sk-ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}, "sk-ssh-ed25519@openssh.com": {"hostkey_size": 256}, "ssh-ed25519": {"hostkey_size": 256}, "ssh-ed25519-cert-v01@openssh.com": {"ca_key_size": 256, "ca_key_type": "ssh-ed25519", "hostkey_size": 256}}, 'dh_modulus_sizes': {}, 'server_policy': True},
|
||||||
|
|
||||||
|
|
||||||
# Amazon Linux Policies
|
# Amazon Linux Policies
|
||||||
|
|
||||||
'Hardened Amazon Linux Client 2023 (version 1)': {'version': '1', 'changelog': 'Initial version.', 'banner': None, 'compressions': None, 'host_keys': ['sk-ssh-ed25519-cert-v01@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'sk-ssh-ed25519@openssh.com', 'ssh-ed25519', 'rsa-sha2-512', 'rsa-sha2-256'], 'optional_host_keys': None, 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'ext-info-c', 'kex-strict-c-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'dh_modulus_sizes': None, 'server_policy': False},
|
'Hardened Amazon Linux Client 2023 (version 1)': {'version': '1', 'changelog': 'Initial version.', 'banner': None, 'compressions': None, 'host_keys': ['sk-ssh-ed25519-cert-v01@openssh.com', 'ssh-ed25519-cert-v01@openssh.com', 'rsa-sha2-512-cert-v01@openssh.com', 'rsa-sha2-256-cert-v01@openssh.com', 'sk-ssh-ed25519@openssh.com', 'ssh-ed25519', 'rsa-sha2-512', 'rsa-sha2-256'], 'optional_host_keys': None, 'kex': ['sntrup761x25519-sha512@openssh.com', 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'diffie-hellman-group16-sha512', 'diffie-hellman-group18-sha512', 'diffie-hellman-group-exchange-sha256', 'ext-info-c', 'kex-strict-c-v00@openssh.com'], 'ciphers': ['chacha20-poly1305@openssh.com', 'aes256-gcm@openssh.com', 'aes128-gcm@openssh.com', 'aes256-ctr', 'aes192-ctr', 'aes128-ctr'], 'macs': ['hmac-sha2-256-etm@openssh.com', 'hmac-sha2-512-etm@openssh.com', 'umac-128-etm@openssh.com'], 'hostkey_sizes': None, 'dh_modulus_sizes': None, 'server_policy': False},
|
||||||
|
|||||||
+67
-37
@@ -194,7 +194,9 @@ class DHEat:
|
|||||||
self.num_connect_timeouts = 0
|
self.num_connect_timeouts = 0
|
||||||
self.num_read_timeouts = 0
|
self.num_read_timeouts = 0
|
||||||
self.num_socket_exceptions = 0
|
self.num_socket_exceptions = 0
|
||||||
self.num_openssh_throttled_connections = 0
|
self.num_em = 0 # Number of "Exceeded MaxStartups" messages.
|
||||||
|
self.num_naatt = 0 # Number of "No allowed at this time" messages.
|
||||||
|
self.num_invalid_banners = 0
|
||||||
|
|
||||||
# The time we started the attack.
|
# The time we started the attack.
|
||||||
self.start_timer = 0.0
|
self.start_timer = 0.0
|
||||||
@@ -351,10 +353,10 @@ class DHEat:
|
|||||||
|
|
||||||
rate_str = ""
|
rate_str = ""
|
||||||
if aconf.conn_rate_test_target_rate > 0:
|
if aconf.conn_rate_test_target_rate > 0:
|
||||||
rate_str = " at a max rate of %s%u%s connections per second" % (DHEat.WHITEB, aconf.conn_rate_test_target_rate, DHEat.CLEAR)
|
rate_str = f" at a max rate of {DHEat.WHITEB}{aconf.conn_rate_test_target_rate:d}{DHEat.CLEAR} connections per second"
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print("Performing non-disruptive rate test against %s[%s]:%u%s with %s%u%s concurrent sockets%s. No Diffie-Hellman requests will be sent." % (DHEat.WHITEB, target_ip_address, aconf.port, DHEat.CLEAR, DHEat.WHITEB, concurrent_sockets, DHEat.CLEAR, rate_str))
|
print(f"Performing non-disruptive rate test against {DHEat.WHITEB}[{target_ip_address}]:{aconf.port:d}{DHEat.CLEAR} with {DHEat.WHITEB}{concurrent_sockets:d}{DHEat.CLEAR} concurrent sockets{rate_str}. No Diffie-Hellman requests will be sent.\n")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
# Make room for the multi-line output.
|
# Make room for the multi-line output.
|
||||||
@@ -369,14 +371,17 @@ class DHEat:
|
|||||||
server_dh_kex.append(server_kex)
|
server_dh_kex.append(server_kex)
|
||||||
|
|
||||||
if len(server_dh_kex) == 0:
|
if len(server_dh_kex) == 0:
|
||||||
out.d("Skipping DHEat.dh_rate_test() since server does not support any DH algorithms: [%s]" % ", ".join(kex.kex_algorithms))
|
kex_list = ", ".join(kex.kex_algorithms)
|
||||||
|
out.d(f"Skipping DHEat.dh_rate_test() since server does not support any DH algorithms: [{kex_list}]")
|
||||||
return ""
|
return ""
|
||||||
else:
|
else:
|
||||||
out.d("DHEat.dh_rate_test(): starting test; parameters: %f seconds, %u max connections, %u concurrent sockets." % (max_time, max_connections, concurrent_sockets), write_now=True)
|
out.d(f"DHEat.dh_rate_test(): starting test; parameters: {max_time:,.1f} seconds, {max_connections:,d} max connections, {concurrent_sockets:,d} concurrent sockets.", write_now=True)
|
||||||
|
|
||||||
num_attempted_connections = 0
|
num_attempted_connections = 0
|
||||||
num_opened_connections = 0
|
num_opened_connections = 0
|
||||||
num_exceeded_maxstartups = 0
|
num_em = 0 # Number of "Exceeded MaxStartups" messages.
|
||||||
|
num_naatt = 0 # Number of "Not available at this time" messages.
|
||||||
|
num_invalid_banners = 0
|
||||||
socket_dict: Dict[socket.socket, float] = {}
|
socket_dict: Dict[socket.socket, float] = {}
|
||||||
start_timer = time.time()
|
start_timer = time.time()
|
||||||
now = start_timer
|
now = start_timer
|
||||||
@@ -395,13 +400,15 @@ class DHEat:
|
|||||||
if (now - last_update) >= 1.0:
|
if (now - last_update) >= 1.0:
|
||||||
seconds_running = now - start_timer
|
seconds_running = now - start_timer
|
||||||
if multiline_output:
|
if multiline_output:
|
||||||
print("\033[5ARun time: %s%.1f%s seconds" % (DHEat.WHITEB, seconds_running, DHEat.CLEAR))
|
print(f"\033[7A Run time: {DHEat.WHITEB}{seconds_running:,.1f}{DHEat.CLEAR} seconds")
|
||||||
print("TCP SYNs: %s%u%s (total); %s%.1f%s (per second)" % (DHEat.WHITEB, num_attempted_connections, DHEat.CLEAR, DHEat.BLUEB, num_attempted_connections / seconds_running, DHEat.CLEAR))
|
print(f" TCP SYNs: {DHEat.WHITEB}{num_attempted_connections / seconds_running:,.1f}/sec{DHEat.CLEAR}; {DHEat.WHITEB}{num_attempted_connections:,d}{DHEat.CLEAR} total ")
|
||||||
print("Completed connections: %s%u%s (total); %s%.1f%s (per second)" % (DHEat.WHITEB, num_opened_connections, DHEat.CLEAR, DHEat.BLUEB, num_opened_connections / seconds_running, DHEat.CLEAR))
|
print(f" Completed connections: {DHEat.WHITEB}{num_opened_connections / seconds_running:,.1f}/sec{DHEat.CLEAR}; {DHEat.WHITEB}{num_opened_connections:,d}{DHEat.CLEAR} total ")
|
||||||
print("\"Exceeded MaxStartups\" responses: %s%u%s (total); %s%.1f%s (per second)" % (DHEat.WHITEB, num_exceeded_maxstartups, DHEat.CLEAR, DHEat.BLUEB, num_exceeded_maxstartups / seconds_running, DHEat.CLEAR))
|
print(f" \"Exceeded MaxStartups\" responses: {DHEat.WHITEB}{num_em / seconds_running:,.1f}/sec{DHEat.CLEAR}; {DHEat.WHITEB}{num_em:,d}{DHEat.CLEAR} total ")
|
||||||
print("%s%s%s" % (DHEat.WHITEB, spinner[spinner_index], DHEat.CLEAR))
|
print(f"\"Not allowed at this time\" responses: {DHEat.WHITEB}{num_naatt / seconds_running:,.1f}/sec{DHEat.CLEAR}; {DHEat.WHITEB}{num_naatt:,d}{DHEat.CLEAR} total ")
|
||||||
|
print(f" Invalid banners: {DHEat.WHITEB}{num_invalid_banners / seconds_running:,.1f}/sec{DHEat.CLEAR}; {DHEat.WHITEB}{num_invalid_banners:,d}{DHEat.CLEAR} total ")
|
||||||
|
print(DHEat.WHITEB + spinner[spinner_index] + DHEat.CLEAR)
|
||||||
else:
|
else:
|
||||||
print("%s%s%s Run time: %s%.1f%s; TCP SYNs: %s%u%s; Compl. conns: %s%u%s; TCP SYNs/sec: %s%.1f%s; Compl. conns/sec: %s%.1f%s \r" % (DHEat.WHITEB, spinner[spinner_index], DHEat.CLEAR, DHEat.WHITEB, seconds_running, DHEat.CLEAR, DHEat.WHITEB, num_attempted_connections, DHEat.CLEAR, DHEat.WHITEB, num_opened_connections, DHEat.CLEAR, DHEat.BLUEB, num_attempted_connections / seconds_running, DHEat.CLEAR, DHEat.BLUEB, num_opened_connections / seconds_running, DHEat.CLEAR), end="")
|
print(f"{DHEat.WHITEB}{spinner[spinner_index]}{DHEat.CLEAR} Run time: {DHEat.WHITEB}{seconds_running:,.1f}{DHEat.CLEAR}; TCP SYNs: {DHEat.WHITEB}{num_attempted_connections:,d}{DHEat.CLEAR}; Compl. conns: {DHEat.WHITEB}{num_opened_connections:,d}{DHEat.CLEAR}; TCP SYNs/sec: {DHEat.BLUEB}{num_attempted_connections / seconds_running:,.1f}{DHEat.CLEAR}; Compl. conns/sec: {DHEat.BLUEB}{num_opened_connections / seconds_running:,.1f}{DHEat.CLEAR} \r", end="")
|
||||||
last_update = now
|
last_update = now
|
||||||
spinner_index = (spinner_index + 1) % 4
|
spinner_index = (spinner_index + 1) % 4
|
||||||
|
|
||||||
@@ -467,8 +474,14 @@ class DHEat:
|
|||||||
num_opened_connections += 1
|
num_opened_connections += 1
|
||||||
# out.d("Number of opened connections: %u (max: %u)." % (num_opened_connections, max_connections))
|
# out.d("Number of opened connections: %u (max: %u)." % (num_opened_connections, max_connections))
|
||||||
elif buf == b"Exceeded":
|
elif buf == b"Exceeded":
|
||||||
num_exceeded_maxstartups += 1
|
num_em += 1
|
||||||
# out.d("Number of \"Exceeded MaxStartups\": %u" % num_exceeded_maxstartups)
|
# out.d("Number of \"Exceeded MaxStartups\": %u" % num_exceeded_maxstartups)
|
||||||
|
elif buf == b"Not allo":
|
||||||
|
num_naatt += 1
|
||||||
|
# out.d("Number of \"Not allowed at this time\": {num_naatt:,d}")
|
||||||
|
else:
|
||||||
|
num_invalid_banners += 1
|
||||||
|
# out.d("Number of invalid banners: {num_invalid_banners:,d}")
|
||||||
|
|
||||||
_close_socket(socket_dict, s)
|
_close_socket(socket_dict, s)
|
||||||
|
|
||||||
@@ -720,35 +733,39 @@ class DHEat:
|
|||||||
# Print extensive statistics on what just happened.
|
# Print extensive statistics on what just happened.
|
||||||
seconds_running = time.time() - self.start_timer
|
seconds_running = time.time() - self.start_timer
|
||||||
print("\n\n")
|
print("\n\n")
|
||||||
print(" %s %sSTATISTICS%s %s" % (self.BAR_CHART, self.WHITEB, self.CLEAR, self.CHART_UPWARDS))
|
print(f" {self.BAR_CHART} {self.WHITEB}STATISTICS{self.CLEAR} {self.CHART_UPWARDS}")
|
||||||
print(" %s----------%s" % (self.WHITEB, self.CLEAR))
|
print(f" {self.WHITEB}----------{self.CLEAR}")
|
||||||
print()
|
print()
|
||||||
print(" Run time: %s%.1f seconds%s" % (self.WHITEB, seconds_running, self.CLEAR))
|
print(f" Run time: {self.WHITEB}{seconds_running:,.1f} seconds{self.CLEAR}")
|
||||||
print()
|
print()
|
||||||
print(" Attempted TCP connections: %s%.1f/sec, %u total%s" % (self.WHITEB, self.num_attempted_tcp_connections / seconds_running, self.num_attempted_tcp_connections, self.CLEAR))
|
print(f" Attempted TCP connections: {self.WHITEB}{self.num_attempted_tcp_connections / seconds_running:,.1f}/sec, {self.num_attempted_tcp_connections:,d} total{self.CLEAR}")
|
||||||
print(" Successful TCP connections: %s%.1f/sec, %u total%s" % (self.WHITEB, self.num_successful_tcp_connections / seconds_running, self.num_successful_tcp_connections, self.CLEAR))
|
print(f" Successful TCP connections: {self.WHITEB}{self.num_successful_tcp_connections / seconds_running:,.1f}/sec, {self.num_successful_tcp_connections:,d} total{self.CLEAR}")
|
||||||
print()
|
print()
|
||||||
print(" Bytes written: %s%s/sec, %s total%s" % (self.WHITEB, DHEat.add_byte_units(self.num_bytes_written / seconds_running), DHEat.add_byte_units(self.num_bytes_written), self.CLEAR))
|
print(f" Bytes written: {self.WHITEB}{DHEat.add_byte_units(self.num_bytes_written / seconds_running)}/sec, {DHEat.add_byte_units(self.num_bytes_written)} total{self.CLEAR}")
|
||||||
print()
|
print()
|
||||||
print(" Successful DH KEX replies: %s%.1f/sec, %u total%s" % (self.WHITEB, self.num_successful_dh_kex / seconds_running, self.num_successful_dh_kex, self.CLEAR))
|
print(f" Successful DH KEX replies: {self.WHITEB}{self.num_successful_dh_kex / seconds_running:,.1f}/sec, {self.num_successful_dh_kex:,d} total{self.CLEAR}")
|
||||||
print(" Unexpected DH KEX replies: %s%.1f/sec, %u total%s" % (self.WHITEB, self.num_failed_dh_kex / seconds_running, self.num_failed_dh_kex, self.CLEAR))
|
print(f" Unexpected DH KEX replies: {self.WHITEB}{self.num_failed_dh_kex / seconds_running:,.1f}/sec, {self.num_failed_dh_kex:,d} total{self.CLEAR}")
|
||||||
print("\"Exceeded MaxStartups\" replies*: %s%.1f/sec, %u total%s" % (self.WHITEB, self.num_openssh_throttled_connections / seconds_running, self.num_openssh_throttled_connections, self.CLEAR))
|
print(f" \"Exceeded MaxStartups\" replies*: {self.WHITEB}{self.num_em / seconds_running:,.1f}/sec, {self.num_em:,d} total{self.CLEAR}")
|
||||||
|
print(f"\"Not allowed at this time\" replies+: {self.WHITEB}{self.num_naatt / seconds_running:,.1f}/sec, {self.num_naatt:,d} total{self.CLEAR}")
|
||||||
|
print(f" Invalid banners: {self.WHITEB}{self.num_invalid_banners / seconds_running:,.1f}/sec, {self.num_invalid_banners:,.1f} total{self.CLEAR}")
|
||||||
print()
|
print()
|
||||||
print(" Connection timeouts: %s%.1f/sec, %u total%s (timeout setting: %.1f sec)" % (self.WHITEB, self.num_connect_timeouts / seconds_running, self.num_connect_timeouts, self.CLEAR, self.connect_timeout))
|
print(f" Connection timeouts: {self.WHITEB}{self.num_connect_timeouts / seconds_running:,.1f}/sec, {self.num_connect_timeouts:,d} total{self.CLEAR} (timeout setting: {self.connect_timeout:,.1f} sec)")
|
||||||
print(" Read timeouts: %s%.1f/sec, %u total%s (timeout setting: %.1f sec)" % (self.WHITEB, self.num_read_timeouts / seconds_running, self.num_read_timeouts, self.CLEAR, self.read_timeout))
|
print(f" Read timeouts: {self.WHITEB}{self.num_read_timeouts / seconds_running:,.1f}/sec, {self.num_read_timeouts:,d} total{self.CLEAR} (timeout setting: {self.read_timeout:,.1f} sec)")
|
||||||
print(" Socket exceptions: %s%.1f/sec, %u total%s" % (self.WHITEB, self.num_socket_exceptions / seconds_running, self.num_socket_exceptions, self.CLEAR))
|
print(f" Socket exceptions: {self.WHITEB}{self.num_socket_exceptions / seconds_running:,.1f}/sec, {self.num_socket_exceptions:,d} total{self.CLEAR}")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
if seconds_running < 5.0:
|
if seconds_running < 5.0:
|
||||||
print("%sTotal run time was under 5 seconds; try running it for longer to get more accurate analysis.%s" % (DHEat.YELLOWB, DHEat.CLEAR))
|
print(f"{DHEat.YELLOWB}Total run time was under 5 seconds; try running it for longer to get more accurate analysis.{DHEat.CLEAR}")
|
||||||
elif self.num_successful_tcp_connections / seconds_running < DHEat.MAX_SAFE_RATE:
|
elif self.num_successful_tcp_connections / seconds_running < DHEat.MAX_SAFE_RATE:
|
||||||
print("Because the number of successful TCP connections per second (%.1f) is less than %.1f, it appears that the target %sis using rate limiting%s to prevent CPU exaustion." % (self.num_successful_tcp_connections / seconds_running, DHEat.MAX_SAFE_RATE, DHEat.GREENB, DHEat.CLEAR))
|
print(f"Because the number of successful TCP connections per second ({self.num_successful_tcp_connections / seconds_running:,.1f}) is less than {DHEat.MAX_SAFE_RATE:,.1f}, it appears that the target {DHEat.GREENB}is using rate limiting{DHEat.CLEAR} to prevent CPU exaustion.")
|
||||||
else:
|
else:
|
||||||
print("Because the number of successful TCP connections per second (%.1f) is greater than %.1f, it appears that the target %sis NOT using rate limiting%s to prevent CPU exaustion." % (self.num_successful_tcp_connections / seconds_running, DHEat.MAX_SAFE_RATE, DHEat.REDB, DHEat.CLEAR))
|
print(f"Because the number of successful TCP connections per second ({self.num_successful_tcp_connections / seconds_running:,.1f}) is greater than {DHEat.MAX_SAFE_RATE:,.1f}, it appears that the target {DHEat.REDB}is NOT using rate limiting{DHEat.CLEAR} to prevent CPU exaustion.")
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print()
|
print()
|
||||||
print(" * OpenSSH has a throttling mechanism (controlled by the MaxStartups directive) to prevent too many pre-authentication connections from overwhelming the server. When triggered, the server will probabilistically return \"Exceeded MaxStartups\" instead of the usual SSH banner, then terminate the connection. In order to maximize the DoS effectiveness, this metric should be greater than zero, though the ideal rate of rejections depends on the target server's CPU resources.")
|
print(" [*] Prior to OpenSSH 9.8, the sole connection throttling mechanism is controlled by the MaxStartups directive. This prevents too many pre-authentication connections from overwhelming the server. When triggered, the server will probabilistically return \"Exceeded MaxStartups\" instead of the usual SSH banner, then terminate the connection. In order to maximize the DoS effectiveness, this metric should be greater than zero, though the ideal rate of rejections depends on the target server's CPU resources.")
|
||||||
|
print()
|
||||||
|
print(" [+] Starting in OpenSSH 9.8, a more fine-gained connection throttling mechanism was added via the PerSourcePenalties directive. When triggered, the server returns \"Not allowed at this time\" instead of the usual SSH banner. This countermeasure is more effective than simply relying on the MaxStartups directive, and seems to block this attack under its default settings.")
|
||||||
print()
|
print()
|
||||||
|
|
||||||
|
|
||||||
@@ -775,7 +792,6 @@ class DHEat:
|
|||||||
def _run(self) -> bool:
|
def _run(self) -> bool:
|
||||||
'''Where all the magic happens.'''
|
'''Where all the magic happens.'''
|
||||||
|
|
||||||
|
|
||||||
self.output()
|
self.output()
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
self.output("%sWARNING:%s this feature has not been thoroughly tested on Windows. It may perform worse than on UNIX OSes." % (self.YELLOWB, self.CLEAR))
|
self.output("%sWARNING:%s this feature has not been thoroughly tested on Windows. It may perform worse than on UNIX OSes." % (self.YELLOWB, self.CLEAR))
|
||||||
@@ -887,14 +903,16 @@ class DHEat:
|
|||||||
self.num_connect_timeouts += thread_statistics['num_connect_timeouts']
|
self.num_connect_timeouts += thread_statistics['num_connect_timeouts']
|
||||||
self.num_read_timeouts += thread_statistics['num_read_timeouts']
|
self.num_read_timeouts += thread_statistics['num_read_timeouts']
|
||||||
self.num_socket_exceptions += thread_statistics['num_socket_exceptions']
|
self.num_socket_exceptions += thread_statistics['num_socket_exceptions']
|
||||||
self.num_openssh_throttled_connections += thread_statistics['num_openssh_throttled_connections']
|
self.num_em += thread_statistics['num_em']
|
||||||
|
self.num_naatt += thread_statistics['num_naatt']
|
||||||
|
self.num_invalid_banners += thread_statistics['num_invalid_banners']
|
||||||
except queue.Empty: # If Queue.get() timeout exceeded.
|
except queue.Empty: # If Queue.get() timeout exceeded.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if (now - last_update) >= 1.0:
|
if (now - last_update) >= 1.0:
|
||||||
seconds_running = now - self.start_timer
|
seconds_running = now - self.start_timer
|
||||||
print("%s%s%s TCP SYNs/sec: %s%u%s; Compl. conns/sec: %s%u%s; Bytes sent/sec: %s%s%s; DH kex/sec: %s%u%s \r" % (self.WHITEB, spinner[spinner_index], self.CLEAR, self.BLUEB, self.num_attempted_tcp_connections / seconds_running, self.CLEAR, self.BLUEB, self.num_successful_tcp_connections / seconds_running, self.CLEAR, self.BLUEB, DHEat.add_byte_units(self.num_bytes_written / seconds_running), self.CLEAR, self.PURPLEB, self.num_successful_dh_kex / seconds_running, self.CLEAR), end="")
|
print(f"{self.WHITEB}{spinner[spinner_index]}{self.CLEAR} TCP SYNs/sec: {self.BLUEB}{int(self.num_attempted_tcp_connections / seconds_running):,d}{self.CLEAR}; Compl. conns/sec: {self.BLUEB}{int(self.num_successful_tcp_connections / seconds_running):,d}{self.CLEAR}; Bytes sent/sec: {self.BLUEB}{DHEat.add_byte_units(self.num_bytes_written / seconds_running)}{self.CLEAR}; DH kex/sec: {self.PURPLEB}{self.num_successful_dh_kex / seconds_running:,.1f}{self.CLEAR} \r", end="")
|
||||||
last_update = now
|
last_update = now
|
||||||
spinner_index = (spinner_index + 1) % 4
|
spinner_index = (spinner_index + 1) % 4
|
||||||
|
|
||||||
@@ -942,13 +960,16 @@ class DHEat:
|
|||||||
num_connect_timeouts = 0
|
num_connect_timeouts = 0
|
||||||
num_read_timeouts = 0
|
num_read_timeouts = 0
|
||||||
num_socket_exceptions = 0
|
num_socket_exceptions = 0
|
||||||
num_openssh_throttled_connections = 0
|
num_em = 0 # Number of "Exceeded MaxStartups" messages received.
|
||||||
|
num_naatt = 0 # Number of "Not allowed at this time" messages received.
|
||||||
|
num_invalid_banners = 0
|
||||||
|
|
||||||
num_loops_since_last_statistics_sync = 0
|
num_loops_since_last_statistics_sync = 0
|
||||||
while True:
|
while True:
|
||||||
num_loops_since_last_statistics_sync += 1
|
num_loops_since_last_statistics_sync += 1
|
||||||
|
|
||||||
# Instead of flooding the parent process with statistics, report our stats only every 5 connections.
|
# Instead of flooding the parent process with statistics, report our stats only every 5 connections.
|
||||||
|
# TODO: this sends too quickly in the case of application-level rate limiting, resulting in client-side CPU exhaustion instead! :O Perhaps a dynamic number of loops should be skipped, based on what would result in 1 message per 2-3 seconds.
|
||||||
if num_loops_since_last_statistics_sync > 5:
|
if num_loops_since_last_statistics_sync > 5:
|
||||||
num_loops_since_last_statistics_sync = 0
|
num_loops_since_last_statistics_sync = 0
|
||||||
|
|
||||||
@@ -961,7 +982,9 @@ class DHEat:
|
|||||||
'num_connect_timeouts': num_connect_timeouts,
|
'num_connect_timeouts': num_connect_timeouts,
|
||||||
'num_read_timeouts': num_read_timeouts,
|
'num_read_timeouts': num_read_timeouts,
|
||||||
'num_socket_exceptions': num_socket_exceptions,
|
'num_socket_exceptions': num_socket_exceptions,
|
||||||
'num_openssh_throttled_connections': num_openssh_throttled_connections,
|
'num_em': num_em,
|
||||||
|
'num_naatt': num_naatt,
|
||||||
|
'num_invalid_banners': num_invalid_banners
|
||||||
})
|
})
|
||||||
|
|
||||||
# Since we sent our statistics, reset them all back to zero.
|
# Since we sent our statistics, reset them all back to zero.
|
||||||
@@ -973,7 +996,9 @@ class DHEat:
|
|||||||
num_connect_timeouts = 0
|
num_connect_timeouts = 0
|
||||||
num_read_timeouts = 0
|
num_read_timeouts = 0
|
||||||
num_socket_exceptions = 0
|
num_socket_exceptions = 0
|
||||||
num_openssh_throttled_connections = 0
|
num_em = 0
|
||||||
|
num_naatt = 0
|
||||||
|
num_invalid_banners = 0
|
||||||
|
|
||||||
s = socket.socket(target_address_family, socket.SOCK_STREAM)
|
s = socket.socket(target_address_family, socket.SOCK_STREAM)
|
||||||
s.settimeout(connect_timeout)
|
s.settimeout(connect_timeout)
|
||||||
@@ -1016,11 +1041,16 @@ class DHEat:
|
|||||||
if banner.startswith(b"SSH-2.0-") or banner.startswith(b"SSH-1"):
|
if banner.startswith(b"SSH-2.0-") or banner.startswith(b"SSH-1"):
|
||||||
num_successful_tcp_connections += 1
|
num_successful_tcp_connections += 1
|
||||||
elif banner == b'Exceeded MaxStartups':
|
elif banner == b'Exceeded MaxStartups':
|
||||||
num_openssh_throttled_connections += 1
|
num_em += 1
|
||||||
|
_close_socket(s)
|
||||||
|
continue
|
||||||
|
elif banner == b'Not allowed at this time':
|
||||||
|
num_naatt += 1
|
||||||
_close_socket(s)
|
_close_socket(s)
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
self.debug("Invalid banner received: %r" % banner)
|
num_invalid_banners += 1
|
||||||
|
# self.debug("Invalid banner received: %r" % banner)
|
||||||
_close_socket(s)
|
_close_socket(s)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (C) 2017-2024 Joe Testa (jtesta@positronsecurity.com)
|
Copyright (C) 2017-2026 Joe Testa (jtesta@positronsecurity.com)
|
||||||
|
|
||||||
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
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
@@ -22,10 +22,10 @@
|
|||||||
THE SOFTWARE.
|
THE SOFTWARE.
|
||||||
"""
|
"""
|
||||||
# The version to display.
|
# The version to display.
|
||||||
VERSION = 'v3.4.0-dev'
|
VERSION = 'v3.9.0'
|
||||||
|
|
||||||
# SSH software to impersonate
|
# SSH software to impersonate
|
||||||
SSH_HEADER = 'SSH-{0}-OpenSSH_8.2'
|
SSH_HEADER = 'SSH-{0}-OpenSSH_10.3'
|
||||||
|
|
||||||
# The URL to the Github issues tracker.
|
# The URL to the Github issues tracker.
|
||||||
GITHUB_ISSUES_URL = 'https://github.com/jtesta/ssh-audit/issues'
|
GITHUB_ISSUES_URL = 'https://github.com/jtesta/ssh-audit/issues'
|
||||||
|
|||||||
@@ -72,10 +72,29 @@ class Software:
|
|||||||
oversion, opatch = mx.group(1), mx.group(2).strip()
|
oversion, opatch = mx.group(1), mx.group(2).strip()
|
||||||
else:
|
else:
|
||||||
oversion, opatch = other, ''
|
oversion, opatch = other, ''
|
||||||
if self.version < oversion:
|
|
||||||
return -1
|
# Attempt to parse the versions into floats.
|
||||||
elif self.version > oversion:
|
float_parse_error = False
|
||||||
return 1
|
selfversion_float = 0.0
|
||||||
|
oversion_float = 0.0
|
||||||
|
try:
|
||||||
|
selfversion_float = float(self.version)
|
||||||
|
oversion_float = float(oversion)
|
||||||
|
except ValueError:
|
||||||
|
float_parse_error = True
|
||||||
|
|
||||||
|
# If the versions could not be parsed into floats, use the old string compare method. This returns incorrect results when comparing modern OpenSSH versions ("9.6" > "10.4" evaluates to True!), but removing this may trigger other bugs, and we have no other good options here, soooo...
|
||||||
|
if float_parse_error:
|
||||||
|
if self.version < oversion:
|
||||||
|
return -1
|
||||||
|
elif self.version > oversion:
|
||||||
|
return 1
|
||||||
|
else: # Float parsing worked, so we'll use this instead. That way (9.6 > 10.4) properly evaluates to False.
|
||||||
|
if selfversion_float < oversion_float: # pylint: disable=else-if-used
|
||||||
|
return -1
|
||||||
|
elif selfversion_float > oversion_float:
|
||||||
|
return 1
|
||||||
|
|
||||||
spatch = self.patch or ''
|
spatch = self.patch or ''
|
||||||
if self.product == Product.DropbearSSH:
|
if self.product == Product.DropbearSSH:
|
||||||
if not re.match(r'^test\d.*$', opatch):
|
if not re.match(r'^test\d.*$', opatch):
|
||||||
|
|||||||
@@ -271,6 +271,7 @@ class SSH2_KexDB: # pylint: disable=too-few-public-methods
|
|||||||
'ssh-mldsa-65': [[], [], [], [INFO_NIST_PQC_LEVEL_3]],
|
'ssh-mldsa-65': [[], [], [], [INFO_NIST_PQC_LEVEL_3]],
|
||||||
'ssh-mldsa-87': [[], [], [], [INFO_NIST_PQC_LEVEL_5]],
|
'ssh-mldsa-87': [[], [], [], [INFO_NIST_PQC_LEVEL_5]],
|
||||||
'ssh-mldsa44': [[], [], [], [INFO_NIST_PQC_LEVEL_2]],
|
'ssh-mldsa44': [[], [], [], [INFO_NIST_PQC_LEVEL_2]],
|
||||||
|
'ssh-mldsa44-ed25519@openssh.com': [['10.4'], [], [], [INFO_NIST_PQC_LEVEL_2]],
|
||||||
'ssh-mldsa65': [[], [], [], [INFO_NIST_PQC_LEVEL_3]],
|
'ssh-mldsa65': [[], [], [], [INFO_NIST_PQC_LEVEL_3]],
|
||||||
'ssh-mldsa87': [[], [], [], [INFO_NIST_PQC_LEVEL_5]],
|
'ssh-mldsa87': [[], [], [], [INFO_NIST_PQC_LEVEL_5]],
|
||||||
'ssh-rsa1': [[], [FAIL_SHA1]],
|
'ssh-rsa1': [[], [FAIL_SHA1]],
|
||||||
|
|||||||
Reference in New Issue
Block a user