From da6c1b06bf9af95191b67d05928ff826098d559c Mon Sep 17 00:00:00 2001 From: terrafrost Date: Tue, 26 Oct 2021 20:04:53 -0500 Subject: [PATCH] SSH2: add support for zlib and zlib@openssh.com compression --- phpseclib/Net/SSH2.php | 172 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 151 insertions(+), 21 deletions(-) diff --git a/phpseclib/Net/SSH2.php b/phpseclib/Net/SSH2.php index 9aeb56c6..a641e9d3 100644 --- a/phpseclib/Net/SSH2.php +++ b/phpseclib/Net/SSH2.php @@ -150,6 +150,23 @@ define('NET_SSH2_READ_REGEX', 2); define('NET_SSH2_READ_NEXT', 3); /**#@-*/ +/**#@+ + * @access private + */ +/** + * No compression + */ +define('NET_SSH2_COMPRESSION_NONE', 1); +/** + * zlib compression + */ +define('NET_SSH2_COMPRESSION_ZLIB', 2); +/** + * zlib@openssh.com + */ +define('NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH', 3); +/**#@-*/ + /** * Pure-PHP implementation of SSHv2. * @@ -980,8 +997,57 @@ class Net_SSH2 * * @see https://tools.ietf.org/html/rfc4252#section-5.1 * @var array|null + * @access private */ - private $auth_methods_to_continue = null; + var $auth_methods_to_continue = null; + + /** + * Compression method + * + * @var int + * @access private + */ + var $compress = NET_SSH2_COMPRESSION_NONE; + + /** + * Decompression method + * + * @var resource|object + * @access private + */ + var $decompress = NET_SSH2_COMPRESSION_NONE; + + /** + * Compression context + * + * @var int + * @access private + */ + var $compress_context; + + /** + * Decompression context + * + * @var resource|object + * @access private + */ + var $decompress_context; + + /** + * Regenerate Compression Context + * + * @var bool + * @access private + */ + var $regenerate_compression_context = false; + + /** + * Regenerate Decompression Context + * + * @var bool + * @access private + */ + var $regenerate_decompression_context = false; /** * Default Constructor. @@ -1590,19 +1656,25 @@ class Net_SSH2 return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); } + $compression_map = array( + 'none' => NET_SSH2_COMPRESSION_NONE, + 'zlib' => NET_SSH2_COMPRESSION_ZLIB, + 'zlib@openssh.com' => NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH + ); + $compression_algorithm_out = $this->_array_intersect_first($c2s_compression_algorithms, $this->compression_algorithms_client_to_server); if ($compression_algorithm_out === false) { user_error('No compatible client to server compression algorithms found'); return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); } - //$this->decompress = $compression_algorithm_out == 'zlib'; + $this->compress = $compression_map[$compression_algorithm_out]; - $compression_algorithm_in = $this->_array_intersect_first($s2c_compression_algorithms, $this->compression_algorithms_client_to_server); + $compression_algorithm_in = $this->_array_intersect_first($s2c_compression_algorithms, $this->compression_algorithms_server_to_client); if ($compression_algorithm_in === false) { user_error('No compatible server to client compression algorithms found'); return $this->_disconnect(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); } - //$this->compress = $compression_algorithm_in == 'zlib'; + $this->decompress = $compression_map[$compression_algorithm_in]; if (strpos($kex_algorithm, 'diffie-hellman-group-exchange') === 0) { $dh_group_sizes_packed = pack( @@ -1996,6 +2068,8 @@ class Net_SSH2 } $this->hmac_check->setKey(substr($key, 0, $checkKeyLength)); + $this->regenerate_compression_context = $this->regenerate_decompression_context = true; + return true; } @@ -3399,7 +3473,7 @@ class Net_SSH2 if (!is_resource($this->fsock) || feof($this->fsock)) { $this->bitmap = 0; - user_error('Connection closed (by server) prematurely'); + user_error('Connection closed (by server) prematurely ' . $elapsed . 's'); return false; } @@ -3470,9 +3544,41 @@ class Net_SSH2 } } - //if ($this->decompress) { - // $payload = gzinflate(substr($payload, 2)); - //} + switch ($this->decompress) { + case NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH: + if (!$this->isAuthenticated()) { + break; + } + case NET_SSH2_COMPRESSION_ZLIB: + if ($this->regenerate_decompression_context) { + $this->regenerate_decompression_context = false; + + $cmf = ord($payload[0]); + $cm = $cmf & 0x0F; + if ($cm != 8) { // deflate + user_error("Only CM = 8 ('deflate') is supported ($cm)"); + } + $cinfo = ($cmf & 0xF0) >> 4; + if ($cinfo > 7) { + user_error("CINFO above 7 is not allowed ($cinfo)"); + } + $windowSize = 1 << ($cinfo + 8); + + $flg = ord($payload[1]); + //$fcheck = $flg && 0x0F; + if ((($cmf << 8) | $flg) % 31) { + user_error('fcheck failed'); + } + $fdict = boolval($flg & 0x20); + $flevel = ($flg & 0xC0) >> 6; + + $this->decompress_context = inflate_init(ZLIB_ENCODING_RAW, ['window' => $cinfo + 8]); + $payload = substr($payload, 2); + } + if ($this->decompress_context) { + $payload = inflate_add($this->decompress_context, $payload, ZLIB_PARTIAL_FLUSH); + } + } $this->get_seq_no++; @@ -4028,11 +4134,27 @@ class Net_SSH2 return false; } - //if ($this->compress) { - // // the -4 removes the checksum: - // // http://php.net/function.gzcompress#57710 - // $data = substr(gzcompress($data), 0, -4); - //} + if (!isset($logged)) { + $logged = $data; + } + + switch ($this->compress) { + case NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH: + if (!$this->isAuthenticated()) { + break; + } + case NET_SSH2_COMPRESSION_ZLIB: + if (!$this->regenerate_compression_context) { + $header = ''; + } else { + $this->regenerate_compression_context = false; + $this->compress_context = deflate_init(ZLIB_ENCODING_RAW, ['window' => 15]); + $header = "\x78\x9C"; + } + if ($this->compress_context) { + $data = $header . deflate_add($this->compress_context, $data, ZLIB_PARTIAL_FLUSH); + } + } // 4 (packet length) + 1 (padding length) + 4 (minimal padding amount) == 9 $packet_length = strlen($data) + 9; @@ -4060,10 +4182,10 @@ class Net_SSH2 if (defined('NET_SSH2_LOGGING')) { $current = strtok(microtime(), ' ') + strtok(''); - $message_number = isset($this->message_numbers[ord($data[0])]) ? $this->message_numbers[ord($data[0])] : 'UNKNOWN (' . ord($data[0]) . ')'; + $message_number = isset($this->message_numbers[ord($logged[0])]) ? $this->message_numbers[ord($logged[0])] : 'UNKNOWN (' . ord($logged[0]) . ')'; $message_number = '-> ' . $message_number . ' (since last: ' . round($current - $this->last_packet, 4) . ', network: ' . round($stop - $start, 4) . 's)'; - $this->_append_log($message_number, isset($logged) ? $logged : $data); + $this->_append_log($message_number, $logged); $this->last_packet = $current; } @@ -4746,10 +4868,12 @@ class Net_SSH2 */ function getSupportedCompressionAlgorithms() { - return array( - 'none' // REQUIRED no compression - //'zlib' // OPTIONAL ZLIB (LZ77) compression - ); + $algos = array('none'); // REQUIRED no compression + if (function_exists('deflate_init')) { + $algos[] = 'zlib@openssh.com'; // https://datatracker.ietf.org/doc/html/draft-miller-secsh-compression-delayed + $algos[] = 'zlib'; + } + return $algos; } /** @@ -4764,18 +4888,24 @@ class Net_SSH2 { $this->_connect(); + $compression_map = array( + NET_SSH2_COMPRESSION_NONE => 'none', + NET_SSH2_COMPRESSION_ZLIB => 'zlib', + NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH => 'zlib@openssh.com' + ); + return array( 'kex' => $this->kex_algorithm, 'hostkey' => $this->signature_format, 'client_to_server' => array( 'crypt' => $this->encrypt->name, 'mac' => $this->hmac_create->name, - 'comp' => 'none', + 'comp' => $compression_map[$this->compress], ), 'server_to_client' => array( 'crypt' => $this->decrypt->name, 'mac' => $this->hmac_check->name, - 'comp' => 'none', + 'comp' => $compression_map[$this->decompress], ) ); }