Rev 1463 | Details | Compare with Previous | Last modification | View Log | RSS feed
Rev | Author | Line No. | Line |
---|---|---|---|
827 | daniel-mar | 1 | <?php |
2 | |||
3 | /** |
||
4 | * Pure-PHP implementation of SSHv2. |
||
5 | * |
||
6 | * PHP version 5 |
||
7 | * |
||
8 | * Here are some examples of how to use this library: |
||
9 | * <code> |
||
10 | * <?php |
||
11 | * include 'vendor/autoload.php'; |
||
12 | * |
||
13 | * $ssh = new \phpseclib3\Net\SSH2('www.domain.tld'); |
||
14 | * if (!$ssh->login('username', 'password')) { |
||
15 | * exit('Login Failed'); |
||
16 | * } |
||
17 | * |
||
18 | * echo $ssh->exec('pwd'); |
||
19 | * echo $ssh->exec('ls -la'); |
||
20 | * ?> |
||
21 | * </code> |
||
22 | * |
||
23 | * <code> |
||
24 | * <?php |
||
25 | * include 'vendor/autoload.php'; |
||
26 | * |
||
27 | * $key = \phpseclib3\Crypt\PublicKeyLoader::load('...', '(optional) password'); |
||
28 | * |
||
29 | * $ssh = new \phpseclib3\Net\SSH2('www.domain.tld'); |
||
30 | * if (!$ssh->login('username', $key)) { |
||
31 | * exit('Login Failed'); |
||
32 | * } |
||
33 | * |
||
34 | * echo $ssh->read('username@username:~$'); |
||
35 | * $ssh->write("ls -la\n"); |
||
36 | * echo $ssh->read('username@username:~$'); |
||
37 | * ?> |
||
38 | * </code> |
||
39 | * |
||
40 | * @author Jim Wigginton <terrafrost@php.net> |
||
41 | * @copyright 2007 Jim Wigginton |
||
42 | * @license http://www.opensource.org/licenses/mit-license.html MIT License |
||
43 | * @link http://phpseclib.sourceforge.net |
||
44 | */ |
||
45 | |||
46 | namespace phpseclib3\Net; |
||
47 | |||
48 | use phpseclib3\Common\Functions\Strings; |
||
49 | use phpseclib3\Crypt\Blowfish; |
||
50 | use phpseclib3\Crypt\ChaCha20; |
||
51 | use phpseclib3\Crypt\Common\AsymmetricKey; |
||
52 | use phpseclib3\Crypt\Common\PrivateKey; |
||
53 | use phpseclib3\Crypt\Common\PublicKey; |
||
54 | use phpseclib3\Crypt\Common\SymmetricKey; |
||
55 | use phpseclib3\Crypt\DH; |
||
56 | use phpseclib3\Crypt\DSA; |
||
57 | use phpseclib3\Crypt\EC; |
||
58 | use phpseclib3\Crypt\Hash; |
||
59 | use phpseclib3\Crypt\Random; |
||
60 | use phpseclib3\Crypt\RC4; |
||
61 | use phpseclib3\Crypt\Rijndael; |
||
62 | use phpseclib3\Crypt\RSA; |
||
874 | daniel-mar | 63 | use phpseclib3\Crypt\TripleDES; // Used to do Diffie-Hellman key exchange and DSA/RSA signature verification. |
827 | daniel-mar | 64 | use phpseclib3\Crypt\Twofish; |
65 | use phpseclib3\Exception\ConnectionClosedException; |
||
66 | use phpseclib3\Exception\InsufficientSetupException; |
||
67 | use phpseclib3\Exception\NoSupportedAlgorithmsException; |
||
68 | use phpseclib3\Exception\UnableToConnectException; |
||
69 | use phpseclib3\Exception\UnsupportedAlgorithmException; |
||
70 | use phpseclib3\Exception\UnsupportedCurveException; |
||
71 | use phpseclib3\Math\BigInteger; |
||
72 | use phpseclib3\System\SSH\Agent; |
||
73 | |||
74 | /** |
||
75 | * Pure-PHP implementation of SSHv2. |
||
76 | * |
||
77 | * @author Jim Wigginton <terrafrost@php.net> |
||
78 | */ |
||
79 | class SSH2 |
||
80 | { |
||
81 | /**#@+ |
||
82 | * Compression Types |
||
83 | * |
||
84 | */ |
||
85 | /** |
||
86 | * No compression |
||
87 | */ |
||
88 | const NET_SSH2_COMPRESSION_NONE = 1; |
||
89 | /** |
||
90 | * zlib compression |
||
91 | */ |
||
92 | const NET_SSH2_COMPRESSION_ZLIB = 2; |
||
93 | /** |
||
94 | * zlib@openssh.com |
||
95 | */ |
||
96 | const NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH = 3; |
||
97 | /**#@-*/ |
||
98 | |||
99 | // Execution Bitmap Masks |
||
100 | const MASK_CONSTRUCTOR = 0x00000001; |
||
101 | const MASK_CONNECTED = 0x00000002; |
||
102 | const MASK_LOGIN_REQ = 0x00000004; |
||
103 | const MASK_LOGIN = 0x00000008; |
||
104 | const MASK_SHELL = 0x00000010; |
||
105 | const MASK_WINDOW_ADJUST = 0x00000020; |
||
106 | |||
107 | /* |
||
108 | * Channel constants |
||
109 | * |
||
110 | * RFC4254 refers not to client and server channels but rather to sender and recipient channels. we don't refer |
||
111 | * to them in that way because RFC4254 toggles the meaning. the client sends a SSH_MSG_CHANNEL_OPEN message with |
||
112 | * a sender channel and the server sends a SSH_MSG_CHANNEL_OPEN_CONFIRMATION in response, with a sender and a |
||
113 | * recipient channel. at first glance, you might conclude that SSH_MSG_CHANNEL_OPEN_CONFIRMATION's sender channel |
||
114 | * would be the same thing as SSH_MSG_CHANNEL_OPEN's sender channel, but it's not, per this snippet: |
||
115 | * The 'recipient channel' is the channel number given in the original |
||
116 | * open request, and 'sender channel' is the channel number allocated by |
||
117 | * the other side. |
||
118 | * |
||
119 | * @see \phpseclib3\Net\SSH2::send_channel_packet() |
||
120 | * @see \phpseclib3\Net\SSH2::get_channel_packet() |
||
121 | */ |
||
122 | const CHANNEL_EXEC = 1; // PuTTy uses 0x100 |
||
123 | const CHANNEL_SHELL = 2; |
||
124 | const CHANNEL_SUBSYSTEM = 3; |
||
125 | const CHANNEL_AGENT_FORWARD = 4; |
||
126 | const CHANNEL_KEEP_ALIVE = 5; |
||
127 | |||
128 | /** |
||
129 | * Returns the message numbers |
||
130 | * |
||
131 | * @see \phpseclib3\Net\SSH2::getLog() |
||
132 | */ |
||
133 | const LOG_SIMPLE = 1; |
||
134 | /** |
||
135 | * Returns the message content |
||
136 | * |
||
137 | * @see \phpseclib3\Net\SSH2::getLog() |
||
138 | */ |
||
139 | const LOG_COMPLEX = 2; |
||
140 | /** |
||
141 | * Outputs the content real-time |
||
142 | */ |
||
143 | const LOG_REALTIME = 3; |
||
144 | /** |
||
145 | * Dumps the content real-time to a file |
||
146 | */ |
||
147 | const LOG_REALTIME_FILE = 4; |
||
148 | /** |
||
1042 | daniel-mar | 149 | * Outputs the message numbers real-time |
150 | */ |
||
151 | const LOG_SIMPLE_REALTIME = 5; |
||
152 | /** |
||
827 | daniel-mar | 153 | * Make sure that the log never gets larger than this |
154 | * |
||
155 | * @see \phpseclib3\Net\SSH2::getLog() |
||
156 | */ |
||
157 | const LOG_MAX_SIZE = 1048576; // 1024 * 1024 |
||
158 | |||
159 | /** |
||
160 | * Returns when a string matching $expect exactly is found |
||
161 | * |
||
162 | * @see \phpseclib3\Net\SSH2::read() |
||
163 | */ |
||
164 | const READ_SIMPLE = 1; |
||
165 | /** |
||
166 | * Returns when a string matching the regular expression $expect is found |
||
167 | * |
||
168 | * @see \phpseclib3\Net\SSH2::read() |
||
169 | */ |
||
170 | const READ_REGEX = 2; |
||
171 | /** |
||
172 | * Returns whenever a data packet is received. |
||
173 | * |
||
174 | * Some data packets may only contain a single character so it may be necessary |
||
175 | * to call read() multiple times when using this option |
||
176 | * |
||
177 | * @see \phpseclib3\Net\SSH2::read() |
||
178 | */ |
||
179 | const READ_NEXT = 3; |
||
180 | |||
181 | /** |
||
182 | * The SSH identifier |
||
183 | * |
||
184 | * @var string |
||
185 | */ |
||
186 | private $identifier; |
||
187 | |||
188 | /** |
||
189 | * The Socket Object |
||
190 | * |
||
191 | * @var resource|closed-resource|null |
||
192 | */ |
||
193 | public $fsock; |
||
194 | |||
195 | /** |
||
196 | * Execution Bitmap |
||
197 | * |
||
198 | * The bits that are set represent functions that have been called already. This is used to determine |
||
199 | * if a requisite function has been successfully executed. If not, an error should be thrown. |
||
200 | * |
||
201 | * @var int |
||
202 | */ |
||
203 | protected $bitmap = 0; |
||
204 | |||
205 | /** |
||
206 | * Error information |
||
207 | * |
||
208 | * @see self::getErrors() |
||
209 | * @see self::getLastError() |
||
210 | * @var array |
||
211 | */ |
||
212 | private $errors = []; |
||
213 | |||
214 | /** |
||
215 | * Server Identifier |
||
216 | * |
||
217 | * @see self::getServerIdentification() |
||
218 | * @var string|false |
||
219 | */ |
||
220 | protected $server_identifier = false; |
||
221 | |||
222 | /** |
||
223 | * Key Exchange Algorithms |
||
224 | * |
||
225 | * @see self::getKexAlgorithims() |
||
226 | * @var array|false |
||
227 | */ |
||
228 | private $kex_algorithms = false; |
||
229 | |||
230 | /** |
||
231 | * Key Exchange Algorithm |
||
232 | * |
||
233 | * @see self::getMethodsNegotiated() |
||
234 | * @var string|false |
||
235 | */ |
||
236 | private $kex_algorithm = false; |
||
237 | |||
238 | /** |
||
239 | * Minimum Diffie-Hellman Group Bit Size in RFC 4419 Key Exchange Methods |
||
240 | * |
||
241 | * @see self::_key_exchange() |
||
242 | * @var int |
||
243 | */ |
||
244 | private $kex_dh_group_size_min = 1536; |
||
245 | |||
246 | /** |
||
247 | * Preferred Diffie-Hellman Group Bit Size in RFC 4419 Key Exchange Methods |
||
248 | * |
||
249 | * @see self::_key_exchange() |
||
250 | * @var int |
||
251 | */ |
||
252 | private $kex_dh_group_size_preferred = 2048; |
||
253 | |||
254 | /** |
||
255 | * Maximum Diffie-Hellman Group Bit Size in RFC 4419 Key Exchange Methods |
||
256 | * |
||
257 | * @see self::_key_exchange() |
||
258 | * @var int |
||
259 | */ |
||
260 | private $kex_dh_group_size_max = 4096; |
||
261 | |||
262 | /** |
||
263 | * Server Host Key Algorithms |
||
264 | * |
||
265 | * @see self::getServerHostKeyAlgorithms() |
||
266 | * @var array|false |
||
267 | */ |
||
268 | private $server_host_key_algorithms = false; |
||
269 | |||
270 | /** |
||
1051 | daniel-mar | 271 | * Supported Private Key Algorithms |
272 | * |
||
273 | * In theory this should be the same as the Server Host Key Algorithms but, in practice, |
||
274 | * some servers (eg. Azure) will support rsa-sha2-512 as a server host key algorithm but |
||
275 | * not a private key algorithm |
||
276 | * |
||
277 | * @see self::privatekey_login() |
||
278 | * @var array|false |
||
279 | */ |
||
280 | private $supported_private_key_algorithms = false; |
||
281 | |||
282 | /** |
||
827 | daniel-mar | 283 | * Encryption Algorithms: Client to Server |
284 | * |
||
285 | * @see self::getEncryptionAlgorithmsClient2Server() |
||
286 | * @var array|false |
||
287 | */ |
||
288 | private $encryption_algorithms_client_to_server = false; |
||
289 | |||
290 | /** |
||
291 | * Encryption Algorithms: Server to Client |
||
292 | * |
||
293 | * @see self::getEncryptionAlgorithmsServer2Client() |
||
294 | * @var array|false |
||
295 | */ |
||
296 | private $encryption_algorithms_server_to_client = false; |
||
297 | |||
298 | /** |
||
299 | * MAC Algorithms: Client to Server |
||
300 | * |
||
301 | * @see self::getMACAlgorithmsClient2Server() |
||
302 | * @var array|false |
||
303 | */ |
||
304 | private $mac_algorithms_client_to_server = false; |
||
305 | |||
306 | /** |
||
307 | * MAC Algorithms: Server to Client |
||
308 | * |
||
309 | * @see self::getMACAlgorithmsServer2Client() |
||
310 | * @var array|false |
||
311 | */ |
||
312 | private $mac_algorithms_server_to_client = false; |
||
313 | |||
314 | /** |
||
315 | * Compression Algorithms: Client to Server |
||
316 | * |
||
317 | * @see self::getCompressionAlgorithmsClient2Server() |
||
318 | * @var array|false |
||
319 | */ |
||
320 | private $compression_algorithms_client_to_server = false; |
||
321 | |||
322 | /** |
||
323 | * Compression Algorithms: Server to Client |
||
324 | * |
||
325 | * @see self::getCompressionAlgorithmsServer2Client() |
||
326 | * @var array|false |
||
327 | */ |
||
328 | private $compression_algorithms_server_to_client = false; |
||
329 | |||
330 | /** |
||
331 | * Languages: Server to Client |
||
332 | * |
||
333 | * @see self::getLanguagesServer2Client() |
||
334 | * @var array|false |
||
335 | */ |
||
336 | private $languages_server_to_client = false; |
||
337 | |||
338 | /** |
||
339 | * Languages: Client to Server |
||
340 | * |
||
341 | * @see self::getLanguagesClient2Server() |
||
342 | * @var array|false |
||
343 | */ |
||
344 | private $languages_client_to_server = false; |
||
345 | |||
346 | /** |
||
347 | * Preferred Algorithms |
||
348 | * |
||
349 | * @see self::setPreferredAlgorithms() |
||
350 | * @var array |
||
351 | */ |
||
352 | private $preferred = []; |
||
353 | |||
354 | /** |
||
355 | * Block Size for Server to Client Encryption |
||
356 | * |
||
357 | * "Note that the length of the concatenation of 'packet_length', |
||
358 | * 'padding_length', 'payload', and 'random padding' MUST be a multiple |
||
359 | * of the cipher block size or 8, whichever is larger. This constraint |
||
360 | * MUST be enforced, even when using stream ciphers." |
||
361 | * |
||
362 | * -- http://tools.ietf.org/html/rfc4253#section-6 |
||
363 | * |
||
364 | * @see self::__construct() |
||
365 | * @see self::_send_binary_packet() |
||
366 | * @var int |
||
367 | */ |
||
368 | private $encrypt_block_size = 8; |
||
369 | |||
370 | /** |
||
371 | * Block Size for Client to Server Encryption |
||
372 | * |
||
373 | * @see self::__construct() |
||
374 | * @see self::_get_binary_packet() |
||
375 | * @var int |
||
376 | */ |
||
377 | private $decrypt_block_size = 8; |
||
378 | |||
379 | /** |
||
380 | * Server to Client Encryption Object |
||
381 | * |
||
382 | * @see self::_get_binary_packet() |
||
383 | * @var SymmetricKey|false |
||
384 | */ |
||
385 | private $decrypt = false; |
||
386 | |||
387 | /** |
||
388 | * Decryption Algorithm Name |
||
389 | * |
||
390 | * @var string|null |
||
391 | */ |
||
392 | private $decryptName; |
||
393 | |||
394 | /** |
||
395 | * Decryption Invocation Counter |
||
396 | * |
||
397 | * Used by GCM |
||
398 | * |
||
399 | * @var string|null |
||
400 | */ |
||
401 | private $decryptInvocationCounter; |
||
402 | |||
403 | /** |
||
404 | * Fixed Part of Nonce |
||
405 | * |
||
406 | * Used by GCM |
||
407 | * |
||
408 | * @var string|null |
||
409 | */ |
||
410 | private $decryptFixedPart; |
||
411 | |||
412 | /** |
||
413 | * Server to Client Length Encryption Object |
||
414 | * |
||
415 | * @see self::_get_binary_packet() |
||
416 | * @var object |
||
417 | */ |
||
418 | private $lengthDecrypt = false; |
||
419 | |||
420 | /** |
||
421 | * Client to Server Encryption Object |
||
422 | * |
||
423 | * @see self::_send_binary_packet() |
||
424 | * @var SymmetricKey|false |
||
425 | */ |
||
426 | private $encrypt = false; |
||
427 | |||
428 | /** |
||
429 | * Encryption Algorithm Name |
||
430 | * |
||
431 | * @var string|null |
||
432 | */ |
||
433 | private $encryptName; |
||
434 | |||
435 | /** |
||
436 | * Encryption Invocation Counter |
||
437 | * |
||
438 | * Used by GCM |
||
439 | * |
||
440 | * @var string|null |
||
441 | */ |
||
442 | private $encryptInvocationCounter; |
||
443 | |||
444 | /** |
||
445 | * Fixed Part of Nonce |
||
446 | * |
||
447 | * Used by GCM |
||
448 | * |
||
449 | * @var string|null |
||
450 | */ |
||
451 | private $encryptFixedPart; |
||
452 | |||
453 | /** |
||
454 | * Client to Server Length Encryption Object |
||
455 | * |
||
456 | * @see self::_send_binary_packet() |
||
457 | * @var object |
||
458 | */ |
||
459 | private $lengthEncrypt = false; |
||
460 | |||
461 | /** |
||
462 | * Client to Server HMAC Object |
||
463 | * |
||
464 | * @see self::_send_binary_packet() |
||
465 | * @var object |
||
466 | */ |
||
467 | private $hmac_create = false; |
||
468 | |||
469 | /** |
||
470 | * Client to Server HMAC Name |
||
471 | * |
||
472 | * @var string|false |
||
473 | */ |
||
474 | private $hmac_create_name; |
||
475 | |||
476 | /** |
||
477 | * Client to Server ETM |
||
478 | * |
||
479 | * @var int|false |
||
480 | */ |
||
481 | private $hmac_create_etm; |
||
482 | |||
483 | /** |
||
484 | * Server to Client HMAC Object |
||
485 | * |
||
486 | * @see self::_get_binary_packet() |
||
487 | * @var object |
||
488 | */ |
||
489 | private $hmac_check = false; |
||
490 | |||
491 | /** |
||
492 | * Server to Client HMAC Name |
||
493 | * |
||
494 | * @var string|false |
||
495 | */ |
||
496 | private $hmac_check_name; |
||
497 | |||
498 | /** |
||
499 | * Server to Client ETM |
||
500 | * |
||
501 | * @var int|false |
||
502 | */ |
||
503 | private $hmac_check_etm; |
||
504 | |||
505 | /** |
||
506 | * Size of server to client HMAC |
||
507 | * |
||
508 | * We need to know how big the HMAC will be for the server to client direction so that we know how many bytes to read. |
||
509 | * For the client to server side, the HMAC object will make the HMAC as long as it needs to be. All we need to do is |
||
510 | * append it. |
||
511 | * |
||
512 | * @see self::_get_binary_packet() |
||
513 | * @var int |
||
514 | */ |
||
515 | private $hmac_size = false; |
||
516 | |||
517 | /** |
||
518 | * Server Public Host Key |
||
519 | * |
||
520 | * @see self::getServerPublicHostKey() |
||
521 | * @var string |
||
522 | */ |
||
523 | private $server_public_host_key; |
||
524 | |||
525 | /** |
||
526 | * Session identifier |
||
527 | * |
||
528 | * "The exchange hash H from the first key exchange is additionally |
||
529 | * used as the session identifier, which is a unique identifier for |
||
530 | * this connection." |
||
531 | * |
||
532 | * -- http://tools.ietf.org/html/rfc4253#section-7.2 |
||
533 | * |
||
534 | * @see self::_key_exchange() |
||
535 | * @var string |
||
536 | */ |
||
537 | private $session_id = false; |
||
538 | |||
539 | /** |
||
540 | * Exchange hash |
||
541 | * |
||
542 | * The current exchange hash |
||
543 | * |
||
544 | * @see self::_key_exchange() |
||
545 | * @var string |
||
546 | */ |
||
547 | private $exchange_hash = false; |
||
548 | |||
549 | /** |
||
874 | daniel-mar | 550 | * Message Numbers |
551 | * |
||
552 | * @see self::__construct() |
||
553 | * @var array |
||
554 | * @access private |
||
555 | */ |
||
1117 | daniel-mar | 556 | private static $message_numbers = []; |
874 | daniel-mar | 557 | |
558 | /** |
||
559 | * Disconnection Message 'reason codes' defined in RFC4253 |
||
560 | * |
||
561 | * @see self::__construct() |
||
562 | * @var array |
||
563 | * @access private |
||
564 | */ |
||
1117 | daniel-mar | 565 | private static $disconnect_reasons = []; |
874 | daniel-mar | 566 | |
567 | /** |
||
568 | * SSH_MSG_CHANNEL_OPEN_FAILURE 'reason codes', defined in RFC4254 |
||
569 | * |
||
570 | * @see self::__construct() |
||
571 | * @var array |
||
572 | * @access private |
||
573 | */ |
||
1117 | daniel-mar | 574 | private static $channel_open_failure_reasons = []; |
874 | daniel-mar | 575 | |
576 | /** |
||
577 | * Terminal Modes |
||
578 | * |
||
579 | * @link http://tools.ietf.org/html/rfc4254#section-8 |
||
580 | * @see self::__construct() |
||
581 | * @var array |
||
582 | * @access private |
||
583 | */ |
||
1117 | daniel-mar | 584 | private static $terminal_modes = []; |
874 | daniel-mar | 585 | |
586 | /** |
||
587 | * SSH_MSG_CHANNEL_EXTENDED_DATA's data_type_codes |
||
588 | * |
||
589 | * @link http://tools.ietf.org/html/rfc4254#section-5.2 |
||
590 | * @see self::__construct() |
||
591 | * @var array |
||
592 | * @access private |
||
593 | */ |
||
1117 | daniel-mar | 594 | private static $channel_extended_data_type_codes = []; |
874 | daniel-mar | 595 | |
596 | /** |
||
827 | daniel-mar | 597 | * Send Sequence Number |
598 | * |
||
599 | * See 'Section 6.4. Data Integrity' of rfc4253 for more info. |
||
600 | * |
||
601 | * @see self::_send_binary_packet() |
||
602 | * @var int |
||
603 | */ |
||
604 | private $send_seq_no = 0; |
||
605 | |||
606 | /** |
||
607 | * Get Sequence Number |
||
608 | * |
||
609 | * See 'Section 6.4. Data Integrity' of rfc4253 for more info. |
||
610 | * |
||
611 | * @see self::_get_binary_packet() |
||
612 | * @var int |
||
613 | */ |
||
614 | private $get_seq_no = 0; |
||
615 | |||
616 | /** |
||
617 | * Server Channels |
||
618 | * |
||
619 | * Maps client channels to server channels |
||
620 | * |
||
621 | * @see self::get_channel_packet() |
||
622 | * @see self::exec() |
||
623 | * @var array |
||
624 | */ |
||
625 | protected $server_channels = []; |
||
626 | |||
627 | /** |
||
628 | * Channel Buffers |
||
629 | * |
||
630 | * If a client requests a packet from one channel but receives two packets from another those packets should |
||
631 | * be placed in a buffer |
||
632 | * |
||
633 | * @see self::get_channel_packet() |
||
634 | * @see self::exec() |
||
635 | * @var array |
||
636 | */ |
||
637 | private $channel_buffers = []; |
||
638 | |||
639 | /** |
||
640 | * Channel Status |
||
641 | * |
||
642 | * Contains the type of the last sent message |
||
643 | * |
||
644 | * @see self::get_channel_packet() |
||
645 | * @var array |
||
646 | */ |
||
647 | protected $channel_status = []; |
||
648 | |||
649 | /** |
||
1117 | daniel-mar | 650 | * The identifier of the interactive channel which was opened most recently |
651 | * |
||
652 | * @see self::getInteractiveChannelId() |
||
653 | * @var int |
||
654 | */ |
||
655 | private $channel_id_last_interactive = 0; |
||
656 | |||
657 | /** |
||
827 | daniel-mar | 658 | * Packet Size |
659 | * |
||
660 | * Maximum packet size indexed by channel |
||
661 | * |
||
662 | * @see self::send_channel_packet() |
||
663 | * @var array |
||
664 | */ |
||
665 | private $packet_size_client_to_server = []; |
||
666 | |||
667 | /** |
||
668 | * Message Number Log |
||
669 | * |
||
670 | * @see self::getLog() |
||
671 | * @var array |
||
672 | */ |
||
673 | private $message_number_log = []; |
||
674 | |||
675 | /** |
||
676 | * Message Log |
||
677 | * |
||
678 | * @see self::getLog() |
||
679 | * @var array |
||
680 | */ |
||
681 | private $message_log = []; |
||
682 | |||
683 | /** |
||
684 | * The Window Size |
||
685 | * |
||
686 | * Bytes the other party can send before it must wait for the window to be adjusted (0x7FFFFFFF = 2GB) |
||
687 | * |
||
688 | * @var int |
||
689 | * @see self::send_channel_packet() |
||
690 | * @see self::exec() |
||
691 | */ |
||
692 | protected $window_size = 0x7FFFFFFF; |
||
693 | |||
694 | /** |
||
695 | * What we resize the window to |
||
696 | * |
||
697 | * When PuTTY resizes the window it doesn't add an additional 0x7FFFFFFF bytes - it adds 0x40000000 bytes. |
||
698 | * Some SFTP clients (GoAnywhere) don't support adding 0x7FFFFFFF to the window size after the fact so |
||
699 | * we'll just do what PuTTY does |
||
700 | * |
||
701 | * @var int |
||
702 | * @see self::_send_channel_packet() |
||
703 | * @see self::exec() |
||
704 | */ |
||
705 | private $window_resize = 0x40000000; |
||
706 | |||
707 | /** |
||
708 | * Window size, server to client |
||
709 | * |
||
710 | * Window size indexed by channel |
||
711 | * |
||
712 | * @see self::send_channel_packet() |
||
713 | * @var array |
||
714 | */ |
||
715 | protected $window_size_server_to_client = []; |
||
716 | |||
717 | /** |
||
718 | * Window size, client to server |
||
719 | * |
||
720 | * Window size indexed by channel |
||
721 | * |
||
722 | * @see self::get_channel_packet() |
||
723 | * @var array |
||
724 | */ |
||
725 | private $window_size_client_to_server = []; |
||
726 | |||
727 | /** |
||
728 | * Server signature |
||
729 | * |
||
730 | * Verified against $this->session_id |
||
731 | * |
||
732 | * @see self::getServerPublicHostKey() |
||
733 | * @var string |
||
734 | */ |
||
735 | private $signature = ''; |
||
736 | |||
737 | /** |
||
738 | * Server signature format |
||
739 | * |
||
740 | * ssh-rsa or ssh-dss. |
||
741 | * |
||
742 | * @see self::getServerPublicHostKey() |
||
743 | * @var string |
||
744 | */ |
||
745 | private $signature_format = ''; |
||
746 | |||
747 | /** |
||
748 | * Interactive Buffer |
||
749 | * |
||
750 | * @see self::read() |
||
751 | * @var string |
||
752 | */ |
||
753 | private $interactiveBuffer = ''; |
||
754 | |||
755 | /** |
||
756 | * Current log size |
||
757 | * |
||
758 | * Should never exceed self::LOG_MAX_SIZE |
||
759 | * |
||
760 | * @see self::_send_binary_packet() |
||
761 | * @see self::_get_binary_packet() |
||
762 | * @var int |
||
763 | */ |
||
764 | private $log_size; |
||
765 | |||
766 | /** |
||
767 | * Timeout |
||
768 | * |
||
769 | * @see self::setTimeout() |
||
770 | */ |
||
771 | protected $timeout; |
||
772 | |||
773 | /** |
||
774 | * Current Timeout |
||
775 | * |
||
776 | * @see self::get_channel_packet() |
||
777 | */ |
||
778 | protected $curTimeout; |
||
779 | |||
780 | /** |
||
781 | * Keep Alive Interval |
||
782 | * |
||
783 | * @see self::setKeepAlive() |
||
784 | */ |
||
785 | private $keepAlive; |
||
786 | |||
787 | /** |
||
788 | * Real-time log file pointer |
||
789 | * |
||
790 | * @see self::_append_log() |
||
791 | * @var resource|closed-resource |
||
792 | */ |
||
793 | private $realtime_log_file; |
||
794 | |||
795 | /** |
||
796 | * Real-time log file size |
||
797 | * |
||
798 | * @see self::_append_log() |
||
799 | * @var int |
||
800 | */ |
||
801 | private $realtime_log_size; |
||
802 | |||
803 | /** |
||
804 | * Has the signature been validated? |
||
805 | * |
||
806 | * @see self::getServerPublicHostKey() |
||
807 | * @var bool |
||
808 | */ |
||
809 | private $signature_validated = false; |
||
810 | |||
811 | /** |
||
812 | * Real-time log file wrap boolean |
||
813 | * |
||
814 | * @see self::_append_log() |
||
1042 | daniel-mar | 815 | * @var bool |
827 | daniel-mar | 816 | */ |
817 | private $realtime_log_wrap; |
||
818 | |||
819 | /** |
||
820 | * Flag to suppress stderr from output |
||
821 | * |
||
822 | * @see self::enableQuietMode() |
||
823 | */ |
||
824 | private $quiet_mode = false; |
||
825 | |||
826 | /** |
||
827 | * Time of first network activity |
||
828 | * |
||
829 | * @var float |
||
830 | */ |
||
831 | private $last_packet; |
||
832 | |||
833 | /** |
||
834 | * Exit status returned from ssh if any |
||
835 | * |
||
836 | * @var int |
||
837 | */ |
||
838 | private $exit_status; |
||
839 | |||
840 | /** |
||
841 | * Flag to request a PTY when using exec() |
||
842 | * |
||
843 | * @var bool |
||
844 | * @see self::enablePTY() |
||
845 | */ |
||
846 | private $request_pty = false; |
||
847 | |||
848 | /** |
||
849 | * Contents of stdError |
||
850 | * |
||
851 | * @var string |
||
852 | */ |
||
853 | private $stdErrorLog; |
||
854 | |||
855 | /** |
||
856 | * The Last Interactive Response |
||
857 | * |
||
858 | * @see self::_keyboard_interactive_process() |
||
859 | * @var string |
||
860 | */ |
||
861 | private $last_interactive_response = ''; |
||
862 | |||
863 | /** |
||
864 | * Keyboard Interactive Request / Responses |
||
865 | * |
||
866 | * @see self::_keyboard_interactive_process() |
||
867 | * @var array |
||
868 | */ |
||
869 | private $keyboard_requests_responses = []; |
||
870 | |||
871 | /** |
||
872 | * Banner Message |
||
873 | * |
||
874 | * Quoting from the RFC, "in some jurisdictions, sending a warning message before |
||
875 | * authentication may be relevant for getting legal protection." |
||
876 | * |
||
877 | * @see self::_filter() |
||
878 | * @see self::getBannerMessage() |
||
879 | * @var string |
||
880 | */ |
||
881 | private $banner_message = ''; |
||
882 | |||
883 | /** |
||
884 | * Did read() timeout or return normally? |
||
885 | * |
||
886 | * @see self::isTimeout() |
||
887 | * @var bool |
||
888 | */ |
||
889 | private $is_timeout = false; |
||
890 | |||
891 | /** |
||
892 | * Log Boundary |
||
893 | * |
||
894 | * @see self::_format_log() |
||
895 | * @var string |
||
896 | */ |
||
897 | private $log_boundary = ':'; |
||
898 | |||
899 | /** |
||
900 | * Log Long Width |
||
901 | * |
||
902 | * @see self::_format_log() |
||
903 | * @var int |
||
904 | */ |
||
905 | private $log_long_width = 65; |
||
906 | |||
907 | /** |
||
908 | * Log Short Width |
||
909 | * |
||
910 | * @see self::_format_log() |
||
911 | * @var int |
||
912 | */ |
||
913 | private $log_short_width = 16; |
||
914 | |||
915 | /** |
||
916 | * Hostname |
||
917 | * |
||
918 | * @see self::__construct() |
||
919 | * @see self::_connect() |
||
920 | * @var string |
||
921 | */ |
||
922 | private $host; |
||
923 | |||
924 | /** |
||
925 | * Port Number |
||
926 | * |
||
927 | * @see self::__construct() |
||
928 | * @see self::_connect() |
||
929 | * @var int |
||
930 | */ |
||
931 | private $port; |
||
932 | |||
933 | /** |
||
934 | * Number of columns for terminal window size |
||
935 | * |
||
936 | * @see self::getWindowColumns() |
||
937 | * @see self::setWindowColumns() |
||
938 | * @see self::setWindowSize() |
||
939 | * @var int |
||
940 | */ |
||
941 | private $windowColumns = 80; |
||
942 | |||
943 | /** |
||
944 | * Number of columns for terminal window size |
||
945 | * |
||
946 | * @see self::getWindowRows() |
||
947 | * @see self::setWindowRows() |
||
948 | * @see self::setWindowSize() |
||
949 | * @var int |
||
950 | */ |
||
951 | private $windowRows = 24; |
||
952 | |||
953 | /** |
||
954 | * Crypto Engine |
||
955 | * |
||
956 | * @see self::setCryptoEngine() |
||
957 | * @see self::_key_exchange() |
||
958 | * @var int |
||
959 | */ |
||
960 | private static $crypto_engine = false; |
||
961 | |||
962 | /** |
||
963 | * A System_SSH_Agent for use in the SSH2 Agent Forwarding scenario |
||
964 | * |
||
965 | * @var Agent |
||
966 | */ |
||
967 | private $agent; |
||
968 | |||
969 | /** |
||
970 | * Connection storage to replicates ssh2 extension functionality: |
||
971 | * {@link http://php.net/manual/en/wrappers.ssh2.php#refsect1-wrappers.ssh2-examples} |
||
972 | * |
||
973 | * @var array<string, SSH2|\WeakReference<SSH2>> |
||
974 | */ |
||
975 | private static $connections; |
||
976 | |||
977 | /** |
||
978 | * Send the identification string first? |
||
979 | * |
||
980 | * @var bool |
||
981 | */ |
||
982 | private $send_id_string_first = true; |
||
983 | |||
984 | /** |
||
985 | * Send the key exchange initiation packet first? |
||
986 | * |
||
987 | * @var bool |
||
988 | */ |
||
989 | private $send_kex_first = true; |
||
990 | |||
991 | /** |
||
992 | * Some versions of OpenSSH incorrectly calculate the key size |
||
993 | * |
||
994 | * @var bool |
||
995 | */ |
||
996 | private $bad_key_size_fix = false; |
||
997 | |||
998 | /** |
||
999 | * Should we try to re-connect to re-establish keys? |
||
1000 | * |
||
1001 | * @var bool |
||
1002 | */ |
||
1003 | private $retry_connect = false; |
||
1004 | |||
1005 | /** |
||
1006 | * Binary Packet Buffer |
||
1007 | * |
||
1008 | * @var string|false |
||
1009 | */ |
||
1010 | private $binary_packet_buffer = false; |
||
1011 | |||
1012 | /** |
||
1013 | * Preferred Signature Format |
||
1014 | * |
||
1015 | * @var string|false |
||
1016 | */ |
||
1017 | protected $preferred_signature_format = false; |
||
1018 | |||
1019 | /** |
||
1020 | * Authentication Credentials |
||
1021 | * |
||
1022 | * @var array |
||
1023 | */ |
||
1024 | protected $auth = []; |
||
1025 | |||
1026 | /** |
||
1027 | * Terminal |
||
1028 | * |
||
1029 | * @var string |
||
1030 | */ |
||
1031 | private $term = 'vt100'; |
||
1032 | |||
1033 | /** |
||
1034 | * The authentication methods that may productively continue authentication. |
||
1035 | * |
||
1036 | * @see https://tools.ietf.org/html/rfc4252#section-5.1 |
||
1037 | * @var array|null |
||
1038 | */ |
||
1039 | private $auth_methods_to_continue = null; |
||
1040 | |||
1041 | /** |
||
1042 | * Compression method |
||
1043 | * |
||
1044 | * @var int |
||
1045 | */ |
||
1046 | private $compress = self::NET_SSH2_COMPRESSION_NONE; |
||
1047 | |||
1048 | /** |
||
1049 | * Decompression method |
||
1050 | * |
||
1051 | * @var int |
||
1052 | */ |
||
1053 | private $decompress = self::NET_SSH2_COMPRESSION_NONE; |
||
1054 | |||
1055 | /** |
||
1056 | * Compression context |
||
1057 | * |
||
1058 | * @var resource|false|null |
||
1059 | */ |
||
1060 | private $compress_context; |
||
1061 | |||
1062 | /** |
||
1063 | * Decompression context |
||
1064 | * |
||
1065 | * @var resource|object |
||
1066 | */ |
||
1067 | private $decompress_context; |
||
1068 | |||
1069 | /** |
||
1070 | * Regenerate Compression Context |
||
1071 | * |
||
1072 | * @var bool |
||
1073 | */ |
||
1074 | private $regenerate_compression_context = false; |
||
1075 | |||
1076 | /** |
||
1077 | * Regenerate Decompression Context |
||
1078 | * |
||
1079 | * @var bool |
||
1080 | */ |
||
1081 | private $regenerate_decompression_context = false; |
||
1082 | |||
1083 | /** |
||
1084 | * Smart multi-factor authentication flag |
||
1085 | * |
||
1086 | * @var bool |
||
1087 | */ |
||
1088 | private $smartMFA = true; |
||
1089 | |||
1090 | /** |
||
1284 | daniel-mar | 1091 | * How many channels are currently opened |
1092 | * |
||
1093 | * @var int |
||
1094 | */ |
||
1095 | private $channelCount = 0; |
||
1096 | |||
1097 | /** |
||
1098 | * Does the server support multiple channels? If not then error out |
||
1099 | * when multiple channels are attempted to be opened |
||
1100 | * |
||
1101 | * @var bool |
||
1102 | */ |
||
1103 | private $errorOnMultipleChannels; |
||
1104 | |||
1105 | /** |
||
1448 | daniel-mar | 1106 | * Terrapin Countermeasure |
1107 | * |
||
1108 | * "During initial KEX, terminate the connection if any unexpected or out-of-sequence packet is received" |
||
1109 | * -- https://github.com/openssh/openssh-portable/commit/1edb00c58f8a6875fad6a497aa2bacf37f9e6cd5 |
||
1110 | * |
||
1111 | * @var int |
||
1112 | */ |
||
1113 | private $extra_packets; |
||
1114 | |||
1115 | /** |
||
827 | daniel-mar | 1116 | * Default Constructor. |
1117 | * |
||
1118 | * $host can either be a string, representing the host, or a stream resource. |
||
1463 | daniel-mar | 1119 | * If $host is a stream resource then $port doesn't do anything, altho $timeout |
1120 | * still will be used |
||
827 | daniel-mar | 1121 | * |
1122 | * @param mixed $host |
||
1123 | * @param int $port |
||
1124 | * @param int $timeout |
||
1125 | * @see self::login() |
||
1126 | */ |
||
1127 | public function __construct($host, $port = 22, $timeout = 10) |
||
1128 | { |
||
1117 | daniel-mar | 1129 | if (empty(self::$message_numbers)) { |
1130 | self::$message_numbers = [ |
||
1131 | 1 => 'NET_SSH2_MSG_DISCONNECT', |
||
1132 | 2 => 'NET_SSH2_MSG_IGNORE', |
||
1133 | 3 => 'NET_SSH2_MSG_UNIMPLEMENTED', |
||
1134 | 4 => 'NET_SSH2_MSG_DEBUG', |
||
1135 | 5 => 'NET_SSH2_MSG_SERVICE_REQUEST', |
||
1136 | 6 => 'NET_SSH2_MSG_SERVICE_ACCEPT', |
||
1439 | daniel-mar | 1137 | 7 => 'NET_SSH2_MSG_EXT_INFO', // RFC 8308 |
1117 | daniel-mar | 1138 | 20 => 'NET_SSH2_MSG_KEXINIT', |
1139 | 21 => 'NET_SSH2_MSG_NEWKEYS', |
||
1140 | 30 => 'NET_SSH2_MSG_KEXDH_INIT', |
||
1141 | 31 => 'NET_SSH2_MSG_KEXDH_REPLY', |
||
1142 | 50 => 'NET_SSH2_MSG_USERAUTH_REQUEST', |
||
1143 | 51 => 'NET_SSH2_MSG_USERAUTH_FAILURE', |
||
1144 | 52 => 'NET_SSH2_MSG_USERAUTH_SUCCESS', |
||
1145 | 53 => 'NET_SSH2_MSG_USERAUTH_BANNER', |
||
874 | daniel-mar | 1146 | |
1117 | daniel-mar | 1147 | 80 => 'NET_SSH2_MSG_GLOBAL_REQUEST', |
1148 | 81 => 'NET_SSH2_MSG_REQUEST_SUCCESS', |
||
1149 | 82 => 'NET_SSH2_MSG_REQUEST_FAILURE', |
||
1150 | 90 => 'NET_SSH2_MSG_CHANNEL_OPEN', |
||
1151 | 91 => 'NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION', |
||
1152 | 92 => 'NET_SSH2_MSG_CHANNEL_OPEN_FAILURE', |
||
1153 | 93 => 'NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST', |
||
1154 | 94 => 'NET_SSH2_MSG_CHANNEL_DATA', |
||
1155 | 95 => 'NET_SSH2_MSG_CHANNEL_EXTENDED_DATA', |
||
1156 | 96 => 'NET_SSH2_MSG_CHANNEL_EOF', |
||
1157 | 97 => 'NET_SSH2_MSG_CHANNEL_CLOSE', |
||
1158 | 98 => 'NET_SSH2_MSG_CHANNEL_REQUEST', |
||
1159 | 99 => 'NET_SSH2_MSG_CHANNEL_SUCCESS', |
||
1160 | 100 => 'NET_SSH2_MSG_CHANNEL_FAILURE' |
||
1161 | ]; |
||
1162 | self::$disconnect_reasons = [ |
||
1163 | 1 => 'NET_SSH2_DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT', |
||
1164 | 2 => 'NET_SSH2_DISCONNECT_PROTOCOL_ERROR', |
||
1165 | 3 => 'NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED', |
||
1166 | 4 => 'NET_SSH2_DISCONNECT_RESERVED', |
||
1167 | 5 => 'NET_SSH2_DISCONNECT_MAC_ERROR', |
||
1168 | 6 => 'NET_SSH2_DISCONNECT_COMPRESSION_ERROR', |
||
1169 | 7 => 'NET_SSH2_DISCONNECT_SERVICE_NOT_AVAILABLE', |
||
1170 | 8 => 'NET_SSH2_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED', |
||
1171 | 9 => 'NET_SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE', |
||
1172 | 10 => 'NET_SSH2_DISCONNECT_CONNECTION_LOST', |
||
1173 | 11 => 'NET_SSH2_DISCONNECT_BY_APPLICATION', |
||
1174 | 12 => 'NET_SSH2_DISCONNECT_TOO_MANY_CONNECTIONS', |
||
1175 | 13 => 'NET_SSH2_DISCONNECT_AUTH_CANCELLED_BY_USER', |
||
1176 | 14 => 'NET_SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE', |
||
1177 | 15 => 'NET_SSH2_DISCONNECT_ILLEGAL_USER_NAME' |
||
1178 | ]; |
||
1179 | self::$channel_open_failure_reasons = [ |
||
1180 | 1 => 'NET_SSH2_OPEN_ADMINISTRATIVELY_PROHIBITED' |
||
1181 | ]; |
||
1182 | self::$terminal_modes = [ |
||
1183 | |||
1184 | ]; |
||
1185 | self::$channel_extended_data_type_codes = [ |
||
1186 | 1 => 'NET_SSH2_EXTENDED_DATA_STDERR' |
||
1187 | ]; |
||
874 | daniel-mar | 1188 | |
1117 | daniel-mar | 1189 | self::define_array( |
1190 | self::$message_numbers, |
||
1191 | self::$disconnect_reasons, |
||
1192 | self::$channel_open_failure_reasons, |
||
1193 | self::$terminal_modes, |
||
1194 | self::$channel_extended_data_type_codes, |
||
1195 | [60 => 'NET_SSH2_MSG_USERAUTH_PASSWD_CHANGEREQ'], |
||
1196 | [60 => 'NET_SSH2_MSG_USERAUTH_PK_OK'], |
||
1197 | [60 => 'NET_SSH2_MSG_USERAUTH_INFO_REQUEST', |
||
1198 | 61 => 'NET_SSH2_MSG_USERAUTH_INFO_RESPONSE'], |
||
1199 | // RFC 4419 - diffie-hellman-group-exchange-sha{1,256} |
||
1200 | [30 => 'NET_SSH2_MSG_KEXDH_GEX_REQUEST_OLD', |
||
1201 | 31 => 'NET_SSH2_MSG_KEXDH_GEX_GROUP', |
||
1202 | 32 => 'NET_SSH2_MSG_KEXDH_GEX_INIT', |
||
1203 | 33 => 'NET_SSH2_MSG_KEXDH_GEX_REPLY', |
||
1204 | 34 => 'NET_SSH2_MSG_KEXDH_GEX_REQUEST'], |
||
1205 | // RFC 5656 - Elliptic Curves (for curve25519-sha256@libssh.org) |
||
1206 | [30 => 'NET_SSH2_MSG_KEX_ECDH_INIT', |
||
1207 | 31 => 'NET_SSH2_MSG_KEX_ECDH_REPLY'] |
||
1208 | ); |
||
1209 | } |
||
874 | daniel-mar | 1210 | |
827 | daniel-mar | 1211 | /** |
1212 | * Typehint is required due to a bug in Psalm: https://github.com/vimeo/psalm/issues/7508 |
||
1213 | * @var \WeakReference<SSH2>|SSH2 |
||
1214 | */ |
||
1215 | self::$connections[$this->getResourceId()] = class_exists('WeakReference') |
||
1216 | ? \WeakReference::create($this) |
||
1217 | : $this; |
||
1218 | |||
1463 | daniel-mar | 1219 | $this->timeout = $timeout; |
1220 | |||
827 | daniel-mar | 1221 | if (is_resource($host)) { |
1222 | $this->fsock = $host; |
||
1223 | return; |
||
1224 | } |
||
1225 | |||
1226 | if (Strings::is_stringable($host)) { |
||
1227 | $this->host = $host; |
||
1228 | $this->port = $port; |
||
1229 | } |
||
1230 | } |
||
1231 | |||
1232 | /** |
||
1233 | * Set Crypto Engine Mode |
||
1234 | * |
||
1235 | * Possible $engine values: |
||
1236 | * OpenSSL, mcrypt, Eval, PHP |
||
1237 | * |
||
1238 | * @param int $engine |
||
1239 | */ |
||
1240 | public static function setCryptoEngine($engine) |
||
1241 | { |
||
1242 | self::$crypto_engine = $engine; |
||
1243 | } |
||
1244 | |||
1245 | /** |
||
1246 | * Send Identification String First |
||
1247 | * |
||
1248 | * https://tools.ietf.org/html/rfc4253#section-4.2 says "when the connection has been established, |
||
1249 | * both sides MUST send an identification string". It does not say which side sends it first. In |
||
1250 | * theory it shouldn't matter but it is a fact of life that some SSH servers are simply buggy |
||
1251 | * |
||
1252 | */ |
||
1253 | public function sendIdentificationStringFirst() |
||
1254 | { |
||
1255 | $this->send_id_string_first = true; |
||
1256 | } |
||
1257 | |||
1258 | /** |
||
1259 | * Send Identification String Last |
||
1260 | * |
||
1261 | * https://tools.ietf.org/html/rfc4253#section-4.2 says "when the connection has been established, |
||
1262 | * both sides MUST send an identification string". It does not say which side sends it first. In |
||
1263 | * theory it shouldn't matter but it is a fact of life that some SSH servers are simply buggy |
||
1264 | * |
||
1265 | */ |
||
1266 | public function sendIdentificationStringLast() |
||
1267 | { |
||
1268 | $this->send_id_string_first = false; |
||
1269 | } |
||
1270 | |||
1271 | /** |
||
1272 | * Send SSH_MSG_KEXINIT First |
||
1273 | * |
||
1274 | * https://tools.ietf.org/html/rfc4253#section-7.1 says "key exchange begins by each sending |
||
1275 | * sending the [SSH_MSG_KEXINIT] packet". It does not say which side sends it first. In theory |
||
1276 | * it shouldn't matter but it is a fact of life that some SSH servers are simply buggy |
||
1277 | * |
||
1278 | */ |
||
1279 | public function sendKEXINITFirst() |
||
1280 | { |
||
1281 | $this->send_kex_first = true; |
||
1282 | } |
||
1283 | |||
1284 | /** |
||
1285 | * Send SSH_MSG_KEXINIT Last |
||
1286 | * |
||
1287 | * https://tools.ietf.org/html/rfc4253#section-7.1 says "key exchange begins by each sending |
||
1288 | * sending the [SSH_MSG_KEXINIT] packet". It does not say which side sends it first. In theory |
||
1289 | * it shouldn't matter but it is a fact of life that some SSH servers are simply buggy |
||
1290 | * |
||
1291 | */ |
||
1292 | public function sendKEXINITLast() |
||
1293 | { |
||
1294 | $this->send_kex_first = false; |
||
1295 | } |
||
1296 | |||
1297 | /** |
||
1324 | daniel-mar | 1298 | * stream_select wrapper |
1299 | * |
||
1300 | * Quoting https://stackoverflow.com/a/14262151/569976, |
||
1301 | * "The general approach to `EINTR` is to simply handle the error and retry the operation again" |
||
1302 | * |
||
1303 | * This wrapper does that loop |
||
1304 | */ |
||
1305 | private static function stream_select(&$read, &$write, &$except, $seconds, $microseconds = null) |
||
1306 | { |
||
1307 | $remaining = $seconds + $microseconds / 1000000; |
||
1308 | $start = microtime(true); |
||
1309 | while (true) { |
||
1310 | $result = @stream_select($read, $write, $except, $seconds, $microseconds); |
||
1311 | if ($result !== false) { |
||
1312 | return $result; |
||
1313 | } |
||
1314 | $elapsed = microtime(true) - $start; |
||
1315 | $seconds = (int) ($remaining - floor($elapsed)); |
||
1316 | $microseconds = (int) (1000000 * ($remaining - $seconds)); |
||
1317 | if ($elapsed >= $remaining) { |
||
1318 | return false; |
||
1319 | } |
||
1320 | } |
||
1321 | } |
||
1322 | |||
1323 | /** |
||
827 | daniel-mar | 1324 | * Connect to an SSHv2 server |
1325 | * |
||
1326 | * @throws \UnexpectedValueException on receipt of unexpected packets |
||
1327 | * @throws \RuntimeException on other errors |
||
1328 | */ |
||
1329 | private function connect() |
||
1330 | { |
||
1331 | if ($this->bitmap & self::MASK_CONSTRUCTOR) { |
||
1332 | return; |
||
1333 | } |
||
1334 | |||
1335 | $this->bitmap |= self::MASK_CONSTRUCTOR; |
||
1336 | |||
1337 | $this->curTimeout = $this->timeout; |
||
1338 | |||
1339 | $this->last_packet = microtime(true); |
||
1340 | |||
1341 | if (!is_resource($this->fsock)) { |
||
1342 | $start = microtime(true); |
||
1343 | // with stream_select a timeout of 0 means that no timeout takes place; |
||
1344 | // with fsockopen a timeout of 0 means that you instantly timeout |
||
1345 | // to resolve this incompatibility a timeout of 100,000 will be used for fsockopen if timeout is 0 |
||
1346 | $this->fsock = @fsockopen($this->host, $this->port, $errno, $errstr, $this->curTimeout == 0 ? 100000 : $this->curTimeout); |
||
1347 | if (!$this->fsock) { |
||
1348 | $host = $this->host . ':' . $this->port; |
||
1349 | throw new UnableToConnectException(rtrim("Cannot connect to $host. Error $errno. $errstr")); |
||
1350 | } |
||
1351 | $elapsed = microtime(true) - $start; |
||
1352 | |||
1353 | if ($this->curTimeout) { |
||
1354 | $this->curTimeout -= $elapsed; |
||
1355 | if ($this->curTimeout < 0) { |
||
1356 | throw new \RuntimeException('Connection timed out whilst attempting to open socket connection'); |
||
1357 | } |
||
1358 | } |
||
1359 | } |
||
1360 | |||
1361 | $this->identifier = $this->generate_identifier(); |
||
1362 | |||
1363 | if ($this->send_id_string_first) { |
||
1364 | fputs($this->fsock, $this->identifier . "\r\n"); |
||
1365 | } |
||
1366 | |||
1367 | /* According to the SSH2 specs, |
||
1368 | |||
1369 | "The server MAY send other lines of data before sending the version |
||
1370 | string. Each line SHOULD be terminated by a Carriage Return and Line |
||
1371 | Feed. Such lines MUST NOT begin with "SSH-", and SHOULD be encoded |
||
1372 | in ISO-10646 UTF-8 [RFC3629] (language is not specified). Clients |
||
1373 | MUST be able to process such lines." */ |
||
1374 | $data = ''; |
||
1375 | while (!feof($this->fsock) && !preg_match('#(.*)^(SSH-(\d\.\d+).*)#ms', $data, $matches)) { |
||
1376 | $line = ''; |
||
1377 | while (true) { |
||
1378 | if ($this->curTimeout) { |
||
1379 | if ($this->curTimeout < 0) { |
||
1380 | throw new \RuntimeException('Connection timed out whilst receiving server identification string'); |
||
1381 | } |
||
1382 | $read = [$this->fsock]; |
||
1383 | $write = $except = null; |
||
1384 | $start = microtime(true); |
||
1385 | $sec = (int) floor($this->curTimeout); |
||
1386 | $usec = (int) (1000000 * ($this->curTimeout - $sec)); |
||
1324 | daniel-mar | 1387 | if (static::stream_select($read, $write, $except, $sec, $usec) === false) { |
827 | daniel-mar | 1388 | throw new \RuntimeException('Connection timed out whilst receiving server identification string'); |
1389 | } |
||
1390 | $elapsed = microtime(true) - $start; |
||
1391 | $this->curTimeout -= $elapsed; |
||
1392 | } |
||
1393 | |||
1394 | $temp = stream_get_line($this->fsock, 255, "\n"); |
||
1395 | if ($temp === false) { |
||
1396 | throw new \RuntimeException('Error reading from socket'); |
||
1397 | } |
||
1398 | if (strlen($temp) == 255) { |
||
1399 | continue; |
||
1400 | } |
||
1401 | |||
1402 | $line .= "$temp\n"; |
||
1403 | |||
1404 | // quoting RFC4253, "Implementers who wish to maintain |
||
1405 | // compatibility with older, undocumented versions of this protocol may |
||
1406 | // want to process the identification string without expecting the |
||
1407 | // presence of the carriage return character for reasons described in |
||
1408 | // Section 5 of this document." |
||
1409 | |||
1410 | //if (substr($line, -2) == "\r\n") { |
||
1411 | // break; |
||
1412 | //} |
||
1413 | |||
1414 | break; |
||
1415 | } |
||
1416 | |||
1417 | $data .= $line; |
||
1418 | } |
||
1419 | |||
1420 | if (feof($this->fsock)) { |
||
1421 | $this->bitmap = 0; |
||
1422 | throw new ConnectionClosedException('Connection closed by server'); |
||
1423 | } |
||
1424 | |||
1425 | $extra = $matches[1]; |
||
1426 | |||
1427 | if (defined('NET_SSH2_LOGGING')) { |
||
1428 | $this->append_log('<-', $matches[0]); |
||
1429 | $this->append_log('->', $this->identifier . "\r\n"); |
||
1430 | } |
||
1431 | |||
1432 | $this->server_identifier = trim($temp, "\r\n"); |
||
1433 | if (strlen($extra)) { |
||
1434 | $this->errors[] = $data; |
||
1435 | } |
||
1436 | |||
1437 | if (version_compare($matches[3], '1.99', '<')) { |
||
1438 | $this->bitmap = 0; |
||
1439 | throw new UnableToConnectException("Cannot connect to SSH $matches[3] servers"); |
||
1440 | } |
||
1441 | |||
1284 | daniel-mar | 1442 | // Ubuntu's OpenSSH from 5.8 to 6.9 didn't work with multiple channels. see |
1443 | // https://bugs.launchpad.net/ubuntu/+source/openssh/+bug/1334916 for more info. |
||
1444 | // https://lists.ubuntu.com/archives/oneiric-changes/2011-July/005772.html discusses |
||
1445 | // when consolekit was incorporated. |
||
1446 | // https://marc.info/?l=openssh-unix-dev&m=163409903417589&w=2 discusses some of the |
||
1447 | // issues with how Ubuntu incorporated consolekit |
||
1448 | $pattern = '#^SSH-2\.0-OpenSSH_([\d.]+)[^ ]* Ubuntu-.*$#'; |
||
1449 | $match = preg_match($pattern, $this->server_identifier, $matches); |
||
1450 | $match = $match && version_compare('5.8', $matches[1], '<='); |
||
1451 | $match = $match && version_compare('6.9', $matches[1], '>='); |
||
1452 | $this->errorOnMultipleChannels = $match; |
||
1453 | |||
827 | daniel-mar | 1454 | if (!$this->send_id_string_first) { |
1455 | fputs($this->fsock, $this->identifier . "\r\n"); |
||
1456 | } |
||
1457 | |||
1458 | if (!$this->send_kex_first) { |
||
1459 | $response = $this->get_binary_packet(); |
||
1460 | |||
874 | daniel-mar | 1461 | if (is_bool($response) || !strlen($response) || ord($response[0]) != NET_SSH2_MSG_KEXINIT) { |
827 | daniel-mar | 1462 | $this->bitmap = 0; |
1463 | throw new \UnexpectedValueException('Expected SSH_MSG_KEXINIT'); |
||
1464 | } |
||
1465 | |||
1466 | $this->key_exchange($response); |
||
1467 | } |
||
1468 | |||
1469 | if ($this->send_kex_first) { |
||
1470 | $this->key_exchange(); |
||
1471 | } |
||
1472 | |||
1473 | $this->bitmap |= self::MASK_CONNECTED; |
||
1474 | |||
1475 | return true; |
||
1476 | } |
||
1477 | |||
1478 | /** |
||
1479 | * Generates the SSH identifier |
||
1480 | * |
||
1481 | * You should overwrite this method in your own class if you want to use another identifier |
||
1482 | * |
||
1483 | * @return string |
||
1484 | */ |
||
1485 | private function generate_identifier() |
||
1486 | { |
||
1487 | $identifier = 'SSH-2.0-phpseclib_3.0'; |
||
1488 | |||
1489 | $ext = []; |
||
1490 | if (extension_loaded('sodium')) { |
||
1491 | $ext[] = 'libsodium'; |
||
1492 | } |
||
1493 | |||
1494 | if (extension_loaded('openssl')) { |
||
1495 | $ext[] = 'openssl'; |
||
1496 | } elseif (extension_loaded('mcrypt')) { |
||
1497 | $ext[] = 'mcrypt'; |
||
1498 | } |
||
1499 | |||
1500 | if (extension_loaded('gmp')) { |
||
1501 | $ext[] = 'gmp'; |
||
1502 | } elseif (extension_loaded('bcmath')) { |
||
1503 | $ext[] = 'bcmath'; |
||
1504 | } |
||
1505 | |||
1506 | if (!empty($ext)) { |
||
1507 | $identifier .= ' (' . implode(', ', $ext) . ')'; |
||
1508 | } |
||
1509 | |||
1510 | return $identifier; |
||
1511 | } |
||
1512 | |||
1513 | /** |
||
1514 | * Key Exchange |
||
1515 | * |
||
1516 | * @return bool |
||
1517 | * @param string|bool $kexinit_payload_server optional |
||
1518 | * @throws \UnexpectedValueException on receipt of unexpected packets |
||
1519 | * @throws \RuntimeException on other errors |
||
1520 | * @throws \phpseclib3\Exception\NoSupportedAlgorithmsException when none of the algorithms phpseclib has loaded are compatible |
||
1521 | */ |
||
1522 | private function key_exchange($kexinit_payload_server = false) |
||
1523 | { |
||
1524 | $preferred = $this->preferred; |
||
1525 | $send_kex = true; |
||
1526 | |||
1527 | $kex_algorithms = isset($preferred['kex']) ? |
||
1528 | $preferred['kex'] : |
||
1529 | SSH2::getSupportedKEXAlgorithms(); |
||
1530 | $server_host_key_algorithms = isset($preferred['hostkey']) ? |
||
1531 | $preferred['hostkey'] : |
||
1532 | SSH2::getSupportedHostKeyAlgorithms(); |
||
1533 | $s2c_encryption_algorithms = isset($preferred['server_to_client']['crypt']) ? |
||
1534 | $preferred['server_to_client']['crypt'] : |
||
1535 | SSH2::getSupportedEncryptionAlgorithms(); |
||
1536 | $c2s_encryption_algorithms = isset($preferred['client_to_server']['crypt']) ? |
||
1537 | $preferred['client_to_server']['crypt'] : |
||
1538 | SSH2::getSupportedEncryptionAlgorithms(); |
||
1539 | $s2c_mac_algorithms = isset($preferred['server_to_client']['mac']) ? |
||
1540 | $preferred['server_to_client']['mac'] : |
||
1541 | SSH2::getSupportedMACAlgorithms(); |
||
1542 | $c2s_mac_algorithms = isset($preferred['client_to_server']['mac']) ? |
||
1543 | $preferred['client_to_server']['mac'] : |
||
1544 | SSH2::getSupportedMACAlgorithms(); |
||
1545 | $s2c_compression_algorithms = isset($preferred['server_to_client']['comp']) ? |
||
1546 | $preferred['server_to_client']['comp'] : |
||
1547 | SSH2::getSupportedCompressionAlgorithms(); |
||
1548 | $c2s_compression_algorithms = isset($preferred['client_to_server']['comp']) ? |
||
1549 | $preferred['client_to_server']['comp'] : |
||
1550 | SSH2::getSupportedCompressionAlgorithms(); |
||
1551 | |||
1448 | daniel-mar | 1552 | $kex_algorithms = array_merge($kex_algorithms, ['ext-info-c', 'kex-strict-c-v00@openssh.com']); |
1439 | daniel-mar | 1553 | |
827 | daniel-mar | 1554 | // some SSH servers have buggy implementations of some of the above algorithms |
1555 | switch (true) { |
||
1556 | case $this->server_identifier == 'SSH-2.0-SSHD': |
||
1557 | case substr($this->server_identifier, 0, 13) == 'SSH-2.0-DLINK': |
||
1558 | if (!isset($preferred['server_to_client']['mac'])) { |
||
1559 | $s2c_mac_algorithms = array_values(array_diff( |
||
1560 | $s2c_mac_algorithms, |
||
1561 | ['hmac-sha1-96', 'hmac-md5-96'] |
||
1562 | )); |
||
1563 | } |
||
1564 | if (!isset($preferred['client_to_server']['mac'])) { |
||
1565 | $c2s_mac_algorithms = array_values(array_diff( |
||
1566 | $c2s_mac_algorithms, |
||
1567 | ['hmac-sha1-96', 'hmac-md5-96'] |
||
1568 | )); |
||
1569 | } |
||
1424 | daniel-mar | 1570 | break; |
1571 | case substr($this->server_identifier, 0, 24) == 'SSH-2.0-TurboFTP_SERVER_': |
||
1572 | if (!isset($preferred['server_to_client']['crypt'])) { |
||
1573 | $s2c_encryption_algorithms = array_values(array_diff( |
||
1574 | $s2c_encryption_algorithms, |
||
1575 | ['aes128-gcm@openssh.com', 'aes256-gcm@openssh.com'] |
||
1576 | )); |
||
1577 | } |
||
1578 | if (!isset($preferred['client_to_server']['crypt'])) { |
||
1579 | $c2s_encryption_algorithms = array_values(array_diff( |
||
1580 | $c2s_encryption_algorithms, |
||
1581 | ['aes128-gcm@openssh.com', 'aes256-gcm@openssh.com'] |
||
1582 | )); |
||
1583 | } |
||
827 | daniel-mar | 1584 | } |
1585 | |||
1586 | $client_cookie = Random::string(16); |
||
1587 | |||
874 | daniel-mar | 1588 | $kexinit_payload_client = pack('Ca*', NET_SSH2_MSG_KEXINIT, $client_cookie); |
827 | daniel-mar | 1589 | $kexinit_payload_client .= Strings::packSSH2( |
1590 | 'L10bN', |
||
1591 | $kex_algorithms, |
||
1592 | $server_host_key_algorithms, |
||
1593 | $c2s_encryption_algorithms, |
||
1594 | $s2c_encryption_algorithms, |
||
1595 | $c2s_mac_algorithms, |
||
1596 | $s2c_mac_algorithms, |
||
1597 | $c2s_compression_algorithms, |
||
1598 | $s2c_compression_algorithms, |
||
1599 | [], // language, client to server |
||
1600 | [], // language, server to client |
||
1601 | false, // first_kex_packet_follows |
||
1602 | |||
1603 | ); |
||
1604 | |||
1605 | if ($kexinit_payload_server === false) { |
||
1606 | $this->send_binary_packet($kexinit_payload_client); |
||
1607 | |||
1448 | daniel-mar | 1608 | $this->extra_packets = 0; |
827 | daniel-mar | 1609 | $kexinit_payload_server = $this->get_binary_packet(); |
1610 | |||
1611 | if ( |
||
1612 | is_bool($kexinit_payload_server) |
||
1613 | || !strlen($kexinit_payload_server) |
||
874 | daniel-mar | 1614 | || ord($kexinit_payload_server[0]) != NET_SSH2_MSG_KEXINIT |
827 | daniel-mar | 1615 | ) { |
874 | daniel-mar | 1616 | $this->disconnect_helper(NET_SSH2_DISCONNECT_PROTOCOL_ERROR); |
827 | daniel-mar | 1617 | throw new \UnexpectedValueException('Expected SSH_MSG_KEXINIT'); |
1618 | } |
||
1619 | |||
1620 | $send_kex = false; |
||
1621 | } |
||
1622 | |||
1623 | $response = $kexinit_payload_server; |
||
1624 | Strings::shift($response, 1); // skip past the message number (it should be SSH_MSG_KEXINIT) |
||
1625 | $server_cookie = Strings::shift($response, 16); |
||
1626 | |||
1627 | list( |
||
1628 | $this->kex_algorithms, |
||
1629 | $this->server_host_key_algorithms, |
||
1630 | $this->encryption_algorithms_client_to_server, |
||
1631 | $this->encryption_algorithms_server_to_client, |
||
1632 | $this->mac_algorithms_client_to_server, |
||
1633 | $this->mac_algorithms_server_to_client, |
||
1634 | $this->compression_algorithms_client_to_server, |
||
1635 | $this->compression_algorithms_server_to_client, |
||
1636 | $this->languages_client_to_server, |
||
1637 | $this->languages_server_to_client, |
||
1638 | $first_kex_packet_follows |
||
1639 | ) = Strings::unpackSSH2('L10C', $response); |
||
1448 | daniel-mar | 1640 | if (in_array('kex-strict-s-v00@openssh.com', $this->kex_algorithms)) { |
1641 | if ($this->session_id === false && $this->extra_packets) { |
||
1642 | throw new \UnexpectedValueException('Possible Terrapin Attack detected'); |
||
1643 | } |
||
1644 | } |
||
827 | daniel-mar | 1645 | |
1051 | daniel-mar | 1646 | $this->supported_private_key_algorithms = $this->server_host_key_algorithms; |
1647 | |||
827 | daniel-mar | 1648 | if ($send_kex) { |
1649 | $this->send_binary_packet($kexinit_payload_client); |
||
1650 | } |
||
1651 | |||
1652 | // we need to decide upon the symmetric encryption algorithms before we do the diffie-hellman key exchange |
||
1653 | |||
1654 | // we don't initialize any crypto-objects, yet - we do that, later. for now, we need the lengths to make the |
||
1655 | // diffie-hellman key exchange as fast as possible |
||
1656 | $decrypt = self::array_intersect_first($s2c_encryption_algorithms, $this->encryption_algorithms_server_to_client); |
||
1657 | $decryptKeyLength = $this->encryption_algorithm_to_key_size($decrypt); |
||
1658 | if ($decryptKeyLength === null) { |
||
874 | daniel-mar | 1659 | $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); |
827 | daniel-mar | 1660 | throw new NoSupportedAlgorithmsException('No compatible server to client encryption algorithms found'); |
1661 | } |
||
1662 | |||
1663 | $encrypt = self::array_intersect_first($c2s_encryption_algorithms, $this->encryption_algorithms_client_to_server); |
||
1664 | $encryptKeyLength = $this->encryption_algorithm_to_key_size($encrypt); |
||
1665 | if ($encryptKeyLength === null) { |
||
874 | daniel-mar | 1666 | $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); |
827 | daniel-mar | 1667 | throw new NoSupportedAlgorithmsException('No compatible client to server encryption algorithms found'); |
1668 | } |
||
1669 | |||
1670 | // through diffie-hellman key exchange a symmetric key is obtained |
||
1671 | $this->kex_algorithm = self::array_intersect_first($kex_algorithms, $this->kex_algorithms); |
||
1672 | if ($this->kex_algorithm === false) { |
||
874 | daniel-mar | 1673 | $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); |
827 | daniel-mar | 1674 | throw new NoSupportedAlgorithmsException('No compatible key exchange algorithms found'); |
1675 | } |
||
1676 | |||
1677 | $server_host_key_algorithm = self::array_intersect_first($server_host_key_algorithms, $this->server_host_key_algorithms); |
||
1678 | if ($server_host_key_algorithm === false) { |
||
874 | daniel-mar | 1679 | $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); |
827 | daniel-mar | 1680 | throw new NoSupportedAlgorithmsException('No compatible server host key algorithms found'); |
1681 | } |
||
1682 | |||
1683 | $mac_algorithm_out = self::array_intersect_first($c2s_mac_algorithms, $this->mac_algorithms_client_to_server); |
||
1684 | if ($mac_algorithm_out === false) { |
||
874 | daniel-mar | 1685 | $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); |
827 | daniel-mar | 1686 | throw new NoSupportedAlgorithmsException('No compatible client to server message authentication algorithms found'); |
1687 | } |
||
1688 | |||
1689 | $mac_algorithm_in = self::array_intersect_first($s2c_mac_algorithms, $this->mac_algorithms_server_to_client); |
||
1690 | if ($mac_algorithm_in === false) { |
||
874 | daniel-mar | 1691 | $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); |
827 | daniel-mar | 1692 | throw new NoSupportedAlgorithmsException('No compatible server to client message authentication algorithms found'); |
1693 | } |
||
1694 | |||
1695 | $compression_map = [ |
||
1696 | 'none' => self::NET_SSH2_COMPRESSION_NONE, |
||
1697 | 'zlib' => self::NET_SSH2_COMPRESSION_ZLIB, |
||
1698 | 'zlib@openssh.com' => self::NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH |
||
1699 | ]; |
||
1700 | |||
1701 | $compression_algorithm_in = self::array_intersect_first($s2c_compression_algorithms, $this->compression_algorithms_server_to_client); |
||
1702 | if ($compression_algorithm_in === false) { |
||
874 | daniel-mar | 1703 | $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); |
827 | daniel-mar | 1704 | throw new NoSupportedAlgorithmsException('No compatible server to client compression algorithms found'); |
1705 | } |
||
1706 | $this->decompress = $compression_map[$compression_algorithm_in]; |
||
1707 | |||
1708 | $compression_algorithm_out = self::array_intersect_first($c2s_compression_algorithms, $this->compression_algorithms_client_to_server); |
||
1709 | if ($compression_algorithm_out === false) { |
||
874 | daniel-mar | 1710 | $this->disconnect_helper(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); |
827 | daniel-mar | 1711 | throw new NoSupportedAlgorithmsException('No compatible client to server compression algorithms found'); |
1712 | } |
||
1713 | $this->compress = $compression_map[$compression_algorithm_out]; |
||
1714 | |||
1715 | switch ($this->kex_algorithm) { |
||
1716 | case 'diffie-hellman-group15-sha512': |
||
1717 | case 'diffie-hellman-group16-sha512': |
||
1718 | case 'diffie-hellman-group17-sha512': |
||
1719 | case 'diffie-hellman-group18-sha512': |
||
1720 | case 'ecdh-sha2-nistp521': |
||
1721 | $kexHash = new Hash('sha512'); |
||
1722 | break; |
||
1723 | case 'ecdh-sha2-nistp384': |
||
1724 | $kexHash = new Hash('sha384'); |
||
1725 | break; |
||
1726 | case 'diffie-hellman-group-exchange-sha256': |
||
1727 | case 'diffie-hellman-group14-sha256': |
||
1728 | case 'ecdh-sha2-nistp256': |
||
1729 | case 'curve25519-sha256@libssh.org': |
||
1730 | case 'curve25519-sha256': |
||
1731 | $kexHash = new Hash('sha256'); |
||
1732 | break; |
||
1733 | default: |
||
1734 | $kexHash = new Hash('sha1'); |
||
1735 | } |
||
1736 | |||
1737 | // Only relevant in diffie-hellman-group-exchange-sha{1,256}, otherwise empty. |
||
1738 | |||
1739 | $exchange_hash_rfc4419 = ''; |
||
1740 | |||
1741 | if (strpos($this->kex_algorithm, 'curve25519-sha256') === 0 || strpos($this->kex_algorithm, 'ecdh-sha2-nistp') === 0) { |
||
1742 | $curve = strpos($this->kex_algorithm, 'curve25519-sha256') === 0 ? |
||
1743 | 'Curve25519' : |
||
1744 | substr($this->kex_algorithm, 10); |
||
1745 | $ourPrivate = EC::createKey($curve); |
||
1746 | $ourPublicBytes = $ourPrivate->getPublicKey()->getEncodedCoordinates(); |
||
874 | daniel-mar | 1747 | $clientKexInitMessage = 'NET_SSH2_MSG_KEX_ECDH_INIT'; |
1748 | $serverKexReplyMessage = 'NET_SSH2_MSG_KEX_ECDH_REPLY'; |
||
827 | daniel-mar | 1749 | } else { |
1750 | if (strpos($this->kex_algorithm, 'diffie-hellman-group-exchange') === 0) { |
||
1751 | $dh_group_sizes_packed = pack( |
||
1752 | 'NNN', |
||
1753 | $this->kex_dh_group_size_min, |
||
1754 | $this->kex_dh_group_size_preferred, |
||
1755 | $this->kex_dh_group_size_max |
||
1756 | ); |
||
1757 | $packet = pack( |
||
1758 | 'Ca*', |
||
874 | daniel-mar | 1759 | NET_SSH2_MSG_KEXDH_GEX_REQUEST, |
827 | daniel-mar | 1760 | $dh_group_sizes_packed |
1761 | ); |
||
1762 | $this->send_binary_packet($packet); |
||
874 | daniel-mar | 1763 | $this->updateLogHistory('UNKNOWN (34)', 'NET_SSH2_MSG_KEXDH_GEX_REQUEST'); |
827 | daniel-mar | 1764 | |
1765 | $response = $this->get_binary_packet(); |
||
1766 | |||
1767 | list($type, $primeBytes, $gBytes) = Strings::unpackSSH2('Css', $response); |
||
874 | daniel-mar | 1768 | if ($type != NET_SSH2_MSG_KEXDH_GEX_GROUP) { |
1769 | $this->disconnect_helper(NET_SSH2_DISCONNECT_PROTOCOL_ERROR); |
||
827 | daniel-mar | 1770 | throw new \UnexpectedValueException('Expected SSH_MSG_KEX_DH_GEX_GROUP'); |
1771 | } |
||
874 | daniel-mar | 1772 | $this->updateLogHistory('NET_SSH2_MSG_KEXDH_REPLY', 'NET_SSH2_MSG_KEXDH_GEX_GROUP'); |
827 | daniel-mar | 1773 | $prime = new BigInteger($primeBytes, -256); |
1774 | $g = new BigInteger($gBytes, -256); |
||
1775 | |||
1776 | $exchange_hash_rfc4419 = $dh_group_sizes_packed . Strings::packSSH2( |
||
1777 | 'ss', |
||
1778 | $primeBytes, |
||
1779 | $gBytes |
||
1780 | ); |
||
1781 | |||
1782 | $params = DH::createParameters($prime, $g); |
||
874 | daniel-mar | 1783 | $clientKexInitMessage = 'NET_SSH2_MSG_KEXDH_GEX_INIT'; |
1784 | $serverKexReplyMessage = 'NET_SSH2_MSG_KEXDH_GEX_REPLY'; |
||
827 | daniel-mar | 1785 | } else { |
1786 | $params = DH::createParameters($this->kex_algorithm); |
||
874 | daniel-mar | 1787 | $clientKexInitMessage = 'NET_SSH2_MSG_KEXDH_INIT'; |
1788 | $serverKexReplyMessage = 'NET_SSH2_MSG_KEXDH_REPLY'; |
||
827 | daniel-mar | 1789 | } |
1790 | |||
1791 | $keyLength = min($kexHash->getLengthInBytes(), max($encryptKeyLength, $decryptKeyLength)); |
||
1792 | |||
1793 | $ourPrivate = DH::createKey($params, 16 * $keyLength); // 2 * 8 * $keyLength |
||
1794 | $ourPublic = $ourPrivate->getPublicKey()->toBigInteger(); |
||
1795 | $ourPublicBytes = $ourPublic->toBytes(true); |
||
1796 | } |
||
1797 | |||
874 | daniel-mar | 1798 | $data = pack('CNa*', constant($clientKexInitMessage), strlen($ourPublicBytes), $ourPublicBytes); |
827 | daniel-mar | 1799 | |
1800 | $this->send_binary_packet($data); |
||
1801 | |||
1802 | switch ($clientKexInitMessage) { |
||
874 | daniel-mar | 1803 | case 'NET_SSH2_MSG_KEX_ECDH_INIT': |
1804 | $this->updateLogHistory('NET_SSH2_MSG_KEXDH_INIT', 'NET_SSH2_MSG_KEX_ECDH_INIT'); |
||
827 | daniel-mar | 1805 | break; |
874 | daniel-mar | 1806 | case 'NET_SSH2_MSG_KEXDH_GEX_INIT': |
1807 | $this->updateLogHistory('UNKNOWN (32)', 'NET_SSH2_MSG_KEXDH_GEX_INIT'); |
||
827 | daniel-mar | 1808 | } |
1809 | |||
1810 | $response = $this->get_binary_packet(); |
||
1811 | |||
1812 | list( |
||
1813 | $type, |
||
1814 | $server_public_host_key, |
||
1815 | $theirPublicBytes, |
||
1816 | $this->signature |
||
1817 | ) = Strings::unpackSSH2('Csss', $response); |
||
1818 | |||
874 | daniel-mar | 1819 | if ($type != constant($serverKexReplyMessage)) { |
1820 | $this->disconnect_helper(NET_SSH2_DISCONNECT_PROTOCOL_ERROR); |
||
827 | daniel-mar | 1821 | throw new \UnexpectedValueException("Expected $serverKexReplyMessage"); |
1822 | } |
||
1823 | switch ($serverKexReplyMessage) { |
||
874 | daniel-mar | 1824 | case 'NET_SSH2_MSG_KEX_ECDH_REPLY': |
1825 | $this->updateLogHistory('NET_SSH2_MSG_KEXDH_REPLY', 'NET_SSH2_MSG_KEX_ECDH_REPLY'); |
||
827 | daniel-mar | 1826 | break; |
874 | daniel-mar | 1827 | case 'NET_SSH2_MSG_KEXDH_GEX_REPLY': |
1828 | $this->updateLogHistory('UNKNOWN (33)', 'NET_SSH2_MSG_KEXDH_GEX_REPLY'); |
||
827 | daniel-mar | 1829 | } |
1830 | |||
1831 | $this->server_public_host_key = $server_public_host_key; |
||
1832 | list($public_key_format) = Strings::unpackSSH2('s', $server_public_host_key); |
||
1833 | if (strlen($this->signature) < 4) { |
||
1834 | throw new \LengthException('The signature needs at least four bytes'); |
||
1835 | } |
||
1836 | $temp = unpack('Nlength', substr($this->signature, 0, 4)); |
||
1837 | $this->signature_format = substr($this->signature, 4, $temp['length']); |
||
1838 | |||
1839 | $keyBytes = DH::computeSecret($ourPrivate, $theirPublicBytes); |
||
1840 | if (($keyBytes & "\xFF\x80") === "\x00\x00") { |
||
1841 | $keyBytes = substr($keyBytes, 1); |
||
1842 | } elseif (($keyBytes[0] & "\x80") === "\x80") { |
||
1843 | $keyBytes = "\0$keyBytes"; |
||
1844 | } |
||
1845 | |||
1846 | $this->exchange_hash = Strings::packSSH2( |
||
1847 | 's5', |
||
1848 | $this->identifier, |
||
1849 | $this->server_identifier, |
||
1850 | $kexinit_payload_client, |
||
1851 | $kexinit_payload_server, |
||
1852 | $this->server_public_host_key |
||
1853 | ); |
||
1854 | $this->exchange_hash .= $exchange_hash_rfc4419; |
||
1855 | $this->exchange_hash .= Strings::packSSH2( |
||
1856 | 's3', |
||
1857 | $ourPublicBytes, |
||
1858 | $theirPublicBytes, |
||
1859 | $keyBytes |
||
1860 | ); |
||
1861 | |||
1862 | $this->exchange_hash = $kexHash->hash($this->exchange_hash); |
||
1863 | |||
1864 | if ($this->session_id === false) { |
||
1865 | $this->session_id = $this->exchange_hash; |
||
1866 | } |
||
1867 | |||
1868 | switch ($server_host_key_algorithm) { |
||
1869 | case 'rsa-sha2-256': |
||
1870 | case 'rsa-sha2-512': |
||
1871 | //case 'ssh-rsa': |
||
1872 | $expected_key_format = 'ssh-rsa'; |
||
1873 | break; |
||
1874 | default: |
||
1875 | $expected_key_format = $server_host_key_algorithm; |
||
1876 | } |
||
1877 | if ($public_key_format != $expected_key_format || $this->signature_format != $server_host_key_algorithm) { |
||
1878 | switch (true) { |
||
1879 | case $this->signature_format == $server_host_key_algorithm: |
||
1880 | case $server_host_key_algorithm != 'rsa-sha2-256' && $server_host_key_algorithm != 'rsa-sha2-512': |
||
1881 | case $this->signature_format != 'ssh-rsa': |
||
874 | daniel-mar | 1882 | $this->disconnect_helper(NET_SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE); |
827 | daniel-mar | 1883 | throw new \RuntimeException('Server Host Key Algorithm Mismatch (' . $this->signature_format . ' vs ' . $server_host_key_algorithm . ')'); |
1884 | } |
||
1885 | } |
||
1886 | |||
874 | daniel-mar | 1887 | $packet = pack('C', NET_SSH2_MSG_NEWKEYS); |
827 | daniel-mar | 1888 | $this->send_binary_packet($packet); |
1889 | |||
1890 | $response = $this->get_binary_packet(); |
||
1891 | |||
1892 | if ($response === false) { |
||
874 | daniel-mar | 1893 | $this->disconnect_helper(NET_SSH2_DISCONNECT_CONNECTION_LOST); |
827 | daniel-mar | 1894 | throw new ConnectionClosedException('Connection closed by server'); |
1895 | } |
||
1896 | |||
1897 | list($type) = Strings::unpackSSH2('C', $response); |
||
874 | daniel-mar | 1898 | if ($type != NET_SSH2_MSG_NEWKEYS) { |
1899 | $this->disconnect_helper(NET_SSH2_DISCONNECT_PROTOCOL_ERROR); |
||
827 | daniel-mar | 1900 | throw new \UnexpectedValueException('Expected SSH_MSG_NEWKEYS'); |
1901 | } |
||
1902 | |||
1448 | daniel-mar | 1903 | if (in_array('kex-strict-s-v00@openssh.com', $this->kex_algorithms)) { |
1904 | $this->get_seq_no = $this->send_seq_no = 0; |
||
1905 | } |
||
1906 | |||
827 | daniel-mar | 1907 | $keyBytes = pack('Na*', strlen($keyBytes), $keyBytes); |
1908 | |||
1909 | $this->encrypt = self::encryption_algorithm_to_crypt_instance($encrypt); |
||
1910 | if ($this->encrypt) { |
||
1911 | if (self::$crypto_engine) { |
||
1912 | $this->encrypt->setPreferredEngine(self::$crypto_engine); |
||
1913 | } |
||
1914 | if ($this->encrypt->getBlockLengthInBytes()) { |
||
1915 | $this->encrypt_block_size = $this->encrypt->getBlockLengthInBytes(); |
||
1916 | } |
||
1917 | $this->encrypt->disablePadding(); |
||
1918 | |||
1919 | if ($this->encrypt->usesIV()) { |
||
1920 | $iv = $kexHash->hash($keyBytes . $this->exchange_hash . 'A' . $this->session_id); |
||
1921 | while ($this->encrypt_block_size > strlen($iv)) { |
||
1922 | $iv .= $kexHash->hash($keyBytes . $this->exchange_hash . $iv); |
||
1923 | } |
||
1924 | $this->encrypt->setIV(substr($iv, 0, $this->encrypt_block_size)); |
||
1925 | } |
||
1926 | |||
1927 | switch ($encrypt) { |
||
1928 | case 'aes128-gcm@openssh.com': |
||
1929 | case 'aes256-gcm@openssh.com': |
||
1930 | $nonce = $kexHash->hash($keyBytes . $this->exchange_hash . 'A' . $this->session_id); |
||
1931 | $this->encryptFixedPart = substr($nonce, 0, 4); |
||
1932 | $this->encryptInvocationCounter = substr($nonce, 4, 8); |
||
1933 | // fall-through |
||
1934 | case 'chacha20-poly1305@openssh.com': |
||
1935 | break; |
||
1936 | default: |
||
1937 | $this->encrypt->enableContinuousBuffer(); |
||
1938 | } |
||
1939 | |||
1940 | $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'C' . $this->session_id); |
||
1941 | while ($encryptKeyLength > strlen($key)) { |
||
1942 | $key .= $kexHash->hash($keyBytes . $this->exchange_hash . $key); |
||
1943 | } |
||
1944 | switch ($encrypt) { |
||
1945 | case 'chacha20-poly1305@openssh.com': |
||
1946 | $encryptKeyLength = 32; |
||
1947 | $this->lengthEncrypt = self::encryption_algorithm_to_crypt_instance($encrypt); |
||
1948 | $this->lengthEncrypt->setKey(substr($key, 32, 32)); |
||
1949 | } |
||
1950 | $this->encrypt->setKey(substr($key, 0, $encryptKeyLength)); |
||
1951 | $this->encryptName = $encrypt; |
||
1952 | } |
||
1953 | |||
1954 | $this->decrypt = self::encryption_algorithm_to_crypt_instance($decrypt); |
||
1955 | if ($this->decrypt) { |
||
1956 | if (self::$crypto_engine) { |
||
1957 | $this->decrypt->setPreferredEngine(self::$crypto_engine); |
||
1958 | } |
||
1959 | if ($this->decrypt->getBlockLengthInBytes()) { |
||
1960 | $this->decrypt_block_size = $this->decrypt->getBlockLengthInBytes(); |
||
1961 | } |
||
1962 | $this->decrypt->disablePadding(); |
||
1963 | |||
1964 | if ($this->decrypt->usesIV()) { |
||
1965 | $iv = $kexHash->hash($keyBytes . $this->exchange_hash . 'B' . $this->session_id); |
||
1966 | while ($this->decrypt_block_size > strlen($iv)) { |
||
1967 | $iv .= $kexHash->hash($keyBytes . $this->exchange_hash . $iv); |
||
1968 | } |
||
1969 | $this->decrypt->setIV(substr($iv, 0, $this->decrypt_block_size)); |
||
1970 | } |
||
1971 | |||
1972 | switch ($decrypt) { |
||
1973 | case 'aes128-gcm@openssh.com': |
||
1974 | case 'aes256-gcm@openssh.com': |
||
1975 | // see https://tools.ietf.org/html/rfc5647#section-7.1 |
||
1976 | $nonce = $kexHash->hash($keyBytes . $this->exchange_hash . 'B' . $this->session_id); |
||
1977 | $this->decryptFixedPart = substr($nonce, 0, 4); |
||
1978 | $this->decryptInvocationCounter = substr($nonce, 4, 8); |
||
1979 | // fall-through |
||
1980 | case 'chacha20-poly1305@openssh.com': |
||
1981 | break; |
||
1982 | default: |
||
1983 | $this->decrypt->enableContinuousBuffer(); |
||
1984 | } |
||
1985 | |||
1986 | $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'D' . $this->session_id); |
||
1987 | while ($decryptKeyLength > strlen($key)) { |
||
1988 | $key .= $kexHash->hash($keyBytes . $this->exchange_hash . $key); |
||
1989 | } |
||
1990 | switch ($decrypt) { |
||
1991 | case 'chacha20-poly1305@openssh.com': |
||
1992 | $decryptKeyLength = 32; |
||
1993 | $this->lengthDecrypt = self::encryption_algorithm_to_crypt_instance($decrypt); |
||
1994 | $this->lengthDecrypt->setKey(substr($key, 32, 32)); |
||
1995 | } |
||
1996 | $this->decrypt->setKey(substr($key, 0, $decryptKeyLength)); |
||
1997 | $this->decryptName = $decrypt; |
||
1998 | } |
||
1999 | |||
2000 | /* The "arcfour128" algorithm is the RC4 cipher, as described in |
||
2001 | [SCHNEIER], using a 128-bit key. The first 1536 bytes of keystream |
||
2002 | generated by the cipher MUST be discarded, and the first byte of the |
||
2003 | first encrypted packet MUST be encrypted using the 1537th byte of |
||
2004 | keystream. |
||
2005 | |||
2006 | -- http://tools.ietf.org/html/rfc4345#section-4 */ |
||
2007 | if ($encrypt == 'arcfour128' || $encrypt == 'arcfour256') { |
||
2008 | $this->encrypt->encrypt(str_repeat("\0", 1536)); |
||
2009 | } |
||
2010 | if ($decrypt == 'arcfour128' || $decrypt == 'arcfour256') { |
||
2011 | $this->decrypt->decrypt(str_repeat("\0", 1536)); |
||
2012 | } |
||
2013 | |||
2014 | if (!$this->encrypt->usesNonce()) { |
||
2015 | list($this->hmac_create, $createKeyLength) = self::mac_algorithm_to_hash_instance($mac_algorithm_out); |
||
2016 | } else { |
||
2017 | $this->hmac_create = new \stdClass(); |
||
2018 | $this->hmac_create_name = $mac_algorithm_out; |
||
2019 | //$mac_algorithm_out = 'none'; |
||
2020 | $createKeyLength = 0; |
||
2021 | } |
||
2022 | |||
2023 | if ($this->hmac_create instanceof Hash) { |
||
2024 | $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'E' . $this->session_id); |
||
2025 | while ($createKeyLength > strlen($key)) { |
||
2026 | $key .= $kexHash->hash($keyBytes . $this->exchange_hash . $key); |
||
2027 | } |
||
2028 | $this->hmac_create->setKey(substr($key, 0, $createKeyLength)); |
||
2029 | $this->hmac_create_name = $mac_algorithm_out; |
||
2030 | $this->hmac_create_etm = preg_match('#-etm@openssh\.com$#', $mac_algorithm_out); |
||
2031 | } |
||
2032 | |||
2033 | if (!$this->decrypt->usesNonce()) { |
||
2034 | list($this->hmac_check, $checkKeyLength) = self::mac_algorithm_to_hash_instance($mac_algorithm_in); |
||
2035 | $this->hmac_size = $this->hmac_check->getLengthInBytes(); |
||
2036 | } else { |
||
2037 | $this->hmac_check = new \stdClass(); |
||
2038 | $this->hmac_check_name = $mac_algorithm_in; |
||
2039 | //$mac_algorithm_in = 'none'; |
||
2040 | $checkKeyLength = 0; |
||
2041 | $this->hmac_size = 0; |
||
2042 | } |
||
2043 | |||
2044 | if ($this->hmac_check instanceof Hash) { |
||
2045 | $key = $kexHash->hash($keyBytes . $this->exchange_hash . 'F' . $this->session_id); |
||
2046 | while ($checkKeyLength > strlen($key)) { |
||
2047 | $key .= $kexHash->hash($keyBytes . $this->exchange_hash . $key); |
||
2048 | } |
||
2049 | $this->hmac_check->setKey(substr($key, 0, $checkKeyLength)); |
||
2050 | $this->hmac_check_name = $mac_algorithm_in; |
||
2051 | $this->hmac_check_etm = preg_match('#-etm@openssh\.com$#', $mac_algorithm_in); |
||
2052 | } |
||
2053 | |||
2054 | $this->regenerate_compression_context = $this->regenerate_decompression_context = true; |
||
2055 | |||
2056 | return true; |
||
2057 | } |
||
2058 | |||
2059 | /** |
||
2060 | * Maps an encryption algorithm name to the number of key bytes. |
||
2061 | * |
||
2062 | * @param string $algorithm Name of the encryption algorithm |
||
2063 | * @return int|null Number of bytes as an integer or null for unknown |
||
2064 | */ |
||
2065 | private function encryption_algorithm_to_key_size($algorithm) |
||
2066 | { |
||
2067 | if ($this->bad_key_size_fix && self::bad_algorithm_candidate($algorithm)) { |
||
2068 | return 16; |
||
2069 | } |
||
2070 | |||
2071 | switch ($algorithm) { |
||
2072 | case 'none': |
||
2073 | return 0; |
||
2074 | case 'aes128-gcm@openssh.com': |
||
2075 | case 'aes128-cbc': |
||
2076 | case 'aes128-ctr': |
||
2077 | case 'arcfour': |
||
2078 | case 'arcfour128': |
||
2079 | case 'blowfish-cbc': |
||
2080 | case 'blowfish-ctr': |
||
2081 | case 'twofish128-cbc': |
||
2082 | case 'twofish128-ctr': |
||
2083 | return 16; |
||
2084 | case '3des-cbc': |
||
2085 | case '3des-ctr': |
||
2086 | case 'aes192-cbc': |
||
2087 | case 'aes192-ctr': |
||
2088 | case 'twofish192-cbc': |
||
2089 | case 'twofish192-ctr': |
||
2090 | return 24; |
||
2091 | case 'aes256-gcm@openssh.com': |
||
2092 | case 'aes256-cbc': |
||
2093 | case 'aes256-ctr': |
||
2094 | case 'arcfour256': |
||
2095 | case 'twofish-cbc': |
||
2096 | case 'twofish256-cbc': |
||
2097 | case 'twofish256-ctr': |
||
2098 | return 32; |
||
2099 | case 'chacha20-poly1305@openssh.com': |
||
2100 | return 64; |
||
2101 | } |
||
2102 | return null; |
||
2103 | } |
||
2104 | |||
2105 | /** |
||
2106 | * Maps an encryption algorithm name to an instance of a subclass of |
||
2107 | * \phpseclib3\Crypt\Common\SymmetricKey. |
||
2108 | * |
||
2109 | * @param string $algorithm Name of the encryption algorithm |
||
2110 | * @return SymmetricKey|null |
||
2111 | */ |
||
2112 | private static function encryption_algorithm_to_crypt_instance($algorithm) |
||
2113 | { |
||
2114 | switch ($algorithm) { |
||
2115 | case '3des-cbc': |
||
2116 | return new TripleDES('cbc'); |
||
2117 | case '3des-ctr': |
||
2118 | return new TripleDES('ctr'); |
||
2119 | case 'aes256-cbc': |
||
2120 | case 'aes192-cbc': |
||
2121 | case 'aes128-cbc': |
||
2122 | return new Rijndael('cbc'); |
||
2123 | case 'aes256-ctr': |
||
2124 | case 'aes192-ctr': |
||
2125 | case 'aes128-ctr': |
||
2126 | return new Rijndael('ctr'); |
||
2127 | case 'blowfish-cbc': |
||
2128 | return new Blowfish('cbc'); |
||
2129 | case 'blowfish-ctr': |
||
2130 | return new Blowfish('ctr'); |
||
2131 | case 'twofish128-cbc': |
||
2132 | case 'twofish192-cbc': |
||
2133 | case 'twofish256-cbc': |
||
2134 | case 'twofish-cbc': |
||
2135 | return new Twofish('cbc'); |
||
2136 | case 'twofish128-ctr': |
||
2137 | case 'twofish192-ctr': |
||
2138 | case 'twofish256-ctr': |
||
2139 | return new Twofish('ctr'); |
||
2140 | case 'arcfour': |
||
2141 | case 'arcfour128': |
||
2142 | case 'arcfour256': |
||
2143 | return new RC4(); |
||
2144 | case 'aes128-gcm@openssh.com': |
||
2145 | case 'aes256-gcm@openssh.com': |
||
2146 | return new Rijndael('gcm'); |
||
2147 | case 'chacha20-poly1305@openssh.com': |
||
2148 | return new ChaCha20(); |
||
2149 | } |
||
2150 | return null; |
||
2151 | } |
||
2152 | |||
2153 | /** |
||
2154 | * Maps an encryption algorithm name to an instance of a subclass of |
||
2155 | * \phpseclib3\Crypt\Hash. |
||
2156 | * |
||
2157 | * @param string $algorithm Name of the encryption algorithm |
||
2158 | * @return array{Hash, int}|null |
||
2159 | */ |
||
2160 | private static function mac_algorithm_to_hash_instance($algorithm) |
||
2161 | { |
||
2162 | switch ($algorithm) { |
||
2163 | case 'umac-64@openssh.com': |
||
2164 | case 'umac-64-etm@openssh.com': |
||
2165 | return [new Hash('umac-64'), 16]; |
||
2166 | case 'umac-128@openssh.com': |
||
2167 | case 'umac-128-etm@openssh.com': |
||
2168 | return [new Hash('umac-128'), 16]; |
||
2169 | case 'hmac-sha2-512': |
||
2170 | case 'hmac-sha2-512-etm@openssh.com': |
||
2171 | return [new Hash('sha512'), 64]; |
||
2172 | case 'hmac-sha2-256': |
||
2173 | case 'hmac-sha2-256-etm@openssh.com': |
||
2174 | return [new Hash('sha256'), 32]; |
||
2175 | case 'hmac-sha1': |
||
2176 | case 'hmac-sha1-etm@openssh.com': |
||
2177 | return [new Hash('sha1'), 20]; |
||
2178 | case 'hmac-sha1-96': |
||
2179 | return [new Hash('sha1-96'), 20]; |
||
2180 | case 'hmac-md5': |
||
2181 | return [new Hash('md5'), 16]; |
||
2182 | case 'hmac-md5-96': |
||
2183 | return [new Hash('md5-96'), 16]; |
||
2184 | } |
||
2185 | } |
||
2186 | |||
2187 | /* |
||
2188 | * Tests whether or not proposed algorithm has a potential for issues |
||
2189 | * |
||
2190 | * @link https://www.chiark.greenend.org.uk/~sgtatham/putty/wishlist/ssh2-aesctr-openssh.html |
||
2191 | * @link https://bugzilla.mindrot.org/show_bug.cgi?id=1291 |
||
2192 | * @param string $algorithm Name of the encryption algorithm |
||
2193 | * @return bool |
||
2194 | */ |
||
2195 | private static function bad_algorithm_candidate($algorithm) |
||
2196 | { |
||
2197 | switch ($algorithm) { |
||
2198 | case 'arcfour256': |
||
2199 | case 'aes192-ctr': |
||
2200 | case 'aes256-ctr': |
||
2201 | return true; |
||
2202 | } |
||
2203 | |||
2204 | return false; |
||
2205 | } |
||
2206 | |||
2207 | /** |
||
2208 | * Login |
||
2209 | * |
||
2210 | * The $password parameter can be a plaintext password, a \phpseclib3\Crypt\RSA|EC|DSA object, a \phpseclib3\System\SSH\Agent object or an array |
||
2211 | * |
||
2212 | * @param string $username |
||
1196 | daniel-mar | 2213 | * @param string|PrivateKey|array[]|Agent|null ...$args |
827 | daniel-mar | 2214 | * @return bool |
2215 | * @see self::_login() |
||
2216 | */ |
||
2217 | public function login($username, ...$args) |
||
2218 | { |
||
1441 | daniel-mar | 2219 | if (!$this->retry_connect) { |
2220 | $this->auth[] = func_get_args(); |
||
2221 | } |
||
827 | daniel-mar | 2222 | |
2223 | // try logging with 'none' as an authentication method first since that's what |
||
2224 | // PuTTY does |
||
2225 | if (substr($this->server_identifier, 0, 15) != 'SSH-2.0-CoreFTP' && $this->auth_methods_to_continue === null) { |
||
2226 | if ($this->sublogin($username)) { |
||
2227 | return true; |
||
2228 | } |
||
2229 | if (!count($args)) { |
||
2230 | return false; |
||
2231 | } |
||
2232 | } |
||
2233 | return $this->sublogin($username, ...$args); |
||
2234 | } |
||
2235 | |||
2236 | /** |
||
2237 | * Login Helper |
||
2238 | * |
||
2239 | * @param string $username |
||
1196 | daniel-mar | 2240 | * @param string|PrivateKey|array[]|Agent|null ...$args |
827 | daniel-mar | 2241 | * @return bool |
2242 | * @see self::_login_helper() |
||
2243 | */ |
||
2244 | protected function sublogin($username, ...$args) |
||
2245 | { |
||
2246 | if (!($this->bitmap & self::MASK_CONSTRUCTOR)) { |
||
2247 | $this->connect(); |
||
2248 | } |
||
2249 | |||
2250 | if (empty($args)) { |
||
2251 | return $this->login_helper($username); |
||
2252 | } |
||
2253 | |||
2254 | foreach ($args as $arg) { |
||
2255 | switch (true) { |
||
2256 | case $arg instanceof PublicKey: |
||
2257 | throw new \UnexpectedValueException('A PublicKey object was passed to the login method instead of a PrivateKey object'); |
||
2258 | case $arg instanceof PrivateKey: |
||
2259 | case $arg instanceof Agent: |
||
2260 | case is_array($arg): |
||
2261 | case Strings::is_stringable($arg): |
||
2262 | break; |
||
2263 | default: |
||
2264 | throw new \UnexpectedValueException('$password needs to either be an instance of \phpseclib3\Crypt\Common\PrivateKey, \System\SSH\Agent, an array or a string'); |
||
2265 | } |
||
2266 | } |
||
2267 | |||
2268 | while (count($args)) { |
||
2269 | if (!$this->auth_methods_to_continue || !$this->smartMFA) { |
||
2270 | $newargs = $args; |
||
2271 | $args = []; |
||
2272 | } else { |
||
2273 | $newargs = []; |
||
2274 | foreach ($this->auth_methods_to_continue as $method) { |
||
2275 | switch ($method) { |
||
2276 | case 'publickey': |
||
2277 | foreach ($args as $key => $arg) { |
||
2278 | if ($arg instanceof PrivateKey || $arg instanceof Agent) { |
||
2279 | $newargs[] = $arg; |
||
2280 | unset($args[$key]); |
||
2281 | break; |
||
2282 | } |
||
2283 | } |
||
2284 | break; |
||
2285 | case 'keyboard-interactive': |
||
2286 | $hasArray = $hasString = false; |
||
2287 | foreach ($args as $arg) { |
||
2288 | if ($hasArray || is_array($arg)) { |
||
2289 | $hasArray = true; |
||
2290 | break; |
||
2291 | } |
||
2292 | if ($hasString || Strings::is_stringable($arg)) { |
||
2293 | $hasString = true; |
||
2294 | break; |
||
2295 | } |
||
2296 | } |
||
2297 | if ($hasArray && $hasString) { |
||
2298 | foreach ($args as $key => $arg) { |
||
2299 | if (is_array($arg)) { |
||
2300 | $newargs[] = $arg; |
||
2301 | break 2; |
||
2302 | } |
||
2303 | } |
||
2304 | } |
||
2305 | // fall-through |
||
2306 | case 'password': |
||
2307 | foreach ($args as $key => $arg) { |
||
2308 | $newargs[] = $arg; |
||
2309 | unset($args[$key]); |
||
2310 | break; |
||
2311 | } |
||
2312 | } |
||
2313 | } |
||
2314 | } |
||
2315 | |||
2316 | if (!count($newargs)) { |
||
2317 | return false; |
||
2318 | } |
||
2319 | |||
2320 | foreach ($newargs as $arg) { |
||
2321 | if ($this->login_helper($username, $arg)) { |
||
2322 | return true; |
||
2323 | } |
||
2324 | } |
||
2325 | } |
||
2326 | return false; |
||
2327 | } |
||
2328 | |||
2329 | /** |
||
2330 | * Login Helper |
||
2331 | * |
||
2332 | * {@internal It might be worthwhile, at some point, to protect against {@link http://tools.ietf.org/html/rfc4251#section-9.3.9 traffic analysis} |
||
2333 | * by sending dummy SSH_MSG_IGNORE messages.} |
||
2334 | * |
||
2335 | * @param string $username |
||
2336 | * @param string|AsymmetricKey|array[]|Agent|null ...$args |
||
2337 | * @return bool |
||
2338 | * @throws \UnexpectedValueException on receipt of unexpected packets |
||
2339 | * @throws \RuntimeException on other errors |
||
2340 | */ |
||
2341 | private function login_helper($username, $password = null) |
||
2342 | { |
||
2343 | if (!($this->bitmap & self::MASK_CONNECTED)) { |
||
2344 | return false; |
||
2345 | } |
||
2346 | |||
2347 | if (!($this->bitmap & self::MASK_LOGIN_REQ)) { |
||
874 | daniel-mar | 2348 | $packet = Strings::packSSH2('Cs', NET_SSH2_MSG_SERVICE_REQUEST, 'ssh-userauth'); |
827 | daniel-mar | 2349 | $this->send_binary_packet($packet); |
2350 | |||
2351 | try { |
||
2352 | $response = $this->get_binary_packet(); |
||
2353 | } catch (\Exception $e) { |
||
2354 | if ($this->retry_connect) { |
||
2355 | $this->retry_connect = false; |
||
2356 | $this->connect(); |
||
2357 | return $this->login_helper($username, $password); |
||
2358 | } |
||
874 | daniel-mar | 2359 | $this->disconnect_helper(NET_SSH2_DISCONNECT_CONNECTION_LOST); |
1424 | daniel-mar | 2360 | throw $e; |
827 | daniel-mar | 2361 | } |
2362 | |||
1439 | daniel-mar | 2363 | list($type) = Strings::unpackSSH2('C', $response); |
2364 | |||
2365 | if ($type == NET_SSH2_MSG_EXT_INFO) { |
||
2366 | list($nr_extensions) = Strings::unpackSSH2('N', $response); |
||
2367 | for ($i = 0; $i < $nr_extensions; $i++) { |
||
2368 | list($extension_name, $extension_value) = Strings::unpackSSH2('ss', $response); |
||
2369 | if ($extension_name == 'server-sig-algs') { |
||
2370 | $this->supported_private_key_algorithms = explode(',', $extension_value); |
||
2371 | } |
||
2372 | } |
||
2373 | |||
2374 | $response = $this->get_binary_packet(); |
||
2375 | list($type) = Strings::unpackSSH2('C', $response); |
||
2376 | } |
||
2377 | |||
2378 | list($service) = Strings::unpackSSH2('s', $response); |
||
2379 | |||
874 | daniel-mar | 2380 | if ($type != NET_SSH2_MSG_SERVICE_ACCEPT || $service != 'ssh-userauth') { |
2381 | $this->disconnect_helper(NET_SSH2_DISCONNECT_PROTOCOL_ERROR); |
||
827 | daniel-mar | 2382 | throw new \UnexpectedValueException('Expected SSH_MSG_SERVICE_ACCEPT'); |
2383 | } |
||
2384 | $this->bitmap |= self::MASK_LOGIN_REQ; |
||
2385 | } |
||
2386 | |||
2387 | if (strlen($this->last_interactive_response)) { |
||
2388 | return !Strings::is_stringable($password) && !is_array($password) ? false : $this->keyboard_interactive_process($password); |
||
2389 | } |
||
2390 | |||
2391 | if ($password instanceof PrivateKey) { |
||
2392 | return $this->privatekey_login($username, $password); |
||
2393 | } |
||
2394 | |||
2395 | if ($password instanceof Agent) { |
||
2396 | return $this->ssh_agent_login($username, $password); |
||
2397 | } |
||
2398 | |||
2399 | if (is_array($password)) { |
||
2400 | if ($this->keyboard_interactive_login($username, $password)) { |
||
2401 | $this->bitmap |= self::MASK_LOGIN; |
||
2402 | return true; |
||
2403 | } |
||
2404 | return false; |
||
2405 | } |
||
2406 | |||
2407 | if (!isset($password)) { |
||
2408 | $packet = Strings::packSSH2( |
||
2409 | 'Cs3', |
||
874 | daniel-mar | 2410 | NET_SSH2_MSG_USERAUTH_REQUEST, |
827 | daniel-mar | 2411 | $username, |
2412 | 'ssh-connection', |
||
2413 | 'none' |
||
2414 | ); |
||
2415 | |||
2416 | $this->send_binary_packet($packet); |
||
2417 | |||
2418 | $response = $this->get_binary_packet(); |
||
2419 | |||
2420 | list($type) = Strings::unpackSSH2('C', $response); |
||
2421 | switch ($type) { |
||
874 | daniel-mar | 2422 | case NET_SSH2_MSG_USERAUTH_SUCCESS: |
827 | daniel-mar | 2423 | $this->bitmap |= self::MASK_LOGIN; |
2424 | return true; |
||
874 | daniel-mar | 2425 | case NET_SSH2_MSG_USERAUTH_FAILURE: |
827 | daniel-mar | 2426 | list($auth_methods) = Strings::unpackSSH2('L', $response); |
2427 | $this->auth_methods_to_continue = $auth_methods; |
||
2428 | // fall-through |
||
2429 | default: |
||
2430 | return false; |
||
2431 | } |
||
2432 | } |
||
2433 | |||
2434 | $packet = Strings::packSSH2( |
||
2435 | 'Cs3bs', |
||
874 | daniel-mar | 2436 | NET_SSH2_MSG_USERAUTH_REQUEST, |
827 | daniel-mar | 2437 | $username, |
2438 | 'ssh-connection', |
||
2439 | 'password', |
||
2440 | false, |
||
2441 | $password |
||
2442 | ); |
||
2443 | |||
2444 | // remove the username and password from the logged packet |
||
2445 | if (!defined('NET_SSH2_LOGGING')) { |
||
2446 | $logged = null; |
||
2447 | } else { |
||
2448 | $logged = Strings::packSSH2( |
||
2449 | 'Cs3bs', |
||
874 | daniel-mar | 2450 | NET_SSH2_MSG_USERAUTH_REQUEST, |
827 | daniel-mar | 2451 | $username, |
2452 | 'ssh-connection', |
||
2453 | 'password', |
||
2454 | false, |
||
2455 | 'password' |
||
2456 | ); |
||
2457 | } |
||
2458 | |||
2459 | $this->send_binary_packet($packet, $logged); |
||
2460 | |||
2461 | $response = $this->get_binary_packet(); |
||
1042 | daniel-mar | 2462 | if ($response === false) { |
2463 | return false; |
||
2464 | } |
||
827 | daniel-mar | 2465 | list($type) = Strings::unpackSSH2('C', $response); |
2466 | switch ($type) { |
||
874 | daniel-mar | 2467 | case NET_SSH2_MSG_USERAUTH_PASSWD_CHANGEREQ: // in theory, the password can be changed |
2468 | $this->updateLogHistory('UNKNOWN (60)', 'NET_SSH2_MSG_USERAUTH_PASSWD_CHANGEREQ'); |
||
827 | daniel-mar | 2469 | |
2470 | list($message) = Strings::unpackSSH2('s', $response); |
||
2471 | $this->errors[] = 'SSH_MSG_USERAUTH_PASSWD_CHANGEREQ: ' . $message; |
||
2472 | |||
874 | daniel-mar | 2473 | return $this->disconnect_helper(NET_SSH2_DISCONNECT_AUTH_CANCELLED_BY_USER); |
2474 | case NET_SSH2_MSG_USERAUTH_FAILURE: |
||
827 | daniel-mar | 2475 | // can we use keyboard-interactive authentication? if not then either the login is bad or the server employees |
2476 | // multi-factor authentication |
||
2477 | list($auth_methods, $partial_success) = Strings::unpackSSH2('Lb', $response); |
||
2478 | $this->auth_methods_to_continue = $auth_methods; |
||
2479 | if (!$partial_success && in_array('keyboard-interactive', $auth_methods)) { |
||
2480 | if ($this->keyboard_interactive_login($username, $password)) { |
||
2481 | $this->bitmap |= self::MASK_LOGIN; |
||
2482 | return true; |
||
2483 | } |
||
2484 | return false; |
||
2485 | } |
||
2486 | return false; |
||
874 | daniel-mar | 2487 | case NET_SSH2_MSG_USERAUTH_SUCCESS: |
827 | daniel-mar | 2488 | $this->bitmap |= self::MASK_LOGIN; |
2489 | return true; |
||
2490 | } |
||
2491 | |||
2492 | return false; |
||
2493 | } |
||
2494 | |||
2495 | /** |
||
2496 | * Login via keyboard-interactive authentication |
||
2497 | * |
||
2498 | * See {@link http://tools.ietf.org/html/rfc4256 RFC4256} for details. This is not a full-featured keyboard-interactive authenticator. |
||
2499 | * |
||
2500 | * @param string $username |
||
2501 | * @param string|array $password |
||
2502 | * @return bool |
||
2503 | */ |
||
2504 | private function keyboard_interactive_login($username, $password) |
||
2505 | { |
||
2506 | $packet = Strings::packSSH2( |
||
2507 | 'Cs5', |
||
874 | daniel-mar | 2508 | NET_SSH2_MSG_USERAUTH_REQUEST, |
827 | daniel-mar | 2509 | $username, |
2510 | 'ssh-connection', |
||
2511 | 'keyboard-interactive', |
||
2512 | '', // language tag |
||
2513 | '' // submethods |
||
2514 | ); |
||
2515 | $this->send_binary_packet($packet); |
||
2516 | |||
2517 | return $this->keyboard_interactive_process($password); |
||
2518 | } |
||
2519 | |||
2520 | /** |
||
2521 | * Handle the keyboard-interactive requests / responses. |
||
2522 | * |
||
2523 | * @param string|array ...$responses |
||
2524 | * @return bool |
||
2525 | * @throws \RuntimeException on connection error |
||
2526 | */ |
||
2527 | private function keyboard_interactive_process(...$responses) |
||
2528 | { |
||
2529 | if (strlen($this->last_interactive_response)) { |
||
2530 | $response = $this->last_interactive_response; |
||
2531 | } else { |
||
2532 | $orig = $response = $this->get_binary_packet(); |
||
2533 | } |
||
2534 | |||
2535 | list($type) = Strings::unpackSSH2('C', $response); |
||
2536 | switch ($type) { |
||
874 | daniel-mar | 2537 | case NET_SSH2_MSG_USERAUTH_INFO_REQUEST: |
827 | daniel-mar | 2538 | list( |
2539 | , // name; may be empty |
||
2540 | , // instruction; may be empty |
||
2541 | , // language tag; may be empty |
||
2542 | $num_prompts |
||
2543 | ) = Strings::unpackSSH2('s3N', $response); |
||
2544 | |||
2545 | for ($i = 0; $i < count($responses); $i++) { |
||
2546 | if (is_array($responses[$i])) { |
||
2547 | foreach ($responses[$i] as $key => $value) { |
||
2548 | $this->keyboard_requests_responses[$key] = $value; |
||
2549 | } |
||
2550 | unset($responses[$i]); |
||
2551 | } |
||
2552 | } |
||
2553 | $responses = array_values($responses); |
||
2554 | |||
2555 | if (isset($this->keyboard_requests_responses)) { |
||
2556 | for ($i = 0; $i < $num_prompts; $i++) { |
||
2557 | list( |
||
2558 | $prompt, // prompt - ie. "Password: "; must not be empty |
||
2559 | // echo |
||
2560 | ) = Strings::unpackSSH2('sC', $response); |
||
2561 | foreach ($this->keyboard_requests_responses as $key => $value) { |
||
2562 | if (substr($prompt, 0, strlen($key)) == $key) { |
||
2563 | $responses[] = $value; |
||
2564 | break; |
||
2565 | } |
||
2566 | } |
||
2567 | } |
||
2568 | } |
||
2569 | |||
2570 | // see http://tools.ietf.org/html/rfc4256#section-3.2 |
||
2571 | if (strlen($this->last_interactive_response)) { |
||
2572 | $this->last_interactive_response = ''; |
||
2573 | } else { |
||
874 | daniel-mar | 2574 | $this->updateLogHistory('UNKNOWN (60)', 'NET_SSH2_MSG_USERAUTH_INFO_REQUEST'); |
827 | daniel-mar | 2575 | } |
2576 | |||
2577 | if (!count($responses) && $num_prompts) { |
||
2578 | $this->last_interactive_response = $orig; |
||
2579 | return false; |
||
2580 | } |
||
2581 | |||
2582 | /* |
||
2583 | After obtaining the requested information from the user, the client |
||
2584 | MUST respond with an SSH_MSG_USERAUTH_INFO_RESPONSE message. |
||
2585 | */ |
||
2586 | // see http://tools.ietf.org/html/rfc4256#section-3.4 |
||
874 | daniel-mar | 2587 | $packet = $logged = pack('CN', NET_SSH2_MSG_USERAUTH_INFO_RESPONSE, count($responses)); |
827 | daniel-mar | 2588 | for ($i = 0; $i < count($responses); $i++) { |
2589 | $packet .= Strings::packSSH2('s', $responses[$i]); |
||
2590 | $logged .= Strings::packSSH2('s', 'dummy-answer'); |
||
2591 | } |
||
2592 | |||
2593 | $this->send_binary_packet($packet, $logged); |
||
2594 | |||
874 | daniel-mar | 2595 | $this->updateLogHistory('UNKNOWN (61)', 'NET_SSH2_MSG_USERAUTH_INFO_RESPONSE'); |
827 | daniel-mar | 2596 | |
2597 | /* |
||
2598 | After receiving the response, the server MUST send either an |
||
2599 | SSH_MSG_USERAUTH_SUCCESS, SSH_MSG_USERAUTH_FAILURE, or another |
||
2600 | SSH_MSG_USERAUTH_INFO_REQUEST message. |
||
2601 | */ |
||
2602 | // maybe phpseclib should force close the connection after x request / responses? unless something like that is done |
||
2603 | // there could be an infinite loop of request / responses. |
||
2604 | return $this->keyboard_interactive_process(); |
||
874 | daniel-mar | 2605 | case NET_SSH2_MSG_USERAUTH_SUCCESS: |
827 | daniel-mar | 2606 | return true; |
874 | daniel-mar | 2607 | case NET_SSH2_MSG_USERAUTH_FAILURE: |
827 | daniel-mar | 2608 | list($auth_methods) = Strings::unpackSSH2('L', $response); |
2609 | $this->auth_methods_to_continue = $auth_methods; |
||
2610 | return false; |
||
2611 | } |
||
2612 | |||
2613 | return false; |
||
2614 | } |
||
2615 | |||
2616 | /** |
||
2617 | * Login with an ssh-agent provided key |
||
2618 | * |
||
2619 | * @param string $username |
||
2620 | * @param \phpseclib3\System\SSH\Agent $agent |
||
2621 | * @return bool |
||
2622 | */ |
||
2623 | private function ssh_agent_login($username, Agent $agent) |
||
2624 | { |
||
2625 | $this->agent = $agent; |
||
2626 | $keys = $agent->requestIdentities(); |
||
2627 | foreach ($keys as $key) { |
||
2628 | if ($this->privatekey_login($username, $key)) { |
||
2629 | return true; |
||
2630 | } |
||
2631 | } |
||
2632 | |||
2633 | return false; |
||
2634 | } |
||
2635 | |||
2636 | /** |
||
2637 | * Login with an RSA private key |
||
2638 | * |
||
2639 | * {@internal It might be worthwhile, at some point, to protect against {@link http://tools.ietf.org/html/rfc4251#section-9.3.9 traffic analysis} |
||
2640 | * by sending dummy SSH_MSG_IGNORE messages.} |
||
2641 | * |
||
2642 | * @param string $username |
||
2643 | * @param \phpseclib3\Crypt\Common\PrivateKey $privatekey |
||
2644 | * @return bool |
||
2645 | * @throws \RuntimeException on connection error |
||
2646 | */ |
||
2647 | private function privatekey_login($username, PrivateKey $privatekey) |
||
2648 | { |
||
2649 | $publickey = $privatekey->getPublicKey(); |
||
2650 | |||
2651 | if ($publickey instanceof RSA) { |
||
2652 | $privatekey = $privatekey->withPadding(RSA::SIGNATURE_PKCS1); |
||
2653 | $algos = ['rsa-sha2-256', 'rsa-sha2-512', 'ssh-rsa']; |
||
2654 | if (isset($this->preferred['hostkey'])) { |
||
1439 | daniel-mar | 2655 | $algos = array_intersect($algos, $this->preferred['hostkey']); |
827 | daniel-mar | 2656 | } |
1051 | daniel-mar | 2657 | $algo = self::array_intersect_first($algos, $this->supported_private_key_algorithms); |
827 | daniel-mar | 2658 | switch ($algo) { |
2659 | case 'rsa-sha2-512': |
||
2660 | $hash = 'sha512'; |
||
2661 | $signatureType = 'rsa-sha2-512'; |
||
2662 | break; |
||
2663 | case 'rsa-sha2-256': |
||
2664 | $hash = 'sha256'; |
||
2665 | $signatureType = 'rsa-sha2-256'; |
||
2666 | break; |
||
2667 | //case 'ssh-rsa': |
||
2668 | default: |
||
2669 | $hash = 'sha1'; |
||
2670 | $signatureType = 'ssh-rsa'; |
||
2671 | } |
||
2672 | } elseif ($publickey instanceof EC) { |
||
2673 | $privatekey = $privatekey->withSignatureFormat('SSH2'); |
||
2674 | $curveName = $privatekey->getCurve(); |
||
2675 | switch ($curveName) { |
||
2676 | case 'Ed25519': |
||
2677 | $hash = 'sha512'; |
||
2678 | $signatureType = 'ssh-ed25519'; |
||
2679 | break; |
||
2680 | case 'secp256r1': // nistp256 |
||
2681 | $hash = 'sha256'; |
||
2682 | $signatureType = 'ecdsa-sha2-nistp256'; |
||
2683 | break; |
||
2684 | case 'secp384r1': // nistp384 |
||
2685 | $hash = 'sha384'; |
||
2686 | $signatureType = 'ecdsa-sha2-nistp384'; |
||
2687 | break; |
||
2688 | case 'secp521r1': // nistp521 |
||
2689 | $hash = 'sha512'; |
||
2690 | $signatureType = 'ecdsa-sha2-nistp521'; |
||
2691 | break; |
||
2692 | default: |
||
2693 | if (is_array($curveName)) { |
||
2694 | throw new UnsupportedCurveException('Specified Curves are not supported by SSH2'); |
||
2695 | } |
||
2696 | throw new UnsupportedCurveException('Named Curve of ' . $curveName . ' is not supported by phpseclib3\'s SSH2 implementation'); |
||
2697 | } |
||
2698 | } elseif ($publickey instanceof DSA) { |
||
2699 | $privatekey = $privatekey->withSignatureFormat('SSH2'); |
||
2700 | $hash = 'sha1'; |
||
2701 | $signatureType = 'ssh-dss'; |
||
2702 | } else { |
||
2703 | throw new UnsupportedAlgorithmException('Please use either an RSA key, an EC one or a DSA key'); |
||
2704 | } |
||
2705 | |||
2706 | $publickeyStr = $publickey->toString('OpenSSH', ['binary' => true]); |
||
2707 | |||
2708 | $part1 = Strings::packSSH2( |
||
2709 | 'Csss', |
||
874 | daniel-mar | 2710 | NET_SSH2_MSG_USERAUTH_REQUEST, |
827 | daniel-mar | 2711 | $username, |
2712 | 'ssh-connection', |
||
2713 | 'publickey' |
||
2714 | ); |
||
2715 | $part2 = Strings::packSSH2('ss', $signatureType, $publickeyStr); |
||
2716 | |||
2717 | $packet = $part1 . chr(0) . $part2; |
||
2718 | $this->send_binary_packet($packet); |
||
2719 | |||
2720 | $response = $this->get_binary_packet(); |
||
2721 | |||
2722 | list($type) = Strings::unpackSSH2('C', $response); |
||
2723 | switch ($type) { |
||
874 | daniel-mar | 2724 | case NET_SSH2_MSG_USERAUTH_FAILURE: |
827 | daniel-mar | 2725 | list($auth_methods) = Strings::unpackSSH2('L', $response); |
1051 | daniel-mar | 2726 | if (in_array('publickey', $auth_methods) && substr($signatureType, 0, 9) == 'rsa-sha2-') { |
2727 | $this->supported_private_key_algorithms = array_diff($this->supported_private_key_algorithms, ['rsa-sha2-256', 'rsa-sha2-512']); |
||
2728 | return $this->privatekey_login($username, $privatekey); |
||
2729 | } |
||
827 | daniel-mar | 2730 | $this->auth_methods_to_continue = $auth_methods; |
2731 | $this->errors[] = 'SSH_MSG_USERAUTH_FAILURE'; |
||
2732 | return false; |
||
874 | daniel-mar | 2733 | case NET_SSH2_MSG_USERAUTH_PK_OK: |
827 | daniel-mar | 2734 | // we'll just take it on faith that the public key blob and the public key algorithm name are as |
2735 | // they should be |
||
874 | daniel-mar | 2736 | $this->updateLogHistory('UNKNOWN (60)', 'NET_SSH2_MSG_USERAUTH_PK_OK'); |
827 | daniel-mar | 2737 | break; |
874 | daniel-mar | 2738 | case NET_SSH2_MSG_USERAUTH_SUCCESS: |
827 | daniel-mar | 2739 | $this->bitmap |= self::MASK_LOGIN; |
2740 | return true; |
||
2741 | default: |
||
874 | daniel-mar | 2742 | $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION); |
827 | daniel-mar | 2743 | throw new ConnectionClosedException('Unexpected response to publickey authentication pt 1'); |
2744 | } |
||
2745 | |||
2746 | $packet = $part1 . chr(1) . $part2; |
||
2747 | $privatekey = $privatekey->withHash($hash); |
||
2748 | $signature = $privatekey->sign(Strings::packSSH2('s', $this->session_id) . $packet); |
||
2749 | if ($publickey instanceof RSA) { |
||
2750 | $signature = Strings::packSSH2('ss', $signatureType, $signature); |
||
2751 | } |
||
2752 | $packet .= Strings::packSSH2('s', $signature); |
||
2753 | |||
2754 | $this->send_binary_packet($packet); |
||
2755 | |||
2756 | $response = $this->get_binary_packet(); |
||
2757 | |||
2758 | list($type) = Strings::unpackSSH2('C', $response); |
||
2759 | switch ($type) { |
||
874 | daniel-mar | 2760 | case NET_SSH2_MSG_USERAUTH_FAILURE: |
827 | daniel-mar | 2761 | // either the login is bad or the server employs multi-factor authentication |
2762 | list($auth_methods) = Strings::unpackSSH2('L', $response); |
||
2763 | $this->auth_methods_to_continue = $auth_methods; |
||
2764 | return false; |
||
874 | daniel-mar | 2765 | case NET_SSH2_MSG_USERAUTH_SUCCESS: |
827 | daniel-mar | 2766 | $this->bitmap |= self::MASK_LOGIN; |
2767 | return true; |
||
2768 | } |
||
2769 | |||
874 | daniel-mar | 2770 | $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION); |
827 | daniel-mar | 2771 | throw new ConnectionClosedException('Unexpected response to publickey authentication pt 2'); |
2772 | } |
||
2773 | |||
2774 | /** |
||
1114 | daniel-mar | 2775 | * Return the currently configured timeout |
2776 | * |
||
2777 | * @return int |
||
2778 | */ |
||
2779 | public function getTimeout() |
||
2780 | { |
||
2781 | return $this->timeout; |
||
2782 | } |
||
2783 | |||
2784 | /** |
||
827 | daniel-mar | 2785 | * Set Timeout |
2786 | * |
||
2787 | * $ssh->exec('ping 127.0.0.1'); on a Linux host will never return and will run indefinitely. setTimeout() makes it so it'll timeout. |
||
2788 | * Setting $timeout to false or 0 will mean there is no timeout. |
||
2789 | * |
||
2790 | * @param mixed $timeout |
||
2791 | */ |
||
2792 | public function setTimeout($timeout) |
||
2793 | { |
||
2794 | $this->timeout = $this->curTimeout = $timeout; |
||
2795 | } |
||
2796 | |||
2797 | /** |
||
2798 | * Set Keep Alive |
||
2799 | * |
||
2800 | * Sends an SSH2_MSG_IGNORE message every x seconds, if x is a positive non-zero number. |
||
2801 | * |
||
2802 | * @param int $interval |
||
2803 | */ |
||
2804 | public function setKeepAlive($interval) |
||
2805 | { |
||
2806 | $this->keepAlive = $interval; |
||
2807 | } |
||
2808 | |||
2809 | /** |
||
2810 | * Get the output from stdError |
||
2811 | * |
||
2812 | */ |
||
2813 | public function getStdError() |
||
2814 | { |
||
2815 | return $this->stdErrorLog; |
||
2816 | } |
||
2817 | |||
2818 | /** |
||
2819 | * Execute Command |
||
2820 | * |
||
2821 | * If $callback is set to false then \phpseclib3\Net\SSH2::get_channel_packet(self::CHANNEL_EXEC) will need to be called manually. |
||
2822 | * In all likelihood, this is not a feature you want to be taking advantage of. |
||
2823 | * |
||
2824 | * @param string $command |
||
2825 | * @return string|bool |
||
2826 | * @psalm-return ($callback is callable ? bool : string|bool) |
||
2827 | * @throws \RuntimeException on connection error |
||
2828 | */ |
||
2829 | public function exec($command, callable $callback = null) |
||
2830 | { |
||
2831 | $this->curTimeout = $this->timeout; |
||
2832 | $this->is_timeout = false; |
||
2833 | $this->stdErrorLog = ''; |
||
2834 | |||
2835 | if (!$this->isAuthenticated()) { |
||
2836 | return false; |
||
2837 | } |
||
2838 | |||
1284 | daniel-mar | 2839 | //if ($this->isPTYOpen()) { |
2840 | // throw new \RuntimeException('If you want to run multiple exec()\'s you will need to disable (and re-enable if appropriate) a PTY for each one.'); |
||
2841 | //} |
||
827 | daniel-mar | 2842 | |
1466 | daniel-mar | 2843 | $this->open_channel(self::CHANNEL_EXEC); |
827 | daniel-mar | 2844 | |
2845 | if ($this->request_pty === true) { |
||
874 | daniel-mar | 2846 | $terminal_modes = pack('C', NET_SSH2_TTY_OP_END); |
827 | daniel-mar | 2847 | $packet = Strings::packSSH2( |
2848 | 'CNsCsN4s', |
||
874 | daniel-mar | 2849 | NET_SSH2_MSG_CHANNEL_REQUEST, |
827 | daniel-mar | 2850 | $this->server_channels[self::CHANNEL_EXEC], |
2851 | 'pty-req', |
||
2852 | 1, |
||
2853 | $this->term, |
||
2854 | $this->windowColumns, |
||
2855 | $this->windowRows, |
||
2856 | 0, |
||
2857 | 0, |
||
2858 | $terminal_modes |
||
2859 | ); |
||
2860 | |||
2861 | $this->send_binary_packet($packet); |
||
2862 | |||
874 | daniel-mar | 2863 | $this->channel_status[self::CHANNEL_EXEC] = NET_SSH2_MSG_CHANNEL_REQUEST; |
827 | daniel-mar | 2864 | if (!$this->get_channel_packet(self::CHANNEL_EXEC)) { |
874 | daniel-mar | 2865 | $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION); |
827 | daniel-mar | 2866 | throw new \RuntimeException('Unable to request pseudo-terminal'); |
2867 | } |
||
2868 | } |
||
2869 | |||
2870 | // sending a pty-req SSH_MSG_CHANNEL_REQUEST message is unnecessary and, in fact, in most cases, slows things |
||
2871 | // down. the one place where it might be desirable is if you're doing something like \phpseclib3\Net\SSH2::exec('ping localhost &'). |
||
2872 | // with a pty-req SSH_MSG_CHANNEL_REQUEST, exec() will return immediately and the ping process will then |
||
2873 | // then immediately terminate. without such a request exec() will loop indefinitely. the ping process won't end but |
||
2874 | // neither will your script. |
||
2875 | |||
2876 | // although, in theory, the size of SSH_MSG_CHANNEL_REQUEST could exceed the maximum packet size established by |
||
2877 | // SSH_MSG_CHANNEL_OPEN_CONFIRMATION, RFC4254#section-5.1 states that the "maximum packet size" refers to the |
||
2878 | // "maximum size of an individual data packet". ie. SSH_MSG_CHANNEL_DATA. RFC4254#section-5.2 corroborates. |
||
2879 | $packet = Strings::packSSH2( |
||
2880 | 'CNsCs', |
||
874 | daniel-mar | 2881 | NET_SSH2_MSG_CHANNEL_REQUEST, |
827 | daniel-mar | 2882 | $this->server_channels[self::CHANNEL_EXEC], |
2883 | 'exec', |
||
2884 | 1, |
||
2885 | $command |
||
2886 | ); |
||
2887 | $this->send_binary_packet($packet); |
||
2888 | |||
874 | daniel-mar | 2889 | $this->channel_status[self::CHANNEL_EXEC] = NET_SSH2_MSG_CHANNEL_REQUEST; |
827 | daniel-mar | 2890 | |
2891 | if (!$this->get_channel_packet(self::CHANNEL_EXEC)) { |
||
2892 | return false; |
||
2893 | } |
||
2894 | |||
874 | daniel-mar | 2895 | $this->channel_status[self::CHANNEL_EXEC] = NET_SSH2_MSG_CHANNEL_DATA; |
827 | daniel-mar | 2896 | |
1117 | daniel-mar | 2897 | if ($this->request_pty === true) { |
2898 | $this->channel_id_last_interactive = self::CHANNEL_EXEC; |
||
827 | daniel-mar | 2899 | return true; |
2900 | } |
||
2901 | |||
2902 | $output = ''; |
||
2903 | while (true) { |
||
2904 | $temp = $this->get_channel_packet(self::CHANNEL_EXEC); |
||
2905 | switch (true) { |
||
2906 | case $temp === true: |
||
2907 | return is_callable($callback) ? true : $output; |
||
2908 | case $temp === false: |
||
2909 | return false; |
||
2910 | default: |
||
2911 | if (is_callable($callback)) { |
||
2912 | if ($callback($temp) === true) { |
||
2913 | $this->close_channel(self::CHANNEL_EXEC); |
||
2914 | return true; |
||
2915 | } |
||
2916 | } else { |
||
2917 | $output .= $temp; |
||
2918 | } |
||
2919 | } |
||
2920 | } |
||
2921 | } |
||
2922 | |||
2923 | /** |
||
1284 | daniel-mar | 2924 | * How many channels are currently open? |
827 | daniel-mar | 2925 | * |
1284 | daniel-mar | 2926 | * @return int |
2927 | */ |
||
2928 | public function getOpenChannelCount() |
||
2929 | { |
||
2930 | return $this->channelCount; |
||
2931 | } |
||
2932 | |||
2933 | /** |
||
2934 | * Opens a channel |
||
1117 | daniel-mar | 2935 | * |
1284 | daniel-mar | 2936 | * @param string $channel |
2937 | * @param bool $skip_extended |
||
827 | daniel-mar | 2938 | * @return bool |
2939 | */ |
||
1466 | daniel-mar | 2940 | protected function open_channel($channel, $skip_extended = false) |
827 | daniel-mar | 2941 | { |
1284 | daniel-mar | 2942 | if (isset($this->channel_status[$channel]) && $this->channel_status[$channel] != NET_SSH2_MSG_CHANNEL_CLOSE) { |
2943 | throw new \RuntimeException('Please close the channel (' . $channel . ') before trying to open it again'); |
||
827 | daniel-mar | 2944 | } |
2945 | |||
1284 | daniel-mar | 2946 | $this->channelCount++; |
2947 | |||
2948 | if ($this->channelCount > 1 && $this->errorOnMultipleChannels) { |
||
2949 | throw new \RuntimeException("Ubuntu's OpenSSH from 5.8 to 6.9 doesn't work with multiple channels"); |
||
1117 | daniel-mar | 2950 | } |
2951 | |||
1284 | daniel-mar | 2952 | // RFC4254 defines the (client) window size as "bytes the other party can send before it must wait for the window to |
2953 | // be adjusted". 0x7FFFFFFF is, at 2GB, the max size. technically, it should probably be decremented, but, |
||
2954 | // honestly, if you're transferring more than 2GB, you probably shouldn't be using phpseclib, anyway. |
||
2955 | // see http://tools.ietf.org/html/rfc4254#section-5.2 for more info |
||
2956 | $this->window_size_server_to_client[$channel] = $this->window_size; |
||
2957 | // 0x8000 is the maximum max packet size, per http://tools.ietf.org/html/rfc4253#section-6.1, although since PuTTy |
||
2958 | // uses 0x4000, that's what will be used here, as well. |
||
827 | daniel-mar | 2959 | $packet_size = 0x4000; |
2960 | |||
2961 | $packet = Strings::packSSH2( |
||
2962 | 'CsN3', |
||
874 | daniel-mar | 2963 | NET_SSH2_MSG_CHANNEL_OPEN, |
827 | daniel-mar | 2964 | 'session', |
1284 | daniel-mar | 2965 | $channel, |
2966 | $this->window_size_server_to_client[$channel], |
||
827 | daniel-mar | 2967 | $packet_size |
2968 | ); |
||
2969 | |||
2970 | $this->send_binary_packet($packet); |
||
2971 | |||
1284 | daniel-mar | 2972 | $this->channel_status[$channel] = NET_SSH2_MSG_CHANNEL_OPEN; |
827 | daniel-mar | 2973 | |
1284 | daniel-mar | 2974 | return $this->get_channel_packet($channel, $skip_extended); |
2975 | } |
||
827 | daniel-mar | 2976 | |
1284 | daniel-mar | 2977 | /** |
2978 | * Creates an interactive shell |
||
2979 | * |
||
2980 | * Returns bool(true) if the shell was opened. |
||
2981 | * Returns bool(false) if the shell was already open. |
||
2982 | * |
||
2983 | * @see self::isShellOpen() |
||
2984 | * @see self::read() |
||
2985 | * @see self::write() |
||
2986 | * @return bool |
||
2987 | * @throws InsufficientSetupException if not authenticated |
||
2988 | * @throws \UnexpectedValueException on receipt of unexpected packets |
||
2989 | * @throws \RuntimeException on other errors |
||
2990 | */ |
||
2991 | public function openShell() |
||
2992 | { |
||
2993 | if (!$this->isAuthenticated()) { |
||
2994 | throw new InsufficientSetupException('Operation disallowed prior to login()'); |
||
2995 | } |
||
2996 | |||
1466 | daniel-mar | 2997 | $this->open_channel(self::CHANNEL_SHELL); |
1284 | daniel-mar | 2998 | |
874 | daniel-mar | 2999 | $terminal_modes = pack('C', NET_SSH2_TTY_OP_END); |
827 | daniel-mar | 3000 | $packet = Strings::packSSH2( |
3001 | 'CNsbsN4s', |
||
874 | daniel-mar | 3002 | NET_SSH2_MSG_CHANNEL_REQUEST, |
827 | daniel-mar | 3003 | $this->server_channels[self::CHANNEL_SHELL], |
3004 | 'pty-req', |
||
3005 | true, // want reply |
||
3006 | $this->term, |
||
3007 | $this->windowColumns, |
||
3008 | $this->windowRows, |
||
3009 | 0, |
||
3010 | 0, |
||
3011 | $terminal_modes |
||
3012 | ); |
||
3013 | |||
3014 | $this->send_binary_packet($packet); |
||
3015 | |||
874 | daniel-mar | 3016 | $this->channel_status[self::CHANNEL_SHELL] = NET_SSH2_MSG_CHANNEL_REQUEST; |
827 | daniel-mar | 3017 | |
3018 | if (!$this->get_channel_packet(self::CHANNEL_SHELL)) { |
||
3019 | throw new \RuntimeException('Unable to request pty'); |
||
3020 | } |
||
3021 | |||
3022 | $packet = Strings::packSSH2( |
||
3023 | 'CNsb', |
||
874 | daniel-mar | 3024 | NET_SSH2_MSG_CHANNEL_REQUEST, |
827 | daniel-mar | 3025 | $this->server_channels[self::CHANNEL_SHELL], |
3026 | 'shell', |
||
3027 | true // want reply |
||
3028 | ); |
||
3029 | $this->send_binary_packet($packet); |
||
3030 | |||
3031 | $response = $this->get_channel_packet(self::CHANNEL_SHELL); |
||
3032 | if ($response === false) { |
||
3033 | throw new \RuntimeException('Unable to request shell'); |
||
3034 | } |
||
3035 | |||
874 | daniel-mar | 3036 | $this->channel_status[self::CHANNEL_SHELL] = NET_SSH2_MSG_CHANNEL_DATA; |
827 | daniel-mar | 3037 | |
1117 | daniel-mar | 3038 | $this->channel_id_last_interactive = self::CHANNEL_SHELL; |
3039 | |||
827 | daniel-mar | 3040 | $this->bitmap |= self::MASK_SHELL; |
3041 | |||
3042 | return true; |
||
3043 | } |
||
3044 | |||
3045 | /** |
||
1117 | daniel-mar | 3046 | * Return the channel to be used with read(), write(), and reset(), if none were specified |
3047 | * @deprecated for lack of transparency in intended channel target, to be potentially replaced |
||
3048 | * with method which guarantees open-ness of all yielded channels and throws |
||
3049 | * error for multiple open channels |
||
827 | daniel-mar | 3050 | * @see self::read() |
3051 | * @see self::write() |
||
3052 | * @return int |
||
3053 | */ |
||
3054 | private function get_interactive_channel() |
||
3055 | { |
||
3056 | switch (true) { |
||
1117 | daniel-mar | 3057 | case $this->is_channel_status_data(self::CHANNEL_SUBSYSTEM): |
827 | daniel-mar | 3058 | return self::CHANNEL_SUBSYSTEM; |
1117 | daniel-mar | 3059 | case $this->is_channel_status_data(self::CHANNEL_EXEC): |
827 | daniel-mar | 3060 | return self::CHANNEL_EXEC; |
3061 | default: |
||
3062 | return self::CHANNEL_SHELL; |
||
3063 | } |
||
3064 | } |
||
3065 | |||
3066 | /** |
||
1117 | daniel-mar | 3067 | * Indicates the DATA status on the given channel |
3068 | * |
||
3069 | * @param int $channel The channel number to evaluate |
||
3070 | * @return bool |
||
3071 | */ |
||
3072 | private function is_channel_status_data($channel) |
||
3073 | { |
||
3074 | return isset($this->channel_status[$channel]) && $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_DATA; |
||
3075 | } |
||
3076 | |||
3077 | /** |
||
827 | daniel-mar | 3078 | * Return an available open channel |
3079 | * |
||
3080 | * @return int |
||
3081 | */ |
||
3082 | private function get_open_channel() |
||
3083 | { |
||
3084 | $channel = self::CHANNEL_EXEC; |
||
3085 | do { |
||
874 | daniel-mar | 3086 | if (isset($this->channel_status[$channel]) && $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_OPEN) { |
827 | daniel-mar | 3087 | return $channel; |
3088 | } |
||
3089 | } while ($channel++ < self::CHANNEL_SUBSYSTEM); |
||
3090 | |||
3091 | return false; |
||
3092 | } |
||
3093 | |||
3094 | /** |
||
3095 | * Request agent forwarding of remote server |
||
3096 | * |
||
3097 | * @return bool |
||
3098 | */ |
||
3099 | public function requestAgentForwarding() |
||
3100 | { |
||
3101 | $request_channel = $this->get_open_channel(); |
||
3102 | if ($request_channel === false) { |
||
3103 | return false; |
||
3104 | } |
||
3105 | |||
3106 | $packet = Strings::packSSH2( |
||
3107 | 'CNsC', |
||
874 | daniel-mar | 3108 | NET_SSH2_MSG_CHANNEL_REQUEST, |
827 | daniel-mar | 3109 | $this->server_channels[$request_channel], |
3110 | 'auth-agent-req@openssh.com', |
||
3111 | 1 |
||
3112 | ); |
||
3113 | |||
874 | daniel-mar | 3114 | $this->channel_status[$request_channel] = NET_SSH2_MSG_CHANNEL_REQUEST; |
827 | daniel-mar | 3115 | |
3116 | $this->send_binary_packet($packet); |
||
3117 | |||
3118 | if (!$this->get_channel_packet($request_channel)) { |
||
3119 | return false; |
||
3120 | } |
||
3121 | |||
874 | daniel-mar | 3122 | $this->channel_status[$request_channel] = NET_SSH2_MSG_CHANNEL_OPEN; |
827 | daniel-mar | 3123 | |
3124 | return true; |
||
3125 | } |
||
3126 | |||
3127 | /** |
||
3128 | * Returns the output of an interactive shell |
||
3129 | * |
||
3130 | * Returns when there's a match for $expect, which can take the form of a string literal or, |
||
3131 | * if $mode == self::READ_REGEX, a regular expression. |
||
3132 | * |
||
1117 | daniel-mar | 3133 | * If not specifying a channel, an open interactive channel will be selected, or, if there are |
3134 | * no open channels, an interactive shell will be created. If there are multiple open |
||
3135 | * interactive channels, a legacy behavior will apply in which channel selection prioritizes |
||
3136 | * an active subsystem, the exec pty, and, lastly, the shell. If using multiple interactive |
||
3137 | * channels, callers are discouraged from relying on this legacy behavior and should specify |
||
3138 | * the intended channel. |
||
3139 | * |
||
827 | daniel-mar | 3140 | * @see self::write() |
3141 | * @param string $expect |
||
1117 | daniel-mar | 3142 | * @param int $mode One of the self::READ_* constants |
3143 | * @param int|null $channel Channel id returned by self::getInteractiveChannelId() |
||
827 | daniel-mar | 3144 | * @return string|bool|null |
3145 | * @throws \RuntimeException on connection error |
||
1117 | daniel-mar | 3146 | * @throws InsufficientSetupException on unexpected channel status, possibly due to closure |
827 | daniel-mar | 3147 | */ |
1117 | daniel-mar | 3148 | public function read($expect = '', $mode = self::READ_SIMPLE, $channel = null) |
827 | daniel-mar | 3149 | { |
1284 | daniel-mar | 3150 | if (!$this->isAuthenticated()) { |
3151 | throw new InsufficientSetupException('Operation disallowed prior to login()'); |
||
3152 | } |
||
3153 | |||
827 | daniel-mar | 3154 | $this->curTimeout = $this->timeout; |
3155 | $this->is_timeout = false; |
||
3156 | |||
1117 | daniel-mar | 3157 | if ($channel === null) { |
3158 | $channel = $this->get_interactive_channel(); |
||
827 | daniel-mar | 3159 | } |
3160 | |||
1284 | daniel-mar | 3161 | if (!$this->is_channel_status_data($channel) && empty($this->channel_buffers[$channel])) { |
1117 | daniel-mar | 3162 | if ($channel != self::CHANNEL_SHELL) { |
3163 | throw new InsufficientSetupException('Data is not available on channel'); |
||
3164 | } elseif (!$this->openShell()) { |
||
3165 | throw new \RuntimeException('Unable to initiate an interactive shell session'); |
||
3166 | } |
||
827 | daniel-mar | 3167 | } |
3168 | |||
3169 | if ($mode == self::READ_NEXT) { |
||
3170 | return $this->get_channel_packet($channel); |
||
3171 | } |
||
3172 | |||
3173 | $match = $expect; |
||
3174 | while (true) { |
||
3175 | if ($mode == self::READ_REGEX) { |
||
3176 | preg_match($expect, substr($this->interactiveBuffer, -1024), $matches); |
||
3177 | $match = isset($matches[0]) ? $matches[0] : ''; |
||
3178 | } |
||
3179 | $pos = strlen($match) ? strpos($this->interactiveBuffer, $match) : false; |
||
3180 | if ($pos !== false) { |
||
3181 | return Strings::shift($this->interactiveBuffer, $pos + strlen($match)); |
||
3182 | } |
||
3183 | $response = $this->get_channel_packet($channel); |
||
3184 | if ($response === true) { |
||
3185 | return Strings::shift($this->interactiveBuffer, strlen($this->interactiveBuffer)); |
||
3186 | } |
||
3187 | |||
3188 | $this->interactiveBuffer .= $response; |
||
3189 | } |
||
3190 | } |
||
3191 | |||
3192 | /** |
||
3193 | * Inputs a command into an interactive shell. |
||
3194 | * |
||
1117 | daniel-mar | 3195 | * If not specifying a channel, an open interactive channel will be selected, or, if there are |
3196 | * no open channels, an interactive shell will be created. If there are multiple open |
||
3197 | * interactive channels, a legacy behavior will apply in which channel selection prioritizes |
||
3198 | * an active subsystem, the exec pty, and, lastly, the shell. If using multiple interactive |
||
3199 | * channels, callers are discouraged from relying on this legacy behavior and should specify |
||
3200 | * the intended channel. |
||
3201 | * |
||
827 | daniel-mar | 3202 | * @see SSH2::read() |
3203 | * @param string $cmd |
||
1117 | daniel-mar | 3204 | * @param int|null $channel Channel id returned by self::getInteractiveChannelId() |
827 | daniel-mar | 3205 | * @return void |
3206 | * @throws \RuntimeException on connection error |
||
1117 | daniel-mar | 3207 | * @throws InsufficientSetupException on unexpected channel status, possibly due to closure |
827 | daniel-mar | 3208 | */ |
1117 | daniel-mar | 3209 | public function write($cmd, $channel = null) |
827 | daniel-mar | 3210 | { |
1284 | daniel-mar | 3211 | if (!$this->isAuthenticated()) { |
3212 | throw new InsufficientSetupException('Operation disallowed prior to login()'); |
||
3213 | } |
||
3214 | |||
1117 | daniel-mar | 3215 | if ($channel === null) { |
3216 | $channel = $this->get_interactive_channel(); |
||
827 | daniel-mar | 3217 | } |
3218 | |||
1284 | daniel-mar | 3219 | if (!$this->is_channel_status_data($channel)) { |
1117 | daniel-mar | 3220 | if ($channel != self::CHANNEL_SHELL) { |
3221 | throw new InsufficientSetupException('Data is not available on channel'); |
||
3222 | } elseif (!$this->openShell()) { |
||
3223 | throw new \RuntimeException('Unable to initiate an interactive shell session'); |
||
3224 | } |
||
827 | daniel-mar | 3225 | } |
3226 | |||
1117 | daniel-mar | 3227 | $this->send_channel_packet($channel, $cmd); |
827 | daniel-mar | 3228 | } |
3229 | |||
3230 | /** |
||
3231 | * Start a subsystem. |
||
3232 | * |
||
3233 | * Right now only one subsystem at a time is supported. To support multiple subsystem's stopSubsystem() could accept |
||
3234 | * a string that contained the name of the subsystem, but at that point, only one subsystem of each type could be opened. |
||
3235 | * To support multiple subsystem's of the same name maybe it'd be best if startSubsystem() generated a new channel id and |
||
3236 | * returns that and then that that was passed into stopSubsystem() but that'll be saved for a future date and implemented |
||
3237 | * if there's sufficient demand for such a feature. |
||
3238 | * |
||
3239 | * @see self::stopSubsystem() |
||
3240 | * @param string $subsystem |
||
3241 | * @return bool |
||
3242 | */ |
||
3243 | public function startSubsystem($subsystem) |
||
3244 | { |
||
1466 | daniel-mar | 3245 | $this->open_channel(self::CHANNEL_SUBSYSTEM); |
827 | daniel-mar | 3246 | |
3247 | $packet = Strings::packSSH2( |
||
3248 | 'CNsCs', |
||
874 | daniel-mar | 3249 | NET_SSH2_MSG_CHANNEL_REQUEST, |
827 | daniel-mar | 3250 | $this->server_channels[self::CHANNEL_SUBSYSTEM], |
3251 | 'subsystem', |
||
3252 | 1, |
||
3253 | $subsystem |
||
3254 | ); |
||
3255 | $this->send_binary_packet($packet); |
||
3256 | |||
874 | daniel-mar | 3257 | $this->channel_status[self::CHANNEL_SUBSYSTEM] = NET_SSH2_MSG_CHANNEL_REQUEST; |
827 | daniel-mar | 3258 | |
3259 | if (!$this->get_channel_packet(self::CHANNEL_SUBSYSTEM)) { |
||
3260 | return false; |
||
3261 | } |
||
3262 | |||
874 | daniel-mar | 3263 | $this->channel_status[self::CHANNEL_SUBSYSTEM] = NET_SSH2_MSG_CHANNEL_DATA; |
827 | daniel-mar | 3264 | |
1117 | daniel-mar | 3265 | $this->channel_id_last_interactive = self::CHANNEL_SUBSYSTEM; |
827 | daniel-mar | 3266 | |
3267 | return true; |
||
3268 | } |
||
3269 | |||
3270 | /** |
||
3271 | * Stops a subsystem. |
||
3272 | * |
||
3273 | * @see self::startSubsystem() |
||
3274 | * @return bool |
||
3275 | */ |
||
3276 | public function stopSubsystem() |
||
3277 | { |
||
1117 | daniel-mar | 3278 | if ($this->isInteractiveChannelOpen(self::CHANNEL_SUBSYSTEM)) { |
3279 | $this->close_channel(self::CHANNEL_SUBSYSTEM); |
||
3280 | } |
||
827 | daniel-mar | 3281 | return true; |
3282 | } |
||
3283 | |||
3284 | /** |
||
3285 | * Closes a channel |
||
3286 | * |
||
3287 | * If read() timed out you might want to just close the channel and have it auto-restart on the next read() call |
||
3288 | * |
||
1117 | daniel-mar | 3289 | * If not specifying a channel, an open interactive channel will be selected. If there are |
3290 | * multiple open interactive channels, a legacy behavior will apply in which channel selection |
||
3291 | * prioritizes an active subsystem, the exec pty, and, lastly, the shell. If using multiple |
||
3292 | * interactive channels, callers are discouraged from relying on this legacy behavior and |
||
3293 | * should specify the intended channel. |
||
3294 | * |
||
3295 | * @param int|null $channel Channel id returned by self::getInteractiveChannelId() |
||
3296 | * @return void |
||
827 | daniel-mar | 3297 | */ |
1117 | daniel-mar | 3298 | public function reset($channel = null) |
827 | daniel-mar | 3299 | { |
1117 | daniel-mar | 3300 | if ($channel === null) { |
3301 | $channel = $this->get_interactive_channel(); |
||
3302 | } |
||
3303 | if ($this->isInteractiveChannelOpen($channel)) { |
||
3304 | $this->close_channel($channel); |
||
3305 | } |
||
827 | daniel-mar | 3306 | } |
3307 | |||
3308 | /** |
||
3309 | * Is timeout? |
||
3310 | * |
||
3311 | * Did exec() or read() return because they timed out or because they encountered the end? |
||
3312 | * |
||
3313 | */ |
||
3314 | public function isTimeout() |
||
3315 | { |
||
3316 | return $this->is_timeout; |
||
3317 | } |
||
3318 | |||
3319 | /** |
||
3320 | * Disconnect |
||
3321 | * |
||
3322 | */ |
||
3323 | public function disconnect() |
||
3324 | { |
||
874 | daniel-mar | 3325 | $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION); |
827 | daniel-mar | 3326 | if (isset($this->realtime_log_file) && is_resource($this->realtime_log_file)) { |
3327 | fclose($this->realtime_log_file); |
||
3328 | } |
||
3329 | unset(self::$connections[$this->getResourceId()]); |
||
3330 | } |
||
3331 | |||
3332 | /** |
||
3333 | * Destructor. |
||
3334 | * |
||
3335 | * Will be called, automatically, if you're supporting just PHP5. If you're supporting PHP4, you'll need to call |
||
3336 | * disconnect(). |
||
3337 | * |
||
3338 | */ |
||
3339 | public function __destruct() |
||
3340 | { |
||
3341 | $this->disconnect(); |
||
3342 | } |
||
3343 | |||
3344 | /** |
||
3345 | * Is the connection still active? |
||
3346 | * |
||
1466 | daniel-mar | 3347 | * $level has 3x possible values: |
3348 | * 0 (default): phpseclib takes a passive approach to see if the connection is still active by calling feof() |
||
3349 | * on the socket |
||
3350 | * 1: phpseclib takes an active approach to see if the connection is still active by sending an SSH_MSG_IGNORE |
||
3351 | * packet that doesn't require a response |
||
3352 | * 2: phpseclib takes an active approach to see if the connection is still active by sending an SSH_MSG_CHANNEL_OPEN |
||
3353 | * packet and imediately trying to close that channel. some routers, in particular, however, will only let you |
||
3354 | * open one channel, so this approach could yield false positives |
||
3355 | * |
||
3356 | * @param int $level |
||
827 | daniel-mar | 3357 | * @return bool |
3358 | */ |
||
1466 | daniel-mar | 3359 | public function isConnected($level = 0) |
827 | daniel-mar | 3360 | { |
1466 | daniel-mar | 3361 | if (!is_int($level) || $level < 0 || $level > 2) { |
3362 | throw new \InvalidArgumentException('$level must be 0, 1 or 2'); |
||
3363 | } |
||
3364 | |||
3365 | if ($level == 0) { |
||
3366 | return ($this->bitmap & self::MASK_CONNECTED) && is_resource($this->fsock) && !feof($this->fsock); |
||
3367 | } |
||
3368 | try { |
||
3369 | if ($level == 1) { |
||
3370 | $this->send_binary_packet(pack('CN', NET_SSH2_MSG_IGNORE, 0)); |
||
3371 | } else { |
||
3372 | $this->open_channel(self::CHANNEL_KEEP_ALIVE); |
||
3373 | $this->close_channel(self::CHANNEL_KEEP_ALIVE); |
||
3374 | } |
||
3375 | return true; |
||
3376 | } catch (\Exception $e) { |
||
3377 | return false; |
||
3378 | } |
||
827 | daniel-mar | 3379 | } |
3380 | |||
3381 | /** |
||
3382 | * Have you successfully been logged in? |
||
3383 | * |
||
3384 | * @return bool |
||
3385 | */ |
||
3386 | public function isAuthenticated() |
||
3387 | { |
||
3388 | return (bool) ($this->bitmap & self::MASK_LOGIN); |
||
3389 | } |
||
3390 | |||
3391 | /** |
||
1117 | daniel-mar | 3392 | * Is the interactive shell active? |
3393 | * |
||
3394 | * @return bool |
||
3395 | */ |
||
3396 | public function isShellOpen() |
||
3397 | { |
||
3398 | return $this->isInteractiveChannelOpen(self::CHANNEL_SHELL); |
||
3399 | } |
||
3400 | |||
3401 | /** |
||
3402 | * Is the exec pty active? |
||
3403 | * |
||
3404 | * @return bool |
||
3405 | */ |
||
3406 | public function isPTYOpen() |
||
3407 | { |
||
3408 | return $this->isInteractiveChannelOpen(self::CHANNEL_EXEC); |
||
3409 | } |
||
3410 | |||
3411 | /** |
||
3412 | * Is the given interactive channel active? |
||
3413 | * |
||
3414 | * @param int $channel Channel id returned by self::getInteractiveChannelId() |
||
3415 | * @return bool |
||
3416 | */ |
||
3417 | public function isInteractiveChannelOpen($channel) |
||
3418 | { |
||
3419 | return $this->isAuthenticated() && $this->is_channel_status_data($channel); |
||
3420 | } |
||
3421 | |||
3422 | /** |
||
3423 | * Returns a channel identifier, presently of the last interactive channel opened, regardless of current status. |
||
3424 | * Returns 0 if no interactive channel has been opened. |
||
3425 | * |
||
3426 | * @see self::isInteractiveChannelOpen() |
||
3427 | * @return int |
||
3428 | */ |
||
3429 | public function getInteractiveChannelId() |
||
3430 | { |
||
3431 | return $this->channel_id_last_interactive; |
||
3432 | } |
||
3433 | |||
3434 | /** |
||
827 | daniel-mar | 3435 | * Pings a server connection, or tries to reconnect if the connection has gone down |
3436 | * |
||
3437 | * Inspired by http://php.net/manual/en/mysqli.ping.php |
||
3438 | * |
||
3439 | * @return bool |
||
3440 | */ |
||
3441 | public function ping() |
||
3442 | { |
||
3443 | if (!$this->isAuthenticated()) { |
||
3444 | if (!empty($this->auth)) { |
||
3445 | return $this->reconnect(); |
||
3446 | } |
||
3447 | return false; |
||
3448 | } |
||
3449 | |||
3450 | try { |
||
1466 | daniel-mar | 3451 | $this->open_channel(self::CHANNEL_KEEP_ALIVE); |
827 | daniel-mar | 3452 | } catch (\RuntimeException $e) { |
3453 | return $this->reconnect(); |
||
3454 | } |
||
3455 | |||
3456 | $this->close_channel(self::CHANNEL_KEEP_ALIVE); |
||
3457 | return true; |
||
3458 | } |
||
3459 | |||
3460 | /** |
||
3461 | * In situ reconnect method |
||
3462 | * |
||
3463 | * @return boolean |
||
3464 | */ |
||
3465 | private function reconnect() |
||
3466 | { |
||
874 | daniel-mar | 3467 | $this->reset_connection(NET_SSH2_DISCONNECT_CONNECTION_LOST); |
827 | daniel-mar | 3468 | $this->retry_connect = true; |
3469 | $this->connect(); |
||
3470 | foreach ($this->auth as $auth) { |
||
3471 | $result = $this->login(...$auth); |
||
3472 | } |
||
3473 | return $result; |
||
3474 | } |
||
3475 | |||
3476 | /** |
||
3477 | * Resets a connection for re-use |
||
3478 | * |
||
3479 | * @param int $reason |
||
3480 | */ |
||
3481 | protected function reset_connection($reason) |
||
3482 | { |
||
3483 | $this->disconnect_helper($reason); |
||
3484 | $this->decrypt = $this->encrypt = false; |
||
3485 | $this->decrypt_block_size = $this->encrypt_block_size = 8; |
||
3486 | $this->hmac_check = $this->hmac_create = false; |
||
3487 | $this->hmac_size = false; |
||
3488 | $this->session_id = false; |
||
3489 | $this->retry_connect = true; |
||
3490 | $this->get_seq_no = $this->send_seq_no = 0; |
||
1439 | daniel-mar | 3491 | $this->channel_status = []; |
3492 | $this->channel_id_last_interactive = 0; |
||
827 | daniel-mar | 3493 | } |
3494 | |||
3495 | /** |
||
3496 | * Gets Binary Packets |
||
3497 | * |
||
3498 | * See '6. Binary Packet Protocol' of rfc4253 for more info. |
||
3499 | * |
||
3500 | * @see self::_send_binary_packet() |
||
3501 | * @param bool $skip_channel_filter |
||
3502 | * @return bool|string |
||
3503 | */ |
||
3504 | private function get_binary_packet($skip_channel_filter = false) |
||
3505 | { |
||
3506 | if ($skip_channel_filter) { |
||
3507 | if (!is_resource($this->fsock)) { |
||
3508 | throw new \InvalidArgumentException('fsock is not a resource.'); |
||
3509 | } |
||
3510 | $read = [$this->fsock]; |
||
3511 | $write = $except = null; |
||
3512 | |||
3513 | if (!$this->curTimeout) { |
||
3514 | if ($this->keepAlive <= 0) { |
||
1324 | daniel-mar | 3515 | static::stream_select($read, $write, $except, null); |
827 | daniel-mar | 3516 | } else { |
1324 | daniel-mar | 3517 | if (!static::stream_select($read, $write, $except, $this->keepAlive)) { |
874 | daniel-mar | 3518 | $this->send_binary_packet(pack('CN', NET_SSH2_MSG_IGNORE, 0)); |
827 | daniel-mar | 3519 | return $this->get_binary_packet(true); |
3520 | } |
||
3521 | } |
||
3522 | } else { |
||
3523 | if ($this->curTimeout < 0) { |
||
3524 | $this->is_timeout = true; |
||
3525 | return true; |
||
3526 | } |
||
3527 | |||
3528 | $start = microtime(true); |
||
3529 | |||
3530 | if ($this->keepAlive > 0 && $this->keepAlive < $this->curTimeout) { |
||
1324 | daniel-mar | 3531 | if (!static::stream_select($read, $write, $except, $this->keepAlive)) { |
874 | daniel-mar | 3532 | $this->send_binary_packet(pack('CN', NET_SSH2_MSG_IGNORE, 0)); |
827 | daniel-mar | 3533 | $elapsed = microtime(true) - $start; |
3534 | $this->curTimeout -= $elapsed; |
||
3535 | return $this->get_binary_packet(true); |
||
3536 | } |
||
3537 | $elapsed = microtime(true) - $start; |
||
3538 | $this->curTimeout -= $elapsed; |
||
3539 | } |
||
3540 | |||
3541 | $sec = (int) floor($this->curTimeout); |
||
3542 | $usec = (int) (1000000 * ($this->curTimeout - $sec)); |
||
3543 | |||
3544 | // this can return a "stream_select(): unable to select [4]: Interrupted system call" error |
||
1324 | daniel-mar | 3545 | if (!static::stream_select($read, $write, $except, $sec, $usec)) { |
827 | daniel-mar | 3546 | $this->is_timeout = true; |
3547 | return true; |
||
3548 | } |
||
3549 | $elapsed = microtime(true) - $start; |
||
3550 | $this->curTimeout -= $elapsed; |
||
3551 | } |
||
3552 | } |
||
3553 | |||
3554 | if (!is_resource($this->fsock) || feof($this->fsock)) { |
||
3555 | $this->bitmap = 0; |
||
1042 | daniel-mar | 3556 | $str = 'Connection closed (by server) prematurely'; |
3557 | if (isset($elapsed)) { |
||
3558 | $str .= ' ' . $elapsed . 's'; |
||
3559 | } |
||
3560 | throw new ConnectionClosedException($str); |
||
827 | daniel-mar | 3561 | } |
3562 | |||
3563 | $start = microtime(true); |
||
1466 | daniel-mar | 3564 | if ($this->curTimeout) { |
3565 | $sec = (int) floor($this->curTimeout); |
||
3566 | $usec = (int) (1000000 * ($this->curTimeout - $sec)); |
||
3567 | stream_set_timeout($this->fsock, $sec, $usec); |
||
3568 | } |
||
827 | daniel-mar | 3569 | $raw = stream_get_contents($this->fsock, $this->decrypt_block_size); |
3570 | |||
3571 | if (!strlen($raw)) { |
||
3572 | $this->bitmap = 0; |
||
3573 | throw new ConnectionClosedException('No data received from server'); |
||
3574 | } |
||
3575 | |||
3576 | if ($this->decrypt) { |
||
3577 | switch ($this->decryptName) { |
||
3578 | case 'aes128-gcm@openssh.com': |
||
3579 | case 'aes256-gcm@openssh.com': |
||
3580 | $this->decrypt->setNonce( |
||
3581 | $this->decryptFixedPart . |
||
3582 | $this->decryptInvocationCounter |
||
3583 | ); |
||
3584 | Strings::increment_str($this->decryptInvocationCounter); |
||
3585 | $this->decrypt->setAAD($temp = Strings::shift($raw, 4)); |
||
3586 | extract(unpack('Npacket_length', $temp)); |
||
3587 | /** |
||
3588 | * @var integer $packet_length |
||
3589 | */ |
||
3590 | |||
3591 | $raw .= $this->read_remaining_bytes($packet_length - $this->decrypt_block_size + 4); |
||
3592 | $stop = microtime(true); |
||
3593 | $tag = stream_get_contents($this->fsock, $this->decrypt_block_size); |
||
3594 | $this->decrypt->setTag($tag); |
||
3595 | $raw = $this->decrypt->decrypt($raw); |
||
3596 | $raw = $temp . $raw; |
||
3597 | $remaining_length = 0; |
||
3598 | break; |
||
3599 | case 'chacha20-poly1305@openssh.com': |
||
3600 | // This should be impossible, but we are checking anyway to narrow the type for Psalm. |
||
3601 | if (!($this->decrypt instanceof ChaCha20)) { |
||
3602 | throw new \LogicException('$this->decrypt is not a ' . ChaCha20::class); |
||
3603 | } |
||
3604 | |||
3605 | $nonce = pack('N2', 0, $this->get_seq_no); |
||
3606 | |||
3607 | $this->lengthDecrypt->setNonce($nonce); |
||
3608 | $temp = $this->lengthDecrypt->decrypt($aad = Strings::shift($raw, 4)); |
||
3609 | extract(unpack('Npacket_length', $temp)); |
||
3610 | /** |
||
3611 | * @var integer $packet_length |
||
3612 | */ |
||
3613 | |||
3614 | $raw .= $this->read_remaining_bytes($packet_length - $this->decrypt_block_size + 4); |
||
3615 | $stop = microtime(true); |
||
3616 | $tag = stream_get_contents($this->fsock, 16); |
||
3617 | |||
3618 | $this->decrypt->setNonce($nonce); |
||
3619 | $this->decrypt->setCounter(0); |
||
3620 | // this is the same approach that's implemented in Salsa20::createPoly1305Key() |
||
3621 | // but we don't want to use the same AEAD construction that RFC8439 describes |
||
3622 | // for ChaCha20-Poly1305 so we won't rely on it (see Salsa20::poly1305()) |
||
3623 | $this->decrypt->setPoly1305Key( |
||
3624 | $this->decrypt->encrypt(str_repeat("\0", 32)) |
||
3625 | ); |
||
3626 | $this->decrypt->setAAD($aad); |
||
3627 | $this->decrypt->setCounter(1); |
||
3628 | $this->decrypt->setTag($tag); |
||
3629 | $raw = $this->decrypt->decrypt($raw); |
||
3630 | $raw = $temp . $raw; |
||
3631 | $remaining_length = 0; |
||
3632 | break; |
||
3633 | default: |
||
3634 | if (!$this->hmac_check instanceof Hash || !$this->hmac_check_etm) { |
||
3635 | $raw = $this->decrypt->decrypt($raw); |
||
3636 | break; |
||
3637 | } |
||
3638 | extract(unpack('Npacket_length', $temp = Strings::shift($raw, 4))); |
||
3639 | /** |
||
3640 | * @var integer $packet_length |
||
3641 | */ |
||
3642 | $raw .= $this->read_remaining_bytes($packet_length - $this->decrypt_block_size + 4); |
||
3643 | $stop = microtime(true); |
||
3644 | $encrypted = $temp . $raw; |
||
3645 | $raw = $temp . $this->decrypt->decrypt($raw); |
||
3646 | $remaining_length = 0; |
||
3647 | } |
||
3648 | } |
||
3649 | |||
3650 | if (strlen($raw) < 5) { |
||
3651 | $this->bitmap = 0; |
||
3652 | throw new \RuntimeException('Plaintext is too short'); |
||
3653 | } |
||
3654 | extract(unpack('Npacket_length/Cpadding_length', Strings::shift($raw, 5))); |
||
3655 | /** |
||
3656 | * @var integer $packet_length |
||
3657 | * @var integer $padding_length |
||
3658 | */ |
||
3659 | |||
3660 | if (!isset($remaining_length)) { |
||
3661 | $remaining_length = $packet_length + 4 - $this->decrypt_block_size; |
||
3662 | } |
||
3663 | |||
3664 | $buffer = $this->read_remaining_bytes($remaining_length); |
||
3665 | |||
3666 | if (!isset($stop)) { |
||
3667 | $stop = microtime(true); |
||
3668 | } |
||
3669 | if (strlen($buffer)) { |
||
3670 | $raw .= $this->decrypt ? $this->decrypt->decrypt($buffer) : $buffer; |
||
3671 | } |
||
3672 | |||
3673 | $payload = Strings::shift($raw, $packet_length - $padding_length - 1); |
||
3674 | $padding = Strings::shift($raw, $padding_length); // should leave $raw empty |
||
3675 | |||
3676 | if ($this->hmac_check instanceof Hash) { |
||
3677 | $hmac = stream_get_contents($this->fsock, $this->hmac_size); |
||
3678 | if ($hmac === false || strlen($hmac) != $this->hmac_size) { |
||
874 | daniel-mar | 3679 | $this->disconnect_helper(NET_SSH2_DISCONNECT_MAC_ERROR); |
827 | daniel-mar | 3680 | throw new \RuntimeException('Error reading socket'); |
3681 | } |
||
3682 | |||
3683 | $reconstructed = !$this->hmac_check_etm ? |
||
3684 | pack('NCa*', $packet_length, $padding_length, $payload . $padding) : |
||
3685 | $encrypted; |
||
3686 | if (($this->hmac_check->getHash() & "\xFF\xFF\xFF\xFF") == 'umac') { |
||
3687 | $this->hmac_check->setNonce("\0\0\0\0" . pack('N', $this->get_seq_no)); |
||
3688 | if ($hmac != $this->hmac_check->hash($reconstructed)) { |
||
874 | daniel-mar | 3689 | $this->disconnect_helper(NET_SSH2_DISCONNECT_MAC_ERROR); |
827 | daniel-mar | 3690 | throw new \RuntimeException('Invalid UMAC'); |
3691 | } |
||
3692 | } else { |
||
3693 | if ($hmac != $this->hmac_check->hash(pack('Na*', $this->get_seq_no, $reconstructed))) { |
||
874 | daniel-mar | 3694 | $this->disconnect_helper(NET_SSH2_DISCONNECT_MAC_ERROR); |
827 | daniel-mar | 3695 | throw new \RuntimeException('Invalid HMAC'); |
3696 | } |
||
3697 | } |
||
3698 | } |
||
3699 | |||
3700 | switch ($this->decompress) { |
||
3701 | case self::NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH: |
||
3702 | if (!$this->isAuthenticated()) { |
||
3703 | break; |
||
3704 | } |
||
3705 | // fall-through |
||
3706 | case self::NET_SSH2_COMPRESSION_ZLIB: |
||
3707 | if ($this->regenerate_decompression_context) { |
||
3708 | $this->regenerate_decompression_context = false; |
||
3709 | |||
3710 | $cmf = ord($payload[0]); |
||
3711 | $cm = $cmf & 0x0F; |
||
3712 | if ($cm != 8) { // deflate |
||
3713 | user_error("Only CM = 8 ('deflate') is supported ($cm)"); |
||
3714 | } |
||
3715 | $cinfo = ($cmf & 0xF0) >> 4; |
||
3716 | if ($cinfo > 7) { |
||
3717 | user_error("CINFO above 7 is not allowed ($cinfo)"); |
||
3718 | } |
||
3719 | $windowSize = 1 << ($cinfo + 8); |
||
3720 | |||
3721 | $flg = ord($payload[1]); |
||
3722 | //$fcheck = $flg && 0x0F; |
||
3723 | if ((($cmf << 8) | $flg) % 31) { |
||
3724 | user_error('fcheck failed'); |
||
3725 | } |
||
3726 | $fdict = boolval($flg & 0x20); |
||
3727 | $flevel = ($flg & 0xC0) >> 6; |
||
3728 | |||
3729 | $this->decompress_context = inflate_init(ZLIB_ENCODING_RAW, ['window' => $cinfo + 8]); |
||
3730 | $payload = substr($payload, 2); |
||
3731 | } |
||
3732 | if ($this->decompress_context) { |
||
3733 | $payload = inflate_add($this->decompress_context, $payload, ZLIB_PARTIAL_FLUSH); |
||
3734 | } |
||
3735 | } |
||
3736 | |||
3737 | $this->get_seq_no++; |
||
3738 | |||
3739 | if (defined('NET_SSH2_LOGGING')) { |
||
3740 | $current = microtime(true); |
||
1117 | daniel-mar | 3741 | $message_number = isset(self::$message_numbers[ord($payload[0])]) ? self::$message_numbers[ord($payload[0])] : 'UNKNOWN (' . ord($payload[0]) . ')'; |
874 | daniel-mar | 3742 | $message_number = '<- ' . $message_number . |
3743 | ' (since last: ' . round($current - $this->last_packet, 4) . ', network: ' . round($stop - $start, 4) . 's)'; |
||
827 | daniel-mar | 3744 | $this->append_log($message_number, $payload); |
3745 | $this->last_packet = $current; |
||
3746 | } |
||
3747 | |||
3748 | return $this->filter($payload, $skip_channel_filter); |
||
3749 | } |
||
3750 | |||
3751 | /** |
||
3752 | * Read Remaining Bytes |
||
3753 | * |
||
3754 | * @see self::get_binary_packet() |
||
3755 | * @param int $remaining_length |
||
3756 | * @return string |
||
3757 | */ |
||
3758 | private function read_remaining_bytes($remaining_length) |
||
3759 | { |
||
3760 | if (!$remaining_length) { |
||
3761 | return ''; |
||
3762 | } |
||
3763 | |||
3764 | $adjustLength = false; |
||
3765 | if ($this->decrypt) { |
||
3766 | switch (true) { |
||
3767 | case $this->decryptName == 'aes128-gcm@openssh.com': |
||
3768 | case $this->decryptName == 'aes256-gcm@openssh.com': |
||
3769 | case $this->decryptName == 'chacha20-poly1305@openssh.com': |
||
3770 | case $this->hmac_check instanceof Hash && $this->hmac_check_etm: |
||
3771 | $remaining_length += $this->decrypt_block_size - 4; |
||
3772 | $adjustLength = true; |
||
3773 | } |
||
3774 | } |
||
3775 | |||
3776 | // quoting <http://tools.ietf.org/html/rfc4253#section-6.1>, |
||
3777 | // "implementations SHOULD check that the packet length is reasonable" |
||
3778 | // PuTTY uses 0x9000 as the actual max packet size and so to shall we |
||
3779 | // don't do this when GCM mode is used since GCM mode doesn't encrypt the length |
||
3780 | if ($remaining_length < -$this->decrypt_block_size || $remaining_length > 0x9000 || $remaining_length % $this->decrypt_block_size != 0) { |
||
3781 | if (!$this->bad_key_size_fix && self::bad_algorithm_candidate($this->decrypt ? $this->decryptName : '') && !($this->bitmap & SSH2::MASK_LOGIN)) { |
||
3782 | $this->bad_key_size_fix = true; |
||
874 | daniel-mar | 3783 | $this->reset_connection(NET_SSH2_DISCONNECT_KEY_EXCHANGE_FAILED); |
827 | daniel-mar | 3784 | return false; |
3785 | } |
||
3786 | throw new \RuntimeException('Invalid size'); |
||
3787 | } |
||
3788 | |||
3789 | if ($adjustLength) { |
||
3790 | $remaining_length -= $this->decrypt_block_size - 4; |
||
3791 | } |
||
3792 | |||
3793 | $buffer = ''; |
||
3794 | while ($remaining_length > 0) { |
||
3795 | $temp = stream_get_contents($this->fsock, $remaining_length); |
||
3796 | if ($temp === false || feof($this->fsock)) { |
||
874 | daniel-mar | 3797 | $this->disconnect_helper(NET_SSH2_DISCONNECT_CONNECTION_LOST); |
827 | daniel-mar | 3798 | throw new \RuntimeException('Error reading from socket'); |
3799 | } |
||
3800 | $buffer .= $temp; |
||
3801 | $remaining_length -= strlen($temp); |
||
3802 | } |
||
3803 | |||
3804 | return $buffer; |
||
3805 | } |
||
3806 | |||
3807 | /** |
||
3808 | * Filter Binary Packets |
||
3809 | * |
||
3810 | * Because some binary packets need to be ignored... |
||
3811 | * |
||
3812 | * @see self::_get_binary_packet() |
||
3813 | * @param string $payload |
||
3814 | * @param bool $skip_channel_filter |
||
3815 | * @return string|bool |
||
3816 | */ |
||
3817 | private function filter($payload, $skip_channel_filter) |
||
3818 | { |
||
3819 | switch (ord($payload[0])) { |
||
874 | daniel-mar | 3820 | case NET_SSH2_MSG_DISCONNECT: |
827 | daniel-mar | 3821 | Strings::shift($payload, 1); |
3822 | list($reason_code, $message) = Strings::unpackSSH2('Ns', $payload); |
||
1422 | daniel-mar | 3823 | $this->errors[] = 'SSH_MSG_DISCONNECT: ' . self::$disconnect_reasons[$reason_code] . "\r\n$message"; |
827 | daniel-mar | 3824 | $this->bitmap = 0; |
3825 | return false; |
||
874 | daniel-mar | 3826 | case NET_SSH2_MSG_IGNORE: |
1448 | daniel-mar | 3827 | $this->extra_packets++; |
827 | daniel-mar | 3828 | $payload = $this->get_binary_packet($skip_channel_filter); |
3829 | break; |
||
874 | daniel-mar | 3830 | case NET_SSH2_MSG_DEBUG: |
1448 | daniel-mar | 3831 | $this->extra_packets++; |
827 | daniel-mar | 3832 | Strings::shift($payload, 2); // second byte is "always_display" |
3833 | list($message) = Strings::unpackSSH2('s', $payload); |
||
3834 | $this->errors[] = "SSH_MSG_DEBUG: $message"; |
||
3835 | $payload = $this->get_binary_packet($skip_channel_filter); |
||
3836 | break; |
||
874 | daniel-mar | 3837 | case NET_SSH2_MSG_UNIMPLEMENTED: |
827 | daniel-mar | 3838 | return false; |
874 | daniel-mar | 3839 | case NET_SSH2_MSG_KEXINIT: |
1448 | daniel-mar | 3840 | // this is here for key re-exchanges after the initial key exchange |
827 | daniel-mar | 3841 | if ($this->session_id !== false) { |
3842 | if (!$this->key_exchange($payload)) { |
||
3843 | $this->bitmap = 0; |
||
3844 | return false; |
||
3845 | } |
||
3846 | $payload = $this->get_binary_packet($skip_channel_filter); |
||
3847 | } |
||
3848 | } |
||
3849 | |||
3850 | // 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 |
||
874 | daniel-mar | 3851 | if (($this->bitmap & self::MASK_CONNECTED) && !$this->isAuthenticated() && !is_bool($payload) && ord($payload[0]) == NET_SSH2_MSG_USERAUTH_BANNER) { |
827 | daniel-mar | 3852 | Strings::shift($payload, 1); |
3853 | list($this->banner_message) = Strings::unpackSSH2('s', $payload); |
||
3854 | $payload = $this->get_binary_packet(); |
||
3855 | } |
||
3856 | |||
3857 | // only called when we've already logged in |
||
3858 | if (($this->bitmap & self::MASK_CONNECTED) && $this->isAuthenticated()) { |
||
3859 | if (is_bool($payload)) { |
||
3860 | return $payload; |
||
3861 | } |
||
3862 | |||
3863 | switch (ord($payload[0])) { |
||
874 | daniel-mar | 3864 | case NET_SSH2_MSG_CHANNEL_REQUEST: |
827 | daniel-mar | 3865 | if (strlen($payload) == 31) { |
3866 | extract(unpack('cpacket_type/Nchannel/Nlength', $payload)); |
||
3867 | if (substr($payload, 9, $length) == 'keepalive@openssh.com' && isset($this->server_channels[$channel])) { |
||
3868 | if (ord(substr($payload, 9 + $length))) { // want reply |
||
874 | daniel-mar | 3869 | $this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_SUCCESS, $this->server_channels[$channel])); |
827 | daniel-mar | 3870 | } |
3871 | $payload = $this->get_binary_packet($skip_channel_filter); |
||
3872 | } |
||
3873 | } |
||
3874 | break; |
||
874 | daniel-mar | 3875 | case NET_SSH2_MSG_CHANNEL_DATA: |
3876 | case NET_SSH2_MSG_CHANNEL_EXTENDED_DATA: |
||
3877 | case NET_SSH2_MSG_CHANNEL_CLOSE: |
||
3878 | case NET_SSH2_MSG_CHANNEL_EOF: |
||
827 | daniel-mar | 3879 | if (!$skip_channel_filter && !empty($this->server_channels)) { |
3880 | $this->binary_packet_buffer = $payload; |
||
3881 | $this->get_channel_packet(true); |
||
3882 | $payload = $this->get_binary_packet(); |
||
3883 | } |
||
3884 | break; |
||
874 | daniel-mar | 3885 | case NET_SSH2_MSG_GLOBAL_REQUEST: // see http://tools.ietf.org/html/rfc4254#section-4 |
827 | daniel-mar | 3886 | Strings::shift($payload, 1); |
3887 | list($request_name) = Strings::unpackSSH2('s', $payload); |
||
3888 | $this->errors[] = "SSH_MSG_GLOBAL_REQUEST: $request_name"; |
||
3889 | |||
3890 | try { |
||
874 | daniel-mar | 3891 | $this->send_binary_packet(pack('C', NET_SSH2_MSG_REQUEST_FAILURE)); |
827 | daniel-mar | 3892 | } catch (\RuntimeException $e) { |
874 | daniel-mar | 3893 | return $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION); |
827 | daniel-mar | 3894 | } |
3895 | |||
3896 | $payload = $this->get_binary_packet($skip_channel_filter); |
||
3897 | break; |
||
874 | daniel-mar | 3898 | case NET_SSH2_MSG_CHANNEL_OPEN: // see http://tools.ietf.org/html/rfc4254#section-5.1 |
827 | daniel-mar | 3899 | Strings::shift($payload, 1); |
3900 | list($data, $server_channel) = Strings::unpackSSH2('sN', $payload); |
||
3901 | switch ($data) { |
||
3902 | case 'auth-agent': |
||
3903 | case 'auth-agent@openssh.com': |
||
3904 | if (isset($this->agent)) { |
||
3905 | $new_channel = self::CHANNEL_AGENT_FORWARD; |
||
3906 | |||
3907 | list( |
||
3908 | $remote_window_size, |
||
3909 | $remote_maximum_packet_size |
||
3910 | ) = Strings::unpackSSH2('NN', $payload); |
||
3911 | |||
3912 | $this->packet_size_client_to_server[$new_channel] = $remote_window_size; |
||
3913 | $this->window_size_server_to_client[$new_channel] = $remote_maximum_packet_size; |
||
3914 | $this->window_size_client_to_server[$new_channel] = $this->window_size; |
||
3915 | |||
3916 | $packet_size = 0x4000; |
||
3917 | |||
3918 | $packet = pack( |
||
3919 | 'CN4', |
||
874 | daniel-mar | 3920 | NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION, |
827 | daniel-mar | 3921 | $server_channel, |
3922 | $new_channel, |
||
3923 | $packet_size, |
||
3924 | $packet_size |
||
3925 | ); |
||
3926 | |||
3927 | $this->server_channels[$new_channel] = $server_channel; |
||
874 | daniel-mar | 3928 | $this->channel_status[$new_channel] = NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION; |
827 | daniel-mar | 3929 | $this->send_binary_packet($packet); |
3930 | } |
||
3931 | break; |
||
3932 | default: |
||
3933 | $packet = Strings::packSSH2( |
||
3934 | 'CN2ss', |
||
874 | daniel-mar | 3935 | NET_SSH2_MSG_CHANNEL_OPEN_FAILURE, |
827 | daniel-mar | 3936 | $server_channel, |
874 | daniel-mar | 3937 | NET_SSH2_OPEN_ADMINISTRATIVELY_PROHIBITED, |
827 | daniel-mar | 3938 | '', // description |
3939 | '' // language tag |
||
3940 | ); |
||
3941 | |||
3942 | try { |
||
3943 | $this->send_binary_packet($packet); |
||
3944 | } catch (\RuntimeException $e) { |
||
874 | daniel-mar | 3945 | return $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION); |
827 | daniel-mar | 3946 | } |
3947 | } |
||
3948 | |||
3949 | $payload = $this->get_binary_packet($skip_channel_filter); |
||
3950 | break; |
||
874 | daniel-mar | 3951 | case NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST: |
827 | daniel-mar | 3952 | Strings::shift($payload, 1); |
3953 | list($channel, $window_size) = Strings::unpackSSH2('NN', $payload); |
||
3954 | |||
3955 | $this->window_size_client_to_server[$channel] += $window_size; |
||
3956 | |||
3957 | $payload = ($this->bitmap & self::MASK_WINDOW_ADJUST) ? true : $this->get_binary_packet($skip_channel_filter); |
||
3958 | } |
||
3959 | } |
||
3960 | |||
3961 | return $payload; |
||
3962 | } |
||
3963 | |||
3964 | /** |
||
3965 | * Enable Quiet Mode |
||
3966 | * |
||
3967 | * Suppress stderr from output |
||
3968 | * |
||
3969 | */ |
||
3970 | public function enableQuietMode() |
||
3971 | { |
||
3972 | $this->quiet_mode = true; |
||
3973 | } |
||
3974 | |||
3975 | /** |
||
3976 | * Disable Quiet Mode |
||
3977 | * |
||
3978 | * Show stderr in output |
||
3979 | * |
||
3980 | */ |
||
3981 | public function disableQuietMode() |
||
3982 | { |
||
3983 | $this->quiet_mode = false; |
||
3984 | } |
||
3985 | |||
3986 | /** |
||
3987 | * Returns whether Quiet Mode is enabled or not |
||
3988 | * |
||
3989 | * @see self::enableQuietMode() |
||
3990 | * @see self::disableQuietMode() |
||
3991 | * @return bool |
||
3992 | */ |
||
3993 | public function isQuietModeEnabled() |
||
3994 | { |
||
3995 | return $this->quiet_mode; |
||
3996 | } |
||
3997 | |||
3998 | /** |
||
3999 | * Enable request-pty when using exec() |
||
4000 | * |
||
4001 | */ |
||
4002 | public function enablePTY() |
||
4003 | { |
||
4004 | $this->request_pty = true; |
||
4005 | } |
||
4006 | |||
4007 | /** |
||
4008 | * Disable request-pty when using exec() |
||
4009 | * |
||
4010 | */ |
||
4011 | public function disablePTY() |
||
4012 | { |
||
1117 | daniel-mar | 4013 | if ($this->isPTYOpen()) { |
827 | daniel-mar | 4014 | $this->close_channel(self::CHANNEL_EXEC); |
4015 | } |
||
4016 | $this->request_pty = false; |
||
4017 | } |
||
4018 | |||
4019 | /** |
||
4020 | * Returns whether request-pty is enabled or not |
||
4021 | * |
||
4022 | * @see self::enablePTY() |
||
4023 | * @see self::disablePTY() |
||
4024 | * @return bool |
||
4025 | */ |
||
4026 | public function isPTYEnabled() |
||
4027 | { |
||
4028 | return $this->request_pty; |
||
4029 | } |
||
4030 | |||
4031 | /** |
||
4032 | * Gets channel data |
||
4033 | * |
||
4034 | * Returns the data as a string. bool(true) is returned if: |
||
4035 | * |
||
4036 | * - the server closes the channel |
||
4037 | * - if the connection times out |
||
4038 | * - if the channel status is CHANNEL_OPEN and the response was CHANNEL_OPEN_CONFIRMATION |
||
4039 | * - if the channel status is CHANNEL_REQUEST and the response was CHANNEL_SUCCESS |
||
1117 | daniel-mar | 4040 | * - if the channel status is CHANNEL_CLOSE and the response was CHANNEL_CLOSE |
827 | daniel-mar | 4041 | * |
4042 | * bool(false) is returned if: |
||
4043 | * |
||
4044 | * - if the channel status is CHANNEL_REQUEST and the response was CHANNEL_FAILURE |
||
4045 | * |
||
4046 | * @param int $client_channel |
||
4047 | * @param bool $skip_extended |
||
4048 | * @return mixed |
||
4049 | * @throws \RuntimeException on connection error |
||
4050 | */ |
||
4051 | protected function get_channel_packet($client_channel, $skip_extended = false) |
||
4052 | { |
||
4053 | if (!empty($this->channel_buffers[$client_channel])) { |
||
4054 | switch ($this->channel_status[$client_channel]) { |
||
874 | daniel-mar | 4055 | case NET_SSH2_MSG_CHANNEL_REQUEST: |
827 | daniel-mar | 4056 | foreach ($this->channel_buffers[$client_channel] as $i => $packet) { |
4057 | switch (ord($packet[0])) { |
||
874 | daniel-mar | 4058 | case NET_SSH2_MSG_CHANNEL_SUCCESS: |
4059 | case NET_SSH2_MSG_CHANNEL_FAILURE: |
||
827 | daniel-mar | 4060 | unset($this->channel_buffers[$client_channel][$i]); |
4061 | return substr($packet, 1); |
||
4062 | } |
||
4063 | } |
||
4064 | break; |
||
4065 | default: |
||
4066 | return substr(array_shift($this->channel_buffers[$client_channel]), 1); |
||
4067 | } |
||
4068 | } |
||
4069 | |||
4070 | while (true) { |
||
4071 | if ($this->binary_packet_buffer !== false) { |
||
4072 | $response = $this->binary_packet_buffer; |
||
4073 | $this->binary_packet_buffer = false; |
||
4074 | } else { |
||
4075 | $response = $this->get_binary_packet(true); |
||
4076 | if ($response === true && $this->is_timeout) { |
||
4077 | if ($client_channel == self::CHANNEL_EXEC && !$this->request_pty) { |
||
4078 | $this->close_channel($client_channel); |
||
4079 | } |
||
4080 | return true; |
||
4081 | } |
||
4082 | if ($response === false) { |
||
874 | daniel-mar | 4083 | $this->disconnect_helper(NET_SSH2_DISCONNECT_CONNECTION_LOST); |
827 | daniel-mar | 4084 | throw new ConnectionClosedException('Connection closed by server'); |
4085 | } |
||
4086 | } |
||
4087 | |||
4088 | if ($client_channel == -1 && $response === true) { |
||
4089 | return true; |
||
4090 | } |
||
4091 | list($type, $channel) = Strings::unpackSSH2('CN', $response); |
||
4092 | |||
4093 | // will not be setup yet on incoming channel open request |
||
4094 | if (isset($channel) && isset($this->channel_status[$channel]) && isset($this->window_size_server_to_client[$channel])) { |
||
4095 | $this->window_size_server_to_client[$channel] -= strlen($response); |
||
4096 | |||
4097 | // resize the window, if appropriate |
||
4098 | if ($this->window_size_server_to_client[$channel] < 0) { |
||
4099 | // PuTTY does something more analogous to the following: |
||
4100 | //if ($this->window_size_server_to_client[$channel] < 0x3FFFFFFF) { |
||
874 | daniel-mar | 4101 | $packet = pack('CNN', NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST, $this->server_channels[$channel], $this->window_resize); |
827 | daniel-mar | 4102 | $this->send_binary_packet($packet); |
4103 | $this->window_size_server_to_client[$channel] += $this->window_resize; |
||
4104 | } |
||
4105 | |||
4106 | switch ($type) { |
||
874 | daniel-mar | 4107 | case NET_SSH2_MSG_CHANNEL_EXTENDED_DATA: |
827 | daniel-mar | 4108 | /* |
4109 | if ($client_channel == self::CHANNEL_EXEC) { |
||
4110 | $this->send_channel_packet($client_channel, chr(0)); |
||
4111 | } |
||
4112 | */ |
||
4113 | // currently, there's only one possible value for $data_type_code: NET_SSH2_EXTENDED_DATA_STDERR |
||
4114 | list($data_type_code, $data) = Strings::unpackSSH2('Ns', $response); |
||
4115 | $this->stdErrorLog .= $data; |
||
4116 | if ($skip_extended || $this->quiet_mode) { |
||
4117 | continue 2; |
||
4118 | } |
||
874 | daniel-mar | 4119 | if ($client_channel == $channel && $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_DATA) { |
827 | daniel-mar | 4120 | return $data; |
4121 | } |
||
4122 | $this->channel_buffers[$channel][] = chr($type) . $data; |
||
4123 | |||
4124 | continue 2; |
||
874 | daniel-mar | 4125 | case NET_SSH2_MSG_CHANNEL_REQUEST: |
4126 | if ($this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_CLOSE) { |
||
827 | daniel-mar | 4127 | continue 2; |
4128 | } |
||
4129 | list($value) = Strings::unpackSSH2('s', $response); |
||
4130 | switch ($value) { |
||
4131 | case 'exit-signal': |
||
4132 | list( |
||
4133 | , // FALSE |
||
4134 | $signal_name, |
||
4135 | , // core dumped |
||
4136 | $error_message |
||
4137 | ) = Strings::unpackSSH2('bsbs', $response); |
||
4138 | |||
4139 | $this->errors[] = "SSH_MSG_CHANNEL_REQUEST (exit-signal): $signal_name"; |
||
4140 | if (strlen($error_message)) { |
||
4141 | $this->errors[count($this->errors) - 1] .= "\r\n$error_message"; |
||
4142 | } |
||
4143 | |||
874 | daniel-mar | 4144 | $this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_EOF, $this->server_channels[$client_channel])); |
4145 | $this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$channel])); |
||
827 | daniel-mar | 4146 | |
874 | daniel-mar | 4147 | $this->channel_status[$channel] = NET_SSH2_MSG_CHANNEL_EOF; |
827 | daniel-mar | 4148 | |
4149 | continue 3; |
||
4150 | case 'exit-status': |
||
4151 | list(, $this->exit_status) = Strings::unpackSSH2('CN', $response); |
||
4152 | |||
4153 | // "The client MAY ignore these messages." |
||
4154 | // -- http://tools.ietf.org/html/rfc4254#section-6.10 |
||
4155 | |||
4156 | continue 3; |
||
4157 | default: |
||
4158 | // "Some systems may not implement signals, in which case they SHOULD ignore this message." |
||
4159 | // -- http://tools.ietf.org/html/rfc4254#section-6.9 |
||
4160 | continue 3; |
||
4161 | } |
||
4162 | } |
||
4163 | |||
4164 | switch ($this->channel_status[$channel]) { |
||
874 | daniel-mar | 4165 | case NET_SSH2_MSG_CHANNEL_OPEN: |
827 | daniel-mar | 4166 | switch ($type) { |
874 | daniel-mar | 4167 | case NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION: |
827 | daniel-mar | 4168 | list( |
4169 | $this->server_channels[$channel], |
||
4170 | $window_size, |
||
4171 | $this->packet_size_client_to_server[$channel] |
||
4172 | ) = Strings::unpackSSH2('NNN', $response); |
||
4173 | |||
4174 | if ($window_size < 0) { |
||
4175 | $window_size &= 0x7FFFFFFF; |
||
4176 | $window_size += 0x80000000; |
||
4177 | } |
||
4178 | $this->window_size_client_to_server[$channel] = $window_size; |
||
4179 | $result = $client_channel == $channel ? true : $this->get_channel_packet($client_channel, $skip_extended); |
||
4180 | $this->on_channel_open(); |
||
4181 | return $result; |
||
874 | daniel-mar | 4182 | case NET_SSH2_MSG_CHANNEL_OPEN_FAILURE: |
4183 | $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION); |
||
827 | daniel-mar | 4184 | throw new \RuntimeException('Unable to open channel'); |
4185 | default: |
||
4186 | if ($client_channel == $channel) { |
||
874 | daniel-mar | 4187 | $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION); |
827 | daniel-mar | 4188 | throw new \RuntimeException('Unexpected response to open request'); |
4189 | } |
||
4190 | return $this->get_channel_packet($client_channel, $skip_extended); |
||
4191 | } |
||
4192 | break; |
||
874 | daniel-mar | 4193 | case NET_SSH2_MSG_CHANNEL_REQUEST: |
827 | daniel-mar | 4194 | switch ($type) { |
874 | daniel-mar | 4195 | case NET_SSH2_MSG_CHANNEL_SUCCESS: |
827 | daniel-mar | 4196 | return true; |
874 | daniel-mar | 4197 | case NET_SSH2_MSG_CHANNEL_FAILURE: |
827 | daniel-mar | 4198 | return false; |
874 | daniel-mar | 4199 | case NET_SSH2_MSG_CHANNEL_DATA: |
827 | daniel-mar | 4200 | list($data) = Strings::unpackSSH2('s', $response); |
4201 | $this->channel_buffers[$channel][] = chr($type) . $data; |
||
4202 | return $this->get_channel_packet($client_channel, $skip_extended); |
||
4203 | default: |
||
874 | daniel-mar | 4204 | $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION); |
827 | daniel-mar | 4205 | throw new \RuntimeException('Unable to fulfill channel request'); |
4206 | } |
||
874 | daniel-mar | 4207 | case NET_SSH2_MSG_CHANNEL_CLOSE: |
1117 | daniel-mar | 4208 | if ($client_channel == $channel && $type == NET_SSH2_MSG_CHANNEL_CLOSE) { |
4209 | return true; |
||
4210 | } |
||
4211 | return $this->get_channel_packet($client_channel, $skip_extended); |
||
827 | daniel-mar | 4212 | } |
4213 | } |
||
4214 | |||
874 | daniel-mar | 4215 | // ie. $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_DATA |
827 | daniel-mar | 4216 | |
4217 | switch ($type) { |
||
874 | daniel-mar | 4218 | case NET_SSH2_MSG_CHANNEL_DATA: |
827 | daniel-mar | 4219 | /* |
4220 | if ($channel == self::CHANNEL_EXEC) { |
||
4221 | // SCP requires null packets, such as this, be sent. further, in the case of the ssh.com SSH server |
||
4222 | // this actually seems to make things twice as fast. more to the point, the message right after |
||
4223 | // SSH_MSG_CHANNEL_DATA (usually SSH_MSG_IGNORE) won't block for as long as it would have otherwise. |
||
4224 | // in OpenSSH it slows things down but only by a couple thousandths of a second. |
||
4225 | $this->send_channel_packet($channel, chr(0)); |
||
4226 | } |
||
4227 | */ |
||
4228 | list($data) = Strings::unpackSSH2('s', $response); |
||
4229 | |||
4230 | if ($channel == self::CHANNEL_AGENT_FORWARD) { |
||
4231 | $agent_response = $this->agent->forwardData($data); |
||
4232 | if (!is_bool($agent_response)) { |
||
4233 | $this->send_channel_packet($channel, $agent_response); |
||
4234 | } |
||
4235 | break; |
||
4236 | } |
||
4237 | |||
4238 | if ($client_channel == $channel) { |
||
4239 | return $data; |
||
4240 | } |
||
4241 | $this->channel_buffers[$channel][] = chr($type) . $data; |
||
4242 | break; |
||
874 | daniel-mar | 4243 | case NET_SSH2_MSG_CHANNEL_CLOSE: |
827 | daniel-mar | 4244 | $this->curTimeout = 5; |
4245 | |||
1117 | daniel-mar | 4246 | $this->close_channel_bitmap($channel); |
4247 | |||
874 | daniel-mar | 4248 | if ($this->channel_status[$channel] != NET_SSH2_MSG_CHANNEL_EOF) { |
4249 | $this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$channel])); |
||
827 | daniel-mar | 4250 | } |
4251 | |||
874 | daniel-mar | 4252 | $this->channel_status[$channel] = NET_SSH2_MSG_CHANNEL_CLOSE; |
1284 | daniel-mar | 4253 | $this->channelCount--; |
4254 | |||
827 | daniel-mar | 4255 | if ($client_channel == $channel) { |
4256 | return true; |
||
4257 | } |
||
4258 | // fall-through |
||
874 | daniel-mar | 4259 | case NET_SSH2_MSG_CHANNEL_EOF: |
827 | daniel-mar | 4260 | break; |
4261 | default: |
||
874 | daniel-mar | 4262 | $this->disconnect_helper(NET_SSH2_DISCONNECT_BY_APPLICATION); |
827 | daniel-mar | 4263 | throw new \RuntimeException("Error reading channel data ($type)"); |
4264 | } |
||
4265 | } |
||
4266 | } |
||
4267 | |||
4268 | /** |
||
4269 | * Sends Binary Packets |
||
4270 | * |
||
4271 | * See '6. Binary Packet Protocol' of rfc4253 for more info. |
||
4272 | * |
||
4273 | * @param string $data |
||
4274 | * @param string $logged |
||
4275 | * @see self::_get_binary_packet() |
||
4276 | * @return void |
||
4277 | */ |
||
4278 | protected function send_binary_packet($data, $logged = null) |
||
4279 | { |
||
4280 | if (!is_resource($this->fsock) || feof($this->fsock)) { |
||
4281 | $this->bitmap = 0; |
||
4282 | throw new ConnectionClosedException('Connection closed prematurely'); |
||
4283 | } |
||
4284 | |||
4285 | if (!isset($logged)) { |
||
4286 | $logged = $data; |
||
4287 | } |
||
4288 | |||
4289 | switch ($this->compress) { |
||
4290 | case self::NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH: |
||
4291 | if (!$this->isAuthenticated()) { |
||
4292 | break; |
||
4293 | } |
||
4294 | // fall-through |
||
4295 | case self::NET_SSH2_COMPRESSION_ZLIB: |
||
4296 | if (!$this->regenerate_compression_context) { |
||
4297 | $header = ''; |
||
4298 | } else { |
||
4299 | $this->regenerate_compression_context = false; |
||
4300 | $this->compress_context = deflate_init(ZLIB_ENCODING_RAW, ['window' => 15]); |
||
4301 | $header = "\x78\x9C"; |
||
4302 | } |
||
4303 | if ($this->compress_context) { |
||
4304 | $data = $header . deflate_add($this->compress_context, $data, ZLIB_PARTIAL_FLUSH); |
||
4305 | } |
||
4306 | } |
||
4307 | |||
4308 | // 4 (packet length) + 1 (padding length) + 4 (minimal padding amount) == 9 |
||
4309 | $packet_length = strlen($data) + 9; |
||
4310 | if ($this->encrypt && $this->encrypt->usesNonce()) { |
||
4311 | $packet_length -= 4; |
||
4312 | } |
||
4313 | // round up to the nearest $this->encrypt_block_size |
||
4314 | $packet_length += (($this->encrypt_block_size - 1) * $packet_length) % $this->encrypt_block_size; |
||
4315 | // subtracting strlen($data) is obvious - subtracting 5 is necessary because of packet_length and padding_length |
||
4316 | $padding_length = $packet_length - strlen($data) - 5; |
||
4317 | switch (true) { |
||
4318 | case $this->encrypt && $this->encrypt->usesNonce(): |
||
4319 | case $this->hmac_create instanceof Hash && $this->hmac_create_etm: |
||
4320 | $padding_length += 4; |
||
4321 | $packet_length += 4; |
||
4322 | } |
||
4323 | |||
4324 | $padding = Random::string($padding_length); |
||
4325 | |||
4326 | // we subtract 4 from packet_length because the packet_length field isn't supposed to include itself |
||
4327 | $packet = pack('NCa*', $packet_length - 4, $padding_length, $data . $padding); |
||
4328 | |||
4329 | $hmac = ''; |
||
4330 | if ($this->hmac_create instanceof Hash && !$this->hmac_create_etm) { |
||
4331 | if (($this->hmac_create->getHash() & "\xFF\xFF\xFF\xFF") == 'umac') { |
||
4332 | $this->hmac_create->setNonce("\0\0\0\0" . pack('N', $this->send_seq_no)); |
||
4333 | $hmac = $this->hmac_create->hash($packet); |
||
4334 | } else { |
||
4335 | $hmac = $this->hmac_create->hash(pack('Na*', $this->send_seq_no, $packet)); |
||
4336 | } |
||
4337 | } |
||
4338 | |||
4339 | if ($this->encrypt) { |
||
4340 | switch ($this->encryptName) { |
||
4341 | case 'aes128-gcm@openssh.com': |
||
4342 | case 'aes256-gcm@openssh.com': |
||
4343 | $this->encrypt->setNonce( |
||
4344 | $this->encryptFixedPart . |
||
4345 | $this->encryptInvocationCounter |
||
4346 | ); |
||
4347 | Strings::increment_str($this->encryptInvocationCounter); |
||
4348 | $this->encrypt->setAAD($temp = ($packet & "\xFF\xFF\xFF\xFF")); |
||
4349 | $packet = $temp . $this->encrypt->encrypt(substr($packet, 4)); |
||
4350 | break; |
||
4351 | case 'chacha20-poly1305@openssh.com': |
||
4352 | // This should be impossible, but we are checking anyway to narrow the type for Psalm. |
||
4353 | if (!($this->encrypt instanceof ChaCha20)) { |
||
4354 | throw new \LogicException('$this->encrypt is not a ' . ChaCha20::class); |
||
4355 | } |
||
4356 | |||
4357 | $nonce = pack('N2', 0, $this->send_seq_no); |
||
4358 | |||
4359 | $this->encrypt->setNonce($nonce); |
||
4360 | $this->lengthEncrypt->setNonce($nonce); |
||
4361 | |||
4362 | $length = $this->lengthEncrypt->encrypt($packet & "\xFF\xFF\xFF\xFF"); |
||
4363 | |||
4364 | $this->encrypt->setCounter(0); |
||
4365 | // this is the same approach that's implemented in Salsa20::createPoly1305Key() |
||
4366 | // but we don't want to use the same AEAD construction that RFC8439 describes |
||
4367 | // for ChaCha20-Poly1305 so we won't rely on it (see Salsa20::poly1305()) |
||
4368 | $this->encrypt->setPoly1305Key( |
||
4369 | $this->encrypt->encrypt(str_repeat("\0", 32)) |
||
4370 | ); |
||
4371 | $this->encrypt->setAAD($length); |
||
4372 | $this->encrypt->setCounter(1); |
||
4373 | $packet = $length . $this->encrypt->encrypt(substr($packet, 4)); |
||
4374 | break; |
||
4375 | default: |
||
4376 | $packet = $this->hmac_create instanceof Hash && $this->hmac_create_etm ? |
||
4377 | ($packet & "\xFF\xFF\xFF\xFF") . $this->encrypt->encrypt(substr($packet, 4)) : |
||
4378 | $this->encrypt->encrypt($packet); |
||
4379 | } |
||
4380 | } |
||
4381 | |||
4382 | if ($this->hmac_create instanceof Hash && $this->hmac_create_etm) { |
||
4383 | if (($this->hmac_create->getHash() & "\xFF\xFF\xFF\xFF") == 'umac') { |
||
4384 | $this->hmac_create->setNonce("\0\0\0\0" . pack('N', $this->send_seq_no)); |
||
4385 | $hmac = $this->hmac_create->hash($packet); |
||
4386 | } else { |
||
4387 | $hmac = $this->hmac_create->hash(pack('Na*', $this->send_seq_no, $packet)); |
||
4388 | } |
||
4389 | } |
||
4390 | |||
4391 | $this->send_seq_no++; |
||
4392 | |||
4393 | $packet .= $this->encrypt && $this->encrypt->usesNonce() ? $this->encrypt->getTag() : $hmac; |
||
4394 | |||
4395 | $start = microtime(true); |
||
4396 | $sent = @fputs($this->fsock, $packet); |
||
4397 | $stop = microtime(true); |
||
4398 | |||
4399 | if (defined('NET_SSH2_LOGGING')) { |
||
4400 | $current = microtime(true); |
||
1117 | daniel-mar | 4401 | $message_number = isset(self::$message_numbers[ord($logged[0])]) ? self::$message_numbers[ord($logged[0])] : 'UNKNOWN (' . ord($logged[0]) . ')'; |
874 | daniel-mar | 4402 | $message_number = '-> ' . $message_number . |
4403 | ' (since last: ' . round($current - $this->last_packet, 4) . ', network: ' . round($stop - $start, 4) . 's)'; |
||
827 | daniel-mar | 4404 | $this->append_log($message_number, $logged); |
4405 | $this->last_packet = $current; |
||
4406 | } |
||
4407 | |||
4408 | if (strlen($packet) != $sent) { |
||
4409 | $this->bitmap = 0; |
||
1249 | daniel-mar | 4410 | $message = $sent === false ? |
4411 | 'Unable to write ' . strlen($packet) . ' bytes' : |
||
4412 | "Only $sent of " . strlen($packet) . " bytes were sent"; |
||
4413 | throw new \RuntimeException($message); |
||
827 | daniel-mar | 4414 | } |
4415 | } |
||
4416 | |||
4417 | /** |
||
4418 | * Logs data packets |
||
4419 | * |
||
4420 | * Makes sure that only the last 1MB worth of packets will be logged |
||
4421 | * |
||
4422 | * @param string $message_number |
||
4423 | * @param string $message |
||
4424 | */ |
||
4425 | private function append_log($message_number, $message) |
||
4426 | { |
||
1042 | daniel-mar | 4427 | $this->append_log_helper( |
4428 | NET_SSH2_LOGGING, |
||
4429 | $message_number, |
||
4430 | $message, |
||
4431 | $this->message_number_log, |
||
4432 | $this->message_log, |
||
4433 | $this->log_size, |
||
4434 | $this->realtime_log_file, |
||
4435 | $this->realtime_log_wrap, |
||
4436 | $this->realtime_log_size |
||
4437 | ); |
||
4438 | } |
||
4439 | |||
4440 | /** |
||
4441 | * Logs data packet helper |
||
4442 | * |
||
4443 | * @param int $constant |
||
4444 | * @param string $message_number |
||
4445 | * @param string $message |
||
4446 | * @param array &$message_number_log |
||
4447 | * @param array &$message_log |
||
4448 | * @param int &$log_size |
||
4449 | * @param resource &$realtime_log_file |
||
4450 | * @param bool &$realtime_log_wrap |
||
4451 | * @param int &$realtime_log_size |
||
4452 | */ |
||
4453 | protected function append_log_helper($constant, $message_number, $message, array &$message_number_log, array &$message_log, &$log_size, &$realtime_log_file, &$realtime_log_wrap, &$realtime_log_size) |
||
4454 | { |
||
827 | daniel-mar | 4455 | // remove the byte identifying the message type from all but the first two messages (ie. the identification strings) |
4456 | if (strlen($message_number) > 2) { |
||
4457 | Strings::shift($message); |
||
4458 | } |
||
4459 | |||
1042 | daniel-mar | 4460 | switch ($constant) { |
827 | daniel-mar | 4461 | // useful for benchmarks |
4462 | case self::LOG_SIMPLE: |
||
1042 | daniel-mar | 4463 | $message_number_log[] = $message_number; |
827 | daniel-mar | 4464 | break; |
1042 | daniel-mar | 4465 | case self::LOG_SIMPLE_REALTIME: |
4466 | echo $message_number; |
||
4467 | echo PHP_SAPI == 'cli' ? "\r\n" : '<br>'; |
||
4468 | @flush(); |
||
4469 | @ob_flush(); |
||
4470 | break; |
||
827 | daniel-mar | 4471 | // the most useful log for SSH2 |
4472 | case self::LOG_COMPLEX: |
||
1042 | daniel-mar | 4473 | $message_number_log[] = $message_number; |
4474 | $log_size += strlen($message); |
||
4475 | $message_log[] = $message; |
||
4476 | while ($log_size > self::LOG_MAX_SIZE) { |
||
4477 | $log_size -= strlen(array_shift($message_log)); |
||
4478 | array_shift($message_number_log); |
||
827 | daniel-mar | 4479 | } |
4480 | break; |
||
4481 | // dump the output out realtime; packets may be interspersed with non packets, |
||
4482 | // passwords won't be filtered out and select other packets may not be correctly |
||
4483 | // identified |
||
4484 | case self::LOG_REALTIME: |
||
4485 | switch (PHP_SAPI) { |
||
4486 | case 'cli': |
||
4487 | $start = $stop = "\r\n"; |
||
4488 | break; |
||
4489 | default: |
||
4490 | $start = '<pre>'; |
||
4491 | $stop = '</pre>'; |
||
4492 | } |
||
4493 | echo $start . $this->format_log([$message], [$message_number]) . $stop; |
||
4494 | @flush(); |
||
4495 | @ob_flush(); |
||
4496 | break; |
||
4497 | // basically the same thing as self::LOG_REALTIME with the caveat that NET_SSH2_LOG_REALTIME_FILENAME |
||
4498 | // needs to be defined and that the resultant log file will be capped out at self::LOG_MAX_SIZE. |
||
4499 | // the earliest part of the log file is denoted by the first <<< START >>> and is not going to necessarily |
||
4500 | // at the beginning of the file |
||
4501 | case self::LOG_REALTIME_FILE: |
||
1042 | daniel-mar | 4502 | if (!isset($realtime_log_file)) { |
827 | daniel-mar | 4503 | // PHP doesn't seem to like using constants in fopen() |
4504 | $filename = NET_SSH2_LOG_REALTIME_FILENAME; |
||
4505 | $fp = fopen($filename, 'w'); |
||
1042 | daniel-mar | 4506 | $realtime_log_file = $fp; |
827 | daniel-mar | 4507 | } |
1042 | daniel-mar | 4508 | if (!is_resource($realtime_log_file)) { |
827 | daniel-mar | 4509 | break; |
4510 | } |
||
4511 | $entry = $this->format_log([$message], [$message_number]); |
||
1042 | daniel-mar | 4512 | if ($realtime_log_wrap) { |
827 | daniel-mar | 4513 | $temp = "<<< START >>>\r\n"; |
4514 | $entry .= $temp; |
||
1042 | daniel-mar | 4515 | fseek($realtime_log_file, ftell($realtime_log_file) - strlen($temp)); |
827 | daniel-mar | 4516 | } |
1042 | daniel-mar | 4517 | $realtime_log_size += strlen($entry); |
4518 | if ($realtime_log_size > self::LOG_MAX_SIZE) { |
||
4519 | fseek($realtime_log_file, 0); |
||
4520 | $realtime_log_size = strlen($entry); |
||
4521 | $realtime_log_wrap = true; |
||
827 | daniel-mar | 4522 | } |
1042 | daniel-mar | 4523 | fputs($realtime_log_file, $entry); |
827 | daniel-mar | 4524 | } |
4525 | } |
||
4526 | |||
4527 | /** |
||
4528 | * Sends channel data |
||
4529 | * |
||
4530 | * Spans multiple SSH_MSG_CHANNEL_DATAs if appropriate |
||
4531 | * |
||
4532 | * @param int $client_channel |
||
4533 | * @param string $data |
||
4534 | * @return void |
||
4535 | */ |
||
4536 | protected function send_channel_packet($client_channel, $data) |
||
4537 | { |
||
4538 | while (strlen($data)) { |
||
4539 | if (!$this->window_size_client_to_server[$client_channel]) { |
||
4540 | $this->bitmap ^= self::MASK_WINDOW_ADJUST; |
||
4541 | // using an invalid channel will let the buffers be built up for the valid channels |
||
4542 | $this->get_channel_packet(-1); |
||
4543 | $this->bitmap ^= self::MASK_WINDOW_ADJUST; |
||
4544 | } |
||
4545 | |||
4546 | /* The maximum amount of data allowed is determined by the maximum |
||
4547 | packet size for the channel, and the current window size, whichever |
||
4548 | is smaller. |
||
4549 | -- http://tools.ietf.org/html/rfc4254#section-5.2 */ |
||
4550 | $max_size = min( |
||
4551 | $this->packet_size_client_to_server[$client_channel], |
||
4552 | $this->window_size_client_to_server[$client_channel] |
||
4553 | ); |
||
4554 | |||
4555 | $temp = Strings::shift($data, $max_size); |
||
4556 | $packet = Strings::packSSH2( |
||
4557 | 'CNs', |
||
874 | daniel-mar | 4558 | NET_SSH2_MSG_CHANNEL_DATA, |
827 | daniel-mar | 4559 | $this->server_channels[$client_channel], |
4560 | $temp |
||
4561 | ); |
||
4562 | $this->window_size_client_to_server[$client_channel] -= strlen($temp); |
||
4563 | $this->send_binary_packet($packet); |
||
4564 | } |
||
4565 | } |
||
4566 | |||
4567 | /** |
||
4568 | * Closes and flushes a channel |
||
4569 | * |
||
4570 | * \phpseclib3\Net\SSH2 doesn't properly close most channels. For exec() channels are normally closed by the server |
||
4571 | * and for SFTP channels are presumably closed when the client disconnects. This functions is intended |
||
4572 | * for SCP more than anything. |
||
4573 | * |
||
4574 | * @param int $client_channel |
||
4575 | * @param bool $want_reply |
||
4576 | * @return void |
||
4577 | */ |
||
4578 | private function close_channel($client_channel, $want_reply = false) |
||
4579 | { |
||
4580 | // see http://tools.ietf.org/html/rfc4254#section-5.3 |
||
4581 | |||
874 | daniel-mar | 4582 | $this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_EOF, $this->server_channels[$client_channel])); |
827 | daniel-mar | 4583 | |
4584 | if (!$want_reply) { |
||
874 | daniel-mar | 4585 | $this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$client_channel])); |
827 | daniel-mar | 4586 | } |
4587 | |||
874 | daniel-mar | 4588 | $this->channel_status[$client_channel] = NET_SSH2_MSG_CHANNEL_CLOSE; |
1284 | daniel-mar | 4589 | $this->channelCount--; |
827 | daniel-mar | 4590 | |
4591 | $this->curTimeout = 5; |
||
4592 | |||
4593 | while (!is_bool($this->get_channel_packet($client_channel))) { |
||
4594 | } |
||
4595 | |||
4596 | if ($want_reply) { |
||
874 | daniel-mar | 4597 | $this->send_binary_packet(pack('CN', NET_SSH2_MSG_CHANNEL_CLOSE, $this->server_channels[$client_channel])); |
827 | daniel-mar | 4598 | } |
4599 | |||
1117 | daniel-mar | 4600 | $this->close_channel_bitmap($client_channel); |
4601 | } |
||
4602 | |||
4603 | /** |
||
4604 | * Maintains execution state bitmap in response to channel closure |
||
4605 | * |
||
4606 | * @param int $client_channel The channel number to maintain closure status of |
||
4607 | * @return void |
||
4608 | */ |
||
4609 | private function close_channel_bitmap($client_channel) |
||
4610 | { |
||
4611 | switch ($client_channel) { |
||
4612 | case self::CHANNEL_SHELL: |
||
4613 | // Shell status has been maintained in the bitmap for backwards |
||
4614 | // compatibility sake, but can be removed going forward |
||
4615 | if ($this->bitmap & self::MASK_SHELL) { |
||
4616 | $this->bitmap &= ~self::MASK_SHELL; |
||
4617 | } |
||
4618 | break; |
||
827 | daniel-mar | 4619 | } |
4620 | } |
||
4621 | |||
4622 | /** |
||
4623 | * Disconnect |
||
4624 | * |
||
4625 | * @param int $reason |
||
4626 | * @return false |
||
4627 | */ |
||
4628 | protected function disconnect_helper($reason) |
||
4629 | { |
||
4630 | if ($this->bitmap & self::MASK_CONNECTED) { |
||
874 | daniel-mar | 4631 | $data = Strings::packSSH2('CNss', NET_SSH2_MSG_DISCONNECT, $reason, '', ''); |
827 | daniel-mar | 4632 | try { |
4633 | $this->send_binary_packet($data); |
||
4634 | } catch (\Exception $e) { |
||
4635 | } |
||
4636 | } |
||
4637 | |||
4638 | $this->bitmap = 0; |
||
4639 | if (is_resource($this->fsock) && get_resource_type($this->fsock) === 'stream') { |
||
4640 | fclose($this->fsock); |
||
4641 | } |
||
4642 | |||
4643 | return false; |
||
4644 | } |
||
4645 | |||
4646 | /** |
||
874 | daniel-mar | 4647 | * Define Array |
4648 | * |
||
4649 | * Takes any number of arrays whose indices are integers and whose values are strings and defines a bunch of |
||
4650 | * named constants from it, using the value as the name of the constant and the index as the value of the constant. |
||
4651 | * If any of the constants that would be defined already exists, none of the constants will be defined. |
||
4652 | * |
||
4653 | * @param mixed[] ...$args |
||
4654 | * @access protected |
||
4655 | */ |
||
1117 | daniel-mar | 4656 | protected static function define_array(...$args) |
874 | daniel-mar | 4657 | { |
4658 | foreach ($args as $arg) { |
||
4659 | foreach ($arg as $key => $value) { |
||
4660 | if (!defined($value)) { |
||
4661 | define($value, $key); |
||
4662 | } else { |
||
4663 | break 2; |
||
4664 | } |
||
4665 | } |
||
4666 | } |
||
4667 | } |
||
4668 | |||
4669 | /** |
||
827 | daniel-mar | 4670 | * Returns a log of the packets that have been sent and received. |
4671 | * |
||
4672 | * Returns a string if NET_SSH2_LOGGING == self::LOG_COMPLEX, an array if NET_SSH2_LOGGING == self::LOG_SIMPLE and false if !defined('NET_SSH2_LOGGING') |
||
4673 | * |
||
4674 | * @return array|false|string |
||
4675 | */ |
||
4676 | public function getLog() |
||
4677 | { |
||
4678 | if (!defined('NET_SSH2_LOGGING')) { |
||
4679 | return false; |
||
4680 | } |
||
4681 | |||
4682 | switch (NET_SSH2_LOGGING) { |
||
4683 | case self::LOG_SIMPLE: |
||
4684 | return $this->message_number_log; |
||
4685 | case self::LOG_COMPLEX: |
||
4686 | $log = $this->format_log($this->message_log, $this->message_number_log); |
||
4687 | return PHP_SAPI == 'cli' ? $log : '<pre>' . $log . '</pre>'; |
||
4688 | default: |
||
4689 | return false; |
||
4690 | } |
||
4691 | } |
||
4692 | |||
4693 | /** |
||
4694 | * Formats a log for printing |
||
4695 | * |
||
4696 | * @param array $message_log |
||
4697 | * @param array $message_number_log |
||
4698 | * @return string |
||
4699 | */ |
||
1042 | daniel-mar | 4700 | protected function format_log(array $message_log, array $message_number_log) |
827 | daniel-mar | 4701 | { |
4702 | $output = ''; |
||
4703 | for ($i = 0; $i < count($message_log); $i++) { |
||
4704 | $output .= $message_number_log[$i] . "\r\n"; |
||
4705 | $current_log = $message_log[$i]; |
||
4706 | $j = 0; |
||
4707 | do { |
||
4708 | if (strlen($current_log)) { |
||
4709 | $output .= str_pad(dechex($j), 7, '0', STR_PAD_LEFT) . '0 '; |
||
4710 | } |
||
4711 | $fragment = Strings::shift($current_log, $this->log_short_width); |
||
4712 | $hex = substr(preg_replace_callback('#.#s', function ($matches) { |
||
4713 | return $this->log_boundary . str_pad(dechex(ord($matches[0])), 2, '0', STR_PAD_LEFT); |
||
4714 | }, $fragment), strlen($this->log_boundary)); |
||
4715 | // replace non ASCII printable characters with dots |
||
4716 | // http://en.wikipedia.org/wiki/ASCII#ASCII_printable_characters |
||
4717 | // also replace < with a . since < messes up the output on web browsers |
||
4718 | $raw = preg_replace('#[^\x20-\x7E]|<#', '.', $fragment); |
||
4719 | $output .= str_pad($hex, $this->log_long_width - $this->log_short_width, ' ') . $raw . "\r\n"; |
||
4720 | $j++; |
||
4721 | } while (strlen($current_log)); |
||
4722 | $output .= "\r\n"; |
||
4723 | } |
||
4724 | |||
4725 | return $output; |
||
4726 | } |
||
4727 | |||
4728 | /** |
||
4729 | * Helper function for agent->on_channel_open() |
||
4730 | * |
||
4731 | * Used when channels are created to inform agent |
||
4732 | * of said channel opening. Must be called after |
||
4733 | * channel open confirmation received |
||
4734 | * |
||
4735 | */ |
||
4736 | private function on_channel_open() |
||
4737 | { |
||
4738 | if (isset($this->agent)) { |
||
4739 | $this->agent->registerChannelOpen($this); |
||
4740 | } |
||
4741 | } |
||
4742 | |||
4743 | /** |
||
4744 | * Returns the first value of the intersection of two arrays or false if |
||
4745 | * the intersection is empty. The order is defined by the first parameter. |
||
4746 | * |
||
4747 | * @param array $array1 |
||
4748 | * @param array $array2 |
||
4749 | * @return mixed False if intersection is empty, else intersected value. |
||
4750 | */ |
||
1042 | daniel-mar | 4751 | private static function array_intersect_first(array $array1, array $array2) |
827 | daniel-mar | 4752 | { |
4753 | foreach ($array1 as $value) { |
||
4754 | if (in_array($value, $array2)) { |
||
4755 | return $value; |
||
4756 | } |
||
4757 | } |
||
4758 | return false; |
||
4759 | } |
||
4760 | |||
4761 | /** |
||
1463 | daniel-mar | 4762 | * Returns all errors / debug messages on the SSH layer |
827 | daniel-mar | 4763 | * |
1463 | daniel-mar | 4764 | * If you are looking for messages from the SFTP layer, please see SFTP::getSFTPErrors() |
4765 | * |
||
827 | daniel-mar | 4766 | * @return string[] |
4767 | */ |
||
4768 | public function getErrors() |
||
4769 | { |
||
4770 | return $this->errors; |
||
4771 | } |
||
4772 | |||
4773 | /** |
||
1463 | daniel-mar | 4774 | * Returns the last error received on the SSH layer |
827 | daniel-mar | 4775 | * |
1463 | daniel-mar | 4776 | * If you are looking for messages from the SFTP layer, please see SFTP::getLastSFTPError() |
4777 | * |
||
827 | daniel-mar | 4778 | * @return string |
4779 | */ |
||
4780 | public function getLastError() |
||
4781 | { |
||
4782 | $count = count($this->errors); |
||
4783 | |||
4784 | if ($count > 0) { |
||
4785 | return $this->errors[$count - 1]; |
||
4786 | } |
||
4787 | } |
||
4788 | |||
4789 | /** |
||
4790 | * Return the server identification. |
||
4791 | * |
||
4792 | * @return string|false |
||
4793 | */ |
||
4794 | public function getServerIdentification() |
||
4795 | { |
||
4796 | $this->connect(); |
||
4797 | |||
4798 | return $this->server_identifier; |
||
4799 | } |
||
4800 | |||
4801 | /** |
||
4802 | * Returns a list of algorithms the server supports |
||
4803 | * |
||
4804 | * @return array |
||
4805 | */ |
||
4806 | public function getServerAlgorithms() |
||
4807 | { |
||
4808 | $this->connect(); |
||
4809 | |||
4810 | return [ |
||
4811 | 'kex' => $this->kex_algorithms, |
||
4812 | 'hostkey' => $this->server_host_key_algorithms, |
||
4813 | 'client_to_server' => [ |
||
4814 | 'crypt' => $this->encryption_algorithms_client_to_server, |
||
4815 | 'mac' => $this->mac_algorithms_client_to_server, |
||
4816 | 'comp' => $this->compression_algorithms_client_to_server, |
||
4817 | 'lang' => $this->languages_client_to_server |
||
4818 | ], |
||
4819 | 'server_to_client' => [ |
||
4820 | 'crypt' => $this->encryption_algorithms_server_to_client, |
||
4821 | 'mac' => $this->mac_algorithms_server_to_client, |
||
4822 | 'comp' => $this->compression_algorithms_server_to_client, |
||
4823 | 'lang' => $this->languages_server_to_client |
||
4824 | ] |
||
4825 | ]; |
||
4826 | } |
||
4827 | |||
4828 | /** |
||
4829 | * Returns a list of KEX algorithms that phpseclib supports |
||
4830 | * |
||
4831 | * @return array |
||
4832 | */ |
||
4833 | public static function getSupportedKEXAlgorithms() |
||
4834 | { |
||
4835 | $kex_algorithms = [ |
||
4836 | // Elliptic Curve Diffie-Hellman Key Agreement (ECDH) using |
||
4837 | // Curve25519. See doc/curve25519-sha256@libssh.org.txt in the |
||
4838 | // libssh repository for more information. |
||
4839 | 'curve25519-sha256', |
||
4840 | 'curve25519-sha256@libssh.org', |
||
4841 | |||
4842 | 'ecdh-sha2-nistp256', // RFC 5656 |
||
4843 | 'ecdh-sha2-nistp384', // RFC 5656 |
||
4844 | 'ecdh-sha2-nistp521', // RFC 5656 |
||
4845 | |||
4846 | 'diffie-hellman-group-exchange-sha256',// RFC 4419 |
||
4847 | 'diffie-hellman-group-exchange-sha1', // RFC 4419 |
||
4848 | |||
4849 | // Diffie-Hellman Key Agreement (DH) using integer modulo prime |
||
4850 | // groups. |
||
4851 | 'diffie-hellman-group14-sha256', |
||
4852 | 'diffie-hellman-group14-sha1', // REQUIRED |
||
4853 | 'diffie-hellman-group15-sha512', |
||
4854 | 'diffie-hellman-group16-sha512', |
||
4855 | 'diffie-hellman-group17-sha512', |
||
4856 | 'diffie-hellman-group18-sha512', |
||
4857 | |||
4858 | 'diffie-hellman-group1-sha1', // REQUIRED |
||
4859 | ]; |
||
4860 | |||
4861 | return $kex_algorithms; |
||
4862 | } |
||
4863 | |||
4864 | /** |
||
4865 | * Returns a list of host key algorithms that phpseclib supports |
||
4866 | * |
||
4867 | * @return array |
||
4868 | */ |
||
4869 | public static function getSupportedHostKeyAlgorithms() |
||
4870 | { |
||
4871 | return [ |
||
4872 | 'ssh-ed25519', // https://tools.ietf.org/html/draft-ietf-curdle-ssh-ed25519-02 |
||
4873 | 'ecdsa-sha2-nistp256', // RFC 5656 |
||
4874 | 'ecdsa-sha2-nistp384', // RFC 5656 |
||
4875 | 'ecdsa-sha2-nistp521', // RFC 5656 |
||
4876 | 'rsa-sha2-256', // RFC 8332 |
||
4877 | 'rsa-sha2-512', // RFC 8332 |
||
4878 | 'ssh-rsa', // RECOMMENDED sign Raw RSA Key |
||
4879 | 'ssh-dss' // REQUIRED sign Raw DSS Key |
||
4880 | ]; |
||
4881 | } |
||
4882 | |||
4883 | /** |
||
4884 | * Returns a list of symmetric key algorithms that phpseclib supports |
||
4885 | * |
||
4886 | * @return array |
||
4887 | */ |
||
4888 | public static function getSupportedEncryptionAlgorithms() |
||
4889 | { |
||
4890 | $algos = [ |
||
4891 | // from <https://tools.ietf.org/html/rfc5647>: |
||
4892 | 'aes128-gcm@openssh.com', |
||
4893 | 'aes256-gcm@openssh.com', |
||
4894 | |||
4895 | // from <http://tools.ietf.org/html/rfc4345#section-4>: |
||
4896 | 'arcfour256', |
||
4897 | 'arcfour128', |
||
4898 | |||
4899 | //'arcfour', // OPTIONAL the ARCFOUR stream cipher with a 128-bit key |
||
4900 | |||
4901 | // CTR modes from <http://tools.ietf.org/html/rfc4344#section-4>: |
||
4902 | 'aes128-ctr', // RECOMMENDED AES (Rijndael) in SDCTR mode, with 128-bit key |
||
4903 | 'aes192-ctr', // RECOMMENDED AES with 192-bit key |
||
4904 | 'aes256-ctr', // RECOMMENDED AES with 256-bit key |
||
4905 | |||
1042 | daniel-mar | 4906 | // from <https://github.com/openssh/openssh-portable/blob/001aa55/PROTOCOL.chacha20poly1305>: |
827 | daniel-mar | 4907 | // one of the big benefits of chacha20-poly1305 is speed. the problem is... |
4908 | // libsodium doesn't generate the poly1305 keys in the way ssh does and openssl's PHP bindings don't even |
||
4909 | // seem to support poly1305 currently. so even if libsodium or openssl are being used for the chacha20 |
||
4910 | // part, pure-PHP has to be used for the poly1305 part and that's gonna cause a big slow down. |
||
4911 | // speed-wise it winds up being faster to use AES (when openssl or mcrypt are available) and some HMAC |
||
4912 | // (which is always gonna be super fast to compute thanks to the hash extension, which |
||
4913 | // "is bundled and compiled into PHP by default") |
||
4914 | 'chacha20-poly1305@openssh.com', |
||
4915 | |||
4916 | 'twofish128-ctr', // OPTIONAL Twofish in SDCTR mode, with 128-bit key |
||
4917 | 'twofish192-ctr', // OPTIONAL Twofish with 192-bit key |
||
4918 | 'twofish256-ctr', // OPTIONAL Twofish with 256-bit key |
||
4919 | |||
4920 | 'aes128-cbc', // RECOMMENDED AES with a 128-bit key |
||
4921 | 'aes192-cbc', // OPTIONAL AES with a 192-bit key |
||
4922 | 'aes256-cbc', // OPTIONAL AES in CBC mode, with a 256-bit key |
||
4923 | |||
4924 | 'twofish128-cbc', // OPTIONAL Twofish with a 128-bit key |
||
4925 | 'twofish192-cbc', // OPTIONAL Twofish with a 192-bit key |
||
4926 | 'twofish256-cbc', |
||
4927 | 'twofish-cbc', // OPTIONAL alias for "twofish256-cbc" |
||
4928 | // (this is being retained for historical reasons) |
||
4929 | |||
4930 | 'blowfish-ctr', // OPTIONAL Blowfish in SDCTR mode |
||
4931 | |||
4932 | 'blowfish-cbc', // OPTIONAL Blowfish in CBC mode |
||
4933 | |||
4934 | '3des-ctr', // RECOMMENDED Three-key 3DES in SDCTR mode |
||
4935 | |||
4936 | '3des-cbc', // REQUIRED three-key 3DES in CBC mode |
||
4937 | |||
4938 | //'none' // OPTIONAL no encryption; NOT RECOMMENDED |
||
4939 | ]; |
||
4940 | |||
4941 | if (self::$crypto_engine) { |
||
4942 | $engines = [self::$crypto_engine]; |
||
4943 | } else { |
||
4944 | $engines = [ |
||
4945 | 'libsodium', |
||
4946 | 'OpenSSL (GCM)', |
||
4947 | 'OpenSSL', |
||
4948 | 'mcrypt', |
||
4949 | 'Eval', |
||
4950 | 'PHP' |
||
4951 | ]; |
||
4952 | } |
||
4953 | |||
4954 | $ciphers = []; |
||
4955 | |||
4956 | foreach ($engines as $engine) { |
||
4957 | foreach ($algos as $algo) { |
||
4958 | $obj = self::encryption_algorithm_to_crypt_instance($algo); |
||
4959 | if ($obj instanceof Rijndael) { |
||
4960 | $obj->setKeyLength(preg_replace('#[^\d]#', '', $algo)); |
||
4961 | } |
||
4962 | switch ($algo) { |
||
4963 | case 'chacha20-poly1305@openssh.com': |
||
4964 | case 'arcfour128': |
||
4965 | case 'arcfour256': |
||
4966 | if ($engine != 'Eval') { |
||
4967 | continue 2; |
||
4968 | } |
||
4969 | break; |
||
4970 | case 'aes128-gcm@openssh.com': |
||
4971 | case 'aes256-gcm@openssh.com': |
||
4972 | if ($engine == 'OpenSSL') { |
||
4973 | continue 2; |
||
4974 | } |
||
4975 | $obj->setNonce('dummydummydu'); |
||
4976 | } |
||
4977 | if ($obj->isValidEngine($engine)) { |
||
4978 | $algos = array_diff($algos, [$algo]); |
||
4979 | $ciphers[] = $algo; |
||
4980 | } |
||
4981 | } |
||
4982 | } |
||
4983 | |||
4984 | return $ciphers; |
||
4985 | } |
||
4986 | |||
4987 | /** |
||
4988 | * Returns a list of MAC algorithms that phpseclib supports |
||
4989 | * |
||
4990 | * @return array |
||
4991 | */ |
||
4992 | public static function getSupportedMACAlgorithms() |
||
4993 | { |
||
4994 | return [ |
||
4995 | 'hmac-sha2-256-etm@openssh.com', |
||
4996 | 'hmac-sha2-512-etm@openssh.com', |
||
4997 | 'umac-64-etm@openssh.com', |
||
4998 | 'umac-128-etm@openssh.com', |
||
4999 | 'hmac-sha1-etm@openssh.com', |
||
5000 | |||
5001 | // from <http://www.ietf.org/rfc/rfc6668.txt>: |
||
5002 | 'hmac-sha2-256',// RECOMMENDED HMAC-SHA256 (digest length = key length = 32) |
||
5003 | 'hmac-sha2-512',// OPTIONAL HMAC-SHA512 (digest length = key length = 64) |
||
5004 | |||
5005 | // from <https://tools.ietf.org/html/draft-miller-secsh-umac-01>: |
||
5006 | 'umac-64@openssh.com', |
||
5007 | 'umac-128@openssh.com', |
||
5008 | |||
5009 | 'hmac-sha1-96', // RECOMMENDED first 96 bits of HMAC-SHA1 (digest length = 12, key length = 20) |
||
5010 | 'hmac-sha1', // REQUIRED HMAC-SHA1 (digest length = key length = 20) |
||
5011 | 'hmac-md5-96', // OPTIONAL first 96 bits of HMAC-MD5 (digest length = 12, key length = 16) |
||
5012 | 'hmac-md5', // OPTIONAL HMAC-MD5 (digest length = key length = 16) |
||
5013 | //'none' // OPTIONAL no MAC; NOT RECOMMENDED |
||
5014 | ]; |
||
5015 | } |
||
5016 | |||
5017 | /** |
||
5018 | * Returns a list of compression algorithms that phpseclib supports |
||
5019 | * |
||
5020 | * @return array |
||
5021 | */ |
||
5022 | public static function getSupportedCompressionAlgorithms() |
||
5023 | { |
||
5024 | $algos = ['none']; // REQUIRED no compression |
||
5025 | if (function_exists('deflate_init')) { |
||
5026 | $algos[] = 'zlib@openssh.com'; // https://datatracker.ietf.org/doc/html/draft-miller-secsh-compression-delayed |
||
5027 | $algos[] = 'zlib'; |
||
5028 | } |
||
5029 | return $algos; |
||
5030 | } |
||
5031 | |||
5032 | /** |
||
5033 | * Return list of negotiated algorithms |
||
5034 | * |
||
5035 | * Uses the same format as https://www.php.net/ssh2-methods-negotiated |
||
5036 | * |
||
5037 | * @return array |
||
5038 | */ |
||
5039 | public function getAlgorithmsNegotiated() |
||
5040 | { |
||
5041 | $this->connect(); |
||
5042 | |||
5043 | $compression_map = [ |
||
5044 | self::NET_SSH2_COMPRESSION_NONE => 'none', |
||
5045 | self::NET_SSH2_COMPRESSION_ZLIB => 'zlib', |
||
5046 | self::NET_SSH2_COMPRESSION_ZLIB_AT_OPENSSH => 'zlib@openssh.com' |
||
5047 | ]; |
||
5048 | |||
5049 | return [ |
||
5050 | 'kex' => $this->kex_algorithm, |
||
5051 | 'hostkey' => $this->signature_format, |
||
5052 | 'client_to_server' => [ |
||
5053 | 'crypt' => $this->encryptName, |
||
5054 | 'mac' => $this->hmac_create_name, |
||
5055 | 'comp' => $compression_map[$this->compress], |
||
5056 | ], |
||
5057 | 'server_to_client' => [ |
||
5058 | 'crypt' => $this->decryptName, |
||
5059 | 'mac' => $this->hmac_check_name, |
||
5060 | 'comp' => $compression_map[$this->decompress], |
||
5061 | ] |
||
5062 | ]; |
||
5063 | } |
||
5064 | |||
5065 | /** |
||
1284 | daniel-mar | 5066 | * Force multiple channels (even if phpseclib has decided to disable them) |
5067 | */ |
||
5068 | public function forceMultipleChannels() |
||
5069 | { |
||
5070 | $this->errorOnMultipleChannels = false; |
||
5071 | } |
||
5072 | |||
5073 | /** |
||
827 | daniel-mar | 5074 | * Allows you to set the terminal |
5075 | * |
||
5076 | * @param string $term |
||
5077 | */ |
||
5078 | public function setTerminal($term) |
||
5079 | { |
||
5080 | $this->term = $term; |
||
5081 | } |
||
5082 | |||
5083 | /** |
||
5084 | * Accepts an associative array with up to four parameters as described at |
||
5085 | * <https://www.php.net/manual/en/function.ssh2-connect.php> |
||
5086 | * |
||
5087 | * @param array $methods |
||
5088 | */ |
||
5089 | public function setPreferredAlgorithms(array $methods) |
||
5090 | { |
||
5091 | $preferred = $methods; |
||
5092 | |||
5093 | if (isset($preferred['kex'])) { |
||
5094 | $preferred['kex'] = array_intersect( |
||
5095 | $preferred['kex'], |
||
5096 | static::getSupportedKEXAlgorithms() |
||
5097 | ); |
||
5098 | } |
||
5099 | |||
5100 | if (isset($preferred['hostkey'])) { |
||
5101 | $preferred['hostkey'] = array_intersect( |
||
5102 | $preferred['hostkey'], |
||
5103 | static::getSupportedHostKeyAlgorithms() |
||
5104 | ); |
||
5105 | } |
||
5106 | |||
5107 | $keys = ['client_to_server', 'server_to_client']; |
||
5108 | foreach ($keys as $key) { |
||
5109 | if (isset($preferred[$key])) { |
||
5110 | $a = &$preferred[$key]; |
||
5111 | if (isset($a['crypt'])) { |
||
5112 | $a['crypt'] = array_intersect( |
||
5113 | $a['crypt'], |
||
5114 | static::getSupportedEncryptionAlgorithms() |
||
5115 | ); |
||
5116 | } |
||
5117 | if (isset($a['comp'])) { |
||
5118 | $a['comp'] = array_intersect( |
||
5119 | $a['comp'], |
||
5120 | static::getSupportedCompressionAlgorithms() |
||
5121 | ); |
||
5122 | } |
||
5123 | if (isset($a['mac'])) { |
||
5124 | $a['mac'] = array_intersect( |
||
5125 | $a['mac'], |
||
5126 | static::getSupportedMACAlgorithms() |
||
5127 | ); |
||
5128 | } |
||
5129 | } |
||
5130 | } |
||
5131 | |||
5132 | $keys = [ |
||
5133 | 'kex', |
||
5134 | 'hostkey', |
||
5135 | 'client_to_server/crypt', |
||
5136 | 'client_to_server/comp', |
||
5137 | 'client_to_server/mac', |
||
5138 | 'server_to_client/crypt', |
||
5139 | 'server_to_client/comp', |
||
5140 | 'server_to_client/mac', |
||
5141 | ]; |
||
5142 | foreach ($keys as $key) { |
||
5143 | $p = $preferred; |
||
5144 | $m = $methods; |
||
5145 | |||
5146 | $subkeys = explode('/', $key); |
||
5147 | foreach ($subkeys as $subkey) { |
||
5148 | if (!isset($p[$subkey])) { |
||
5149 | continue 2; |
||
5150 | } |
||
5151 | $p = $p[$subkey]; |
||
5152 | $m = $m[$subkey]; |
||
5153 | } |
||
5154 | |||
5155 | if (count($p) != count($m)) { |
||
5156 | $diff = array_diff($m, $p); |
||
5157 | $msg = count($diff) == 1 ? |
||
5158 | ' is not a supported algorithm' : |
||
5159 | ' are not supported algorithms'; |
||
5160 | throw new UnsupportedAlgorithmException(implode(', ', $diff) . $msg); |
||
5161 | } |
||
5162 | } |
||
5163 | |||
5164 | $this->preferred = $preferred; |
||
5165 | } |
||
5166 | |||
5167 | /** |
||
5168 | * Returns the banner message. |
||
5169 | * |
||
5170 | * Quoting from the RFC, "in some jurisdictions, sending a warning message before |
||
5171 | * authentication may be relevant for getting legal protection." |
||
5172 | * |
||
5173 | * @return string |
||
5174 | */ |
||
5175 | public function getBannerMessage() |
||
5176 | { |
||
5177 | return $this->banner_message; |
||
5178 | } |
||
5179 | |||
5180 | /** |
||
5181 | * Returns the server public host key. |
||
5182 | * |
||
5183 | * Caching this the first time you connect to a server and checking the result on subsequent connections |
||
5184 | * is recommended. Returns false if the server signature is not signed correctly with the public host key. |
||
5185 | * |
||
5186 | * @return string|false |
||
5187 | * @throws \RuntimeException on badly formatted keys |
||
5188 | * @throws \phpseclib3\Exception\NoSupportedAlgorithmsException when the key isn't in a supported format |
||
5189 | */ |
||
5190 | public function getServerPublicHostKey() |
||
5191 | { |
||
5192 | if (!($this->bitmap & self::MASK_CONSTRUCTOR)) { |
||
5193 | $this->connect(); |
||
5194 | } |
||
5195 | |||
5196 | $signature = $this->signature; |
||
5197 | $server_public_host_key = base64_encode($this->server_public_host_key); |
||
5198 | |||
5199 | if ($this->signature_validated) { |
||
5200 | return $this->bitmap ? |
||
5201 | $this->signature_format . ' ' . $server_public_host_key : |
||
5202 | false; |
||
5203 | } |
||
5204 | |||
5205 | $this->signature_validated = true; |
||
5206 | |||
5207 | switch ($this->signature_format) { |
||
5208 | case 'ssh-ed25519': |
||
5209 | case 'ecdsa-sha2-nistp256': |
||
5210 | case 'ecdsa-sha2-nistp384': |
||
5211 | case 'ecdsa-sha2-nistp521': |
||
5212 | $key = EC::loadFormat('OpenSSH', $server_public_host_key) |
||
5213 | ->withSignatureFormat('SSH2'); |
||
5214 | switch ($this->signature_format) { |
||
5215 | case 'ssh-ed25519': |
||
5216 | $hash = 'sha512'; |
||
5217 | break; |
||
5218 | case 'ecdsa-sha2-nistp256': |
||
5219 | $hash = 'sha256'; |
||
5220 | break; |
||
5221 | case 'ecdsa-sha2-nistp384': |
||
5222 | $hash = 'sha384'; |
||
5223 | break; |
||
5224 | case 'ecdsa-sha2-nistp521': |
||
5225 | $hash = 'sha512'; |
||
5226 | } |
||
5227 | $key = $key->withHash($hash); |
||
5228 | break; |
||
5229 | case 'ssh-dss': |
||
5230 | $key = DSA::loadFormat('OpenSSH', $server_public_host_key) |
||
5231 | ->withSignatureFormat('SSH2') |
||
5232 | ->withHash('sha1'); |
||
5233 | break; |
||
5234 | case 'ssh-rsa': |
||
5235 | case 'rsa-sha2-256': |
||
5236 | case 'rsa-sha2-512': |
||
5237 | // could be ssh-rsa, rsa-sha2-256, rsa-sha2-512 |
||
5238 | // we don't check here because we already checked in key_exchange |
||
5239 | // some signatures have the type embedded within the message and some don't |
||
5240 | list(, $signature) = Strings::unpackSSH2('ss', $signature); |
||
5241 | |||
5242 | $key = RSA::loadFormat('OpenSSH', $server_public_host_key) |
||
5243 | ->withPadding(RSA::SIGNATURE_PKCS1); |
||
5244 | switch ($this->signature_format) { |
||
5245 | case 'rsa-sha2-512': |
||
5246 | $hash = 'sha512'; |
||
5247 | break; |
||
5248 | case 'rsa-sha2-256': |
||
5249 | $hash = 'sha256'; |
||
5250 | break; |
||
5251 | //case 'ssh-rsa': |
||
5252 | default: |
||
5253 | $hash = 'sha1'; |
||
5254 | } |
||
5255 | $key = $key->withHash($hash); |
||
5256 | break; |
||
5257 | default: |
||
874 | daniel-mar | 5258 | $this->disconnect_helper(NET_SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE); |
827 | daniel-mar | 5259 | throw new NoSupportedAlgorithmsException('Unsupported signature format'); |
5260 | } |
||
5261 | |||
5262 | if (!$key->verify($this->exchange_hash, $signature)) { |
||
874 | daniel-mar | 5263 | return $this->disconnect_helper(NET_SSH2_DISCONNECT_HOST_KEY_NOT_VERIFIABLE); |
827 | daniel-mar | 5264 | }; |
5265 | |||
5266 | return $this->signature_format . ' ' . $server_public_host_key; |
||
5267 | } |
||
5268 | |||
5269 | /** |
||
5270 | * Returns the exit status of an SSH command or false. |
||
5271 | * |
||
5272 | * @return false|int |
||
5273 | */ |
||
5274 | public function getExitStatus() |
||
5275 | { |
||
5276 | if (is_null($this->exit_status)) { |
||
5277 | return false; |
||
5278 | } |
||
5279 | return $this->exit_status; |
||
5280 | } |
||
5281 | |||
5282 | /** |
||
5283 | * Returns the number of columns for the terminal window size. |
||
5284 | * |
||
5285 | * @return int |
||
5286 | */ |
||
5287 | public function getWindowColumns() |
||
5288 | { |
||
5289 | return $this->windowColumns; |
||
5290 | } |
||
5291 | |||
5292 | /** |
||
5293 | * Returns the number of rows for the terminal window size. |
||
5294 | * |
||
5295 | * @return int |
||
5296 | */ |
||
5297 | public function getWindowRows() |
||
5298 | { |
||
5299 | return $this->windowRows; |
||
5300 | } |
||
5301 | |||
5302 | /** |
||
5303 | * Sets the number of columns for the terminal window size. |
||
5304 | * |
||
5305 | * @param int $value |
||
5306 | */ |
||
5307 | public function setWindowColumns($value) |
||
5308 | { |
||
5309 | $this->windowColumns = $value; |
||
5310 | } |
||
5311 | |||
5312 | /** |
||
5313 | * Sets the number of rows for the terminal window size. |
||
5314 | * |
||
5315 | * @param int $value |
||
5316 | */ |
||
5317 | public function setWindowRows($value) |
||
5318 | { |
||
5319 | $this->windowRows = $value; |
||
5320 | } |
||
5321 | |||
5322 | /** |
||
5323 | * Sets the number of columns and rows for the terminal window size. |
||
5324 | * |
||
5325 | * @param int $columns |
||
5326 | * @param int $rows |
||
5327 | */ |
||
5328 | public function setWindowSize($columns = 80, $rows = 24) |
||
5329 | { |
||
5330 | $this->windowColumns = $columns; |
||
5331 | $this->windowRows = $rows; |
||
5332 | } |
||
5333 | |||
5334 | /** |
||
5335 | * To String Magic Method |
||
5336 | * |
||
5337 | * @return string |
||
5338 | */ |
||
5339 | #[\ReturnTypeWillChange] |
||
5340 | public function __toString() |
||
5341 | { |
||
5342 | return $this->getResourceId(); |
||
5343 | } |
||
5344 | |||
5345 | /** |
||
5346 | * Get Resource ID |
||
5347 | * |
||
5348 | * We use {} because that symbols should not be in URL according to |
||
5349 | * {@link http://tools.ietf.org/html/rfc3986#section-2 RFC}. |
||
5350 | * It will safe us from any conflicts, because otherwise regexp will |
||
5351 | * match all alphanumeric domains. |
||
5352 | * |
||
5353 | * @return string |
||
5354 | */ |
||
5355 | public function getResourceId() |
||
5356 | { |
||
5357 | return '{' . spl_object_hash($this) . '}'; |
||
5358 | } |
||
5359 | |||
5360 | /** |
||
5361 | * Return existing connection |
||
5362 | * |
||
5363 | * @param string $id |
||
5364 | * |
||
5365 | * @return bool|SSH2 will return false if no such connection |
||
5366 | */ |
||
5367 | public static function getConnectionByResourceId($id) |
||
5368 | { |
||
5369 | if (isset(self::$connections[$id])) { |
||
5370 | return self::$connections[$id] instanceof \WeakReference ? self::$connections[$id]->get() : self::$connections[$id]; |
||
5371 | } |
||
5372 | return false; |
||
5373 | } |
||
5374 | |||
5375 | /** |
||
5376 | * Return all excising connections |
||
5377 | * |
||
5378 | * @return array<string, SSH2> |
||
5379 | */ |
||
5380 | public static function getConnections() |
||
5381 | { |
||
5382 | if (!class_exists('WeakReference')) { |
||
5383 | /** @var array<string, SSH2> */ |
||
5384 | return self::$connections; |
||
5385 | } |
||
5386 | $temp = []; |
||
5387 | foreach (self::$connections as $key => $ref) { |
||
5388 | $temp[$key] = $ref->get(); |
||
5389 | } |
||
5390 | return $temp; |
||
5391 | } |
||
5392 | |||
874 | daniel-mar | 5393 | /* |
827 | daniel-mar | 5394 | * Update packet types in log history |
5395 | * |
||
5396 | * @param string $old |
||
5397 | * @param string $new |
||
5398 | */ |
||
5399 | private function updateLogHistory($old, $new) |
||
5400 | { |
||
5401 | if (defined('NET_SSH2_LOGGING') && NET_SSH2_LOGGING == self::LOG_COMPLEX) { |
||
5402 | $this->message_number_log[count($this->message_number_log) - 1] = str_replace( |
||
5403 | $old, |
||
5404 | $new, |
||
5405 | $this->message_number_log[count($this->message_number_log) - 1] |
||
5406 | ); |
||
5407 | } |
||
5408 | } |
||
5409 | |||
5410 | /** |
||
5411 | * Return the list of authentication methods that may productively continue authentication. |
||
5412 | * |
||
5413 | * @see https://tools.ietf.org/html/rfc4252#section-5.1 |
||
5414 | * @return array|null |
||
5415 | */ |
||
5416 | public function getAuthMethodsToContinue() |
||
5417 | { |
||
5418 | return $this->auth_methods_to_continue; |
||
5419 | } |
||
5420 | |||
5421 | /** |
||
5422 | * Enables "smart" multi-factor authentication (MFA) |
||
5423 | */ |
||
5424 | public function enableSmartMFA() |
||
5425 | { |
||
5426 | $this->smartMFA = true; |
||
5427 | } |
||
5428 | |||
5429 | /** |
||
5430 | * Disables "smart" multi-factor authentication (MFA) |
||
5431 | */ |
||
5432 | public function disableSmartMFA() |
||
5433 | { |
||
5434 | $this->smartMFA = false; |
||
5435 | } |
||
5436 | } |