mirror of
https://github.com/phpseclib/phpseclib.git
synced 2025-01-27 00:58:25 +00:00
Merge pull request #2006 from rposky/ssh-binary-packet-buffer
SSH2: Reorganize get_binary_packet to fetch entire packet before processing
This commit is contained in:
commit
38de2d9e55
10
phpseclib/Exception/InvalidPacketLengthException.php
Normal file
10
phpseclib/Exception/InvalidPacketLengthException.php
Normal file
@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace phpseclib3\Exception;
|
||||
|
||||
/**
|
||||
* Indicates an absent or malformed packet length header
|
||||
*/
|
||||
class InvalidPacketLengthException extends ConnectionClosedException
|
||||
{
|
||||
}
|
@ -3298,12 +3298,10 @@ class SFTP extends SSH2
|
||||
|
||||
/**
|
||||
* Resets a connection for re-use
|
||||
*
|
||||
* @param int $reason
|
||||
*/
|
||||
protected function reset_connection($reason)
|
||||
protected function reset_connection()
|
||||
{
|
||||
parent::reset_connection($reason);
|
||||
parent::reset_connection();
|
||||
$this->reset_sftp();
|
||||
}
|
||||
|
||||
|
@ -64,6 +64,7 @@ use phpseclib3\Crypt\TripleDES; // Used to do Diffie-Hellman key exchange and DS
|
||||
use phpseclib3\Crypt\Twofish;
|
||||
use phpseclib3\Exception\ConnectionClosedException;
|
||||
use phpseclib3\Exception\InsufficientSetupException;
|
||||
use phpseclib3\Exception\InvalidPacketLengthException;
|
||||
use phpseclib3\Exception\NoSupportedAlgorithmsException;
|
||||
use phpseclib3\Exception\UnableToConnectException;
|
||||
use phpseclib3\Exception\UnsupportedAlgorithmException;
|
||||
@ -784,6 +785,14 @@ class SSH2
|
||||
*/
|
||||
private $keepAlive;
|
||||
|
||||
/**
|
||||
* Timestamp for the last sent keep alive message
|
||||
*
|
||||
* @see self::send_keep_alive()
|
||||
* @var float|null
|
||||
*/
|
||||
private $keep_alive_sent = null;
|
||||
|
||||
/**
|
||||
* Real-time log file pointer
|
||||
*
|
||||
@ -1005,9 +1014,9 @@ class SSH2
|
||||
/**
|
||||
* Binary Packet Buffer
|
||||
*
|
||||
* @var string|false
|
||||
* @var object|null
|
||||
*/
|
||||
private $binary_packet_buffer = false;
|
||||
private $binary_packet_buffer = null;
|
||||
|
||||
/**
|
||||
* Preferred Signature Format
|
||||
@ -1654,15 +1663,13 @@ class SSH2
|
||||
// we don't initialize any crypto-objects, yet - we do that, later. for now, we need the lengths to make the
|
||||
// diffie-hellman key exchange as fast as possible
|
||||
$decrypt = self::array_intersect_first($s2c_encryption_algorithms, $this->encryption_algorithms_server_to_client);
|
||||
$decryptKeyLength = $this->encryption_algorithm_to_key_size($decrypt);
|
||||
if ($decryptKeyLength === null) {
|
||||
if (!$decrypt || ($decryptKeyLength = $this->encryption_algorithm_to_key_size($decrypt)) === null) {
|
||||
$this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
|
||||
throw new NoSupportedAlgorithmsException('No compatible server to client encryption algorithms found');
|
||||
}
|
||||
|
||||
$encrypt = self::array_intersect_first($c2s_encryption_algorithms, $this->encryption_algorithms_client_to_server);
|
||||
$encryptKeyLength = $this->encryption_algorithm_to_key_size($encrypt);
|
||||
if ($encryptKeyLength === null) {
|
||||
if (!$encrypt || ($encryptKeyLength = $this->encryption_algorithm_to_key_size($encrypt)) === null) {
|
||||
$this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
|
||||
throw new NoSupportedAlgorithmsException('No compatible client to server encryption algorithms found');
|
||||
}
|
||||
@ -2184,7 +2191,7 @@ class SSH2
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* Tests whether or not proposed algorithm has a potential for issues
|
||||
*
|
||||
* @link https://www.chiark.greenend.org.uk/~sgtatham/putty/wishlist/ssh2-aesctr-openssh.html
|
||||
@ -2350,17 +2357,18 @@ class SSH2
|
||||
$this->send_binary_packet($packet);
|
||||
|
||||
try {
|
||||
$bad_key_size_fix = $this->bad_key_size_fix;
|
||||
$response = $this->get_binary_packet();
|
||||
} catch (\Exception $e) {
|
||||
// bad_key_size_fix is only ever re-assigned to true
|
||||
// under certain conditions. when it's newly set we'll
|
||||
// retry the connection with that new setting but we'll
|
||||
// only try it once.
|
||||
if ($bad_key_size_fix != $this->bad_key_size_fix) {
|
||||
$this->connect();
|
||||
return $this->login_helper($username, $password);
|
||||
} catch (InvalidPacketLengthException $e) {
|
||||
// the first opportunity to encounter the "bad key size" error
|
||||
if (!$this->bad_key_size_fix && $this->decryptName != null && self::bad_algorithm_candidate($this->decryptName)) {
|
||||
// bad_key_size_fix is only ever re-assigned to true here
|
||||
// retry the connection with that new setting but we'll
|
||||
// only try it once.
|
||||
$this->bad_key_size_fix = true;
|
||||
return $this->reconnect();
|
||||
}
|
||||
throw $e;
|
||||
} catch (\Exception $e) {
|
||||
$this->disconnect_helper(NET_SSH2_DISCONNECT_CONNECTION_LOST);
|
||||
throw $e;
|
||||
}
|
||||
@ -3457,7 +3465,7 @@ class SSH2
|
||||
*/
|
||||
private function reconnect()
|
||||
{
|
||||
$this->reset_connection(NET_SSH2_DISCONNECT_CONNECTION_LOST);
|
||||
$this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION);
|
||||
$this->connect();
|
||||
foreach ($this->auth as $auth) {
|
||||
$result = $this->login(...$auth);
|
||||
@ -3467,103 +3475,113 @@ class SSH2
|
||||
|
||||
/**
|
||||
* Resets a connection for re-use
|
||||
*
|
||||
* @param int $reason
|
||||
*/
|
||||
protected function reset_connection($reason)
|
||||
protected function reset_connection()
|
||||
{
|
||||
$this->disconnect_helper($reason);
|
||||
if (is_resource($this->fsock) && get_resource_type($this->fsock) === 'stream') {
|
||||
fclose($this->fsock);
|
||||
}
|
||||
$this->fsock = null;
|
||||
$this->bitmap = 0;
|
||||
$this->binary_packet_buffer = null;
|
||||
$this->decrypt = $this->encrypt = false;
|
||||
$this->decrypt_block_size = $this->encrypt_block_size = 8;
|
||||
$this->hmac_check = $this->hmac_create = false;
|
||||
$this->hmac_size = false;
|
||||
$this->session_id = false;
|
||||
$this->keep_alive_sent = null;
|
||||
$this->get_seq_no = $this->send_seq_no = 0;
|
||||
$this->channel_status = [];
|
||||
$this->channel_id_last_interactive = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int[] second and microsecond stream timeout options based on user-requested timeout and keep-alive, 0 by default
|
||||
*/
|
||||
private function get_stream_timeout()
|
||||
{
|
||||
$sec = 0;
|
||||
$usec = 0;
|
||||
if ($this->curTimeout > 0) {
|
||||
$sec = (int) floor($this->curTimeout);
|
||||
$usec = (int) (1000000 * ($this->curTimeout - $sec));
|
||||
}
|
||||
if ($this->keepAlive > 0) {
|
||||
$elapsed = microtime(true) - $this->keep_alive_sent;
|
||||
if ($elapsed < $this->curTimeout) {
|
||||
$sec = (int) floor($elapsed);
|
||||
$usec = (int) (1000000 * ($elapsed - $sec));
|
||||
}
|
||||
}
|
||||
return [$sec, $usec];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets Binary Packets
|
||||
*
|
||||
* See '6. Binary Packet Protocol' of rfc4253 for more info.
|
||||
*
|
||||
* @see self::_send_binary_packet()
|
||||
* @param bool $skip_channel_filter
|
||||
* @return bool|string
|
||||
*/
|
||||
private function get_binary_packet($skip_channel_filter = false)
|
||||
private function get_binary_packet()
|
||||
{
|
||||
if ($skip_channel_filter) {
|
||||
if (!is_resource($this->fsock)) {
|
||||
throw new \InvalidArgumentException('fsock is not a resource.');
|
||||
if (!is_resource($this->fsock)) {
|
||||
throw new \InvalidArgumentException('fsock is not a resource.');
|
||||
}
|
||||
if ($this->binary_packet_buffer == null) {
|
||||
// buffer the packet to permit continued reads across timeouts
|
||||
$this->binary_packet_buffer = (object) [
|
||||
'raw' => '', // the raw payload read from the socket
|
||||
'plain' => '', // the packet in plain text, excluding packet_length header
|
||||
'packet_length' => null, // the packet_length value pulled from the payload
|
||||
'size' => $this->decrypt_block_size, // the total size of this packet to be read from the socket
|
||||
// initialize to read single block until packet_length is available
|
||||
];
|
||||
}
|
||||
$packet = $this->binary_packet_buffer;
|
||||
while (strlen($packet->raw) < $packet->size) {
|
||||
if (feof($this->fsock)) {
|
||||
$this->disconnect_helper(NET_SSH2_DISCONNECT_CONNECTION_LOST);
|
||||
throw new ConnectionClosedException('Connection closed by server');
|
||||
}
|
||||
$read = [$this->fsock];
|
||||
$write = $except = null;
|
||||
if ($this->curTimeout < 0) {
|
||||
$this->is_timeout = true;
|
||||
return true;
|
||||
}
|
||||
$this->send_keep_alive();
|
||||
|
||||
if (!$this->curTimeout) {
|
||||
if ($this->keepAlive <= 0) {
|
||||
static::stream_select($read, $write, $except, null);
|
||||
} else {
|
||||
if (!static::stream_select($read, $write, $except, $this->keepAlive)) {
|
||||
$this->send_binary_packet(pack('CN', NET_SSH2_MSG_IGNORE, 0));
|
||||
return $this->get_binary_packet(true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($this->curTimeout < 0) {
|
||||
$this->is_timeout = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
$start = microtime(true);
|
||||
|
||||
if ($this->keepAlive > 0 && $this->keepAlive < $this->curTimeout) {
|
||||
if (!static::stream_select($read, $write, $except, $this->keepAlive)) {
|
||||
$this->send_binary_packet(pack('CN', NET_SSH2_MSG_IGNORE, 0));
|
||||
$elapsed = microtime(true) - $start;
|
||||
$this->curTimeout -= $elapsed;
|
||||
return $this->get_binary_packet(true);
|
||||
}
|
||||
$elapsed = microtime(true) - $start;
|
||||
$this->curTimeout -= $elapsed;
|
||||
}
|
||||
|
||||
$sec = (int) floor($this->curTimeout);
|
||||
$usec = (int) (1000000 * ($this->curTimeout - $sec));
|
||||
|
||||
// this can return a "stream_select(): unable to select [4]: Interrupted system call" error
|
||||
if (!static::stream_select($read, $write, $except, $sec, $usec)) {
|
||||
$this->is_timeout = true;
|
||||
return true;
|
||||
}
|
||||
$elapsed = microtime(true) - $start;
|
||||
list($sec, $usec) = $this->get_stream_timeout();
|
||||
stream_set_timeout($this->fsock, $sec, $usec);
|
||||
$start = microtime(true);
|
||||
$raw = stream_get_contents($this->fsock, $packet->size - strlen($packet->raw));
|
||||
$elapsed = microtime(true) - $start;
|
||||
if ($this->curTimeout > 0) {
|
||||
$this->curTimeout -= $elapsed;
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_resource($this->fsock) || feof($this->fsock)) {
|
||||
$this->bitmap = 0;
|
||||
$str = 'Connection closed (by server) prematurely';
|
||||
if (isset($elapsed)) {
|
||||
$str .= ' ' . $elapsed . 's';
|
||||
if ($raw === false) {
|
||||
$this->disconnect_helper(NET_SSH2_DISCONNECT_CONNECTION_LOST);
|
||||
throw new ConnectionClosedException('Connection closed by server');
|
||||
} elseif (!strlen($raw)) {
|
||||
continue;
|
||||
}
|
||||
throw new ConnectionClosedException($str);
|
||||
}
|
||||
$packet->raw .= $raw;
|
||||
if (!$packet->packet_length) {
|
||||
$this->get_binary_packet_size($packet);
|
||||
}
|
||||
};
|
||||
|
||||
$start = microtime(true);
|
||||
if ($this->curTimeout) {
|
||||
$sec = (int) floor($this->curTimeout);
|
||||
$usec = (int) (1000000 * ($this->curTimeout - $sec));
|
||||
stream_set_timeout($this->fsock, $sec, $usec);
|
||||
if (strlen($packet->raw) != $packet->size) {
|
||||
throw new \RuntimeException('Size of packet was not expected length');
|
||||
}
|
||||
$raw = stream_get_contents($this->fsock, $this->decrypt_block_size);
|
||||
|
||||
if (!strlen($raw)) {
|
||||
$this->bitmap = 0;
|
||||
throw new ConnectionClosedException('No data received from server');
|
||||
// destroy buffer as packet represents the entire payload and should be processed in full
|
||||
$this->binary_packet_buffer = null;
|
||||
// copy the raw payload, so as not to destroy original
|
||||
$raw = $packet->raw;
|
||||
if ($this->hmac_check instanceof Hash) {
|
||||
$hmac = Strings::pop($raw, $this->hmac_size);
|
||||
}
|
||||
|
||||
$packet_length_header_size = 4;
|
||||
if ($this->decrypt) {
|
||||
switch ($this->decryptName) {
|
||||
case 'aes128-gcm@openssh.com':
|
||||
@ -3573,40 +3591,16 @@ class SSH2
|
||||
$this->decryptInvocationCounter
|
||||
);
|
||||
Strings::increment_str($this->decryptInvocationCounter);
|
||||
$this->decrypt->setAAD($temp = Strings::shift($raw, 4));
|
||||
extract(unpack('Npacket_length', $temp));
|
||||
/**
|
||||
* @var integer $packet_length
|
||||
*/
|
||||
|
||||
$raw .= $this->read_remaining_bytes($packet_length - $this->decrypt_block_size + 4);
|
||||
$stop = microtime(true);
|
||||
$tag = stream_get_contents($this->fsock, $this->decrypt_block_size);
|
||||
$this->decrypt->setTag($tag);
|
||||
$raw = $this->decrypt->decrypt($raw);
|
||||
$raw = $temp . $raw;
|
||||
$remaining_length = 0;
|
||||
$this->decrypt->setAAD(Strings::shift($raw, $packet_length_header_size));
|
||||
$this->decrypt->setTag(Strings::pop($raw, $this->decrypt_block_size));
|
||||
$packet->plain = $this->decrypt->decrypt($raw);
|
||||
break;
|
||||
case 'chacha20-poly1305@openssh.com':
|
||||
// This should be impossible, but we are checking anyway to narrow the type for Psalm.
|
||||
if (!($this->decrypt instanceof ChaCha20)) {
|
||||
throw new \LogicException('$this->decrypt is not a ' . ChaCha20::class);
|
||||
}
|
||||
|
||||
$nonce = pack('N2', 0, $this->get_seq_no);
|
||||
|
||||
$this->lengthDecrypt->setNonce($nonce);
|
||||
$temp = $this->lengthDecrypt->decrypt($aad = Strings::shift($raw, 4));
|
||||
extract(unpack('Npacket_length', $temp));
|
||||
/**
|
||||
* @var integer $packet_length
|
||||
*/
|
||||
|
||||
$raw .= $this->read_remaining_bytes($packet_length - $this->decrypt_block_size + 4);
|
||||
$stop = microtime(true);
|
||||
$tag = stream_get_contents($this->fsock, 16);
|
||||
|
||||
$this->decrypt->setNonce($nonce);
|
||||
$this->decrypt->setNonce(pack('N2', 0, $this->get_seq_no));
|
||||
$this->decrypt->setCounter(0);
|
||||
// this is the same approach that's implemented in Salsa20::createPoly1305Key()
|
||||
// but we don't want to use the same AEAD construction that RFC8439 describes
|
||||
@ -3614,79 +3608,55 @@ class SSH2
|
||||
$this->decrypt->setPoly1305Key(
|
||||
$this->decrypt->encrypt(str_repeat("\0", 32))
|
||||
);
|
||||
$this->decrypt->setAAD($aad);
|
||||
$this->decrypt->setAAD(Strings::shift($raw, $packet_length_header_size));
|
||||
$this->decrypt->setCounter(1);
|
||||
$this->decrypt->setTag($tag);
|
||||
$raw = $this->decrypt->decrypt($raw);
|
||||
$raw = $temp . $raw;
|
||||
$remaining_length = 0;
|
||||
$this->decrypt->setTag(Strings::pop($raw, 16));
|
||||
$packet->plain = $this->decrypt->decrypt($raw);
|
||||
break;
|
||||
default:
|
||||
if (!$this->hmac_check instanceof Hash || !$this->hmac_check_etm) {
|
||||
$raw = $this->decrypt->decrypt($raw);
|
||||
break;
|
||||
// first block was already decrypted for contained packet_length header
|
||||
Strings::shift($raw, $this->decrypt_block_size);
|
||||
if (strlen($raw) > 0) {
|
||||
$packet->plain .= $this->decrypt->decrypt($raw);
|
||||
}
|
||||
} else {
|
||||
Strings::shift($raw, $packet_length_header_size);
|
||||
$packet->plain = $this->decrypt->decrypt($raw);
|
||||
}
|
||||
extract(unpack('Npacket_length', $temp = Strings::shift($raw, 4)));
|
||||
/**
|
||||
* @var integer $packet_length
|
||||
*/
|
||||
$raw .= $this->read_remaining_bytes($packet_length - $this->decrypt_block_size + 4);
|
||||
$stop = microtime(true);
|
||||
$encrypted = $temp . $raw;
|
||||
$raw = $temp . $this->decrypt->decrypt($raw);
|
||||
$remaining_length = 0;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
Strings::shift($raw, $packet_length_header_size);
|
||||
$packet->plain = $raw;
|
||||
}
|
||||
|
||||
if (strlen($raw) < 5) {
|
||||
$this->bitmap = 0;
|
||||
throw new \RuntimeException('Plaintext is too short');
|
||||
}
|
||||
extract(unpack('Npacket_length/Cpadding_length', Strings::shift($raw, 5)));
|
||||
/**
|
||||
* @var integer $packet_length
|
||||
* @var integer $padding_length
|
||||
*/
|
||||
|
||||
if (!isset($remaining_length)) {
|
||||
$remaining_length = $packet_length + 4 - $this->decrypt_block_size;
|
||||
}
|
||||
|
||||
$buffer = $this->read_remaining_bytes($remaining_length);
|
||||
|
||||
if (!isset($stop)) {
|
||||
$stop = microtime(true);
|
||||
}
|
||||
if (strlen($buffer)) {
|
||||
$raw .= $this->decrypt ? $this->decrypt->decrypt($buffer) : $buffer;
|
||||
}
|
||||
|
||||
$payload = Strings::shift($raw, $packet_length - $padding_length - 1);
|
||||
$padding = Strings::shift($raw, $padding_length); // should leave $raw empty
|
||||
|
||||
if ($this->hmac_check instanceof Hash) {
|
||||
$hmac = stream_get_contents($this->fsock, $this->hmac_size);
|
||||
if ($hmac === false || strlen($hmac) != $this->hmac_size) {
|
||||
$this->disconnect_helper(NET_SSH2_DISCONNECT_MAC_ERROR);
|
||||
throw new \RuntimeException('Error reading socket');
|
||||
}
|
||||
|
||||
$reconstructed = !$this->hmac_check_etm ?
|
||||
pack('NCa*', $packet_length, $padding_length, $payload . $padding) :
|
||||
$encrypted;
|
||||
pack('Na*', $packet->packet_length, $packet->plain) :
|
||||
substr($packet->raw, 0, -$this->hmac_size);
|
||||
if (($this->hmac_check->getHash() & "\xFF\xFF\xFF\xFF") == 'umac') {
|
||||
$this->hmac_check->setNonce("\0\0\0\0" . pack('N', $this->get_seq_no));
|
||||
if ($hmac != $this->hmac_check->hash($reconstructed)) {
|
||||
$this->disconnect_helper(NET_SSH2_DISCONNECT_MAC_ERROR);
|
||||
throw new \RuntimeException('Invalid UMAC');
|
||||
throw new ConnectionClosedException('Invalid UMAC');
|
||||
}
|
||||
} else {
|
||||
if ($hmac != $this->hmac_check->hash(pack('Na*', $this->get_seq_no, $reconstructed))) {
|
||||
$this->disconnect_helper(NET_SSH2_DISCONNECT_MAC_ERROR);
|
||||
throw new \RuntimeException('Invalid HMAC');
|
||||
throw new ConnectionClosedException('Invalid HMAC');
|
||||
}
|
||||
}
|
||||
}
|
||||
$padding_length = 0;
|
||||
$payload = $packet->plain;
|
||||
extract(unpack('Cpadding_length', Strings::shift($payload, 1)));
|
||||
if ($padding_length > 0) {
|
||||
Strings::pop($payload, $padding_length);
|
||||
}
|
||||
if (empty($payload)) {
|
||||
$this->disconnect_helper(NET_SSH2_DISCONNECT_PROTOCOL_ERROR);
|
||||
throw new ConnectionClosedException('Plaintext is too short');
|
||||
}
|
||||
|
||||
switch ($this->decompress) {
|
||||
case self::NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH:
|
||||
@ -3736,63 +3706,69 @@ class SSH2
|
||||
$this->last_packet = $current;
|
||||
}
|
||||
|
||||
return $this->filter($payload, $skip_channel_filter);
|
||||
return $this->filter($payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read Remaining Bytes
|
||||
*
|
||||
* @see self::get_binary_packet()
|
||||
* @param int $remaining_length
|
||||
* @return string
|
||||
* @param object $packet The packet object being constructed, passed by reference
|
||||
* The size, packet_length, and plain properties of this object may be modified in processing
|
||||
* @throws InvalidPacketLengthException if the packet length header is invalid
|
||||
*/
|
||||
private function read_remaining_bytes($remaining_length)
|
||||
private function get_binary_packet_size(&$packet)
|
||||
{
|
||||
if (!$remaining_length) {
|
||||
return '';
|
||||
$packet_length_header_size = 4;
|
||||
if (strlen($packet->raw) < $packet_length_header_size) {
|
||||
return;
|
||||
}
|
||||
|
||||
$adjustLength = false;
|
||||
$packet_length = 0;
|
||||
$added_validation_length = 0; // indicates when the packet length header is included when validating packet length against block size
|
||||
if ($this->decrypt) {
|
||||
switch (true) {
|
||||
case $this->decryptName == 'aes128-gcm@openssh.com':
|
||||
case $this->decryptName == 'aes256-gcm@openssh.com':
|
||||
case $this->decryptName == 'chacha20-poly1305@openssh.com':
|
||||
case $this->hmac_check instanceof Hash && $this->hmac_check_etm:
|
||||
$remaining_length += $this->decrypt_block_size - 4;
|
||||
$adjustLength = true;
|
||||
switch ($this->decryptName) {
|
||||
case 'aes128-gcm@openssh.com':
|
||||
case 'aes256-gcm@openssh.com':
|
||||
extract(unpack('Npacket_length', substr($packet->raw, 0, $packet_length_header_size)));
|
||||
$packet->size = $packet_length_header_size + $packet_length + $this->decrypt_block_size; // expect tag
|
||||
break;
|
||||
case 'chacha20-poly1305@openssh.com':
|
||||
$this->lengthDecrypt->setNonce(pack('N2', 0, $this->get_seq_no));
|
||||
$packet_length_header = $this->lengthDecrypt->decrypt(substr($packet->raw, 0, $packet_length_header_size));
|
||||
extract(unpack('Npacket_length', $packet_length_header));
|
||||
$packet->size = $packet_length_header_size + $packet_length + 16; // expect tag
|
||||
break;
|
||||
default:
|
||||
if (!$this->hmac_check instanceof Hash || !$this->hmac_check_etm) {
|
||||
if (strlen($packet->raw) < $this->decrypt_block_size) {
|
||||
return;
|
||||
}
|
||||
$packet->plain = $this->decrypt->decrypt(substr($packet->raw, 0, $this->decrypt_block_size));
|
||||
extract(unpack('Npacket_length', Strings::shift($packet->plain, $packet_length_header_size)));
|
||||
$packet->size = $packet_length_header_size + $packet_length;
|
||||
$added_validation_length = $packet_length_header_size;
|
||||
} else {
|
||||
extract(unpack('Npacket_length', substr($packet->raw, 0, $packet_length_header_size)));
|
||||
$packet->size = $packet_length_header_size + $packet_length;
|
||||
}
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
extract(unpack('Npacket_length', substr($packet->raw, 0, $packet_length_header_size)));
|
||||
$packet->size = $packet_length_header_size + $packet_length;
|
||||
$added_validation_length = $packet_length_header_size;
|
||||
}
|
||||
|
||||
// quoting <http://tools.ietf.org/html/rfc4253#section-6.1>,
|
||||
// "implementations SHOULD check that the packet length is reasonable"
|
||||
// PuTTY uses 0x9000 as the actual max packet size and so to shall we
|
||||
// don't do this when GCM mode is used since GCM mode doesn't encrypt the length
|
||||
if ($remaining_length < -$this->decrypt_block_size || $remaining_length > 0x9000 || $remaining_length % $this->decrypt_block_size != 0) {
|
||||
if (!$this->bad_key_size_fix && self::bad_algorithm_candidate($this->decrypt ? $this->decryptName : '') && !($this->bitmap & SSH2::MASK_LOGIN)) {
|
||||
$this->bad_key_size_fix = true;
|
||||
$this->reset_connection(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED);
|
||||
return false;
|
||||
}
|
||||
throw new \RuntimeException('Invalid size');
|
||||
if (
|
||||
$packet_length <= 0 || $packet_length > 0x9000
|
||||
|| ($packet_length + $added_validation_length) % $this->decrypt_block_size != 0
|
||||
) {
|
||||
$this->disconnect_helper(NET_SSH2_DISCONNECT_PROTOCOL_ERROR);
|
||||
throw new InvalidPacketLengthException('Invalid packet length');
|
||||
}
|
||||
|
||||
if ($adjustLength) {
|
||||
$remaining_length -= $this->decrypt_block_size - 4;
|
||||
if ($this->hmac_check instanceof Hash) {
|
||||
$packet->size += $this->hmac_size;
|
||||
}
|
||||
|
||||
$buffer = '';
|
||||
while ($remaining_length > 0) {
|
||||
$temp = stream_get_contents($this->fsock, $remaining_length);
|
||||
if ($temp === false || feof($this->fsock)) {
|
||||
$this->disconnect_helper(NET_SSH2_DISCONNECT_CONNECTION_LOST);
|
||||
throw new \RuntimeException('Error reading from socket');
|
||||
}
|
||||
$buffer .= $temp;
|
||||
$remaining_length -= strlen($temp);
|
||||
}
|
||||
|
||||
return $buffer;
|
||||
$packet->packet_length = $packet_length;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -3802,10 +3778,9 @@ class SSH2
|
||||
*
|
||||
* @see self::_get_binary_packet()
|
||||
* @param string $payload
|
||||
* @param bool $skip_channel_filter
|
||||
* @return string|bool
|
||||
*/
|
||||
private function filter($payload, $skip_channel_filter)
|
||||
private function filter($payload)
|
||||
{
|
||||
switch (ord($payload[0])) {
|
||||
case NET_SSH2_MSG_DISCONNECT:
|
||||
@ -3816,14 +3791,14 @@ class SSH2
|
||||
return false;
|
||||
case NET_SSH2_MSG_IGNORE:
|
||||
$this->extra_packets++;
|
||||
$payload = $this->get_binary_packet($skip_channel_filter);
|
||||
$payload = $this->get_binary_packet();
|
||||
break;
|
||||
case NET_SSH2_MSG_DEBUG:
|
||||
$this->extra_packets++;
|
||||
Strings::shift($payload, 2); // second byte is "always_display"
|
||||
list($message) = Strings::unpackSSH2('s', $payload);
|
||||
$this->errors[] = "SSH_MSG_DEBUG: $message";
|
||||
$payload = $this->get_binary_packet($skip_channel_filter);
|
||||
$payload = $this->get_binary_packet();
|
||||
break;
|
||||
case NET_SSH2_MSG_UNIMPLEMENTED:
|
||||
return false;
|
||||
@ -3834,7 +3809,7 @@ class SSH2
|
||||
$this->bitmap = 0;
|
||||
return false;
|
||||
}
|
||||
$payload = $this->get_binary_packet($skip_channel_filter);
|
||||
$payload = $this->get_binary_packet();
|
||||
}
|
||||
break;
|
||||
case NET_SSH2_MSG_EXT_INFO:
|
||||
@ -3846,7 +3821,7 @@ class SSH2
|
||||
$this->supported_private_key_algorithms = explode(',', $extension_value);
|
||||
}
|
||||
}
|
||||
$payload = $this->get_binary_packet($skip_channel_filter);
|
||||
$payload = $this->get_binary_packet();
|
||||
}
|
||||
|
||||
// see http://tools.ietf.org/html/rfc4252#section-5.4; only called when the encryption has been activated and when we haven't already logged in
|
||||
@ -3870,20 +3845,10 @@ class SSH2
|
||||
if (ord(substr($payload, 9 + $length))) { // want reply
|
||||
$this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_SUCCESS, $this->server_channels[$channel]));
|
||||
}
|
||||
$payload = $this->get_binary_packet($skip_channel_filter);
|
||||
$payload = $this->get_binary_packet();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case NET_SSH2_MSG_CHANNEL_DATA:
|
||||
case NET_SSH2_MSG_CHANNEL_EXTENDED_DATA:
|
||||
case NET_SSH2_MSG_CHANNEL_CLOSE:
|
||||
case NET_SSH2_MSG_CHANNEL_EOF:
|
||||
if (!$skip_channel_filter && !empty($this->server_channels)) {
|
||||
$this->binary_packet_buffer = $payload;
|
||||
$this->get_channel_packet(true);
|
||||
$payload = $this->get_binary_packet();
|
||||
}
|
||||
break;
|
||||
case NET_SSH2_MSG_GLOBAL_REQUEST: // see http://tools.ietf.org/html/rfc4254#section-4
|
||||
Strings::shift($payload, 1);
|
||||
list($request_name) = Strings::unpackSSH2('s', $payload);
|
||||
@ -3895,7 +3860,7 @@ class SSH2
|
||||
return $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION);
|
||||
}
|
||||
|
||||
$payload = $this->get_binary_packet($skip_channel_filter);
|
||||
$payload = $this->get_binary_packet();
|
||||
break;
|
||||
case NET_SSH2_MSG_CHANNEL_OPEN: // see http://tools.ietf.org/html/rfc4254#section-5.1
|
||||
Strings::shift($payload, 1);
|
||||
@ -3948,7 +3913,7 @@ class SSH2
|
||||
}
|
||||
}
|
||||
|
||||
$payload = $this->get_binary_packet($skip_channel_filter);
|
||||
$payload = $this->get_binary_packet();
|
||||
break;
|
||||
case NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST:
|
||||
Strings::shift($payload, 1);
|
||||
@ -3956,7 +3921,7 @@ class SSH2
|
||||
|
||||
$this->window_size_client_to_server[$channel] += $window_size;
|
||||
|
||||
$payload = ($this->bitmap & self::MASK_WINDOW_ADJUST) ? true : $this->get_binary_packet($skip_channel_filter);
|
||||
$payload = ($this->bitmap & self::MASK_WINDOW_ADJUST) ? true : $this->get_binary_packet();
|
||||
}
|
||||
}
|
||||
|
||||
@ -4070,23 +4035,17 @@ class SSH2
|
||||
}
|
||||
|
||||
while (true) {
|
||||
if ($this->binary_packet_buffer !== false) {
|
||||
$response = $this->binary_packet_buffer;
|
||||
$this->binary_packet_buffer = false;
|
||||
} else {
|
||||
$response = $this->get_binary_packet(true);
|
||||
if ($response === true && $this->is_timeout) {
|
||||
if ($client_channel == self::CHANNEL_EXEC && !$this->request_pty) {
|
||||
$this->close_channel($client_channel);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if ($response === false) {
|
||||
$this->disconnect_helper(NET_SSH2_DISCONNECT_CONNECTION_LOST);
|
||||
throw new ConnectionClosedException('Connection closed by server');
|
||||
$response = $this->get_binary_packet();
|
||||
if ($response === true && $this->is_timeout) {
|
||||
if ($client_channel == self::CHANNEL_EXEC && !$this->request_pty) {
|
||||
$this->close_channel($client_channel);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if ($response === false) {
|
||||
$this->disconnect_helper(NET_SSH2_DISCONNECT_CONNECTION_LOST);
|
||||
throw new ConnectionClosedException('Connection closed by server');
|
||||
}
|
||||
|
||||
if ($client_channel == -1 && $response === true) {
|
||||
return true;
|
||||
}
|
||||
@ -4416,6 +4375,18 @@ class SSH2
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a keep-alive message, if keep-alive is enabled and interval is met
|
||||
*/
|
||||
private function send_keep_alive()
|
||||
{
|
||||
$elapsed = microtime(true) - $this->keep_alive_sent;
|
||||
if ($this->keepAlive > 0 && $elapsed >= $this->keepAlive) {
|
||||
$this->keep_alive_sent = microtime(true);
|
||||
$this->send_binary_packet(pack('CN', NET_SSH2_MSG_IGNORE, 0));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs data packets
|
||||
*
|
||||
@ -4637,10 +4608,7 @@ class SSH2
|
||||
}
|
||||
}
|
||||
|
||||
$this->bitmap = 0;
|
||||
if (is_resource($this->fsock) && get_resource_type($this->fsock) === 'stream') {
|
||||
fclose($this->fsock);
|
||||
}
|
||||
$this->reset_connection();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
@ -8,6 +8,7 @@
|
||||
|
||||
namespace phpseclib3\Tests\Functional\Net;
|
||||
|
||||
use phpseclib3\Exception\NoSupportedAlgorithmsException;
|
||||
use phpseclib3\Net\SSH2;
|
||||
use phpseclib3\Tests\PhpseclibFunctionalTestCase;
|
||||
|
||||
@ -559,4 +560,96 @@ class SSH2Test extends PhpseclibFunctionalTestCase
|
||||
$ssh->reset(SSH2::CHANNEL_SHELL);
|
||||
$this->assertSame(0, $ssh->getOpenChannelCount());
|
||||
}
|
||||
|
||||
public function testPing()
|
||||
{
|
||||
$ssh = $this->getSSH2();
|
||||
// assert on unauthenticated ssh2
|
||||
$this->assertNotEmpty($ssh->getServerIdentification());
|
||||
$this->assertFalse($ssh->ping());
|
||||
$this->assertTrue($ssh->isConnected());
|
||||
$this->assertSame(0, $ssh->getOpenChannelCount());
|
||||
|
||||
$ssh = $this->getSSH2Login();
|
||||
$this->assertTrue($ssh->ping());
|
||||
$this->assertSame(0, $ssh->getOpenChannelCount());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getCryptoAlgorithms()
|
||||
{
|
||||
$map = [
|
||||
'kex' => SSH2::getSupportedKEXAlgorithms(),
|
||||
'hostkey' => SSH2::getSupportedHostKeyAlgorithms(),
|
||||
'comp' => SSH2::getSupportedCompressionAlgorithms(),
|
||||
'crypt' => SSH2::getSupportedEncryptionAlgorithms(),
|
||||
'mac' => SSH2::getSupportedMACAlgorithms(),
|
||||
];
|
||||
$tests = [];
|
||||
foreach ($map as $type => $algorithms) {
|
||||
foreach ($algorithms as $algorithm) {
|
||||
$tests[] = [$type, $algorithm];
|
||||
}
|
||||
}
|
||||
return $tests;
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getCryptoAlgorithms
|
||||
* @param string $type
|
||||
* @param string $algorithm
|
||||
*/
|
||||
public function testCryptoAlgorithms($type, $algorithm)
|
||||
{
|
||||
$ssh = $this->getSSH2();
|
||||
try {
|
||||
switch ($type) {
|
||||
case 'kex':
|
||||
case 'hostkey':
|
||||
$ssh->setPreferredAlgorithms([$type => [$algorithm]]);
|
||||
$this->assertEquals($algorithm, $ssh->getAlgorithmsNegotiated()[$type]);
|
||||
break;
|
||||
case 'comp':
|
||||
case 'crypt':
|
||||
$ssh->setPreferredAlgorithms([
|
||||
'client_to_server' => [$type => [$algorithm]],
|
||||
'server_to_client' => [$type => [$algorithm]],
|
||||
]);
|
||||
$this->assertEquals($algorithm, $ssh->getAlgorithmsNegotiated()['client_to_server'][$type]);
|
||||
$this->assertEquals($algorithm, $ssh->getAlgorithmsNegotiated()['server_to_client'][$type]);
|
||||
break;
|
||||
case 'mac':
|
||||
$macCryptAlgorithms = array_filter(
|
||||
SSH2::getSupportedEncryptionAlgorithms(),
|
||||
function ($algorithm) use ($ssh) {
|
||||
return !self::callFunc($ssh, 'encryption_algorithm_to_crypt_instance', [$algorithm])
|
||||
->usesNonce();
|
||||
}
|
||||
);
|
||||
$ssh->setPreferredAlgorithms([
|
||||
'client_to_server' => ['crypt' => $macCryptAlgorithms, 'mac' => [$algorithm]],
|
||||
'server_to_client' => ['crypt' => $macCryptAlgorithms, 'mac' => [$algorithm]],
|
||||
]);
|
||||
$this->assertEquals($algorithm, $ssh->getAlgorithmsNegotiated()['client_to_server']['mac']);
|
||||
$this->assertEquals($algorithm, $ssh->getAlgorithmsNegotiated()['server_to_client']['mac']);
|
||||
break;
|
||||
}
|
||||
} catch (NoSupportedAlgorithmsException $e) {
|
||||
self::markTestSkipped("{$type} algorithm {$algorithm} is not supported by server");
|
||||
}
|
||||
|
||||
$username = $this->getEnv('SSH_USERNAME');
|
||||
$password = $this->getEnv('SSH_PASSWORD');
|
||||
$this->assertTrue(
|
||||
$ssh->login($username, $password),
|
||||
"SSH2 login using {$type} {$algorithm} failed."
|
||||
);
|
||||
|
||||
$ssh->setTimeout(1);
|
||||
$ssh->write("pwd\n");
|
||||
$this->assertNotEmpty($ssh->read('', SSH2::READ_NEXT));
|
||||
$ssh->disconnect();
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user