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