diff --git a/.travis.yml b/.travis.yml index bc9febad..e7108450 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,7 +14,7 @@ env: before_install: true install: - - wget http://ftp.gnu.org/gnu/parallel/parallel-20120522.tar.bz2 + - wget http://ftp.gnu.org/gnu/parallel/parallel-20170822.tar.bz2 - tar -xvjf parallel* - cd parallel* - ./configure diff --git a/README.md b/README.md index 25df8779..ba4cedff 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ AES, Blowfish, Twofish, SSH-1, SSH-2, SFTP, and X.509 * [Browse Git](https://github.com/phpseclib/phpseclib) * [Code Coverage Report](https://coverage.phpseclib.org/master/latest/) +* Support phpseclib development by [![Becoming a patron](https://img.shields.io/badge/become-patron-brightgreen.svg)](https://www.patreon.com/phpseclib) ## Documentation diff --git a/composer.json b/composer.json index 872574a2..a68d0c84 100644 --- a/composer.json +++ b/composer.json @@ -51,9 +51,9 @@ } ], "require": { - "paragonie/constant_time_encoding": "^1|^2", + "paragonie/constant_time_encoding": "^1", "paragonie/random_compat": "^1.4|^2.0", - "php": ">=7.0" + "php": ">=5.6" }, "require-dev": { "phing/phing": "~2.7", diff --git a/phpseclib/File/ASN1.php b/phpseclib/File/ASN1.php index 53ac6ff2..1a8c02d9 100644 --- a/phpseclib/File/ASN1.php +++ b/phpseclib/File/ASN1.php @@ -27,6 +27,8 @@ use ParagonIE\ConstantTime\Base64; use phpseclib\File\ASN1\Element; use phpseclib\Math\BigInteger; use phpseclib\Common\Functions\Strings; +use DateTime; +use DateTimeZone; /** * Pure-PHP ASN.1 Parser @@ -739,7 +741,7 @@ abstract class ASN1 if (isset($mapping['implicit'])) { $decoded['content'] = self::decodeTime($decoded['content'], $decoded['type']); } - return @date(self::$format, $decoded['content']); + return $decoded['content'] ? $decoded['content']->format(self::$format) : false; case self::TYPE_BIT_STRING: if (isset($mapping['mapping'])) { $offset = ord($decoded['content'][0]); @@ -989,7 +991,8 @@ abstract class ASN1 case self::TYPE_GENERALIZED_TIME: $format = $mapping['type'] == self::TYPE_UTC_TIME ? 'y' : 'Y'; $format.= 'mdHis'; - $value = @gmdate($format, strtotime($source)) . 'Z'; + $date = new DateTime($source, new DateTimeZone('GMT')); + $value = $date->format($format) . 'Z'; break; case self::TYPE_BIT_STRING: if (isset($mapping['mapping'])) { @@ -1151,33 +1154,32 @@ abstract class ASN1 http://tools.ietf.org/html/rfc5280#section-4.1.2.5.2 http://www.obj-sys.com/asn1tutorial/node14.html */ - $pattern = $tag == self::TYPE_UTC_TIME ? - '#^(..)(..)(..)(..)(..)(..)?(.*)$#' : - '#(....)(..)(..)(..)(..)(..).*([Z+-].*)$#'; - - preg_match($pattern, $content, $matches); - - list(, $year, $month, $day, $hour, $minute, $second, $timezone) = $matches; + $format = 'YmdHis'; if ($tag == self::TYPE_UTC_TIME) { - $year = $year >= 50 ? "19$year" : "20$year"; - } - - if ($timezone == 'Z') { - $mktime = 'gmmktime'; - $timezone = 0; - } elseif (preg_match('#([+-])(\d\d)(\d\d)#', $timezone, $matches)) { - $mktime = 'gmmktime'; - $timezone = 60 * $matches[3] + 3600 * $matches[2]; - if ($matches[1] == '-') { - $timezone = -$timezone; + // https://www.itu.int/ITU-T/studygroups/com17/languages/X.690-0207.pdf#page=28 says "the seconds + // element shall always be present" but none-the-less I've seen X509 certs where it isn't and if the + // browsers parse it phpseclib ought to too + if (preg_match('#^(\d{10})(Z|[+-]\d{4})$#', $content, $matches)) { + $content = $matches[1] . '00' . $matches[2]; } - } else { - $mktime = 'mktime'; - $timezone = 0; + $prefix = substr($content, 0, 2) >= 50 ? '19' : '20'; + $content = $prefix . $content; + } elseif (strpos($content, '.') !== false) { + $format.= '.u'; } - return @$mktime((int)$hour, (int)$minute, (int)$second, (int)$month, (int)$day, (int)$year) + $timezone; + if ($content[strlen($content) - 1] == 'Z') { + $content = substr($content, 0, -1) . '+0000'; + } + + if (strpos($content, '-') !== false || strpos($content, '+') !== false) { + $format.= 'O'; + } + + // error supression isn't necessary as of PHP 7.0: + // http://php.net/manual/en/migration70.other-changes.php + return @DateTime::createFromFormat($format, $content); } /** diff --git a/phpseclib/File/X509.php b/phpseclib/File/X509.php index 51161b21..c5e36909 100644 --- a/phpseclib/File/X509.php +++ b/phpseclib/File/X509.php @@ -35,7 +35,8 @@ use phpseclib\Exception\UnsupportedAlgorithmException; use phpseclib\File\ASN1\Element; use phpseclib\Math\BigInteger; use phpseclib\File\ASN1\Maps; - +use DateTime; +use DateTimeZone; /** * Pure-PHP X.509 Parser @@ -1061,7 +1062,7 @@ class X509 } if (!isset($date)) { - $date = time(); + $date = new DateTime($date, new DateTimeZone(@date_default_timezone_get())); } $notBefore = $this->currentCert['tbsCertificate']['validity']['notBefore']; @@ -1071,8 +1072,8 @@ class X509 $notAfter = isset($notAfter['generalTime']) ? $notAfter['generalTime'] : $notAfter['utcTime']; switch (true) { - case $date < @strtotime($notBefore): - case $date > @strtotime($notAfter): + case $date < new DateTime($notBefore, new DateTimeZone(@date_default_timezone_get())): + case $date > new DateTime($notAfter, new DateTimeZone(@date_default_timezone_get())): return false; } @@ -2290,7 +2291,11 @@ class X509 */ private function timeField($date) { - $year = @gmdate("Y", @strtotime($date)); // the same way ASN1.php parses this + if ($date instanceof Element) { + return $date; + } + $dateObj = new DateTime($date, new DateTimeZone('GMT')); + $year = $dateObj->format('Y'); // the same way ASN1.php parses this if ($year < 2050) { return ['utcTime' => $date]; } else { @@ -2355,8 +2360,12 @@ class X509 return false; } - $startDate = !empty($this->startDate) ? $this->startDate : @date('D, d M Y H:i:s O'); - $endDate = !empty($this->endDate) ? $this->endDate : @date('D, d M Y H:i:s O', strtotime('+1 year')); + $startDate = new DateTime('now', new DateTimeZone(@date_default_timezone_get())); + $startDate = !empty($this->startDate) ? $this->startDate : $startDate->format('D, d M Y H:i:s O'); + + $endDate = new DateTime('+1 year', new DateTimeZone(@date_default_timezone_get())); + $endDate = !empty($this->endDate) ? $this->endDate : $endDate->format('D, d M Y H:i:s O'); + /* "The serial number MUST be a positive integer" "Conforming CAs MUST NOT use serialNumber values longer than 20 octets." -- https://tools.ietf.org/html/rfc5280#section-4.1.2.2 @@ -2624,7 +2633,9 @@ class X509 $currentCert = isset($this->currentCert) ? $this->currentCert : null; $signatureSubject = isset($this->signatureSubject) ? $this->signatureSubject : null; - $thisUpdate = !empty($this->startDate) ? $this->startDate : @date('D, d M Y H:i:s O'); + + $thisUpdate = new DateTime('now', new DateTimeZone(@date_default_timezone_get())); + $thisUpdate = !empty($this->startDate) ? $this->startDate : $thisUpdate->format('D, d M Y H:i:s O'); if (isset($crl->currentCert) && is_array($crl->currentCert) && isset($crl->currentCert['tbsCertList'])) { $this->currentCert = $crl->currentCert; @@ -2777,7 +2788,11 @@ class X509 */ public function setStartDate($date) { - $this->startDate = @date('D, d M Y H:i:s O', @strtotime($date)); + if (!is_object($date) || !is_a($date, 'DateTime')) { + $date = new DateTime($date); + } + + $this->startDate = $date->format('D, d M Y H:i:s O', new DateTimeZone(@date_default_timezone_get())); } /** @@ -2800,7 +2815,11 @@ class X509 $temp = chr(ASN1::TYPE_GENERALIZED_TIME) . ASN1::encodeLength(strlen($temp)) . $temp; $this->endDate = new Element($temp); } else { - $this->endDate = @date('D, d M Y H:i:s O', @strtotime($date)); + if (!is_object($date) || !is_a($date, 'DateTime')) { + $date = new DateTime($date); + } + + $this->endDate = $date->format('D, d M Y H:i:s O', new DateTimeZone(@date_default_timezone_get())); } } @@ -3530,8 +3549,9 @@ class X509 } $i = count($rclist); + $revocationDate = new DateTime('now', new DateTimeZone(@date_default_timezone_get())); $rclist[] = ['userCertificate' => $serial, - 'revocationDate' => $this->timeField(@date('D, d M Y H:i:s O'))]; + 'revocationDate' => $this->timeField($revocationDate->format('D, d M Y H:i:s O'))]; return $i; } diff --git a/phpseclib/Net/SFTP.php b/phpseclib/Net/SFTP.php index 63496e69..6aef3f04 100644 --- a/phpseclib/Net/SFTP.php +++ b/phpseclib/Net/SFTP.php @@ -1969,7 +1969,7 @@ class SFTP extends SSH2 if (isset($fp)) { $stat = fstat($fp); - $size = $stat['size']; + $size = !empty($stat) ? $stat['size'] : 0; if ($local_start >= 0) { fseek($fp, $local_start); diff --git a/phpseclib/Net/SSH2.php b/phpseclib/Net/SSH2.php index 5fc9dc6d..4c809f78 100644 --- a/phpseclib/Net/SSH2.php +++ b/phpseclib/Net/SSH2.php @@ -103,10 +103,10 @@ class SSH2 * @see \phpseclib\Net\SSH2::_get_channel_packet() * @access private */ - const CHANNEL_EXEC = 0; // PuTTy uses 0x100 - const CHANNEL_SHELL = 1; - const CHANNEL_SUBSYSTEM = 2; - const CHANNEL_AGENT_FORWARD = 3; + const CHANNEL_EXEC = 1; // PuTTy uses 0x100 + const CHANNEL_SHELL = 2; + const CHANNEL_SUBSYSTEM = 3; + const CHANNEL_AGENT_FORWARD = 4; /**#@-*/ /**#@+ @@ -897,6 +897,38 @@ class SSH2 */ private $send_kex_first = true; + /** + * Some versions of OpenSSH incorrectly calculate the key size + * + * @var bool + * @access private + */ + private $bad_key_size_fix = false; + + /** + * The selected decryption algorithm + * + * @var string + * @access private + */ + private $decrypt_algorithm = ''; + + /** + * Should we try to re-connect to re-establish keys? + * + * @var bool + * @access private + */ + private $retry_connect = false; + + /** + * Binary Packet Buffer + * + * @var string|false + * @access private + */ + private $binary_packet_buffer = false; + /** * Default Constructor. * @@ -1027,7 +1059,7 @@ class SSH2 * * @access public */ - function sendIdentificationStringFirst() + public function sendIdentificationStringFirst() { $this->send_id_string_first = true; } @@ -1041,7 +1073,7 @@ class SSH2 * * @access public */ - function sendIdentificationStringLast() + public function sendIdentificationStringLast() { $this->send_id_string_first = false; } @@ -1055,7 +1087,7 @@ class SSH2 * * @access public */ - function sendKEXINITFirst() + public function sendKEXINITFirst() { $this->send_kex_first = true; } @@ -1069,7 +1101,7 @@ class SSH2 * * @access public */ - function sendKEXINITLast() + public function sendKEXINITLast() { $this->send_kex_first = false; } @@ -1190,8 +1222,8 @@ class SSH2 $this->errors[] = utf8_decode($data); } - if ($matches[3] != '1.99' && $matches[3] != '2.0') { - throw new \RuntimeException("Cannot connect to SSH $matches[1] servers"); + if (version_compare($matches[3], '1.99', '<')) { + throw new \RuntimeException("Cannot connect to SSH $matches[3] servers"); } if (!$this->send_id_string_first) { @@ -1789,6 +1821,8 @@ class SSH2 throw new \UnexpectedValueException('Expected SSH_MSG_NEWKEYS'); } + $this->decrypt_algorithm = $decrypt; + $keyBytes = pack('Na*', strlen($keyBytes), $keyBytes); $this->encrypt = $this->encryption_algorithm_to_crypt_instance($encrypt); @@ -1959,6 +1993,10 @@ class SSH2 */ private function encryption_algorithm_to_key_size($algorithm) { + if ($this->bad_key_size_fix && $this->bad_algorithm_candidate($algorithm)) { + return 16; + } + switch ($algorithm) { case 'none': return 0; @@ -2033,6 +2071,27 @@ class SSH2 return null; } + /* + * 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 + * @link https://bugzilla.mindrot.org/show_bug.cgi?id=1291 + * @param string $algorithm Name of the encryption algorithm + * @return bool + * @access private + */ + private function bad_algorithm_candidate($algorithm) + { + switch ($algorithm) { + case 'arcfour256': + case 'aes192-ctr': + case 'aes256-ctr': + return true; + } + + return false; + } + /** * Login * @@ -2114,6 +2173,13 @@ class SSH2 $response = $this->get_binary_packet(); if ($response === false) { + if ($this->retry_connect) { + $this->retry_connect = false; + if (!$this->connect()) { + return false; + } + return $this->login_helper($username, $password); + } throw new \RuntimeException('Connection closed by server'); } @@ -2676,7 +2742,7 @@ class SSH2 return false; } - $response = $this->get_binary_packet(); + $response = $this->get_binary_packet(true); if ($response === false) { throw new \RuntimeException('Connection closed by server'); } @@ -3130,6 +3196,24 @@ class SSH2 return (bool) ($this->bitmap & self::MASK_LOGIN); } + /** + * Resets a connection for re-use + * + * @param int $reason + * @access private + */ + private function reset_connection($reason) + { + $this->disconnect_helper($reason); + $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->retry_connect = true; + $this->get_seq_no = $this->send_seq_no = 0; + } + /** * Gets Binary Packets * @@ -3140,7 +3224,7 @@ class SSH2 * @throws \RuntimeException on connection errors * @access private */ - private function get_binary_packet() + private function get_binary_packet($filter_channel_packets = false) { if (!is_resource($this->fsock) || feof($this->fsock)) { $this->bitmap = 0; @@ -3169,6 +3253,11 @@ class SSH2 // "implementations SHOULD check that the packet length is reasonable" // PuTTY uses 0x9000 as the actual max packet size and so to 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->decrypt_algorithm) && !($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'); } @@ -3182,6 +3271,7 @@ class SSH2 $buffer.= $temp; $remaining_length-= strlen($temp); } + $stop = microtime(true); if (strlen($buffer)) { $raw.= $this->decrypt !== false ? $this->decrypt->decrypt($buffer) : $buffer; @@ -3215,7 +3305,7 @@ class SSH2 $this->last_packet = $current; } - return $this->filter($payload); + return $this->filter($payload, $filter_channel_packets); } /** @@ -3227,7 +3317,7 @@ class SSH2 * @return string * @access private */ - private function filter($payload) + private function filter($payload, $filter_channel_packets) { switch (ord($payload[0])) { case NET_SSH2_MSG_DISCONNECT: @@ -3277,6 +3367,17 @@ class SSH2 // only called when we've already logged in if (($this->bitmap & self::MASK_CONNECTED) && $this->isAuthenticated()) { switch (ord($payload[0])) { + case NET_SSH2_MSG_CHANNEL_DATA: + case NET_SSH2_MSG_CHANNEL_EXTENDED_DATA: + case NET_SSH2_MSG_CHANNEL_REQUEST: + case NET_SSH2_MSG_CHANNEL_CLOSE: + case NET_SSH2_MSG_CHANNEL_EOF: + if ($filter_channel_packets) { + $this->binary_packet_buffer = $payload; + $this->get_channel_packet(true); + $payload = $this->get_binary_packet(true); + } + break; case NET_SSH2_MSG_GLOBAL_REQUEST: // see http://tools.ietf.org/html/rfc4254#section-4 if (strlen($payload) < 4) { return false; @@ -3461,31 +3562,37 @@ class SSH2 } while (true) { - if ($this->curTimeout) { - if ($this->curTimeout < 0) { - $this->is_timeout = true; - return true; + if ($this->binary_packet_buffer !== false) { + $response = $this->binary_packet_buffer; + $this->binary_packet_buffer = false; + } else { + if ($this->curTimeout) { + if ($this->curTimeout < 0) { + $this->is_timeout = true; + return true; + } + + $read = [$this->fsock]; + $write = $except = null; + + $start = microtime(true); + $sec = floor($this->curTimeout); + $usec = 1000000 * ($this->curTimeout - $sec); + // on windows this returns a "Warning: Invalid CRT parameters detected" error + if (!@stream_select($read, $write, $except, $sec, $usec) && !count($read)) { + $this->is_timeout = true; + return true; + } + $elapsed = microtime(true) - $start; + $this->curTimeout-= $elapsed; } - $read = [$this->fsock]; - $write = $except = null; - - $start = microtime(true); - $sec = floor($this->curTimeout); - $usec = 1000000 * ($this->curTimeout - $sec); - // on windows this returns a "Warning: Invalid CRT parameters detected" error - if (!@stream_select($read, $write, $except, $sec, $usec) && !count($read)) { - $this->is_timeout = true; - return true; + $response = $this->get_binary_packet(); + if ($response === false) { + throw new \RuntimeException('Connection closed by server'); } - $elapsed = microtime(true) - $start; - $this->curTimeout-= $elapsed; } - $response = $this->get_binary_packet(); - if ($response === false) { - throw new \RuntimeException('Connection closed by server'); - } if ($client_channel == -1 && $response === true) { return true; } @@ -3805,7 +3912,7 @@ class SSH2 @flush(); @ob_flush(); break; - // basically the same thing as self::LOG_REALTIME with the caveat that NET_SFTP_LOG_REALTIME_FILENAME + // basically the same thing as self::LOG_REALTIME with the caveat that NET_SSH2_LOG_REALTIME_FILENAME // needs to be defined and that the resultant log file will be capped out at self::LOG_MAX_SIZE. // the earliest part of the log file is denoted by the first <<< START >>> and is not going to necessarily // at the beginning of the file diff --git a/tests/Functional/Net/SSH2Test.php b/tests/Functional/Net/SSH2Test.php index 7292a978..9a9abf20 100644 --- a/tests/Functional/Net/SSH2Test.php +++ b/tests/Functional/Net/SSH2Test.php @@ -148,5 +148,25 @@ class Functional_Net_SSH2Test extends PhpseclibFunctionalTestCase $ssh->exec('ls -latr'); $ssh->disablePTY(); $ssh->exec('pwd'); + + return $ssh; + } + + /** + * @depends testDisablePTY + * @group github1167 + */ + public function testChannelDataAfterOpen($ssh) + { + $ssh->write("ping 127.0.0.1\n"); + + $ssh->enablePTY(); + $ssh->exec('bash'); + + $ssh->write("ls -latr\n"); + + $ssh->setTimeout(1); + + $ssh->read(); } }