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