diff --git a/SOURCES/CVE-2018-7750.diff b/SOURCES/CVE-2018-7750.diff new file mode 100644 index 0000000..cc52991 --- /dev/null +++ b/SOURCES/CVE-2018-7750.diff @@ -0,0 +1,174 @@ +diff --git a/paramiko/common.py b/paramiko/common.py +index 0b0cc2a..50355f6 100644 +--- a/paramiko/common.py ++++ b/paramiko/common.py +@@ -32,6 +32,7 @@ MSG_USERAUTH_INFO_REQUEST, MSG_USERAUTH_INFO_RESPONSE = range(60, 62) + MSG_USERAUTH_GSSAPI_RESPONSE, MSG_USERAUTH_GSSAPI_TOKEN = range(60, 62) + MSG_USERAUTH_GSSAPI_EXCHANGE_COMPLETE, MSG_USERAUTH_GSSAPI_ERROR,\ + MSG_USERAUTH_GSSAPI_ERRTOK, MSG_USERAUTH_GSSAPI_MIC = range(63, 67) ++HIGHEST_USERAUTH_MESSAGE_ID = 79 + MSG_GLOBAL_REQUEST, MSG_REQUEST_SUCCESS, MSG_REQUEST_FAILURE = range(80, 83) + MSG_CHANNEL_OPEN, MSG_CHANNEL_OPEN_SUCCESS, MSG_CHANNEL_OPEN_FAILURE, \ + MSG_CHANNEL_WINDOW_ADJUST, MSG_CHANNEL_DATA, MSG_CHANNEL_EXTENDED_DATA, \ +diff --git a/paramiko/transport.py b/paramiko/transport.py +index 7906c9f..31df82a 100644 +--- a/paramiko/transport.py ++++ b/paramiko/transport.py +@@ -49,7 +49,8 @@ from paramiko.common import xffffffff, cMSG_CHANNEL_OPEN, cMSG_IGNORE, \ + MSG_CHANNEL_SUCCESS, MSG_CHANNEL_FAILURE, MSG_CHANNEL_DATA, \ + MSG_CHANNEL_EXTENDED_DATA, MSG_CHANNEL_WINDOW_ADJUST, MSG_CHANNEL_REQUEST, \ + MSG_CHANNEL_EOF, MSG_CHANNEL_CLOSE, MIN_WINDOW_SIZE, MIN_PACKET_SIZE, \ +- MAX_WINDOW_SIZE, DEFAULT_WINDOW_SIZE, DEFAULT_MAX_PACKET_SIZE ++ MAX_WINDOW_SIZE, DEFAULT_WINDOW_SIZE, DEFAULT_MAX_PACKET_SIZE, \ ++ HIGHEST_USERAUTH_MESSAGE_ID + from paramiko.compress import ZlibCompressor, ZlibDecompressor + from paramiko.dsskey import DSSKey + from paramiko.kex_gex import KexGex, KexGexSHA256 +@@ -1720,6 +1721,43 @@ class Transport (threading.Thread, ClosingContextManager): + max_packet_size = self.default_max_packet_size + return clamp_value(MIN_PACKET_SIZE, max_packet_size, MAX_WINDOW_SIZE) + ++ def _ensure_authed(self, ptype, message): ++ """ ++ Checks message type against current auth state. ++ ++ If server mode, and auth has not succeeded, and the message is of a ++ post-auth type (channel open or global request) an appropriate error ++ response Message is crafted and returned to caller for sending. ++ ++ Otherwise (client mode, authed, or pre-auth message) returns None. ++ """ ++ if ( ++ not self.server_mode ++ or ptype <= HIGHEST_USERAUTH_MESSAGE_ID ++ or self.is_authenticated() ++ ): ++ return None ++ # WELP. We must be dealing with someone trying to do non-auth things ++ # without being authed. Tell them off, based on message class. ++ reply = Message() ++ # Global requests have no details, just failure. ++ if ptype == MSG_GLOBAL_REQUEST: ++ reply.add_byte(cMSG_REQUEST_FAILURE) ++ # Channel opens let us reject w/ a specific type + message. ++ elif ptype == MSG_CHANNEL_OPEN: ++ kind = message.get_text() ++ chanid = message.get_int() ++ reply.add_byte(cMSG_CHANNEL_OPEN_FAILURE) ++ reply.add_int(chanid) ++ reply.add_int(OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED) ++ reply.add_string('') ++ reply.add_string('en') ++ # NOTE: Post-open channel messages do not need checking; the above will ++ # reject attemps to open channels, meaning that even if a malicious ++ # user tries to send a MSG_CHANNEL_REQUEST, it will simply fall under ++ # the logic that handles unknown channel IDs (as the channel list will ++ # be empty.) ++ return reply + + def run(self): + # (use the exposed "run" method, because if we specify a thread target +@@ -1779,7 +1817,11 @@ class Transport (threading.Thread, ClosingContextManager): + continue + + if ptype in self._handler_table: +- self._handler_table[ptype](self, m) ++ error_msg = self._ensure_authed(ptype, m) ++ if error_msg: ++ self._send_message(error_msg) ++ else: ++ self._handler_table[ptype](self, m) + elif ptype in self._channel_handler_table: + chanid = m.get_int() + chan = self._channels.get(chanid) +diff --git a/tests/test_transport.py b/tests/test_transport.py +index d81ad8f..1305cd5 100644 +--- a/tests/test_transport.py ++++ b/tests/test_transport.py +@@ -32,7 +32,7 @@ from hashlib import sha1 + import unittest + + from paramiko import Transport, SecurityOptions, ServerInterface, RSAKey, DSSKey, \ +- SSHException, ChannelException, Packetizer ++ SSHException, ChannelException, Packetizer, Channel + from paramiko import AUTH_FAILED, AUTH_SUCCESSFUL + from paramiko import OPEN_SUCCEEDED, OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED + from paramiko.common import MSG_KEXINIT, cMSG_CHANNEL_WINDOW_ADJUST, \ +@@ -87,7 +87,11 @@ class NullServer (ServerInterface): + + def check_global_request(self, kind, msg): + self._global_request = kind +- return False ++ # NOTE: for w/e reason, older impl of this returned False always, even ++ # tho that's only supposed to occur if the request cannot be served. ++ # For now, leaving that the default unless test supplies specific ++ # 'acceptable' request kind ++ return kind == 'acceptable' + + def check_channel_x11_request(self, channel, single_connection, auth_protocol, auth_cookie, screen_number): + self._x11_single_connection = single_connection +@@ -125,7 +129,9 @@ class TransportTest(unittest.TestCase): + self.socks.close() + self.sockc.close() + +- def setup_test_server(self, client_options=None, server_options=None): ++ def setup_test_server( ++ self, client_options=None, server_options=None, connect_kwargs=None, ++ ): + host_key = RSAKey.from_private_key_file(test_path('test_rsa.key')) + public_host_key = RSAKey(data=host_key.asbytes()) + self.ts.add_server_key(host_key) +@@ -139,8 +145,13 @@ class TransportTest(unittest.TestCase): + self.server = NullServer() + self.assertTrue(not event.is_set()) + self.ts.start_server(event, self.server) +- self.tc.connect(hostkey=public_host_key, +- username='slowdive', password='pygmalion') ++ if connect_kwargs is None: ++ connect_kwargs = dict( ++ hostkey=public_host_key, ++ username='slowdive', ++ password='pygmalion', ++ ) ++ self.tc.connect(**connect_kwargs) + event.wait(1.0) + self.assertTrue(event.is_set()) + self.assertTrue(self.ts.is_active()) +@@ -846,3 +857,37 @@ class TransportTest(unittest.TestCase): + self.assertEqual([chan], r) + self.assertEqual([], w) + self.assertEqual([], e) ++ ++ def test_server_rejects_open_channel_without_auth(self): ++ try: ++ self.setup_test_server(connect_kwargs={}) ++ self.tc.open_session() ++ except ChannelException as e: ++ assert e.code == OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED ++ else: ++ assert False, "Did not raise ChannelException!" ++ ++ def test_server_rejects_arbitrary_global_request_without_auth(self): ++ self.setup_test_server(connect_kwargs={}) ++ # NOTE: this dummy global request kind would normally pass muster ++ # from the test server. ++ self.tc.global_request('acceptable') ++ # Global requests never raise exceptions, even on failure (not sure why ++ # this was the original design...ugh.) Best we can do to tell failure ++ # happened is that the client transport's global_response was set back ++ # to None; if it had succeeded, it would be the response Message. ++ err = "Unauthed global response incorrectly succeeded!" ++ assert self.tc.global_response is None, err ++ ++ def test_server_rejects_port_forward_without_auth(self): ++ # NOTE: at protocol level port forward requests are treated same as a ++ # regular global request, but Paramiko server implements a special-case ++ # method for it, so it gets its own test. (plus, THAT actually raises ++ # an exception on the client side, unlike the general case...) ++ self.setup_test_server(connect_kwargs={}) ++ try: ++ self.tc.request_port_forward('localhost', 1234) ++ except SSHException as e: ++ assert "forwarding request denied" in str(e) ++ else: ++ assert False, "Did not raise SSHException!" diff --git a/SPECS/python-paramiko.spec b/SPECS/python-paramiko.spec index 2b8a1ef..0e12594 100644 --- a/SPECS/python-paramiko.spec +++ b/SPECS/python-paramiko.spec @@ -10,7 +10,7 @@ Name: python-%{srcname} Version: 2.1.1 -Release: 2%{?dist} +Release: 4%{?dist} Provides: python2-paramiko = %{version}-%{release} Summary: SSH2 protocol library for python @@ -19,12 +19,16 @@ License: LGPLv2+ URL: https://github.com/paramiko/paramiko Source0: %{url}/archive/%{version}/%{srcname}-%{version}.tar.gz +Patch0: CVE-2018-7750.diff + BuildArch: noarch Requires: python-cryptography +Requires: python2-pyasn1 BuildRequires: python2-devel BuildRequires: python-setuptools BuildRequires: python-cryptography +BuildRequires: python2-pyasn1 %global paramiko_desc \ Paramiko (a combination of the esperanto words for "paranoid" and "friend") is\ a module for python 2.3 or greater that implements the SSH2 protocol for secure\ @@ -72,7 +76,7 @@ Requires: %{name} = %{version}-%{release} This is the documentation and demos. %prep -%autosetup -n %{srcname}-%{version} +%autosetup -n %{srcname}-%{version} -p1 chmod a-x demos/* sed -i -e '/^#!/,1d' demos/* @@ -116,6 +120,17 @@ rm -f html/.buildinfo %doc html/ demos/ %changelog +* Thu Mar 22 2018 Pavel Cahyna - 2.1.1-4 +- Add a dependency on python2-pyasn1. It used to be a dependency + of python2-cryptography, but it is not the case with newer versions. + (RHBZ #1559133) + +* Wed Mar 21 2018 Pavel Cahyna - 2.1.1-3 +- Fix a security flaw (CVE-2018-7750) in Paramiko's server + mode (emphasis on **server** mode; this does **not** impact *client* use!) + Backported from 2.1.5. + Resolves #1557142 + * Fri May 12 2017 Pavel Cahyna - 2.1.1-2 - Rebuild for RHEL 7.4 Extras