From aca4ba32860ffdd02f604480cb33bb23fd20e173 Mon Sep 17 00:00:00 2001 From: montdidier Date: Tue, 30 Dec 2014 10:44:31 +0800 Subject: [PATCH 01/12] SSH agent forwarding implementation --- phpseclib/Net/SSH2.php | 165 ++++++++++++++++--------- phpseclib/System/SSH/Agent.php | 153 +++++++++++++++++++++++ tests/Functional/Net/SSH2AgentTest.php | 22 ++++ travis/setup-secure-shell.sh | 2 + 4 files changed, 287 insertions(+), 55 deletions(-) diff --git a/phpseclib/Net/SSH2.php b/phpseclib/Net/SSH2.php index 897c78b4..9ae7d176 100644 --- a/phpseclib/Net/SSH2.php +++ b/phpseclib/Net/SSH2.php @@ -99,6 +99,10 @@ define('NET_SSH2_MASK_WINDOW_ADJUST', 0x00000020); define('NET_SSH2_CHANNEL_EXEC', 0); // PuTTy uses 0x100 define('NET_SSH2_CHANNEL_SHELL', 1); define('NET_SSH2_CHANNEL_SUBSYSTEM', 2); +define('NET_SSH2_CHANNEL_AGENT_REQUEST', 3); +define('NET_SSH2_CHANNEL_AGENT_FORWARD', 4); + + /**#@-*/ /**#@+ @@ -840,6 +844,14 @@ class Net_SSH2 */ var $windowRows = 24; + /** + * A System_SSH_Agent for use in the SSH2 Agent Forwarding scenario + * + * @var System_SSH_Agent + * @access private + */ + var $agent; + /** * Default Constructor. * @@ -2132,9 +2144,11 @@ class Net_SSH2 */ function _ssh_agent_login($username, $agent) { + $this->agent = $agent; $keys = $agent->requestIdentities(); foreach ($keys as $key) { if ($this->_privatekey_login($username, $key)) { + $this->agent->_on_login_success($this); return true; } } @@ -2234,6 +2248,7 @@ class Net_SSH2 return false; } + /** * Set Timeout * @@ -2311,6 +2326,7 @@ class Net_SSH2 if (!$this->_send_binary_packet($packet)) { return false; } + $response = $this->_get_binary_packet(); if ($response === false) { user_error('Connection closed by server'); @@ -2341,6 +2357,7 @@ class Net_SSH2 // "maximum size of an individual data packet". ie. SSH_MSG_CHANNEL_DATA. RFC4254#section-5.2 corroborates. $packet = pack('CNNa*CNa*', NET_SSH2_MSG_CHANNEL_REQUEST, $this->server_channels[NET_SSH2_CHANNEL_EXEC], strlen('exec'), 'exec', 1, strlen($command), $command); + if (!$this->_send_binary_packet($packet)) { return false; } @@ -2808,6 +2825,8 @@ class Net_SSH2 } $payload = $this->_get_binary_packet(); } + default: + break; } // 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 @@ -2830,23 +2849,6 @@ class Net_SSH2 return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); } - $payload = $this->_get_binary_packet(); - break; - case NET_SSH2_MSG_CHANNEL_OPEN: // see http://tools.ietf.org/html/rfc4254#section-5.1 - $this->_string_shift($payload, 1); - extract(unpack('Nlength', $this->_string_shift($payload, 4))); - $this->errors[] = 'SSH_MSG_CHANNEL_OPEN: ' . utf8_decode($this->_string_shift($payload, $length)); - - $this->_string_shift($payload, 4); // skip over client channel - extract(unpack('Nserver_channel', $this->_string_shift($payload, 4))); - - $packet = pack('CN3a*Na*', - NET_SSH2_MSG_REQUEST_FAILURE, $server_channel, NET_SSH2_OPEN_ADMINISTRATIVELY_PROHIBITED, 0, '', 0, ''); - - if (!$this->_send_binary_packet($packet)) { - return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); - } - $payload = $this->_get_binary_packet(); break; case NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST: @@ -2983,53 +2985,97 @@ class Net_SSH2 return ''; } - extract(unpack('Ctype/Nchannel', $this->_string_shift($response, 5))); + extract(unpack('Ctype', $this->_string_shift($response, 1))); - $this->window_size_server_to_client[$channel]-= strlen($response); - - // resize the window, if appropriate - if ($this->window_size_server_to_client[$channel] < 0) { - $packet = pack('CNN', NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST, $this->server_channels[$channel], $this->window_size); - if (!$this->_send_binary_packet($packet)) { - return false; - } - $this->window_size_server_to_client[$channel]+= $this->window_size; + if ($type == NET_SSH2_MSG_CHANNEL_OPEN) { + extract(unpack('Nlength', $this->_string_shift($response, 4))); + } else { + extract(unpack('Nchannel', $this->_string_shift($response, 4))); } - switch ($this->channel_status[$channel]) { - case NET_SSH2_MSG_CHANNEL_OPEN: - switch ($type) { - case NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION: - extract(unpack('Nserver_channel', $this->_string_shift($response, 4))); - $this->server_channels[$channel] = $server_channel; - extract(unpack('Nwindow_size', $this->_string_shift($response, 4))); - $this->window_size_client_to_server[$channel] = $window_size; - $temp = unpack('Npacket_size_client_to_server', $this->_string_shift($response, 4)); - $this->packet_size_client_to_server[$channel] = $temp['packet_size_client_to_server']; - return $client_channel == $channel ? true : $this->_get_channel_packet($client_channel, $skip_extended); - //case NET_SSH2_MSG_CHANNEL_OPEN_FAILURE: - default: - user_error('Unable to open channel'); - return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); + // will not be setup yet on incoming channel open request + if (isset($channel) && isset($this->channel_status[$channel]) && isset($this->window_size_server_to_client[$channel])) { + $this->window_size_server_to_client[$channel]-= strlen($response); + + // resize the window, if appropriate + if ($this->window_size_server_to_client[$channel] < 0) { + $packet = pack('CNN', NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST, $this->server_channels[$channel], $this->window_size); + if (!$this->_send_binary_packet($packet)) { + return false; } - break; - case NET_SSH2_MSG_CHANNEL_REQUEST: - switch ($type) { - case NET_SSH2_MSG_CHANNEL_SUCCESS: - return true; - case NET_SSH2_MSG_CHANNEL_FAILURE: - return false; - default: - user_error('Unable to fulfill channel request'); - return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); - } - case NET_SSH2_MSG_CHANNEL_CLOSE: - return $type == NET_SSH2_MSG_CHANNEL_CLOSE ? true : $this->_get_channel_packet($client_channel, $skip_extended); + $this->window_size_server_to_client[$channel]+= $this->window_size; + } + + switch ($this->channel_status[$channel]) { + case NET_SSH2_MSG_CHANNEL_OPEN:; + switch ($type) { + case NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION: + extract(unpack('Nserver_channel', $this->_string_shift($response, 4))); + $this->server_channels[$channel] = $server_channel; + extract(unpack('Nwindow_size', $this->_string_shift($response, 4))); + $this->window_size_client_to_server[$channel] = $window_size; + $temp = unpack('Npacket_size_client_to_server', $this->_string_shift($response, 4)); + $this->packet_size_client_to_server[$channel] = $temp['packet_size_client_to_server']; + return $client_channel == $channel ? true : $this->_get_channel_packet($client_channel, $skip_extended); + //case NET_SSH2_MSG_CHANNEL_OPEN_FAILURE: + default: + user_error('Unable to open channel'); + return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); + } + break; + case NET_SSH2_MSG_CHANNEL_REQUEST: + switch ($type) { + case NET_SSH2_MSG_CHANNEL_SUCCESS: + return true; + case NET_SSH2_MSG_CHANNEL_FAILURE: + return false; + default: + user_error('Unable to fulfill channel request'); + return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); + } + case NET_SSH2_MSG_CHANNEL_CLOSE: + return $type == NET_SSH2_MSG_CHANNEL_CLOSE ? true : $this->_get_channel_packet($client_channel, $skip_extended); + } } // ie. $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_DATA switch ($type) { + case NET_SSH2_MSG_CHANNEL_OPEN: // see http://tools.ietf.org/html/rfc4254#section-5.1 + $data = $this->_string_shift($response, $length); + switch($data) { + case 'auth-agent': + case 'auth-agent@openssh.com': + if (!isset($this->agent)) { + break 2; + } + break; + default: + break 2; + } + + $new_channel = NET_SSH2_CHANNEL_AGENT_FORWARD; + + extract(unpack('Nserver_channel', $this->_string_shift($response, 4))); + extract(unpack('Nremote_window_size', $this->_string_shift($response, 4))); + extract(unpack('Nremote_maximum_packet_size', $this->_string_shift($response, 4))); + + $this->packet_size_client_to_server[$new_channel] = $remote_window_size; + $this->window_size_server_to_client[$new_channel] = $remote_maximum_packet_size; + $this->window_size_client_to_server[$new_channel] = $this->window_size; + + $packet_size = 0x4000; + + $packet = pack('CN4', + NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION, $server_channel, $new_channel, $packet_size, $packet_size); + + $this->server_channels[$new_channel] = $server_channel; + $this->channel_status[$new_channel] = NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION; + + if (!$this->_send_binary_packet($packet)) { + return false; + } + break; case NET_SSH2_MSG_CHANNEL_DATA: /* if ($channel == NET_SSH2_CHANNEL_EXEC) { @@ -3042,6 +3088,15 @@ class Net_SSH2 */ extract(unpack('Nlength', $this->_string_shift($response, 4))); $data = $this->_string_shift($response, $length); + + if ($channel == NET_SSH2_CHANNEL_AGENT_FORWARD) { + $agent_response = $this->agent->_forward_data($data); + if (!is_bool($agent_response)) { + $this->_send_channel_packet($channel, $agent_response); + } + break; + } + if ($client_channel == $channel) { return $data; } diff --git a/phpseclib/System/SSH/Agent.php b/phpseclib/System/SSH/Agent.php index 4c0ef733..47146045 100644 --- a/phpseclib/System/SSH/Agent.php +++ b/phpseclib/System/SSH/Agent.php @@ -1,4 +1,5 @@ request_forwarding) { + $this->request_forwarding = true; + $this->_request_forwarding($ssh); + } + } + + /** + * Request the remote server to stop forwarding authentication requests to the local SSH agent + * + * @param Net_SSH2 $ssh + * @return Boolean + * @access public + */ + function stopSSHForwarding($ssh) + { + if ($this->request_forwarding) { + $ssh->_close_channel(NET_SSH2_CHANNEL_AGENT_FORWARD); + $this->request_forwarding = false; + } + } + + /** + * The worker function to make a request to a remote server + * asking it to forward authentication requests to the local SSH + * agent + * + * @param Net_SSH2 $ssh + * @return Boolean + * @access private + */ + function _request_forwarding($ssh) + { + $ssh->window_size_server_to_client[NET_SSH2_CHANNEL_AGENT_REQUEST] = $ssh->window_size; + $packet_size = 0x4000; + + $packet = pack('CNa*N3', + NET_SSH2_MSG_CHANNEL_OPEN, strlen('session'), 'session', NET_SSH2_CHANNEL_AGENT_REQUEST, $ssh->window_size_server_to_client[NET_SSH2_CHANNEL_AGENT_REQUEST], $packet_size); + + $ssh->channel_status[NET_SSH2_CHANNEL_AGENT_REQUEST] = NET_SSH2_MSG_CHANNEL_OPEN; + + if (!$ssh->_send_binary_packet($packet)) { + return false; + } + + $response = $ssh->_get_channel_packet(NET_SSH2_CHANNEL_AGENT_REQUEST); + if ($response === false) { + return false; + } + + $packet = pack('CNNa*C', + NET_SSH2_MSG_CHANNEL_REQUEST, $ssh->server_channels[NET_SSH2_CHANNEL_AGENT_REQUEST], strlen('auth-agent-req@openssh.com'), 'auth-agent-req@openssh.com', 1); + + $ssh->channel_status[NET_SSH2_CHANNEL_AGENT_REQUEST] = NET_SSH2_MSG_CHANNEL_REQUEST; + + if (!$ssh->_send_binary_packet($packet)) { + return false; + } + + $response = $ssh->_get_channel_packet(NET_SSH2_CHANNEL_AGENT_REQUEST); + if ($response === false) { + return false; + } + + return true; + } + + /** + * On Successful Login + * + * This method should be called upon successful SSH login if you + * wish to give the SSH Agent an opportunity to take further + * action. i.e. request agent forwarding + * + * @param Net_SSH2 $ssh + * @access private + */ + function _on_login_success($ssh) + { + if ($this->request_forwarding) { + $this->_request_forwarding($ssh); + } + } + + /** + * Forward data to SSH Agent and return data reply + * + * @param String $data + * @return data from SSH Agent + * @access private + */ + function _forward_data($data) + { + if ($this->expected_bytes > 0) { + $this->socket_buffer .= $data; + $this->expected_bytes -= strlen($data); + } else { + $agent_data_bytes = current(unpack('N', $data)); + $current_data_bytes = strlen($data); + $this->socket_buffer = $data; + if ($current_data_bytes != $agent_data_bytes + 4) { + $this->expected_bytes = ($agent_data_bytes + 4) - $current_data_bytes; + return false; + } + } + + if (strlen($this->socket_buffer) != fwrite($this->fsock, $this->socket_buffer)) { + user_error('Connection closed attempting to forward data to SSH agent'); + } + + $this->socket_buffer = ''; + $this->expected_bytes = 0; + + $agent_reply_bytes = current(unpack('N', fread($this->fsock, 4))); + + $agent_reply_data = fread($this->fsock, $agent_reply_bytes); + $agent_reply_data = current(unpack('a*', $agent_reply_data)); + + return pack('Na*', $agent_reply_bytes, $agent_reply_data); + } } diff --git a/tests/Functional/Net/SSH2AgentTest.php b/tests/Functional/Net/SSH2AgentTest.php index d92bb705..1426f66c 100644 --- a/tests/Functional/Net/SSH2AgentTest.php +++ b/tests/Functional/Net/SSH2AgentTest.php @@ -27,5 +27,27 @@ class Functional_Net_SSH2AgentTest extends PhpseclibFunctionalTestCase $ssh->login($this->getEnv('SSH_USERNAME'), $agent), 'SSH2 login using Agent failed.' ); + + return array('ssh' => $ssh, 'ssh-agent' => $agent); + } + + /** + * @depends testAgentLogin + */ + public function testAgentForward($args) + { + $ssh = $args['ssh']; + $agent = $args['ssh-agent']; + + $hostname = $this->getEnv('SSH_HOSTNAME'); + $username = $this->getEnv('SSH_USERNAME'); + + $this->assertEquals($username, trim($ssh->exec('whoami'))); + + $agent->startSSHForwarding($ssh); + $this->assertEquals($username, trim($ssh->exec("ssh " . $username . "@" . $hostname . ' \'whoami\''))); + $agent->stopSSHForwarding($ssh); + + return $args; } } diff --git a/travis/setup-secure-shell.sh b/travis/setup-secure-shell.sh index fdcaaa0d..22395aa7 100755 --- a/travis/setup-secure-shell.sh +++ b/travis/setup-secure-shell.sh @@ -28,4 +28,6 @@ ssh-add "$HOME/.ssh/id_rsa" # Allow the private key of the travis user to log in as phpseclib user sudo mkdir -p "/home/$USERNAME/.ssh/" sudo cp "$HOME/.ssh/id_rsa.pub" "/home/$USERNAME/.ssh/authorized_keys" +sudo ssh-keyscan -t rsa localhost > "/tmp/known_hosts" +sudo cp "/tmp/known_hosts" "/home/$USERNAME/.ssh/known_hosts" sudo chown "$USERNAME:$USERNAME" "/home/$USERNAME/.ssh/" -R From 8dad92f5e5908118b1a23fe4c6aba7d324140f0b Mon Sep 17 00:00:00 2001 From: Chris Kruger Date: Tue, 30 Dec 2014 16:40:47 +0800 Subject: [PATCH 02/12] removed superfluous default case --- phpseclib/Net/SSH2.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/phpseclib/Net/SSH2.php b/phpseclib/Net/SSH2.php index 9ae7d176..e7e1b551 100644 --- a/phpseclib/Net/SSH2.php +++ b/phpseclib/Net/SSH2.php @@ -2825,8 +2825,6 @@ class Net_SSH2 } $payload = $this->_get_binary_packet(); } - default: - break; } // 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 From dd0b3e6bd569377a65e05c2038802a2750b0571f Mon Sep 17 00:00:00 2001 From: montdidier Date: Mon, 12 Jan 2015 17:13:33 +0800 Subject: [PATCH 03/12] addresses low hanging fruit comments from terrafrost and bantu --- phpseclib/Net/SSH2.php | 7 +------ phpseclib/System/SSH/Agent.php | 2 +- tests/Functional/Net/SSH2AgentTest.php | 2 ++ 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/phpseclib/Net/SSH2.php b/phpseclib/Net/SSH2.php index 9ae7d176..0a649fd9 100644 --- a/phpseclib/Net/SSH2.php +++ b/phpseclib/Net/SSH2.php @@ -101,8 +101,6 @@ define('NET_SSH2_CHANNEL_SHELL', 1); define('NET_SSH2_CHANNEL_SUBSYSTEM', 2); define('NET_SSH2_CHANNEL_AGENT_REQUEST', 3); define('NET_SSH2_CHANNEL_AGENT_FORWARD', 4); - - /**#@-*/ /**#@+ @@ -2248,7 +2246,6 @@ class Net_SSH2 return false; } - /** * Set Timeout * @@ -2825,8 +2822,6 @@ class Net_SSH2 } $payload = $this->_get_binary_packet(); } - default: - break; } // 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 @@ -3007,7 +3002,7 @@ class Net_SSH2 } switch ($this->channel_status[$channel]) { - case NET_SSH2_MSG_CHANNEL_OPEN:; + case NET_SSH2_MSG_CHANNEL_OPEN: switch ($type) { case NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION: extract(unpack('Nserver_channel', $this->_string_shift($response, 4))); diff --git a/phpseclib/System/SSH/Agent.php b/phpseclib/System/SSH/Agent.php index 47146045..1b09a34d 100644 --- a/phpseclib/System/SSH/Agent.php +++ b/phpseclib/System/SSH/Agent.php @@ -437,7 +437,7 @@ class System_SSH_Agent function _forward_data($data) { if ($this->expected_bytes > 0) { - $this->socket_buffer .= $data; + $this->socket_buffer.= $data; $this->expected_bytes -= strlen($data); } else { $agent_data_bytes = current(unpack('N', $data)); diff --git a/tests/Functional/Net/SSH2AgentTest.php b/tests/Functional/Net/SSH2AgentTest.php index 1426f66c..af79b60d 100644 --- a/tests/Functional/Net/SSH2AgentTest.php +++ b/tests/Functional/Net/SSH2AgentTest.php @@ -46,7 +46,9 @@ class Functional_Net_SSH2AgentTest extends PhpseclibFunctionalTestCase $agent->startSSHForwarding($ssh); $this->assertEquals($username, trim($ssh->exec("ssh " . $username . "@" . $hostname . ' \'whoami\''))); + $agent->stopSSHForwarding($ssh); + $this->assertEquals($username, 'failure'); return $args; } From 8571d0c6d79123b5ff22a5e13b113a010dd3799d Mon Sep 17 00:00:00 2001 From: montdidier Date: Tue, 13 Jan 2015 09:52:01 +0800 Subject: [PATCH 04/12] determining what failure to expect --- tests/Functional/Net/SSH2AgentTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Functional/Net/SSH2AgentTest.php b/tests/Functional/Net/SSH2AgentTest.php index af79b60d..e291cac2 100644 --- a/tests/Functional/Net/SSH2AgentTest.php +++ b/tests/Functional/Net/SSH2AgentTest.php @@ -48,7 +48,7 @@ class Functional_Net_SSH2AgentTest extends PhpseclibFunctionalTestCase $this->assertEquals($username, trim($ssh->exec("ssh " . $username . "@" . $hostname . ' \'whoami\''))); $agent->stopSSHForwarding($ssh); - $this->assertEquals($username, 'failure'); + $this->assertEquals('failure?', trim($ssh->exec("ssh " . $username . "@" . $hostname . ' \'whoami\''))); return $args; } From 25b328c440cdf1e291ec28b8823a3a615ae28e02 Mon Sep 17 00:00:00 2001 From: montdidier Date: Thu, 5 Feb 2015 13:19:57 +0800 Subject: [PATCH 05/12] removed stopSSHForwarding --- phpseclib/System/SSH/Agent.php | 15 --------------- tests/Functional/Net/SSH2AgentTest.php | 3 --- 2 files changed, 18 deletions(-) diff --git a/phpseclib/System/SSH/Agent.php b/phpseclib/System/SSH/Agent.php index 1b09a34d..f07c267d 100644 --- a/phpseclib/System/SSH/Agent.php +++ b/phpseclib/System/SSH/Agent.php @@ -350,21 +350,6 @@ class System_SSH_Agent } } - /** - * Request the remote server to stop forwarding authentication requests to the local SSH agent - * - * @param Net_SSH2 $ssh - * @return Boolean - * @access public - */ - function stopSSHForwarding($ssh) - { - if ($this->request_forwarding) { - $ssh->_close_channel(NET_SSH2_CHANNEL_AGENT_FORWARD); - $this->request_forwarding = false; - } - } - /** * The worker function to make a request to a remote server * asking it to forward authentication requests to the local SSH diff --git a/tests/Functional/Net/SSH2AgentTest.php b/tests/Functional/Net/SSH2AgentTest.php index e291cac2..7ba57d5c 100644 --- a/tests/Functional/Net/SSH2AgentTest.php +++ b/tests/Functional/Net/SSH2AgentTest.php @@ -47,9 +47,6 @@ class Functional_Net_SSH2AgentTest extends PhpseclibFunctionalTestCase $agent->startSSHForwarding($ssh); $this->assertEquals($username, trim($ssh->exec("ssh " . $username . "@" . $hostname . ' \'whoami\''))); - $agent->stopSSHForwarding($ssh); - $this->assertEquals('failure?', trim($ssh->exec("ssh " . $username . "@" . $hostname . ' \'whoami\''))); - return $args; } } From 1803bcac0bb6fc3329a9f3f43a65b8b38619e136 Mon Sep 17 00:00:00 2001 From: montdidier Date: Fri, 6 Feb 2015 11:28:23 +0800 Subject: [PATCH 06/12] moved agent forwarding channel handling to filter method and reusing existing open channels to request forwarding --- phpseclib/Net/SSH2.php | 116 ++++++++++++++++++++++----------- phpseclib/System/SSH/Agent.php | 68 +++++++++---------- 2 files changed, 111 insertions(+), 73 deletions(-) diff --git a/phpseclib/Net/SSH2.php b/phpseclib/Net/SSH2.php index 0a649fd9..6dd7dc1c 100644 --- a/phpseclib/Net/SSH2.php +++ b/phpseclib/Net/SSH2.php @@ -99,8 +99,7 @@ define('NET_SSH2_MASK_WINDOW_ADJUST', 0x00000020); define('NET_SSH2_CHANNEL_EXEC', 0); // PuTTy uses 0x100 define('NET_SSH2_CHANNEL_SHELL', 1); define('NET_SSH2_CHANNEL_SUBSYSTEM', 2); -define('NET_SSH2_CHANNEL_AGENT_REQUEST', 3); -define('NET_SSH2_CHANNEL_AGENT_FORWARD', 4); +define('NET_SSH2_CHANNEL_AGENT_FORWARD', 3); /**#@-*/ /**#@+ @@ -2146,7 +2145,6 @@ class Net_SSH2 $keys = $agent->requestIdentities(); foreach ($keys as $key) { if ($this->_privatekey_login($username, $key)) { - $this->agent->_on_login_success($this); return true; } } @@ -2491,6 +2489,24 @@ class Net_SSH2 } } + /** + * Return an available open channel + * + * @return Integer + * @access public + */ + function _get_open_channel() + { + $channel = NET_SSH2_CHANNEL_EXEC; + do { + if (array_key_exists($channel, $this->channel_status) && $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_OPEN) { + return $channel; + } + } while ($channel++ < NET_SSH2_CHANNEL_SUBSYSTEM); + + user_error("Unable to find an open channel"); + } + /** * Returns the output of an interactive shell * @@ -2844,6 +2860,45 @@ class Net_SSH2 return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); } + $payload = $this->_get_binary_packet(); + break; + case NET_SSH2_MSG_CHANNEL_OPEN: // see http://tools.ietf.org/html/rfc4254#section-5.1 + $this->_string_shift($payload, 1); + extract(unpack('Nlength', $this->_string_shift($payload, 4))); + $data = $this->_string_shift($payload, $length); + extract(unpack('Nserver_channel', $this->_string_shift($payload, 4))); + switch($data) { + case 'auth-agent': + case 'auth-agent@openssh.com': + if (isset($this->agent)) { + $new_channel = NET_SSH2_CHANNEL_AGENT_FORWARD; + + extract(unpack('Nremote_window_size', $this->_string_shift($payload, 4))); + extract(unpack('Nremote_maximum_packet_size', $this->_string_shift($payload, 4))); + + $this->packet_size_client_to_server[$new_channel] = $remote_window_size; + $this->window_size_server_to_client[$new_channel] = $remote_maximum_packet_size; + $this->window_size_client_to_server[$new_channel] = $this->window_size; + + $packet_size = 0x4000; + + $packet = pack('CN4', + NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION, $server_channel, $new_channel, $packet_size, $packet_size); + + $this->server_channels[$new_channel] = $server_channel; + $this->channel_status[$new_channel] = NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION; + if (!$this->_send_binary_packet($packet)) { + return false; + } + } + default: + $packet = pack('CN3a*Na*', + NET_SSH2_MSG_REQUEST_FAILURE, $server_channel, NET_SSH2_OPEN_ADMINISTRATIVELY_PROHIBITED, 0, '', 0, ''); + + if (!$this->_send_binary_packet($packet)) { + return $this->_disconnect(NET_SSH2_DISCONNECT_BY_APPLICATION); + } + } $payload = $this->_get_binary_packet(); break; case NET_SSH2_MSG_CHANNEL_WINDOW_ADJUST: @@ -3011,7 +3066,9 @@ class Net_SSH2 $this->window_size_client_to_server[$channel] = $window_size; $temp = unpack('Npacket_size_client_to_server', $this->_string_shift($response, 4)); $this->packet_size_client_to_server[$channel] = $temp['packet_size_client_to_server']; - return $client_channel == $channel ? true : $this->_get_channel_packet($client_channel, $skip_extended); + $result = $client_channel == $channel ? true : $this->_get_channel_packet($client_channel, $skip_extended); + $this->_on_channel_open(); + return $result; //case NET_SSH2_MSG_CHANNEL_OPEN_FAILURE: default: user_error('Unable to open channel'); @@ -3036,41 +3093,6 @@ class Net_SSH2 // ie. $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_DATA switch ($type) { - case NET_SSH2_MSG_CHANNEL_OPEN: // see http://tools.ietf.org/html/rfc4254#section-5.1 - $data = $this->_string_shift($response, $length); - switch($data) { - case 'auth-agent': - case 'auth-agent@openssh.com': - if (!isset($this->agent)) { - break 2; - } - break; - default: - break 2; - } - - $new_channel = NET_SSH2_CHANNEL_AGENT_FORWARD; - - extract(unpack('Nserver_channel', $this->_string_shift($response, 4))); - extract(unpack('Nremote_window_size', $this->_string_shift($response, 4))); - extract(unpack('Nremote_maximum_packet_size', $this->_string_shift($response, 4))); - - $this->packet_size_client_to_server[$new_channel] = $remote_window_size; - $this->window_size_server_to_client[$new_channel] = $remote_maximum_packet_size; - $this->window_size_client_to_server[$new_channel] = $this->window_size; - - $packet_size = 0x4000; - - $packet = pack('CN4', - NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION, $server_channel, $new_channel, $packet_size, $packet_size); - - $this->server_channels[$new_channel] = $server_channel; - $this->channel_status[$new_channel] = NET_SSH2_MSG_CHANNEL_OPEN_CONFIRMATION; - - if (!$this->_send_binary_packet($packet)) { - return false; - } - break; case NET_SSH2_MSG_CHANNEL_DATA: /* if ($channel == NET_SSH2_CHANNEL_EXEC) { @@ -3528,6 +3550,22 @@ class Net_SSH2 return $this->log_boundary . str_pad(dechex(ord($matches[0])), 2, '0', STR_PAD_LEFT); } + /** + * Helper function for agent->_on_channel_open() + * + * Used when channels are created to inform agent + * of said channel opening. Must be called after + * channel open confirmation received + * + * @access private + */ + function _on_channel_open() + { + if (isset($this->agent)) { + $this->agent->_on_channel_open($this); + } + } + /** * Returns all errors * diff --git a/phpseclib/System/SSH/Agent.php b/phpseclib/System/SSH/Agent.php index f07c267d..ccdfce02 100644 --- a/phpseclib/System/SSH/Agent.php +++ b/phpseclib/System/SSH/Agent.php @@ -64,6 +64,20 @@ define('SYSTEM_SSH_AGENT_FAILURE', 5); define('SYSTEM_SSH_AGENTC_SIGN_REQUEST', 13); // the SSH1 response is SSH_AGENT_RSA_RESPONSE (4) define('SYSTEM_SSH_AGENT_SIGN_RESPONSE', 14); + + +/**@+ + * Agent forwarding status + * + * @access private + */ +// no forwarding requested and not active +define('SYSTEM_SSH_AGENT_FORWARD_NONE', 0); +// request agent forwarding when opportune +define('SYSTEM_SSH_AGENT_FORWARD_REQUEST', 1); +// forwarding has been request and is active +define('SYSTEM_SSH_AGENT_FORWARD_ACTIVE', 2); + /**#@-*/ /** @@ -227,11 +241,11 @@ class System_SSH_Agent var $fsock; /** - * Did we request agent forwarding + * Agent forwarding status * * @access private */ - var $request_forwarding; + var $forward_status = SYSTEM_SSH_AGENT_FORWARD_NONE; /** * Buffer for accumulating forwarded authentication @@ -336,7 +350,8 @@ class System_SSH_Agent } /** - * Request the remote server to forward authentication requests to the local SSH agent + * Signal that agent forwarding should + * be requested when a channel is opened * * @param Net_SSH2 $ssh * @return Boolean @@ -344,16 +359,13 @@ class System_SSH_Agent */ function startSSHForwarding($ssh) { - if (!$this->request_forwarding) { - $this->request_forwarding = true; - $this->_request_forwarding($ssh); + if ($this->forward_status == SYSTEM_SSH_AGENT_FORWARD_NONE) { + $this->forward_status = SYSTEM_SSH_AGENT_FORWARD_REQUEST; } } /** - * The worker function to make a request to a remote server - * asking it to forward authentication requests to the local SSH - * agent + * Request agent forwarding of remote server * * @param Net_SSH2 $ssh * @return Boolean @@ -361,53 +373,41 @@ class System_SSH_Agent */ function _request_forwarding($ssh) { - $ssh->window_size_server_to_client[NET_SSH2_CHANNEL_AGENT_REQUEST] = $ssh->window_size; - $packet_size = 0x4000; - - $packet = pack('CNa*N3', - NET_SSH2_MSG_CHANNEL_OPEN, strlen('session'), 'session', NET_SSH2_CHANNEL_AGENT_REQUEST, $ssh->window_size_server_to_client[NET_SSH2_CHANNEL_AGENT_REQUEST], $packet_size); - - $ssh->channel_status[NET_SSH2_CHANNEL_AGENT_REQUEST] = NET_SSH2_MSG_CHANNEL_OPEN; - - if (!$ssh->_send_binary_packet($packet)) { - return false; - } - - $response = $ssh->_get_channel_packet(NET_SSH2_CHANNEL_AGENT_REQUEST); - if ($response === false) { - return false; - } + $request_channel = $ssh->_get_open_channel(); $packet = pack('CNNa*C', - NET_SSH2_MSG_CHANNEL_REQUEST, $ssh->server_channels[NET_SSH2_CHANNEL_AGENT_REQUEST], strlen('auth-agent-req@openssh.com'), 'auth-agent-req@openssh.com', 1); + NET_SSH2_MSG_CHANNEL_REQUEST, $ssh->server_channels[$request_channel], strlen('auth-agent-req@openssh.com'), 'auth-agent-req@openssh.com', 1); - $ssh->channel_status[NET_SSH2_CHANNEL_AGENT_REQUEST] = NET_SSH2_MSG_CHANNEL_REQUEST; + $ssh->channel_status[$request_channel] = NET_SSH2_MSG_CHANNEL_REQUEST; if (!$ssh->_send_binary_packet($packet)) { return false; } - $response = $ssh->_get_channel_packet(NET_SSH2_CHANNEL_AGENT_REQUEST); + $response = $ssh->_get_channel_packet($request_channel); if ($response === false) { return false; } + $ssh->channel_status[$request_channel] = NET_SSH2_MSG_CHANNEL_OPEN; + $this->forward_status = SYSTEM_SSH_AGENT_FORWARD_ACTIVE; + return true; } /** - * On Successful Login + * On successful channel open * - * This method should be called upon successful SSH login if you - * wish to give the SSH Agent an opportunity to take further - * action. i.e. request agent forwarding + * This method is called upon successful channel + * open to give the SSH Agent an opportunity + * to take further action. i.e. request agent forwarding * * @param Net_SSH2 $ssh * @access private */ - function _on_login_success($ssh) + function _on_channel_open($ssh) { - if ($this->request_forwarding) { + if ($this->forward_status == SYSTEM_SSH_AGENT_FORWARD_REQUEST) { $this->_request_forwarding($ssh); } } From 23c65c383945cfc9c2293f45a7cbc6f1a68178ec Mon Sep 17 00:00:00 2001 From: terrafrost Date: Thu, 19 Mar 2015 07:53:19 -0500 Subject: [PATCH 07/12] SSH2: timeout improvements make it so that the timeout in the constructor behaves in the same way that timeout's set via setTimeout() do. eg. isTimeout() tells you if a timeout was thrown etc. --- phpseclib/Net/SSH2.php | 49 ++++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/phpseclib/Net/SSH2.php b/phpseclib/Net/SSH2.php index d6134562..54184730 100644 --- a/phpseclib/Net/SSH2.php +++ b/phpseclib/Net/SSH2.php @@ -938,7 +938,7 @@ class Net_SSH2 $this->host = $host; $this->port = $port; - $this->connectionTimeout = $timeout; + $this->timeout = $timeout; } /** @@ -955,36 +955,24 @@ class Net_SSH2 $this->bitmap |= NET_SSH2_MASK_CONSTRUCTOR; - $timeout = $this->connectionTimeout; + $this->curTimeout = $this->timeout; + $host = $this->host . ':' . $this->port; $this->last_packet = strtok(microtime(), ' ') + strtok(''); // == microtime(true) in PHP5 $start = strtok(microtime(), ' ') + strtok(''); // http://php.net/microtime#61838 - $this->fsock = @fsockopen($this->host, $this->port, $errno, $errstr, $timeout); + $this->fsock = @fsockopen($this->host, $this->port, $errno, $errstr, $this->curTimeout); if (!$this->fsock) { user_error(rtrim("Cannot connect to $host. Error $errno. $errstr")); return false; } $elapsed = strtok(microtime(), ' ') + strtok('') - $start; - $timeout-= $elapsed; + $this->curTimeout-= $elapsed; - if ($timeout <= 0) { - user_error("Cannot connect to $host. Timeout error"); - return false; - } - - $read = array($this->fsock); - $write = $except = null; - - $sec = floor($timeout); - $usec = 1000000 * ($timeout - $sec); - - // on windows this returns a "Warning: Invalid CRT parameters detected" error - // the !count() is done as a workaround for - if (!@stream_select($read, $write, $except, $sec, $usec) && !count($read)) { - user_error("Cannot connect to $host. Banner timeout"); + if ($this->curTimeout <= 0) { + $this->is_timeout = true; return false; } @@ -1002,6 +990,29 @@ class Net_SSH2 $extra.= $temp; $temp = ''; } + + $read = array($this->fsock); + + if ($this->curTimeout) { + if ($this->curTimeout < 0) { + $this->is_timeout = true; + return false; + } + $read = array($this->fsock); + $write = $except = null; + $start = microtime(true); + $sec = floor($this->curTimeout); + $usec = 1000000 * ($this->curTimeout - $sec); + // on windows this returns a "Warning: Invalid CRT parameters detected" error + // the !count() is done as a workaround for + if (!@stream_select($read, $write, $except, $sec, $usec) && !count($read)) { + $this->is_timeout = true; + return false; + } + $elapsed = microtime(true) - $start; + $this->curTimeout-= $elapsed; + } + $temp.= fgets($this->fsock, 255); } From dfd57dfb897dd848dc7e434c36cd4885baabb73d Mon Sep 17 00:00:00 2001 From: terrafrost Date: Thu, 19 Mar 2015 22:39:43 -0500 Subject: [PATCH 08/12] SSH2: rm redundant code and make php4 compatable --- phpseclib/Net/SSH2.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/phpseclib/Net/SSH2.php b/phpseclib/Net/SSH2.php index 54184730..10ffd673 100644 --- a/phpseclib/Net/SSH2.php +++ b/phpseclib/Net/SSH2.php @@ -991,8 +991,6 @@ class Net_SSH2 $temp = ''; } - $read = array($this->fsock); - if ($this->curTimeout) { if ($this->curTimeout < 0) { $this->is_timeout = true; @@ -1000,7 +998,7 @@ class Net_SSH2 } $read = array($this->fsock); $write = $except = null; - $start = microtime(true); + $start = strtok(microtime(), ' ') + strtok(''); $sec = floor($this->curTimeout); $usec = 1000000 * ($this->curTimeout - $sec); // on windows this returns a "Warning: Invalid CRT parameters detected" error @@ -1009,7 +1007,7 @@ class Net_SSH2 $this->is_timeout = true; return false; } - $elapsed = microtime(true) - $start; + $elapsed = strtok(microtime(), ' ') + strtok('') - $start; $this->curTimeout-= $elapsed; } From 9723acc8853c5fe7ea9bda4a9a711a3e07575c84 Mon Sep 17 00:00:00 2001 From: montdidier Date: Tue, 24 Mar 2015 13:38:56 +0800 Subject: [PATCH 09/12] preference isset over array_key_exists, return false on failure, break after return channel opened --- phpseclib/Net/SSH2.php | 5 +++-- phpseclib/System/SSH/Agent.php | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/phpseclib/Net/SSH2.php b/phpseclib/Net/SSH2.php index 6dd7dc1c..4f55eb64 100644 --- a/phpseclib/Net/SSH2.php +++ b/phpseclib/Net/SSH2.php @@ -2499,12 +2499,12 @@ class Net_SSH2 { $channel = NET_SSH2_CHANNEL_EXEC; do { - if (array_key_exists($channel, $this->channel_status) && $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_OPEN) { + if (isset($this->channel_status[$channel]) && $this->channel_status[$channel] == NET_SSH2_MSG_CHANNEL_OPEN) { return $channel; } } while ($channel++ < NET_SSH2_CHANNEL_SUBSYSTEM); - user_error("Unable to find an open channel"); + return false; } /** @@ -2891,6 +2891,7 @@ class Net_SSH2 return false; } } + break; default: $packet = pack('CN3a*Na*', NET_SSH2_MSG_REQUEST_FAILURE, $server_channel, NET_SSH2_OPEN_ADMINISTRATIVELY_PROHIBITED, 0, '', 0, ''); diff --git a/phpseclib/System/SSH/Agent.php b/phpseclib/System/SSH/Agent.php index ccdfce02..86ca18ac 100644 --- a/phpseclib/System/SSH/Agent.php +++ b/phpseclib/System/SSH/Agent.php @@ -374,6 +374,10 @@ class System_SSH_Agent function _request_forwarding($ssh) { $request_channel = $ssh->_get_open_channel(); + if ($request_channel === false) { + user_error("failed to request channel"); + return false; + } $packet = pack('CNNa*C', NET_SSH2_MSG_CHANNEL_REQUEST, $ssh->server_channels[$request_channel], strlen('auth-agent-req@openssh.com'), 'auth-agent-req@openssh.com', 1); From 3ff4212b9291f2c863a742f5692ca7312b81decb Mon Sep 17 00:00:00 2001 From: montdidier Date: Tue, 24 Mar 2015 13:40:42 +0800 Subject: [PATCH 10/12] removed unwarrented user_error --- phpseclib/System/SSH/Agent.php | 1 - 1 file changed, 1 deletion(-) diff --git a/phpseclib/System/SSH/Agent.php b/phpseclib/System/SSH/Agent.php index 86ca18ac..da20697d 100644 --- a/phpseclib/System/SSH/Agent.php +++ b/phpseclib/System/SSH/Agent.php @@ -375,7 +375,6 @@ class System_SSH_Agent { $request_channel = $ssh->_get_open_channel(); if ($request_channel === false) { - user_error("failed to request channel"); return false; } From 1294b08675743afc2533932ccce74d420827e6d4 Mon Sep 17 00:00:00 2001 From: terrafrost Date: Fri, 27 Mar 2015 22:32:38 -0500 Subject: [PATCH 11/12] SSH2: rm unused $connectionTimeout variable --- phpseclib/Net/SSH2.php | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/phpseclib/Net/SSH2.php b/phpseclib/Net/SSH2.php index 10ffd673..ce5993d8 100644 --- a/phpseclib/Net/SSH2.php +++ b/phpseclib/Net/SSH2.php @@ -803,21 +803,6 @@ class Net_SSH2 */ var $port; - /** - * Timeout for initial connection - * - * Set by the constructor call. Calling setTimeout() is optional. If it's not called functions like - * exec() won't timeout unless some PHP setting forces it too. The timeout specified in the constructor, - * however, is non-optional. There will be a timeout, whether or not you set it. If you don't it'll be - * 10 seconds. It is used by fsockopen() and the initial stream_select in that function. - * - * @see Net_SSH2::Net_SSH2() - * @see Net_SSH2::_connect() - * @var Integer - * @access private - */ - var $connectionTimeout; - /** * Number of columns for terminal window size * From 33645f5297e92bf240276fb1d1f93e4a19953b5f Mon Sep 17 00:00:00 2001 From: terrafrost Date: Sun, 29 Mar 2015 10:58:05 -0500 Subject: [PATCH 12/12] SSH2: missed a file in the merge --- phpseclib/Net/SSH2.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpseclib/Net/SSH2.php b/phpseclib/Net/SSH2.php index 516fa425..9bd6447c 100644 --- a/phpseclib/Net/SSH2.php +++ b/phpseclib/Net/SSH2.php @@ -3029,7 +3029,7 @@ class SSH2 extract(unpack('Nlength', $this->_string_shift($response, 4))); $data = $this->_string_shift($response, $length); - if ($channel == NET_SSH2_CHANNEL_AGENT_FORWARD) { + if ($channel == self::CHANNEL_AGENT_FORWARD) { $agent_response = $this->agent->_forward_data($data); if (!is_bool($agent_response)) { $this->_send_channel_packet($channel, $agent_response);