/code/classes/Daemon/SSHd/Client.class.php

https://github.com/blekkzor/pinetd2 · PHP · 903 lines · 745 code · 115 blank · 43 comment · 116 complexity · b869cb1be929a1cf23ecf7a1a703cc42 MD5 · raw file

  1. <?php
  2. namespace Daemon\SSHd;
  3. use pinetd\Logger;
  4. use pinetd\SUID;
  5. use pinetd\Crypto;
  6. class Client extends \pinetd\TCP\Client {
  7. private $state;
  8. private $agent;
  9. private $clearbuf = '';
  10. private $capa;
  11. private $payloads = array();
  12. private $skey;
  13. private $session_id = NULL;
  14. private $seq_recv = -1;
  15. private $seq_send = -1;
  16. private $login = NULL;
  17. private $channels = array();
  18. private $kex_sent = false;
  19. private $cipher = array(); // encoding type
  20. const SSH_MSG_DISCONNECT = 1;
  21. const SSH_MSG_IGNORE = 2;
  22. const SSH_MSG_UNIMPLEMENTED = 3;
  23. const SSH_MSG_DEBUG = 4;
  24. const SSH_MSG_SERVICE_REQUEST = 5;
  25. const SSH_MSG_SERVICE_ACCEPT = 6;
  26. const SSH_MSG_KEXINIT = 20;
  27. const SSH_MSG_NEWKEYS = 21;
  28. const SSH_MSG_KEXDH_INIT = 30;
  29. const SSH_MSG_KEXDH_REPLY = 31;
  30. const SSH_MSG_USERAUTH_REQUEST = 50;
  31. const SSH_MSG_USERAUTH_FAILURE = 51;
  32. const SSH_MSG_USERAUTH_SUCCESS = 52;
  33. const SSH_MSG_USERAUTH_BANNER = 53;
  34. const SSH_MSG_USERAUTH_PASSWD_CHANGEREQ = 60;
  35. const SSH_MSG_USERAUTH_PK_OK = 60;
  36. const SSH_MSG_GLOBAL_REQUEST = 80;
  37. const SSH_MSG_REQUEST_SUCCESS = 81;
  38. const SSH_MSG_REQUEST_FAILURE = 82;
  39. const SSH_MSG_CHANNEL_OPEN = 90;
  40. const SSH_MSG_CHANNEL_OPEN_CONFIRMATION = 91;
  41. const SSH_MSG_CHANNEL_OPEN_FAILURE = 92;
  42. const SSH_MSG_CHANNEL_WINDOW_ADJUST = 93;
  43. const SSH_MSG_CHANNEL_DATA = 94;
  44. const SSH_MSG_CHANNEL_EXTENDED_DATA = 95;
  45. const SSH_MSG_CHANNEL_EOF = 96;
  46. const SSH_MSG_CHANNEL_CLOSE = 97;
  47. const SSH_MSG_CHANNEL_REQUEST = 98;
  48. const SSH_MSG_CHANNEL_SUCCESS = 99;
  49. const SSH_MSG_CHANNEL_FAILURE = 100;
  50. const SSH_DISCONNECT_HOST_NOT_ALLOWED_TO_CONNECT = 1;
  51. const SSH_DISCONNECT_PROTOCOL_ERROR = 2;
  52. const SSH_DISCONNECT_KEY_EXCHANGE_FAILED = 3;
  53. const SSH_DISCONNECT_RESERVED = 4;
  54. const SSH_DISCONNECT_MAC_ERROR = 5;
  55. const SSH_DISCONNECT_COMPRESSION_ERROR = 6;
  56. const SSH_DISCONNECT_SERVICE_NOT_AVAILABLE = 7;
  57. const SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED = 8;
  58. const SSH_DISCONNECT_HOST_KEY_NOT_VERIFIABLE = 9;
  59. const SSH_DISCONNECT_CONNECTION_LOST = 10;
  60. const SSH_DISCONNECT_BY_APPLICATION = 11;
  61. const SSH_DISCONNECT_TOO_MANY_CONNECTIONS = 12;
  62. const SSH_DISCONNECT_AUTH_CANCELLED_BY_USER = 13;
  63. const SSH_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE = 14;
  64. const SSH_DISCONNECT_ILLEGAL_USER_NAME = 15;
  65. const SSH_OPEN_ADMINISTRATIVELY_PROHIBITED = 1;
  66. const SSH_OPEN_CONNECT_FAILED = 2;
  67. const SSH_OPEN_UNKNOWN_CHANNEL_TYPE = 3;
  68. const SSH_OPEN_RESOURCE_SHORTAGE = 4;
  69. function welcomeUser() {
  70. $this->state = 'new';
  71. $this->cipher['send'] = array('name' => 'NULL', 'block_size' => 1, 'hmac' => 'none');
  72. $this->cipher['recv'] = array('name' => 'NULL', 'block_size' => 1, 'hmac' => 'none');
  73. $this->setMsgEnd('');
  74. $this->sendMsg("Please wait while resolving your hostname...\r\n");
  75. return true;
  76. }
  77. public function sendBanner() {
  78. $this->sendMsg("SSH-2.0-pinetd PHP/".phpversion()."\r\n");
  79. $this->payloads['V_S'] = "SSH-2.0-pinetd PHP/".phpversion();
  80. }
  81. public function getLogin() {
  82. return $this->login;
  83. }
  84. protected function handlePkt($pkt) {
  85. $id = ord($pkt[0]);
  86. $pkt = substr($pkt, 1);
  87. if (($id > 69) && (is_null($this->login))) {
  88. $this->disconnect(self::SSH_DISCONNECT_PROTOCOL_ERROR, 'need to login first');
  89. return;
  90. }
  91. switch($id) {
  92. case self::SSH_MSG_DISCONNECT: $this->close(); break;
  93. case self::SSH_MSG_IGNORE:
  94. case self::SSH_MSG_UNIMPLEMENTED:
  95. case self::SSH_MSG_DEBUG:
  96. // fake/useless traffic to keep cnx alive and/or confuse listeners
  97. return;
  98. case self::SSH_MSG_SERVICE_REQUEST:
  99. $text = $this->parseStr($pkt);
  100. $res = $this->ssh_serviceInit($text);
  101. if ($res) {
  102. $pkt = chr(self::SSH_MSG_SERVICE_ACCEPT).$this->str($text);
  103. $this->sendPacket($pkt);
  104. } else {
  105. $this->disconnect(self::SSH_DISCONNECT_SERVICE_NOT_AVAILABLE, 'requested service '.$text.' not available');
  106. }
  107. break;
  108. case self::SSH_MSG_KEXINIT:
  109. $this->payloads['I_C'] = chr($id).$pkt;
  110. $data = array();
  111. $data['cookie'] = bin2hex(substr($pkt, 0, 16));
  112. $pkt = substr($pkt, 16); // remove packet code (1 byte) & cookie (16 bytes)
  113. $list_list = array('kex_algorithms','server_host_key_algorithms','encryption_algorithms_client_to_server','encryption_algorithms_server_to_client','mac_algorithms_client_to_server','mac_algorithms_server_to_client','compression_algorithms_client_to_server','compression_algorithms_server_to_client','languages_client_to_server','languages_server_to_client');
  114. foreach($list_list as $list) {
  115. $res = $this->parseNameList($pkt);
  116. if ($res === false) {
  117. $this->disconnect(self::SSH_DISCONNECT_PROTOCOL_ERROR, 'Failed to parse KEX packet');
  118. return;
  119. }
  120. $data[$list] = $res;
  121. }
  122. $data['first_kex_packet_follows'] = ord($pkt[0]);
  123. $data['reserved'] = bin2hex(substr($pkt, 1));
  124. $this->capa = $data;
  125. $this->ssh_determineEncryption();
  126. if (!$this->kex_sent) $this->ssh_sendAlgorithmNegotiationPacket();
  127. $this->kex_sent = false;
  128. break;
  129. case self::SSH_MSG_NEWKEYS:
  130. // initialize encryption [recv/send]
  131. if (!isset($this->capa['K'])) {
  132. Logger::log(Logger::LOG_WARN, "Client trying to enable encryption without key exchange");
  133. $this->disconnect(self::SSH_DISCONNECT_PROTOCOL_ERROR, 'you cannot enable encryption without kex first');
  134. break;
  135. }
  136. $this->sendPacket(chr(self::SSH_MSG_NEWKEYS)); // send before we enable send encryption
  137. $this->ssh_initEncryption();
  138. break;
  139. case self::SSH_MSG_KEXDH_INIT: $this->ssh_KeyExchangeDHInit($pkt); break;
  140. /***************************** USER AUTH ****************************/
  141. case self::SSH_MSG_USERAUTH_REQUEST:
  142. $user = $this->parseStr($pkt);
  143. $service = $this->parseStr($pkt);
  144. $method = $this->parseStr($pkt);
  145. $this->ssh_handleUserAuthRequest($user, $service, $method, $pkt);
  146. break;
  147. /**************************** CHANNELS *****************************/
  148. case self::SSH_MSG_CHANNEL_OPEN:
  149. $type = $this->parseStr($pkt);
  150. list(,$channel, $window, $packet_max) = unpack('N3', $pkt);
  151. $pkt = substr($pkt, 12);
  152. $this->ssh_openChannel($type, $channel, $window, $packet_max, $pkt);
  153. break;
  154. case self::SSH_MSG_CHANNEL_WINDOW_ADJUST:
  155. list(, $channel, $bytes) = unpack('N2', $pkt);
  156. if (!isset($this->channels[$channel])) break;
  157. $this->channels[$channel]->windowAdjust($bytes);
  158. break;
  159. case self::SSH_MSG_CHANNEL_DATA:
  160. list(,$channel) = unpack('N', substr($pkt, 0, 4));
  161. $pkt = substr($pkt, 4);
  162. $data = $this->parseStr($pkt);
  163. if (!isset($this->channels[$channel])) break;
  164. $this->channels[$channel]->recv($data);
  165. break;
  166. case self::SSH_MSG_CHANNEL_EOF:
  167. list(,$channel) = unpack('N', substr($pkt, 0, 4));
  168. if (!isset($this->channels[$channel])) break;
  169. $this->channels[$channel]->gotEof();
  170. break;
  171. case self::SSH_MSG_CHANNEL_CLOSE:
  172. list(,$channel) = unpack('N', substr($pkt, 0, 4));
  173. if (!isset($this->channels[$channel])) break;
  174. $this->channels[$channel]->close();
  175. unset($this->channels[$channel]);
  176. break;
  177. case self::SSH_MSG_CHANNEL_REQUEST:
  178. list(,$channel) = unpack('N', substr($pkt, 0, 4));
  179. $pkt = substr($pkt, 4);
  180. $request = $this->parseStr($pkt);
  181. $want_reply = (bool)ord($pkt[0]);
  182. $pkt = substr($pkt, 1);
  183. if (!isset($this->channels[$channel])) {
  184. if ($want_reply) {
  185. $this->sendPacket(pack('CN', self::SSH_MSG_CHANNEL_FAILURE, $channel));
  186. }
  187. break;
  188. }
  189. $res = $this->channels[$channel]->request($request, $pkt);
  190. if ($want_reply)
  191. $this->sendPacket(pack('CN', $res ? self::SSH_MSG_CHANNEL_SUCCESS : self::SSH_MSG_CHANNEL_FAILURE, $channel));
  192. break;
  193. default:
  194. echo "Unknown packet [$id]: ".bin2hex($pkt)."\n";
  195. $pkt = pack('CN', self::SSH_MSG_UNIMPLEMENTED, $this->seq_recv);
  196. $this->sendPacket($pkt);
  197. }
  198. }
  199. protected function ssh_openChannelError($channel, $errno, $msg) {
  200. $pkt = pack('CNN', self::SSH_MSG_CHANNEL_OPEN_FAILURE, $channel, $errno);
  201. $pkt .= $this->str($msg);
  202. $pkt .= $this->str('');
  203. $this->sendPacket($pkt);
  204. }
  205. public function channelEof($channel) {
  206. if (!isset($this->channels[$channel])) return false;
  207. $pkt = pack('CN', self::SSH_MSG_CHANNEL_EOF, $channel);
  208. $this->sendPacket($pkt);
  209. }
  210. public function channelClose($channel) {
  211. if (!isset($this->channels[$channel])) return false;
  212. $this->channels[$channel]->closed();
  213. $pkt = pack('CN', self::SSH_MSG_CHANNEL_CLOSE, $channel);
  214. $this->sendPacket($pkt);
  215. }
  216. public function channelWrite($channel, $str) {
  217. if (!isset($this->channels[$channel])) return false;
  218. $pkt = pack('CN', self::SSH_MSG_CHANNEL_DATA, $channel);
  219. $pkt.= $this->str($str);
  220. $this->sendPacket($pkt);
  221. }
  222. public function channelWindow($channel, $bytes) {
  223. if (!isset($this->channels[$channel])) return false;
  224. $pkt = pack('CNN', self::SSH_MSG_CHANNEL_WINDOW_ADJUST, $channel, $bytes);
  225. $this->sendPacket($pkt);
  226. }
  227. public function channelChangeObject($channel, $obj) {
  228. if (!isset($this->channels[$channel])) return false;
  229. $this->channels[$channel] = $obj;
  230. return true;
  231. }
  232. protected function ssh_openChannel($type, $channel, $window, $packet_max, $pkt) {
  233. if (isset($this->channels[$channel])) {
  234. $this->disconnect(self::SSH_DISCONNECT_PROTOCOL_ERROR, 'Protocol error: channel is already in use');
  235. return;
  236. }
  237. if ($type != 'session')
  238. return $this->ssh_openChannelError($channel, self::SSH_OPEN_UNKNOWN_CHANNEL_TYPE, 'unknown channel type requested');
  239. $class = relativeclass($this, 'Session');
  240. $class = new $class($this, $channel, $window, $packet_max, $pkt);
  241. $this->channels[$channel] = $class;
  242. $pkt = pack('CNNNN', self::SSH_MSG_CHANNEL_OPEN_CONFIRMATION, $channel, $channel, $class->remoteWindow(), $class->maxPacket());
  243. $pkt.= $class->specificConfirmationData();
  244. $this->sendPacket($pkt);
  245. }
  246. protected function login($login, $password, $service) {
  247. $info = $this->IPC->checkAccess($login, $password, $this->peer, $service);
  248. if (!$info) return false;
  249. return $this->doLogin($info);
  250. }
  251. protected function doLogin($login) {
  252. $SUID = $this->IPC->canSUID();
  253. if ($SUID) {
  254. $user = $login['suid_user'] ?: $SUID['User'];
  255. $group = $login['suid_group'] ?: $SUID['Group'];
  256. $SUID = new SUID($user, $group);
  257. }
  258. // Cannot chroot yet, need to find how we'll handle the loading of the next classes
  259. // if (($this->IPC->canChroot()) && ($login['root'] != '/')) {
  260. // if (chroot($login['root'])) {
  261. // $login['root'] = '/';
  262. // }
  263. // }
  264. if ($SUID) {
  265. if (!$SUID->setIt()) {
  266. $this->disconnect(self::SSH_DISCONNECT_BY_APPLICATION, 'failed to suid to the correct user');
  267. $this->IPC->killSelf($this->fd);
  268. return false;
  269. }
  270. }
  271. $this->login = $login;
  272. return true;
  273. }
  274. protected function loginPK($login, $service, $pk_algo, $pk_key, $pk_fingerprint, $pk_signature) {
  275. $info = $this->IPC->getPublicKeyAccess($login, $pk_algo.':'.$pk_fingerprint, $this->peer, $service);
  276. if (!$info) return false;
  277. if ($info['type'] != $pk_algo) return false; // ?!
  278. if (base64_decode($info['key']) != $pk_key) return false; // not the right public key (md5 collision?)
  279. if (is_null($pk_signature)) return true;
  280. // make packet for sign
  281. $signed = $this->str($this->session_id).chr(self::SSH_MSG_USERAUTH_REQUEST).$this->str($login).$this->str($service).$this->str('publickey')."\x01".$this->str($info['type']).$this->str(base64_decode($info['key']));
  282. switch($pk_algo) {
  283. case 'ssh-rsa':
  284. // RSA signature RFC3447
  285. // parse public key
  286. $tmp = base64_decode($info['key']);
  287. if ($this->parseStr($tmp) != 'ssh-rsa') return false;
  288. $e_bin = $this->parseStr($tmp);
  289. $n_bin = $this->parseStr($tmp);
  290. // read signature
  291. $tmp = $pk_signature;
  292. if ($this->parseStr($tmp) != 'ssh-rsa') return false;
  293. $s_bin = $this->parseStr($tmp);
  294. // convert to gmp
  295. $e = gmp_init(bin2hex($e_bin), 16);
  296. $n = gmp_init(bin2hex($n_bin), 16);
  297. $s = gmp_init(bin2hex($s_bin), 16); // signature
  298. // check
  299. if (gmp_cmp($s, gmp_sub($n, 1)) > 0) return false;
  300. // decode signature
  301. $m = gmp_powm($s, $e, $n);
  302. $m_bin = "\0".$this->gmp_binval($m); // starts with 0x00 0x01, but gmp drops the first 0x00
  303. $emLen = strlen($m_bin);
  304. // EMSA-PKCS1-v1_5 encoding of our own hash
  305. $t = pack('H*', '3021300906052b0e03021a05000414'); // RFC 3447 page 43 - means "SHA-1" in DER encoding
  306. $t.= sha1($signed, true);
  307. $ps = str_repeat("\xff", strlen($m_bin)-strlen($t)-3); // emLen - tLen - 3
  308. $em = "\x00\x01".$ps."\x00".$t; // EM = 0x00 || 0x01 || PS || 0x00 || T
  309. // check signature
  310. if ($m_bin == $em) break;
  311. return false;
  312. case 'ssh-dss':
  313. // DSA signature check as of csrc.nist.gov/publications/fips/archive/fips186-2/fips186-2.pdf
  314. // parse public key
  315. $tmp = base64_decode($info['key']);
  316. if ($this->parseStr($tmp) != 'ssh-dss') return false;
  317. $p_bin = $this->parseStr($tmp);
  318. $q_bin = $this->parseStr($tmp);
  319. $g_bin = $this->parseStr($tmp);
  320. $y_bin = $this->parseStr($tmp);
  321. // parse signature (r_s)
  322. $tmp = $pk_signature;
  323. if ($this->parseStr($tmp) != 'ssh-dss') return false;
  324. $r_s = $this->parseStr($tmp);
  325. if (strlen($r_s) != 40) return false; // 160bits r + 160bits s
  326. $r_bin = substr($r_s, 0, 20);
  327. $s_bin = substr($r_s, 20, 20);
  328. // init gmp
  329. $p = gmp_init(bin2hex($p_bin), 16);
  330. $q = gmp_init(bin2hex($q_bin), 16);
  331. $g = gmp_init(bin2hex($g_bin), 16);
  332. $y = gmp_init(bin2hex($y_bin), 16);
  333. $r = gmp_init(bin2hex($r_bin), 16);
  334. $s = gmp_init(bin2hex($s_bin), 16);
  335. $M = gmp_init(sha1($signed), 16);
  336. if (gmp_cmp($r, $q) > 0) return false;
  337. if (gmp_cmp($s, $q) > 0) return false;
  338. // computation
  339. $w = gmp_invert($s, $q);
  340. $u1 = gmp_mod(gmp_mul($M, $w), $q);
  341. $u2 = gmp_mod(gmp_mul($r, $w), $q);
  342. $v = gmp_mod(gmp_mod(gmp_mul(gmp_powm($g, $u1, $p), gmp_powm($y, $u2, $p)), $p), $q);
  343. if (gmp_cmp($v, $r) == 0) { // sign check success!
  344. break;
  345. }
  346. return false;
  347. default: return false;
  348. }
  349. return $this->doLogin($info);
  350. }
  351. protected function ssh_handleUserAuthRequest($user, $service, $method, $pkt) {
  352. if ($service != 'ssh-connection') {
  353. $pkt = chr(self::SSH_MSG_USERAUTH_FAILURE).$this->str('').chr(0);
  354. $this->sendPacket($pkt);
  355. return;
  356. }
  357. switch($method) {
  358. case 'password':
  359. $change = (bool)ord($pkt[0]);
  360. $pkt = substr($pkt, 1);
  361. $password = $this->parseStr($pkt);
  362. if (!$this->login($user, $password, $service)) {
  363. Logger::log(Logger::LOG_WARN, 'Logging in of '.$user.' failed from '.$this->peer[0]);
  364. sleep(4); // avoid immediate retry
  365. break;
  366. }
  367. Logger::log(Logger::LOG_WARN, 'User '.$user.' logged in from '.$this->peer[0]);
  368. $pkt = chr(self::SSH_MSG_USERAUTH_SUCCESS);
  369. $this->sendPacket($pkt);
  370. return;
  371. case 'publickey':
  372. $authreq = (bool)ord($pkt[0]);
  373. $pkt = substr($pkt, 1);
  374. $pk_algo = $this->parseStr($pkt);
  375. $pk_key = $this->parseStr($pkt);
  376. $pk_fingerprint = md5($pk_key);
  377. $pk_signature = null;
  378. if ($authreq) $pk_signature = $this->parseStr($pkt);
  379. // echo "Pubkey auth, ".($authreq?'AUTH':'KEY').' '.$pk_algo.' '.$pk_fingerprint."\n";
  380. $try = $this->loginPK($user, $service, $pk_algo, $pk_key, $pk_fingerprint, $pk_signature);
  381. if (!$try) break;
  382. if ($authreq) {
  383. // success
  384. $pkt = chr(self::SSH_MSG_USERAUTH_SUCCESS);
  385. $this->sendPacket($pkt);
  386. return;
  387. }
  388. $pkt = chr(self::SSH_MSG_USERAUTH_PK_OK).$this->str($pk_algo).$this->str($pk_key);
  389. $this->sendPacket($pkt);
  390. return;
  391. }
  392. $pkt = chr(self::SSH_MSG_USERAUTH_FAILURE).$this->str('publickey,password').chr(0);
  393. $this->sendPacket($pkt);
  394. }
  395. protected function ssh_serviceInit($svc) {
  396. if ($svc == 'ssh-userauth') return true; // builtin
  397. var_dump($svc);
  398. return false;
  399. }
  400. protected function ssh_initEncryption() {
  401. // init I/O encryption
  402. $mapping = array(
  403. 'aes128-ctr' => array(MCRYPT_RIJNDAEL_128, 'ctr', 16),
  404. 'aes256-cbc' => array(MCRYPT_RIJNDAEL_256, MCRYPT_MODE_CBC, 32),
  405. 'aes192-cbc' => array(MCRYPT_RIJNDAEL_192, MCRYPT_MODE_CBC, 24),
  406. 'aes128-cbc' => array(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC, 16),
  407. 'blowfish-cbc' => array(MCRYPT_BLOWFISH, MCRYPT_MODE_CBC, 32),
  408. 'serpent256-cbc' => array(MCRYPT_SERPENT, MCRYPT_MODE_CBC, 16),
  409. // 'arcfour', // Only cbc for now
  410. 'cast128-cbc' => array(MCRYPT_CAST_128, MCRYPT_MODE_CBC, 16),
  411. '3des-cbc' => array(MCRYPT_3DES, MCRYPT_MODE_CBC, 16),
  412. );
  413. $alg_recv = $mapping[$this->capa['cipher_recv']];
  414. $alg_send = $mapping[$this->capa['cipher_send']];
  415. $hmac_recv = $this->capa['hmac_recv'];
  416. $hmac_send = $this->capa['hmac_send'];
  417. // TODO: handle compression activation too
  418. $iv_recv = sha1($this->capa['K'].$this->capa['H'].'A'.$this->session_id, true);
  419. $iv_send = sha1($this->capa['K'].$this->capa['H'].'B'.$this->session_id, true);
  420. $key_recv = sha1($this->capa['K'].$this->capa['H'].'C'.$this->session_id, true);
  421. $key_send = sha1($this->capa['K'].$this->capa['H'].'D'.$this->session_id, true);
  422. $mac_recv = sha1($this->capa['K'].$this->capa['H'].'E'.$this->session_id, true);
  423. $mac_send = sha1($this->capa['K'].$this->capa['H'].'F'.$this->session_id, true);
  424. $key_recv_len = $alg_recv[2];
  425. $key_send_len = $alg_send[2];
  426. $iv_recv_len = mcrypt_get_iv_size($alg_recv[0], $alg_recv[1]);
  427. $iv_send_len = mcrypt_get_iv_size($alg_send[0], $alg_recv[1]);
  428. $block_recv = mcrypt_module_get_algo_block_size($alg_recv[0]);
  429. $block_send = mcrypt_module_get_algo_block_size($alg_send[0]);
  430. $mac_key_len_recv = strlen($mac_recv);
  431. $mac_key_len_send = strlen($mac_send);
  432. switch($hmac_recv) {
  433. case 'hmac-sha1': case 'hmac-sha1-96': $mac_key_len_recv = 20; break;
  434. case 'hmac-md5': case 'hmac-md5-96': $mac_key_len_recv = 16; break;
  435. }
  436. switch($hmac_send) {
  437. case 'hmac-sha1': case 'hmac-sha1-96': $mac_key_len_send = 20; break;
  438. case 'hmac-md5': case 'hmac-md5-96': $mac_key_len_send = 16; break;
  439. }
  440. if ($key_recv_len < $block_recv) $key_recv_len = $block_recv;
  441. if ($key_send_len < $block_send) $key_send_len = $block_send;
  442. while(strlen($key_recv) < $key_recv_len) $key_recv .= sha1($this->capa['K'].$this->capa['H'].$key_recv, true);
  443. while(strlen($key_send) < $key_send_len) $key_send .= sha1($this->capa['K'].$this->capa['H'].$key_send, true);
  444. if (strlen($key_recv) > $key_recv_len) $key_recv = substr($key_recv, 0, $key_recv_len);
  445. if (strlen($key_send) > $key_send_len) $key_send = substr($key_send, 0, $key_send_len);
  446. while(strlen($iv_recv) < $iv_recv_len) $iv_recv .= sha1($this->capa['K'].$this->capa['H'].$iv_recv, true);
  447. while(strlen($iv_send) < $iv_send_len) $iv_send .= sha1($this->capa['K'].$this->capa['H'].$iv_send, true);
  448. if (strlen($iv_recv) > $iv_recv_len) $iv_recv = substr($iv_recv, 0, $iv_recv_len);
  449. if (strlen($iv_send) > $iv_send_len) $iv_send = substr($iv_send, 0, $iv_send_len);
  450. if (strlen($mac_recv) > $mac_key_len_recv) $mac_recv = substr($mac_recv, 0, $mac_key_len_recv);
  451. if (strlen($mac_send) > $mac_key_len_send) $mac_send = substr($mac_send, 0, $mac_key_len_send);
  452. $mod_recv = mcrypt_module_open($alg_recv[0], '', $alg_recv[1], '');
  453. $mod_send = mcrypt_module_open($alg_send[0], '', $alg_send[1], '');
  454. mcrypt_generic_init($mod_recv, $key_recv, $iv_recv);
  455. mcrypt_generic_init($mod_send, $key_send, $iv_send);
  456. $this->cipher['recv'] = array('name' => $this->capa['cipher_recv'], 'block_size' => $block_recv, 'mod' => $mod_recv, 'hmac' => $hmac_recv, 'hmac_key' => $mac_recv);
  457. $this->cipher['send'] = array('name' => $this->capa['cipher_send'], 'block_size' => $block_send, 'mod' => $mod_send, 'hmac' => $hmac_send, 'hmac_key' => $mac_send);
  458. }
  459. protected function gmp_binval($g) {
  460. $hex = gmp_strval($g, 16);
  461. if (strlen($hex) & 1) $hex = '0'.$hex;
  462. return pack('H*', $hex);
  463. }
  464. protected function ssh_KeyExchangeDHInit($pkt) {
  465. // secure key exchange
  466. if (!$this->loadSkey()) {
  467. Logger::log(Logger::LOG_WARN, 'Could not load server key');
  468. $this->disconnect(self::SSH_DISCONNECT_KEY_EXCHANGE_FAILED, 'server misconfiguration');
  469. return;
  470. }
  471. switch($this->capa['kex']) {
  472. case 'diffie-hellman-group1-sha1':
  473. case 'diffie-hellman-group14-sha1':
  474. // RFC 4253 page 21: 8. Diffie-Hellman Key Exchange
  475. // Both groups have the same generator
  476. if ($this->capa['kex'] == 'diffie-hellman-group1-sha1') {
  477. $p = gmp_init('179769313486231590770839156793787453197860296048756011706444423684197180216158519368947833795864925541502180565485980503646440548199239100050792877003355816639229553136239076508735759914822574862575007425302077447712589550957937778424442426617334727629299387668709205606050270810842907692932019128194467627007');
  478. $y = gmp_init(bin2hex(openssl_random_pseudo_bytes(64)), 16);
  479. } else { // diffie-hellman-group14-sha1
  480. $p = gmp_init('FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF', 16);
  481. $y = gmp_init(bin2hex(openssl_random_pseudo_bytes(128)), 16);
  482. }
  483. // read $e from client
  484. $e_bin = $this->parseStr($pkt);
  485. $e = gmp_init(bin2hex($e_bin), 16); // not really optimized but works
  486. // compute $f and $K
  487. $f = gmp_powm(2, $y, $p);
  488. $f_bin = $this->gmp_binval($f);
  489. if (ord($f_bin[0]) & 0x80) $f_bin = "\0" . $f_bin;
  490. $K = gmp_powm($e, $y, $p);
  491. $K_bin = $this->gmp_binval($K);
  492. if (ord($K_bin[0]) & 0x80) $K_bin = "\0" . $K_bin;
  493. $pub = $this->skey['pub'];
  494. // H = hash(V_C || V_S || I_C || I_S || K_S || e || f || K)
  495. $sha = array(
  496. $this->str($this->payloads['V_C']),
  497. $this->str($this->payloads['V_S']),
  498. $this->str($this->payloads['I_C']),
  499. $this->str($this->payloads['I_S']),
  500. $this->str($pub),
  501. $this->str($e_bin),
  502. $this->str($f_bin),
  503. $this->str($K_bin)
  504. );
  505. $H = sha1(implode('', $sha), true);
  506. // store shared secret in session
  507. $this->capa['K'] = $this->str($K_bin);
  508. $this->capa['H'] = $H;
  509. if (is_null($this->session_id)) $this->session_id = $H;
  510. // sign $H
  511. $s = Crypto::sign($H, $this->skey['pkeyid']);
  512. $s = $this->str($this->skey['type']).$this->str($s);
  513. $pkt = chr(self::SSH_MSG_KEXDH_REPLY);
  514. $pkt .= $this->str($pub);
  515. $pkt .= $this->str($f_bin);
  516. $pkt .= $this->str($s);
  517. $this->sendPacket($pkt);
  518. break;
  519. default:
  520. $this->disconnect(self::SSH_DISCONNECT_KEY_EXCHANGE_FAILED, 'Unsupported key exchange algo');
  521. }
  522. }
  523. protected function ssh_determineEncryption() {
  524. $my_kex_alg = array_flip($this->getKeyExchAlgList());
  525. $my_key_alg = array_flip($this->getPKAlgList());
  526. $my_cipher = array_flip($this->getCipherAlgList());
  527. $my_hmac = array_flip($this->getHmacAlgList());
  528. $my_comp = array_flip($this->getCompAlgList());
  529. $fallback_cnt = 0;
  530. $kex = null;
  531. foreach($this->capa['kex_algorithms'] as $alg) {
  532. if (isset($my_kex_alg[$alg])) { $kex = $alg; break; }
  533. $fallback_cnt++;
  534. }
  535. if (is_null($kex)) { $this->disconnect(self::SSH_DISCONNECT_PROTOCOL_ERROR, 'No matching KEX'); return; }
  536. $key_alg = null;
  537. foreach($this->capa['server_host_key_algorithms'] as $alg) {
  538. if (isset($my_key_alg[$alg])) { $key_alg = $alg; break; }
  539. $fallback_cnt++;
  540. }
  541. if (is_null($key_alg)) { $this->close(); return; }
  542. $cipher_send = null;
  543. foreach($this->capa['encryption_algorithms_server_to_client'] as $alg) {
  544. if (isset($my_cipher[$alg])) { $cipher_send = $alg; break; }
  545. $fallback_cnt++;
  546. }
  547. if (is_null($cipher_send)) { $this->close(); return; }
  548. $cipher_recv = null;
  549. foreach($this->capa['encryption_algorithms_client_to_server'] as $alg) {
  550. if (isset($my_cipher[$alg])) { $cipher_recv = $alg; break; }
  551. $fallback_cnt++;
  552. }
  553. if (is_null($cipher_recv)) { $this->close(); return; }
  554. $hmac_send = null;
  555. foreach($this->capa['mac_algorithms_server_to_client'] as $alg) {
  556. if (isset($my_hmac[$alg])) { $hmac_send = $alg; break; }
  557. $fallback_cnt++;
  558. }
  559. if (is_null($hmac_send)) { $this->close(); return; }
  560. $hmac_recv = null;
  561. foreach($this->capa['mac_algorithms_client_to_server'] as $alg) {
  562. if (isset($my_hmac[$alg])) { $hmac_recv = $alg; break; }
  563. $fallback_cnt++;
  564. }
  565. if (is_null($hmac_recv)) { $this->close(); return; }
  566. $comp_send = null;
  567. foreach($this->capa['compression_algorithms_server_to_client'] as $alg) {
  568. if (isset($my_comp[$alg])) { $comp_send = $alg; break; }
  569. $fallback_cnt++;
  570. }
  571. if (is_null($comp_send)) { $this->close(); return; }
  572. $comp_recv = null;
  573. foreach($this->capa['compression_algorithms_client_to_server'] as $alg) {
  574. if (isset($my_comp[$alg])) { $comp_recv = $alg; break; }
  575. $fallback_cnt++;
  576. }
  577. if (is_null($comp_recv)) { $this->close(); return; }
  578. $this->capa['kex'] = $kex;
  579. $this->capa['key_alg'] = $key_alg;
  580. $this->capa['cipher_send'] = $cipher_send;
  581. $this->capa['cipher_recv'] = $cipher_recv;
  582. $this->capa['hmac_send'] = $hmac_send;
  583. $this->capa['hmac_recv'] = $hmac_recv;
  584. $this->capa['comp_send'] = $comp_send;
  585. $this->capa['comp_recv'] = $comp_recv;
  586. $this->capa['fallback_cnt'] = $fallback_cnt; // if not 0, means the client didn't guess right
  587. }
  588. protected function ssh_sendAlgorithmNegotiationPacket() {
  589. $pkt = pack('CNNNN', self::SSH_MSG_KEXINIT, mt_rand(0,0xffffffff), mt_rand(0,0xffffffff), mt_rand(0,0xffffffff), mt_rand(0,0xffffffff));
  590. $pkt .= $this->nameList($this->getKeyExchAlgList());
  591. $pkt .= $this->nameList($this->getPKAlgList());
  592. $pkt .= $this->nameList($this->getCipherAlgList());
  593. $pkt .= $this->nameList($this->getCipherAlgList());
  594. $pkt .= $this->nameList($this->getHmacAlgList());
  595. $pkt .= $this->nameList($this->getHmacAlgList());
  596. $pkt .= $this->nameList($this->getCompAlgList());
  597. $pkt .= $this->nameList($this->getCompAlgList());
  598. $pkt .= str_repeat("\0", 8); // no languages_client_to_server / languages_server_to_client
  599. $pkt .= "\0"; // boolean first_kex_packet_follows
  600. $pkt .= str_repeat("\0", 4);
  601. $this->payloads['I_S'] = $pkt;
  602. $this->sendPacket($pkt);
  603. $this->kex_sent = true;
  604. }
  605. protected function parseInt32(&$pkt) {
  606. list(,$int) = unpack('N', substr($pkt, 0, 4));
  607. $pkt = substr($pkt, 4);
  608. return $int;
  609. }
  610. protected function parseStr(&$pkt) {
  611. list(,$len) = unpack('N', substr($pkt, 0, 4));
  612. if ($len == 0) {
  613. $pkt = substr($pkt, 4);
  614. return '';
  615. }
  616. if ($len+4 > strlen($pkt)) return false;
  617. $res = substr($pkt, 4, $len);
  618. $pkt = substr($pkt, $len+4);
  619. return $res;
  620. }
  621. protected function parseNameList(&$pkt) {
  622. list(,$len) = unpack('N', substr($pkt, 0, 4));
  623. if ($len == 0) {
  624. $pkt = substr($pkt, 4);
  625. return array();
  626. }
  627. if ($len+4 > strlen($pkt)) return false;
  628. $res = explode(',', substr($pkt, 4, $len));
  629. $pkt = substr($pkt, $len+4);
  630. return $res;
  631. }
  632. protected function loadSkey() {
  633. switch($this->capa['key_alg']) { // ssh-dss,ssh-rsa
  634. case 'ssh-rsa': $key = PINETD_ROOT.'/ssl/ssh_host_rsa_key'; $pkeyid = Crypto::rsa_read_pem(file_get_contents($key)); break;
  635. case 'ssh-dss': $key = PINETD_ROOT.'/ssl/ssh_host_dsa_key'; $pkeyid = Crypto::dsa_read_pem(file_get_contents($key)); break;
  636. default: return false;
  637. }
  638. if ((!file_exists($key)) || (!$pkeyid)) return false;
  639. $pub = Crypto::make_ssh_pk($pkeyid);
  640. $this->skey = array('pub' => $pub, 'pkeyid' => $pkeyid, 'type' => $this->capa['key_alg']);
  641. return true;
  642. }
  643. protected function nameList(array $list) {
  644. $res = implode(',', $list);
  645. return pack('N', strlen($res)).$res;
  646. }
  647. protected function getCompAlgList() {
  648. return array('none'); // zlib@openssl.com
  649. }
  650. protected function getCipherAlgList() {
  651. $mapping = array(
  652. 'aes128-ctr' => 'rijndael-128',
  653. 'aes128-cbc' => 'rijndael-128',
  654. 'blowfish-cbc' => 'blowfish',
  655. 'serpent256-cbc' => 'serpent',
  656. 'cast128-cbc' => 'cast-128',
  657. '3des-cbc' => 'tripledes',
  658. );
  659. $list = array_flip(mcrypt_list_algorithms());
  660. $final = array();
  661. foreach($mapping as $ssh_alg => $alg) {
  662. if (!isset($list[$alg])) continue;
  663. $final[] = $ssh_alg;
  664. }
  665. return $final;
  666. }
  667. protected function getHmacAlgList() {
  668. return array('hmac-sha1','hmac-sha1-96','hmac-md5','hmac-md5-96','none');
  669. }
  670. protected function getKeyExchAlgList() {
  671. return array('diffie-hellman-group14-sha1','diffie-hellman-group1-sha1');
  672. }
  673. protected function getPKAlgList() {
  674. return array('ssh-dss','ssh-rsa');
  675. }
  676. protected function disconnect($code, $text) {
  677. $pkt = chr(self::SSH_MSG_DISCONNECT);
  678. $pkt .= pack('N', $code);
  679. $pkt .= $this->str($text);
  680. $pkt .= $this->str('');
  681. $this->sendPacket($pkt);
  682. $this->close();
  683. }
  684. protected function str($str) {
  685. return pack('N', strlen($str)).$str;
  686. }
  687. protected function sendPacket($packet) {
  688. $this->seq_send++;
  689. $len = strlen($packet);
  690. // compute padding length
  691. $pad_len = max($this->cipher['send']['block_size'], 8) - (($len+5) % max($this->cipher['send']['block_size'], 8));
  692. if ($pad_len < 4) $pad_len += max($this->cipher['send']['block_size'], 8);
  693. $packet = pack('NC', $len+1+$pad_len, $pad_len).$packet.str_repeat("\0", $pad_len);
  694. // compute hmac
  695. $mac = '';
  696. switch($this->cipher['send']['hmac']) {
  697. case 'hmac-sha1': $mac = hash_hmac('sha1', pack('N', $this->seq_send).$packet, $this->cipher['send']['hmac_key'], true); break;
  698. case 'hmac-sha1-96': $mac = substr(hash_hmac('sha1', pack('N', $this->seq_send).$packet, $this->cipher['send']['hmac_key'], true), 0, 12); break;
  699. case 'hmac-md5': $mac = hash_hmac('md5', pack('N', $this->seq_send).$packet, $this->cipher['send']['hmac_key'], true); break;
  700. case 'hmac-md5-96': $mac = substr(hash_hmac('md5', pack('N', $this->seq_send).$packet, $this->cipher['send']['hmac_key'], true), 0, 12); break;
  701. }
  702. // encrypt packet
  703. if (isset($this->cipher['send']['mod'])) {
  704. $packet = mcrypt_generic($this->cipher['send']['mod'], $packet);
  705. }
  706. return $this->sendMsg($packet.$mac);
  707. }
  708. protected function handleProtocol($proto) {
  709. if (!preg_match('/^SSH-2\\.0-([^ -]+)( .*)?$/', $proto, $matches)) {
  710. $this->disconnect(self::SSH_DISCONNECT_PROTOCOL_VERSION_NOT_SUPPORTED, 'could not understand your protocol version');
  711. return;
  712. }
  713. $this->payloads['V_C'] = $proto;
  714. $this->agent = $matches[1].$matches[2];
  715. $this->state = 'login';
  716. $this->ssh_sendAlgorithmNegotiationPacket();
  717. }
  718. protected function parseBuffer() {
  719. while($this->ok) {
  720. if($this->state == 'new') {
  721. $pos = strpos($this->buf, "\n");
  722. if ($pos === false) break;
  723. $pos++;
  724. $lin = substr($this->buf, 0, $pos);
  725. $this->buf = substr($this->buf, $pos);
  726. $this->handleProtocol(rtrim($lin));
  727. continue;
  728. }
  729. $block_size = max(8, $this->cipher['recv']['block_size']);
  730. if (strlen($this->clearbuf) < 8) {
  731. if (strlen($this->buf) < $block_size) break; // not enough data yet
  732. // decrypt first block
  733. $tmp = substr($this->buf, 0, $block_size);
  734. $this->buf = substr($this->buf, $block_size);
  735. if (isset($this->cipher['recv']['mod']))
  736. $tmp = mdecrypt_generic($this->cipher['recv']['mod'], $tmp);
  737. $this->clearbuf .= $tmp;
  738. }
  739. // decode length from clearbuf
  740. list(,$len) = unpack('N', substr($this->clearbuf, 0, 4));
  741. if ($len > 256*1024) {
  742. $this->disconnect(self::SSH_DISCONNECT_PROTOCOL_ERROR, 'disconnected: packet size over 256kB');
  743. }
  744. if ((4+$len) > strlen($this->clearbuf)) { // missing data
  745. $missing_len = (4+$len)-strlen($this->clearbuf);
  746. if (strlen($this->buf) < $missing_len) break; // need more data
  747. $tmp = substr($this->buf, 0, $missing_len);
  748. $this->buf = substr($this->buf, $missing_len);
  749. if (isset($this->cipher['recv']['mod']))
  750. $tmp = mdecrypt_generic($this->cipher['recv']['mod'], $tmp);
  751. $this->clearbuf .= $tmp;
  752. }
  753. // we got one full packet, check for mac...
  754. $mac_len = 0;
  755. switch($this->cipher['recv']['hmac']) {
  756. case 'hmac-sha1': $mac_len = 20; break;
  757. case 'hmac-md5': $mac_len = 16; break;
  758. case 'hmac-sha1-96':
  759. case 'hmac-md5-96': $mac_len = 12; break;
  760. }
  761. if (strlen($this->buf) < $mac_len) break; // need more data
  762. $this->seq_recv++; // we got all the data we needed, we can increment this counter
  763. if ($mac_len > 0) {
  764. switch($this->cipher['recv']['hmac']) {
  765. case 'hmac-sha1': $mac = hash_hmac('sha1', pack('N', $this->seq_recv).$this->clearbuf, $this->cipher['recv']['hmac_key'], true); break;
  766. case 'hmac-sha1-96': $mac = substr(hash_hmac('sha1', pack('N', $this->seq_recv).$this->clearbuf, $this->cipher['recv']['hmac_key'], true), 0, 12); break;
  767. case 'hmac-md5': $mac = hash_hmac('md5', pack('N', $this->seq_recv).$this->clearbuf, $this->cipher['recv']['hmac_key'], true); break;
  768. case 'hmac-md5-96': $mac = substr(hash_hmac('md5', pack('N', $this->seq_recv).$this->clearbuf, $this->cipher['recv']['hmac_key'], true), 0, 12); break;
  769. }
  770. $tmp = substr($this->buf, 0, $mac_len);
  771. $this->buf = substr($this->buf, $mac_len);
  772. if ($tmp != $mac) {
  773. Logger::log(Logger::LOG_WARN, 'Invalid MAC for packet in input stream!');
  774. $this->disconnect(self::SSH_DISCONNECT_MAC_ERROR, 'could not understand mac');
  775. break;
  776. }
  777. }
  778. $pkt = substr($this->clearbuf, 4);
  779. $this->clearbuf = '';
  780. $padding = ord($pkt[0]);
  781. $pkt = substr($pkt, 1, 0-$padding);
  782. $this->handlePkt($pkt);
  783. }
  784. $this->setProcessStatus(); // back to idle
  785. }
  786. function shutdown() {
  787. }
  788. }