From 7f31a1e3e6cbcc969a19ab9bb6104b4bebd688d5 Mon Sep 17 00:00:00 2001 From: terrafrost Date: Fri, 22 Nov 2024 14:29:20 -0600 Subject: [PATCH 1/3] SSH2: make it so phpseclib initiates key re-exchange after 1GB --- phpseclib/Net/SSH2.php | 144 +++++++++++++++++++++++++++++++++-------- 1 file changed, 118 insertions(+), 26 deletions(-) diff --git a/phpseclib/Net/SSH2.php b/phpseclib/Net/SSH2.php index e31be257..a7c687a3 100644 --- a/phpseclib/Net/SSH2.php +++ b/phpseclib/Net/SSH2.php @@ -123,6 +123,10 @@ define('NET_SSH2_LOG_REALTIME', 3); * Dumps the content real-time to a file */ define('NET_SSH2_LOG_REALTIME_FILE', 4); +/** + * Dumps the message numbers real-time + */ +define('NET_SSH2_LOG_REALTIME_SIMPLE', 5); /** * Make sure that the log never gets larger than this */ @@ -1107,6 +1111,46 @@ class Net_SSH2 */ var $extra_packets; + /** + * Bytes Transferred Since Last Key Exchange + * + * Includes outbound and inbound totals + * + * @var int + * @access private + */ + var $bytesTransferredSinceLastKEX = 0; + + /** + * After how many transferred byte should phpseclib initiate a key re-exchange? + * + * @var int + * @access private + */ + var $doKeyReexchangeAfterXBytes = 1024 * 1024 * 1024; + + /** + * Has a key re-exchange been initialized? + * + * @var bool + * @access private + */ + var $keyExchangeInProgress = false; + + /** + * KEX Buffer + * + * If we're in the middle of a key exchange we want to buffer any additional packets we get until + * the key exchange is over + * + * @see self::_get_binary_packet() + * @see self::_key_exchange() + * @see self::exec() + * @var array + * @access private + */ + var $kex_buffer = array(); + /** * Default Constructor. * @@ -1492,8 +1536,13 @@ class Net_SSH2 */ function _key_exchange($kexinit_payload_server = false) { + $this->bytesTransferredSinceLastKEX = 0; + $preferred = $this->preferred; - $send_kex = true; + // for the initial key exchange $send_kex is true (no key re-exchange has been started) + // for phpseclib initiated key exchanges $send_kex is false + $send_kex = !$this->keyExchangeInProgress; + $this->keyExchangeInProgress = true; $kex_algorithms = isset($preferred['kex']) ? $preferred['kex'] : @@ -1579,22 +1628,29 @@ class Net_SSH2 0 ); - if ($kexinit_payload_server === false) { + if ($kexinit_payload_server === false && $send_kex) { if (!$this->_send_binary_packet($kexinit_payload_client)) { return false; } - $this->extra_packets = 0; - $kexinit_payload_server = $this->_get_binary_packet(); - if ($kexinit_payload_server === false) { - $this->bitmap = 0; - user_error('Connection closed by server'); - return false; - } + while (true) { + $kexinit_payload_server = $this->_get_binary_packet(); + if ($kexinit_payload_server === false) { + $this->bitmap = 0; + user_error('Connection closed by server'); + return false; + } + + if (strlen($kexinit_payload_server)) { + switch (ord($kexinit_payload_server[0])) { + case NET_SSH2_MSG_KEXINIT: + break 2; + case NET_SSH2_MSG_DISCONNECT: + return $this->_handleDisconnect($kexinit_payload_server); + } + } - if (!strlen($kexinit_payload_server) || ord($kexinit_payload_server[0]) != NET_SSH2_MSG_KEXINIT) { - user_error('Expected SSH_MSG_KEXINIT'); - return false; + $this->kex_buffer[] = $kexinit_payload_server; } $send_kex = false; @@ -1610,7 +1666,7 @@ class Net_SSH2 $temp = unpack('Nlength', $this->_string_shift($response, 4)); $this->kex_algorithms = explode(',', $this->_string_shift($response, $temp['length'])); if (in_array('kex-strict-s-v00@openssh.com', $this->kex_algorithms)) { - if ($this->session_id === false && $this->extra_packets) { + if ($this->session_id === false && count($this->kex_buffer)) { user_error('Possible Terrapin Attack detected'); return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); } @@ -2001,6 +2057,8 @@ class Net_SSH2 return false; } + $this->keyExchangeInProgress = false; + if (in_array('kex-strict-s-v00@openssh.com', $this->kex_algorithms)) { $this->get_seq_no = $this->send_seq_no = 0; } @@ -3606,6 +3664,10 @@ class Net_SSH2 */ function _get_binary_packet($skip_channel_filter = false) { + if (!$this->keyExchangeInProgress && count($this->kex_buffer)) { + return $this->_filter(array_shift($this->kex_buffer), $skip_channel_filter); + } + if ($skip_channel_filter) { $read = array($this->fsock); $write = $except = null; @@ -3687,9 +3749,11 @@ class Net_SSH2 $remaining_length = $packet_length + 4 - $this->decrypt_block_size; + $this->bytesTransferredSinceLastKEX+= $packet_length + $padding_length + 5; + // quoting , // "implementations SHOULD check that the packet length is reasonable" - // PuTTY uses 0x9000 as the actual max packet size and so to shall we + // PuTTY uses 0x9000 as the actual max packet size and so, too, shall we if ($remaining_length < -$this->decrypt_block_size || $remaining_length > 0x9000 || $remaining_length % $this->decrypt_block_size != 0) { if (!$this->bad_key_size_fix && $this->_bad_algorithm_candidate($this->decryptName) && !($this->bitmap & NET_SSH2_MASK_LOGIN)) { $this->bad_key_size_fix = true; @@ -3779,7 +3843,34 @@ class Net_SSH2 $this->last_packet = $current; } - return $this->_filter($payload, $skip_channel_filter); + if ($this->bytesTransferredSinceLastKEX > $this->doKeyReexchangeAfterXBytes) { + $this->_key_exchange(); + } + + // don't filter if we're in the middle of a key exchange (since _filter might send out packets) + return $this->keyExchangeInProgress ? $payload : $this->_filter($payload, $skip_channel_filter); + } + + /** + * Handle Disconnect + * + * Because some binary packets need to be ignored... + * + * @see self::_filter() + * @see self::_key_exchange + * @return boolean + * @access private + */ + function _handleDisconnect($payload) + { + $this->_string_shift($payload, 1); + if (strlen($payload) < 8) { + return false; + } + extract(unpack('Nreason_code/Nlength', $this->_string_shift($payload, 8))); + $this->errors[] = 'SSH_MSG_DISCONNECT: ' . $this->disconnect_reasons[$reason_code] . "\r\n" . $this->_string_shift($payload, $length); + $this->bitmap = 0; + return false; } /** @@ -3795,20 +3886,11 @@ class Net_SSH2 { switch (ord($payload[0])) { case NET_SSH2_MSG_DISCONNECT: - $this->_string_shift($payload, 1); - if (strlen($payload) < 8) { - return false; - } - extract(unpack('Nreason_code/Nlength', $this->_string_shift($payload, 8))); - $this->errors[] = 'SSH_MSG_DISCONNECT: ' . $this->disconnect_reasons[$reason_code] . "\r\n" . $this->_string_shift($payload, $length); - $this->bitmap = 0; - return false; + return $this->_handleDisconnect($payload); case NET_SSH2_MSG_IGNORE: - $this->extra_packets++; $payload = $this->_get_binary_packet($skip_channel_filter); break; case NET_SSH2_MSG_DEBUG: - $this->extra_packets++; $this->_string_shift($payload, 2); if (strlen($payload) < 4) { return false; @@ -3820,7 +3902,7 @@ class Net_SSH2 case NET_SSH2_MSG_UNIMPLEMENTED: return false; case NET_SSH2_MSG_KEXINIT: - // this is here for key re-exchanges after the initial key exchange + // this is here for server initiated key re-exchanges after the initial key exchange if ($this->session_id !== false) { $this->send_kex_first = false; if (!$this->_key_exchange($payload)) { @@ -4386,6 +4468,8 @@ class Net_SSH2 $packet.= $hmac; + $this->bytesTransferredSinceLastKEX+= strlen($packet); + $start = strtok(microtime(), ' ') + strtok(''); // http://php.net/microtime#61838 $result = strlen($packet) == @fputs($this->fsock, $packet); $stop = strtok(microtime(), ' ') + strtok(''); @@ -4399,6 +4483,10 @@ class Net_SSH2 $this->last_packet = $current; } + if ($this->bytesTransferredSinceLastKEX > $this->doKeyReexchangeAfterXBytes) { + $this->_key_exchange(); + } + return $result; } @@ -4476,6 +4564,10 @@ class Net_SSH2 $this->realtime_log_wrap = true; } fputs($this->realtime_log_file, $entry); + break; + case NET_SSH2_LOG_REALTIME_SIMPLE: + echo $message_number; + echo PHP_SAPI == 'cli' ? "\r\n" : '
'; } } From ccf4b488e8737bdeddf80d90393b28ddf5b9a113 Mon Sep 17 00:00:00 2001 From: terrafrost Date: Fri, 22 Nov 2024 14:54:16 -0600 Subject: [PATCH 2/3] SSH2: PHP didn't support constant expressions until PHP 5.6 --- phpseclib/Net/SSH2.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpseclib/Net/SSH2.php b/phpseclib/Net/SSH2.php index a7c687a3..6a03b4f6 100644 --- a/phpseclib/Net/SSH2.php +++ b/phpseclib/Net/SSH2.php @@ -1127,7 +1127,7 @@ class Net_SSH2 * @var int * @access private */ - var $doKeyReexchangeAfterXBytes = 1024 * 1024 * 1024; + var $doKeyReexchangeAfterXBytes = 1073741824; /** * Has a key re-exchange been initialized? From b38e84972d171fb769bda1fc24cc7dcba377cf77 Mon Sep 17 00:00:00 2001 From: terrafrost Date: Fri, 22 Nov 2024 16:31:06 -0600 Subject: [PATCH 3/3] SSH2: don't count len of packets sent / rcvd during key exchange --- phpseclib/Net/SSH2.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/phpseclib/Net/SSH2.php b/phpseclib/Net/SSH2.php index 6a03b4f6..ed297884 100644 --- a/phpseclib/Net/SSH2.php +++ b/phpseclib/Net/SSH2.php @@ -3749,7 +3749,9 @@ class Net_SSH2 $remaining_length = $packet_length + 4 - $this->decrypt_block_size; - $this->bytesTransferredSinceLastKEX+= $packet_length + $padding_length + 5; + if (!$this->keyExchangeInProgress) { + $this->bytesTransferredSinceLastKEX+= $packet_length + $padding_length + 5; + } // quoting , // "implementations SHOULD check that the packet length is reasonable" @@ -4468,7 +4470,9 @@ class Net_SSH2 $packet.= $hmac; - $this->bytesTransferredSinceLastKEX+= strlen($packet); + if (!$this->keyExchangeInProgress) { + $this->bytesTransferredSinceLastKEX+= strlen($packet); + } $start = strtok(microtime(), ' ') + strtok(''); // http://php.net/microtime#61838 $result = strlen($packet) == @fputs($this->fsock, $packet);