diff --git a/phpseclib/Net/SSH2.php b/phpseclib/Net/SSH2.php index de5414b7..c705f119 100644 --- a/phpseclib/Net/SSH2.php +++ b/phpseclib/Net/SSH2.php @@ -936,6 +936,16 @@ class SSH2 */ private $errorOnMultipleChannels; + /** + * Terrapin Countermeasure + * + * "During initial KEX, terminate the connection if any unexpected or out-of-sequence packet is received" + * -- https://github.com/openssh/openssh-portable/commit/1edb00c58f8a6875fad6a497aa2bacf37f9e6cd5 + * + * @var int + */ + private $extra_packets; + /** * Default Constructor. * @@ -1260,7 +1270,7 @@ class SSH2 $c2s_compression_algorithms = $preferred['client_to_server']['comp'] ?? SSH2::getSupportedCompressionAlgorithms(); - $kex_algorithms = array_merge($kex_algorithms, ['ext-info-c']); + $kex_algorithms = array_merge($kex_algorithms, ['ext-info-c', 'kex-strict-c-v00@openssh.com']); // some SSH servers have buggy implementations of some of the above algorithms switch (true) { @@ -1316,6 +1326,7 @@ class SSH2 if ($kexinit_payload_server === false) { $this->send_binary_packet($kexinit_payload_client); + $this->extra_packets = 0; $kexinit_payload_server = $this->get_binary_packet(); if ( @@ -1347,6 +1358,11 @@ class SSH2 $this->languages_server_to_client, $first_kex_packet_follows ] = Strings::unpackSSH2('L10C', $response); + if (in_array('kex-strict-s-v00@openssh.com', $this->kex_algorithms)) { + if ($this->session_id === false && $this->extra_packets) { + throw new \UnexpectedValueException('Possible Terrapin Attack detected'); + } + } $this->supported_private_key_algorithms = $this->server_host_key_algorithms; @@ -1605,6 +1621,10 @@ class SSH2 throw new UnexpectedValueException('Expected SSH_MSG_NEWKEYS'); } + if (in_array('kex-strict-s-v00@openssh.com', $this->kex_algorithms)) { + $this->get_seq_no = $this->send_seq_no = 0; + } + $keyBytes = pack('Na*', strlen($keyBytes), $keyBytes); $this->encrypt = self::encryption_algorithm_to_crypt_instance($encrypt); @@ -3435,9 +3455,11 @@ class SSH2 $this->bitmap = 0; return false; case MessageType::IGNORE: + $this->extra_packets++; $payload = $this->get_binary_packet($skip_channel_filter); break; case MessageType::DEBUG: + $this->extra_packets++; Strings::shift($payload, 2); // second byte is "always_display" [$message] = Strings::unpackSSH2('s', $payload); $this->errors[] = "SSH_MSG_DEBUG: $message"; @@ -3446,6 +3468,7 @@ class SSH2 case MessageType::UNIMPLEMENTED: return false; case MessageType::KEXINIT: + // this is here for key re-exchanges after the initial key exchange if ($this->session_id !== false) { if (!$this->key_exchange($payload)) { $this->bitmap = 0;