!48 [sync] PR-47: fix CVE-2023-48795

From: @openeuler-sync-bot 
Reviewed-by: @lyn1001 
Signed-off-by: @lyn1001
This commit is contained in:
openeuler-ci-bot 2024-01-15 02:00:35 +00:00 committed by Gitee
commit 664e911b8d
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
8 changed files with 1114 additions and 1 deletions

View File

@ -0,0 +1,67 @@
From be3ffc18cc466e0b0a877d716721353c12561bcc Mon Sep 17 00:00:00 2001
From: Jeff Forcier <jeff@bitprophet.org>
Date: Fri, 15 Dec 2023 22:14:48 -0500
Subject: [PATCH] Make ext-info faux-KexAlgorithm detection more robust
Reference:https://github.com/paramiko/paramiko/commit/be3ffc18cc466e0b0a877d716721353c12561bcc
Conflict:The context of the changelog is adapted due to different versions
---
paramiko/transport.py | 5 +++--
sites/www/changelog.rst | 3 +++
tests/test_transport.py | 8 ++++++--
3 files changed, 12 insertions(+), 4 deletions(-)
diff --git a/paramiko/transport.py b/paramiko/transport.py
index 68cc195..fd26371 100644
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -2429,8 +2429,9 @@ class Transport(threading.Thread, ClosingContextManager):
# Strip out ext-info "kex algo"
self._remote_ext_info = None
- if kex_algo_list[-1].startswith("ext-info-"):
- self._remote_ext_info = kex_algo_list.pop()
+ for i, algo in enumerate(kex_algo_list):
+ if algo.startswith("ext-info-"):
+ self._remote_ext_info = kex_algo_list.pop(i)
# as a server, we pick the first item in the client's list that we
# support.
diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst
index 29754bc..f180e77 100644
--- a/sites/www/changelog.rst
+++ b/sites/www/changelog.rst
@@ -2,6 +2,9 @@
Changelog
=========
+- :bug:`-` Tweak ``ext-info-(c|s)`` detection during KEXINIT protocol phase;
+ the original implementation made assumptions based on an OpenSSH
+ implementation detail.
- :release:`2.11.0 <2022-05-16>`
- :release:`2.10.5 <2022-05-16>`
- :release:`2.9.5 <2022-05-16>`
diff --git a/tests/test_transport.py b/tests/test_transport.py
index 98a7d30..6bc0be8 100644
--- a/tests/test_transport.py
+++ b/tests/test_transport.py
@@ -1350,10 +1350,14 @@ class TestSHA2SignatureKeyExchange(unittest.TestCase):
class TestExtInfo(unittest.TestCase):
- def test_ext_info_handshake(self):
+ def test_ext_info_handshake_exposed_in_client_kexinit(self):
with server() as (tc, _):
+ # NOTE: this is latest KEXINIT /sent by us/ (Transport retains it)
kex = tc._get_latest_kex_init()
- assert kex["kex_algo_list"][-1] == "ext-info-c"
+ # flag in KexAlgorithms list
+ assert "ext-info-c" in kex["kex_algo_list"]
+ # data stored on Transport after hearing back from a compatible
+ # server (such as ourselves in server mode)
assert tc.server_extensions == {
"server-sig-algs": b"ssh-ed25519,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,rsa-sha2-512,rsa-sha2-256,ssh-rsa,ssh-dss" # noqa
}
--
2.33.0

View File

@ -0,0 +1,171 @@
From 773a174fb1e40e1d18dbe2625e16337ea401119e Mon Sep 17 00:00:00 2001
From: Jeff Forcier <jeff@bitprophet.org>
Date: Fri, 15 Dec 2023 23:59:12 -0500
Subject: [PATCH] Basic strict-kex-mode agreement mechanics work
Reference:https://github.com/paramiko/paramiko/commit/773a174fb1e40e1d18dbe2625e16337ea401119e
Conflict:The comments are different. Therefore, the transport.py file is adapted
Due to different versions, some test cases do not exist. Therefore, context adaptation is required when new test cases are added
---
paramiko/transport.py | 38 +++++++++++++++++++++++++++++++++---
tests/test_transport.py | 43 +++++++++++++++++++++++++++++++++++++++++
2 files changed, 78 insertions(+), 3 deletions(-)
diff --git a/paramiko/transport.py b/paramiko/transport.py
index fd26371..2d6d581 100644
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -329,6 +329,7 @@ class Transport(threading.Thread, ClosingContextManager):
gss_deleg_creds=True,
disabled_algorithms=None,
server_sig_algs=True,
+ strict_kex=True,
):
"""
Create a new SSH session over an existing socket, or socket-like
@@ -395,6 +396,10 @@ class Transport(threading.Thread, ClosingContextManager):
Whether to send an extra message to compatible clients, in server
mode, with a list of supported pubkey algorithms. Default:
``True``.
+ :param bool strict_kex:
+ Whether to advertise (and implement, if client also advertises
+ support for) a "strict kex" mode for safer handshaking. Default:
+ ``True``.
.. versionchanged:: 1.15
Added the ``default_window_size`` and ``default_max_packet_size``
@@ -405,10 +410,14 @@ class Transport(threading.Thread, ClosingContextManager):
Added the ``disabled_algorithms`` kwarg.
.. versionchanged:: 2.9
Added the ``server_sig_algs`` kwarg.
+ .. versionchanged:: 3.4
+ Added the ``strict_kex`` kwarg.
"""
self.active = False
self.hostname = None
self.server_extensions = {}
+ self.advertise_strict_kex = strict_kex
+ self.agreed_on_strict_kex = False
if isinstance(sock, string_types):
# convert "host:port" into (host, port)
@@ -2342,12 +2351,18 @@ class Transport(threading.Thread, ClosingContextManager):
)
else:
available_server_keys = self.preferred_keys
- # Signal support for MSG_EXT_INFO.
+ # Signal support for MSG_EXT_INFO so server will send it to us.
# NOTE: doing this here handily means we don't even consider this
# value when agreeing on real kex algo to use (which is a common
# pitfall when adding this apparently).
kex_algos.append("ext-info-c")
+ # Similar to ext-info, but used in both server modes, so done outside
+ # of above if/else.
+ if self.advertise_strict_kex:
+ which = "s" if self.server_mode else "c"
+ kex_algos.append(f"kex-strict-{which}-v00@openssh.com")
+
m = Message()
m.add_byte(cMSG_KEXINIT)
m.add_bytes(os.urandom(16))
@@ -2427,11 +2442,28 @@ class Transport(threading.Thread, ClosingContextManager):
self._log(DEBUG, "kex follows: {}".format(kex_follows))
self._log(DEBUG, "=== Key exchange agreements ===")
- # Strip out ext-info "kex algo"
+ # Record, and strip out, ext-info and/or strict-kex non-algorithms
self._remote_ext_info = None
+ self._remote_strict_kex = None
+ to_pop = []
for i, algo in enumerate(kex_algo_list):
if algo.startswith("ext-info-"):
- self._remote_ext_info = kex_algo_list.pop(i)
+ self._remote_ext_info = algo
+ to_pop.insert(0, i)
+ elif algo.startswith("kex-strict-"):
+ # NOTE: this is what we are expecting from the /remote/ end.
+ which = "c" if self.server_mode else "s"
+ expected = f"kex-strict-{which}-v00@openssh.com"
+ # Set strict mode if agreed.
+ self.agreed_on_strict_kex = (
+ algo == expected and self.advertise_strict_kex
+ )
+ self._log(
+ DEBUG, f"Strict kex mode: {self.agreed_on_strict_kex}"
+ )
+ to_pop.insert(0, i)
+ for i in to_pop:
+ kex_algo_list.pop(i)
# as a server, we pick the first item in the client's list that we
# support.
diff --git a/tests/test_transport.py b/tests/test_transport.py
index 6bc0be8..c8cd498 100644
--- a/tests/test_transport.py
+++ b/tests/test_transport.py
@@ -24,6 +24,7 @@ from __future__ import with_statement
from binascii import hexlify
from contextlib import contextmanager
+import itertools
import select
import socket
import time
@@ -63,6 +64,7 @@ from paramiko.message import Message
from .util import needs_builtin, _support, requires_sha1_signing, slow
from .loop import LoopSocket
+from pytest import skip, mark
LONG_BANNER = """\
@@ -1463,3 +1465,44 @@ class TestSHA2SignaturePubkeys(unittest.TestCase):
) as (tc, ts):
assert tc.is_authenticated()
assert tc._agreed_pubkey_algorithm == "rsa-sha2-256"
+
+
+class TestStrictKex:
+ def test_kex_algos_includes_kex_strict_c(self):
+ with server() as (tc, _):
+ kex = tc._get_latest_kex_init()
+ assert "kex-strict-c-v00@openssh.com" in kex["kex_algo_list"]
+
+ @mark.parametrize(
+ "server_active,client_active",
+ itertools.product([True, False], repeat=2),
+ )
+ def test_mode_agreement(self, server_active, client_active):
+ with server(
+ server_init=dict(strict_kex=server_active),
+ client_init=dict(strict_kex=client_active),
+ ) as (tc, ts):
+ if server_active and client_active:
+ assert tc.agreed_on_strict_kex is True
+ assert ts.agreed_on_strict_kex is True
+ else:
+ assert tc.agreed_on_strict_kex is False
+ assert ts.agreed_on_strict_kex is False
+
+ def test_mode_advertised_by_default(self):
+ # NOTE: no explicit strict_kex overrides...
+ with server() as (tc, ts):
+ assert all(
+ (
+ tc.advertise_strict_kex,
+ tc.agreed_on_strict_kex,
+ ts.advertise_strict_kex,
+ ts.agreed_on_strict_kex,
+ )
+ )
+
+ def test_sequence_numbers_reset_on_newkeys(self):
+ skip()
+
+ def test_error_raised_on_out_of_order_handshakes(self):
+ skip()
--
2.33.0

View File

@ -0,0 +1,115 @@
From f4dedacb9040d27d9844f51c81c28e0247d3e4a3 Mon Sep 17 00:00:00 2001
From: Jeff Forcier <jeff@bitprophet.org>
Date: Sat, 16 Dec 2023 13:02:05 -0500
Subject: [PATCH] Raise new exception type when unexpected messages appear
Reference:https://github.com/paramiko/paramiko/commit/f4dedacb9040d27d9844f51c81c28e0247d3e4a3
Conflict:The changlog file is adapted for different versions. The context of the test case import module is adapted.
---
paramiko/__init__.py | 1 +
paramiko/ssh_exception.py | 9 +++++++++
paramiko/transport.py | 6 +++++-
tests/test_transport.py | 22 +++++++++++++++++++---
4 files changed, 34 insertions(+), 4 deletions(-)
diff --git a/paramiko/__init__.py b/paramiko/__init__.py
index cbc240a..1bc91d0 100644
--- a/paramiko/__init__.py
+++ b/paramiko/__init__.py
@@ -43,6 +43,7 @@ from paramiko.ssh_exception import (
ConfigParseError,
CouldNotCanonicalize,
IncompatiblePeer,
+ MessageOrderError,
PasswordRequiredException,
ProxyCommandFailure,
SSHException,
diff --git a/paramiko/ssh_exception.py b/paramiko/ssh_exception.py
index 620ab25..8a1413b 100644
--- a/paramiko/ssh_exception.py
+++ b/paramiko/ssh_exception.py
@@ -235,3 +235,12 @@ class ConfigParseError(SSHException):
"""
pass
+
+
+class MessageOrderError(SSHException):
+ """
+ Out-of-order protocol messages were received, violating "strict kex" mode.
+ .. versionadded:: 3.4
+ """
+
+ pass
diff --git a/paramiko/transport.py b/paramiko/transport.py
index 2d6d581..eb1bcd6 100644
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -110,6 +110,7 @@ from paramiko.ssh_exception import (
BadAuthenticationType,
ChannelException,
IncompatiblePeer,
+ MessageOrderError,
ProxyCommandFailure,
)
from paramiko.util import retry_on_signal, ClosingContextManager, clamp_value
@@ -2129,7 +2130,10 @@ class Transport(threading.Thread, ClosingContextManager):
continue
if len(self._expected_packet) > 0:
if ptype not in self._expected_packet:
- raise SSHException(
+ exc_class = SSHException
+ if self.agreed_on_strict_kex:
+ exc_class = MessageOrderError
+ raise exc_class(
"Expecting packet from {!r}, got {:d}".format(
self._expected_packet, ptype
)
diff --git a/tests/test_transport.py b/tests/test_transport.py
index c8cd498..19023eb 100644
--- a/tests/test_transport.py
+++ b/tests/test_transport.py
@@ -42,6 +42,7 @@ from paramiko import (
SSHException,
AuthenticationException,
IncompatiblePeer,
+ MessageOrderError,
SecurityOptions,
ServerInterface,
Transport,
@@ -64,7 +65,7 @@ from paramiko.message import Message
from .util import needs_builtin, _support, requires_sha1_signing, slow
from .loop import LoopSocket
-from pytest import skip, mark
+from pytest import skip, mark, raises
LONG_BANNER = """\
@@ -1504,5 +1505,20 @@ class TestStrictKex:
def test_sequence_numbers_reset_on_newkeys(self):
skip()
- def test_error_raised_on_out_of_order_handshakes(self):
- skip()
+ def test_MessageOrderError_raised_on_out_of_order_messages(self):
+ with raises(MessageOrderError):
+ with server() as (tc, _):
+ # A bit artificial as it's outside kexinit/handshake, but much
+ # easier to trigger and still in line with behavior under test
+ tc._expect_packet(MSG_KEXINIT)
+ tc.open_session()
+
+ def test_SSHException_raised_on_out_of_order_messages_when_not_strict(self):
+ # This is kind of dumb (either situation is still fatal!) but whatever,
+ # may as well be strict with our new strict flag...
+ with raises(SSHException) as info: # would be true either way, but
+ with server(client_init=dict(strict_kex=False),
+ ) as (tc, _):
+ tc._expect_packet(MSG_KEXINIT)
+ tc.open_session()
+ assert info.type is SSHException # NOT MessageOrderError!
--
2.33.0

View File

@ -0,0 +1,190 @@
From 75e311d3c0845a316b6e7b3fae2488d86ad5a270 Mon Sep 17 00:00:00 2001
From: Jeff Forcier <jeff@bitprophet.org>
Date: Sat, 16 Dec 2023 16:17:58 -0500
Subject: [PATCH] Enforce zero seqno on kexinit
Reference:https://github.com/paramiko/paramiko/commit/75e311d3c0845a316b6e7b3fae2488d86ad5a270
Conflict:Context adaptation exists in the changelog.rst file due to version inconsistency.
---
paramiko/transport.py | 18 ++++++++++--
sites/www/changelog.rst | 3 ++
tests/test_transport.py | 61 +++++++++++++++++++++++++++++++++++++----
3 files changed, 74 insertions(+), 8 deletions(-)
diff --git a/paramiko/transport.py b/paramiko/transport.py
index eb1bcd6..9f976a2 100644
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -331,6 +331,7 @@ class Transport(threading.Thread, ClosingContextManager):
disabled_algorithms=None,
server_sig_algs=True,
strict_kex=True,
+ packetizer_class=None,
):
"""
Create a new SSH session over an existing socket, or socket-like
@@ -401,6 +402,9 @@ class Transport(threading.Thread, ClosingContextManager):
Whether to advertise (and implement, if client also advertises
support for) a "strict kex" mode for safer handshaking. Default:
``True``.
+ :param packetizer_class:
+ Which class to use for instantiating the internal packet handler.
+ Default: ``None`` (i.e.: use `Packetizer` as normal).
.. versionchanged:: 1.15
Added the ``default_window_size`` and ``default_max_packet_size``
@@ -413,6 +417,8 @@ class Transport(threading.Thread, ClosingContextManager):
Added the ``server_sig_algs`` kwarg.
.. versionchanged:: 3.4
Added the ``strict_kex`` kwarg.
+ .. versionchanged:: 3.4
+ Added the ``packetizer_class`` kwarg.
"""
self.active = False
self.hostname = None
@@ -460,7 +466,7 @@ class Transport(threading.Thread, ClosingContextManager):
self.sock.settimeout(self._active_check_timeout)
# negotiated crypto parameters
- self.packetizer = Packetizer(sock)
+ self.packetizer = (packetizer_class or Packetizer)(sock)
self.local_version = "SSH-" + self._PROTO_ID + "-" + self._CLIENT_ID
self.remote_version = ""
self.local_cipher = self.remote_cipher = ""
@@ -2407,7 +2413,8 @@ class Transport(threading.Thread, ClosingContextManager):
def _get_latest_kex_init(self):
return self._really_parse_kex_init(
- Message(self._latest_kex_init), ignore_first_byte=True
+ Message(self._latest_kex_init),
+ ignore_first_byte=True,
)
def _parse_kex_init(self, m):
@@ -2469,6 +2476,13 @@ class Transport(threading.Thread, ClosingContextManager):
for i in to_pop:
kex_algo_list.pop(i)
+ # CVE mitigation: expect zeroed-out seqno anytime we are performing kex
+ # init phase, if strict mode was negotiated.
+ if self.agreed_on_strict_kex and m.seqno != 0:
+ raise MessageOrderError(
+ f"Got nonzero seqno ({m.seqno}) during strict KEXINIT!"
+ )
+
# as a server, we pick the first item in the client's list that we
# support.
# as a client, we pick the first item in our list that the server
diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst
index f180e77..aeddee0 100644
--- a/sites/www/changelog.rst
+++ b/sites/www/changelog.rst
@@ -2,6 +2,9 @@
Changelog
=========
+- :feature:`-` `Transport` grew a new ``packetizer_class`` kwarg for overriding
+ the packet-handler class used internally. Mostly for testing, but advanced
+ users may find this useful when doing deep hacks.
- :bug:`-` Tweak ``ext-info-(c|s)`` detection during KEXINIT protocol phase;
the original implementation made assumptions based on an OpenSSH
implementation detail.
diff --git a/tests/test_transport.py b/tests/test_transport.py
index 19023eb..de26231 100644
--- a/tests/test_transport.py
+++ b/tests/test_transport.py
@@ -1118,6 +1118,16 @@ class TransportTest(unittest.TestCase):
# Real fix's behavior
self._expect_unimplemented()
+ def test_can_override_packetizer_used(self):
+ class MyPacketizer(Packetizer):
+ pass
+
+ # control case
+ assert Transport(sock=LoopSocket()).packetizer.__class__ is Packetizer
+ # overridden case
+ tweaked = Transport(sock=LoopSocket(), packetizer_class=MyPacketizer)
+ assert tweaked.packetizer.__class__ is MyPacketizer
+
class AlgorithmDisablingTests(unittest.TestCase):
def test_preferred_lists_default_to_private_attribute_contents(self):
@@ -1467,6 +1477,19 @@ class TestSHA2SignaturePubkeys(unittest.TestCase):
assert tc.is_authenticated()
assert tc._agreed_pubkey_algorithm == "rsa-sha2-256"
+class BadSeqPacketizer(Packetizer):
+ def read_message(self):
+ cmd, msg = super().read_message()
+ # Only mess w/ seqno if kexinit.
+ if cmd is MSG_KEXINIT:
+ # NOTE: this is /only/ the copy of the seqno which gets
+ # transmitted up from Packetizer; it's not modifying
+ # Packetizer's own internal seqno. For these tests,
+ # modifying the latter isn't required, and is also harder
+ # to do w/o triggering MAC mismatches.
+ msg.seqno = 17 # arbitrary nonzero int
+ return cmd, msg
+
class TestStrictKex:
def test_kex_algos_includes_kex_strict_c(self):
@@ -1502,9 +1525,6 @@ class TestStrictKex:
)
)
- def test_sequence_numbers_reset_on_newkeys(self):
- skip()
-
def test_MessageOrderError_raised_on_out_of_order_messages(self):
with raises(MessageOrderError):
with server() as (tc, _):
@@ -1513,12 +1533,41 @@ class TestStrictKex:
tc._expect_packet(MSG_KEXINIT)
tc.open_session()
- def test_SSHException_raised_on_out_of_order_messages_when_not_strict(self):
+ def test_SSHException_raised_on_out_of_order_messages_when_not_strict(
+ self,
+ ):
# This is kind of dumb (either situation is still fatal!) but whatever,
# may as well be strict with our new strict flag...
with raises(SSHException) as info: # would be true either way, but
- with server(client_init=dict(strict_kex=False),
- ) as (tc, _):
+ with server(
+ client_init=dict(strict_kex=False),
+ ) as (tc, _):
tc._expect_packet(MSG_KEXINIT)
tc.open_session()
assert info.type is SSHException # NOT MessageOrderError!
+
+ def test_error_not_raised_when_kexinit_not_seq_0_but_unstrict(self):
+ with server(
+ client_init=dict(
+ # Disable strict kex
+ strict_kex=False,
+ # Give our clientside a packetizer that sets all kexinit
+ # Message objects to have .seqno==17, which would trigger the
+ # new logic if we'd forgotten to wrap it in strict-kex check
+ packetizer_class=BadSeqPacketizer,
+ ),
+ ):
+ pass # kexinit happens at connect...
+
+ def test_MessageOrderError_raised_when_kexinit_not_seq_0_and_strict(self):
+ with raises(MessageOrderError):
+ with server(
+ # Give our clientside a packetizer that sets all kexinit
+ # Message objects to have .seqno==17, which should trigger the
+ # new logic (given we are NOT disabling strict-mode)
+ client_init=dict(packetizer_class=BadSeqPacketizer),
+ ):
+ pass # kexinit happens at connect...
+
+ def test_sequence_numbers_reset_on_newkeys(self):
+ skip()
--
2.33.0

View File

@ -0,0 +1,114 @@
From fa46de7feeeb8a01dc471581a0258252ce4f2db6 Mon Sep 17 00:00:00 2001
From: Jeff Forcier <jeff@bitprophet.org>
Date: Sat, 16 Dec 2023 17:12:42 -0500
Subject: [PATCH] Reset sequence numbers on rekey
Reference:https://github.com/paramiko/paramiko/commit/fa46de7feeeb8a01dc471581a0258252ce4f2db6
Conflict:NA
---
paramiko/packet.py | 6 ++++++
paramiko/transport.py | 22 ++++++++++++++++++++--
tests/test_transport.py | 25 +++++++++++++++++++++++--
3 files changed, 49 insertions(+), 4 deletions(-)
diff --git a/paramiko/packet.py b/paramiko/packet.py
index 1266316..1fc06d9 100644
--- a/paramiko/packet.py
+++ b/paramiko/packet.py
@@ -130,6 +130,12 @@ class Packetizer(object):
def closed(self):
return self.__closed
+ def reset_seqno_out(self):
+ self.__sequence_number_out = 0
+
+ def reset_seqno_in(self):
+ self.__sequence_number_in = 0
+
def set_log(self, log):
"""
Set the Python log object to use for logging.
diff --git a/paramiko/transport.py b/paramiko/transport.py
index 83b1c81..0c68668 100644
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -2469,9 +2469,13 @@ class Transport(threading.Thread, ClosingContextManager):
# CVE mitigation: expect zeroed-out seqno anytime we are performing kex
# init phase, if strict mode was negotiated.
- if self.agreed_on_strict_kex and m.seqno != 0:
+ if (
+ self.agreed_on_strict_kex
+ and not self.initial_kex_done
+ and m.seqno != 0
+ ):
raise MessageOrderError(
- f"Got nonzero seqno ({m.seqno}) during strict KEXINIT!"
+ "In strict-kex mode, but KEXINIT was not the first packet!"
)
# as a server, we pick the first item in the client's list that we
@@ -2670,6 +2674,13 @@ class Transport(threading.Thread, ClosingContextManager):
):
self._log(DEBUG, "Switching on inbound compression ...")
self.packetizer.set_inbound_compressor(compress_in())
+ # Reset inbound sequence number if strict mode.
+ if self.agreed_on_strict_kex:
+ self._log(
+ DEBUG,
+ f"Resetting inbound seqno after NEWKEYS due to strict mode",
+ )
+ self.packetizer.reset_seqno_in()
def _activate_outbound(self):
"""switch on newly negotiated encryption parameters for
@@ -2677,6 +2688,13 @@ class Transport(threading.Thread, ClosingContextManager):
m = Message()
m.add_byte(cMSG_NEWKEYS)
self._send_message(m)
+ # Reset outbound sequence number if strict mode.
+ if self.agreed_on_strict_kex:
+ self._log(
+ DEBUG,
+ f"Resetting outbound sequence number after NEWKEYS due to strict mode",
+ )
+ self.packetizer.reset_seqno_out()
block_size = self._cipher_info[self.local_cipher]["block-size"]
if self.server_mode:
IV_out = self._compute_key("B", block_size)
diff --git a/tests/test_transport.py b/tests/test_transport.py
index 7440e88..9c3e8f5 100644
--- a/tests/test_transport.py
+++ b/tests/test_transport.py
@@ -1548,5 +1548,26 @@ class TestStrictKex:
):
pass # kexinit happens at connect...
- def test_sequence_numbers_reset_on_newkeys(self):
- skip()
+ def test_sequence_numbers_reset_on_newkeys_when_strict(self):
+ with server(defer=True) as (tc, ts):
+ # When in strict mode, these should all be zero or close to it
+ # (post-kexinit, pre-auth).
+ # Server->client will be 1 (EXT_INFO got sent after NEWKEYS)
+ assert tc.packetizer._Packetizer__sequence_number_in == 1
+ assert ts.packetizer._Packetizer__sequence_number_out == 1
+ # Client->server will be 0
+ assert tc.packetizer._Packetizer__sequence_number_out == 0
+ assert ts.packetizer._Packetizer__sequence_number_in == 0
+
+ def test_sequence_numbers_not_reset_on_newkeys_when_not_strict(self):
+ with server(defer=True, client_init=dict(strict_kex=False)) as (
+ tc,
+ ts,
+ ):
+ # When not in strict mode, these will all be ~3-4 or so
+ # (post-kexinit, pre-auth). Not encoding exact values as it will
+ # change anytime we mess with the test harness...
+ assert tc.packetizer._Packetizer__sequence_number_in != 0
+ assert tc.packetizer._Packetizer__sequence_number_out != 0
+ assert ts.packetizer._Packetizer__sequence_number_in != 0
+ assert ts.packetizer._Packetizer__sequence_number_out != 0
--
2.33.0

View File

@ -0,0 +1,161 @@
From 96db1e2be856eac66631761bae41167a1ebd2b4e Mon Sep 17 00:00:00 2001
From: Jeff Forcier <jeff@bitprophet.org>
Date: Sun, 17 Dec 2023 17:13:53 -0500
Subject: [PATCH] Raise exception when sequence numbers rollover during initial
kex
Reference:https://github.com/paramiko/paramiko/commit/96db1e2be856eac66631761bae41167a1ebd2b4e
Conflict:Context adaptation exists in the changelog.rst file due to version inconsistency.
---
paramiko/packet.py | 17 +++++++++++++----
paramiko/transport.py | 4 +++-
sites/www/changelog.rst | 28 ++++++++++++++++++++++++++++
tests/test_transport.py | 32 ++++++++++++++++++++++++++++++++
4 files changed, 76 insertions(+), 5 deletions(-)
diff --git a/paramiko/packet.py b/paramiko/packet.py
index 1fc06d9..8b9e6d6 100644
--- a/paramiko/packet.py
+++ b/paramiko/packet.py
@@ -86,6 +86,7 @@ class Packetizer(object):
self.__need_rekey = False
self.__init_count = 0
self.__remainder = bytes()
+ self._initial_kex_done = False
# used for noticing when to re-key:
self.__sent_bytes = 0
@@ -431,9 +432,12 @@ class Packetizer(object):
out += compute_hmac(
self.__mac_key_out, payload, self.__mac_engine_out
)[: self.__mac_size_out]
- self.__sequence_number_out = (
- self.__sequence_number_out + 1
- ) & xffffffff
+ next_seq = (self.__sequence_number_out + 1) & xffffffff
+ if next_seq == 0 and not self._initial_kex_done:
+ raise SSHException(
+ "Sequence number rolled over during initial kex!"
+ )
+ self.__sequence_number_out = next_seq
self.write_all(out)
self.__sent_bytes += len(out)
@@ -537,7 +541,12 @@ class Packetizer(object):
msg = Message(payload[1:])
msg.seqno = self.__sequence_number_in
- self.__sequence_number_in = (self.__sequence_number_in + 1) & xffffffff
+ next_seq = (self.__sequence_number_in + 1) & xffffffff
+ if next_seq == 0 and not self._initial_kex_done:
+ raise SSHException(
+ "Sequence number rolled over during initial kex!"
+ )
+ self.__sequence_number_in = next_seq
# check for rekey
raw_packet_size = packet_size + self.__mac_size_in + 4
diff --git a/paramiko/transport.py b/paramiko/transport.py
index 0c68668..750f9b4 100644
--- a/paramiko/transport.py
+++ b/paramiko/transport.py
@@ -2785,7 +2785,9 @@ class Transport(threading.Thread, ClosingContextManager):
self.auth_handler = AuthHandler(self)
if not self.initial_kex_done:
# this was the first key exchange
- self.initial_kex_done = True
+ # (also signal to packetizer as it sometimes wants to know this
+ # staus as well, eg when seqnos rollover)
+ self.initial_kex_done = self.packetizer._initial_kex_done = True
# send an event?
if self.completion_event is not None:
self.completion_event.set()
diff --git a/sites/www/changelog.rst b/sites/www/changelog.rst
index 0675d04..6a2e4c0 100644
--- a/sites/www/changelog.rst
+++ b/sites/www/changelog.rst
@@ -5,6 +5,34 @@ Changelog
- :feature:`-` `Transport` grew a new ``packetizer_class`` kwarg for overriding
the packet-handler class used internally. Mostly for testing, but advanced
users may find this useful when doing deep hacks.
+- :bug:`-` Address `CVE 2023-48795<https://terrapin-attack.com/>`_ (aka the
+ "Terrapin Attack", a vulnerability found in the SSH protocol re: treatment of
+ packet sequence numbers) as follows:
+ - The vulnerability only impacts encrypt-then-MAC digest algorithms in
+ tandem with CBC ciphers, and ChaCha20-poly1305; of these, Paramiko
+ currently only implements ``hmac-sha2-(256|512)-etm`` in tandem with
+ ``AES-CBC``. If you are unable to upgrade to Paramiko versions containing
+ the below fixes right away, you may instead use the
+ ``disabled_algorithms`` connection option to disable the ETM MACs and/or
+ the CBC ciphers (this option is present in Paramiko >=2.6).
+ - As the fix for the vulnerability requires both ends of the connection to
+ cooperate, the below changes will only take effect when the remote end is
+ OpenSSH >= TK (or equivalent, such as Paramiko in server mode, as of this
+ patch version) and configured to use the new "strict kex" mode. Paramiko
+ will always attempt to use "strict kex" mode if offered by the server,
+ unless you override this by specifying ``strict_kex=False`` in
+ `Transport.__init__`.
+ - Paramiko will now raise an `SSHException` subclass (`MessageOrderError`)
+ when protocol messages are received in unexpected order. (This is not
+ *really* a change in behavior, as most such cases already raised vanilla
+ `SSHException` anyways.)
+ - Key (re)negotiation -- i.e. ``MSG_NEWKEYS``, whenever it is encountered
+ -- now resets packet sequence numbers. (This should be invisible to users
+ during normal operation, only causing exceptions if the exploit is
+ encountered, which will usually result in, again, `MessageOrderError`.)
+ - Sequence number rollover will now raise `SSHException` if it occurs
+ during initial key exchange (regardless of strict mode status).
+
- :bug:`-` Tweak ``ext-info-(c|s)`` detection during KEXINIT protocol phase;
the original implementation made assumptions based on an OpenSSH
implementation detail.
diff --git a/tests/test_transport.py b/tests/test_transport.py
index 9c3e8f5..4ed712e 100644
--- a/tests/test_transport.py
+++ b/tests/test_transport.py
@@ -30,6 +30,7 @@ import socket
import time
import threading
import random
+import sys
import unittest
from mock import Mock
@@ -1571,3 +1572,34 @@ class TestStrictKex:
assert tc.packetizer._Packetizer__sequence_number_out != 0
assert ts.packetizer._Packetizer__sequence_number_in != 0
assert ts.packetizer._Packetizer__sequence_number_out != 0
+
+ def test_sequence_number_rollover_detected(self):
+ class RolloverTransport(Transport):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ # Induce an about-to-rollover seqno, such that it rolls over
+ # during initial kex.
+ setattr(
+ self.packetizer,
+ f"_Packetizer__sequence_number_in",
+ sys.maxsize,
+ )
+ setattr(
+ self.packetizer,
+ f"_Packetizer__sequence_number_out",
+ sys.maxsize,
+ )
+
+ with raises(
+ SSHException,
+ match=r"Sequence number rolled over during initial kex!",
+ ):
+ with server(
+ client_init=dict(
+ # Disable strict kex - this should happen always
+ strict_kex=False,
+ ),
+ # Transport which tickles its packetizer seqno's
+ transport_factory=RolloverTransport,
+ ):
+ pass # kexinit happens at connect...
--
2.33.0

View File

@ -0,0 +1,285 @@
From e22c5ea330814801d8487dc3da347f987bafe5ec Mon Sep 17 00:00:00 2001
From: Jeff Forcier <jeff@bitprophet.org>
Date: Thu, 4 May 2023 13:52:40 -0400
Subject: [PATCH] Start consolidating test server nonsense
Reference:https://github.com/paramiko/paramiko/commit/e22c5ea330814801d8487dc3da347f987bafe5ec
Conflict:Currently, _util.py does not exist due to different versions. Therefore, the reconstruction code of test_transport.py is still stored in this file
The key name must be the same as the current one.
---
tests/test_transport.py | 198 ++++++++++++++++++++++++++++++++++++----
1 file changed, 181 insertions(+), 17 deletions(-)
diff --git a/tests/test_transport.py b/tests/test_transport.py
index 4ed712e..6cdbfd6 100644
--- a/tests/test_transport.py
+++ b/tests/test_transport.py
@@ -33,6 +33,7 @@ import random
import sys
import unittest
from mock import Mock
+from time import sleep
from paramiko import (
AuthHandler,
@@ -1196,6 +1197,146 @@ class AlgorithmDisablingTests(unittest.TestCase):
assert "diffie-hellman-group14-sha256" not in kexen
assert "zlib" not in compressions
+_disable_sha2 = dict(
+ disabled_algorithms=dict(keys=["rsa-sha2-256", "rsa-sha2-512"])
+)
+_disable_sha1 = dict(disabled_algorithms=dict(keys=["ssh-rsa"]))
+_disable_sha2_pubkey = dict(
+ disabled_algorithms=dict(pubkeys=["rsa-sha2-256", "rsa-sha2-512"])
+)
+_disable_sha1_pubkey = dict(disabled_algorithms=dict(pubkeys=["ssh-rsa"]))
+
+
+unicodey = "\u2022"
+
+
+class TestServer(ServerInterface):
+ paranoid_did_password = False
+ paranoid_did_public_key = False
+ # TODO: make this ed25519 or something else modern? (_is_ this used??)
+ paranoid_key = DSSKey.from_private_key_file(_support("test_dss.key"))
+
+ def __init__(self, allowed_keys=None):
+ self.allowed_keys = allowed_keys if allowed_keys is not None else []
+
+ def check_channel_request(self, kind, chanid):
+ if kind == "bogus":
+ return OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED
+ return OPEN_SUCCEEDED
+
+ def check_channel_exec_request(self, channel, command):
+ if command != b"yes":
+ return False
+ return True
+
+ def check_channel_shell_request(self, channel):
+ return True
+
+ def check_global_request(self, kind, msg):
+ self._global_request = kind
+ # 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
+ self._x11_auth_protocol = auth_protocol
+ self._x11_auth_cookie = auth_cookie
+ self._x11_screen_number = screen_number
+ return True
+
+ def check_port_forward_request(self, addr, port):
+ self._listen = socket.socket()
+ self._listen.bind(("127.0.0.1", 0))
+ self._listen.listen(1)
+ return self._listen.getsockname()[1]
+
+ def cancel_port_forward_request(self, addr, port):
+ self._listen.close()
+ self._listen = None
+
+ def check_channel_direct_tcpip_request(self, chanid, origin, destination):
+ self._tcpip_dest = destination
+ return OPEN_SUCCEEDED
+
+ def get_allowed_auths(self, username):
+ if username == "slowdive":
+ return "publickey,password"
+ if username == "paranoid":
+ if (
+ not self.paranoid_did_password
+ and not self.paranoid_did_public_key
+ ):
+ return "publickey,password"
+ elif self.paranoid_did_password:
+ return "publickey"
+ else:
+ return "password"
+ if username == "commie":
+ return "keyboard-interactive"
+ if username == "utf8":
+ return "password"
+ if username == "non-utf8":
+ return "password"
+ return "publickey"
+
+ def check_auth_password(self, username, password):
+ if (username == "slowdive") and (password == "pygmalion"):
+ return AUTH_SUCCESSFUL
+ if (username == "paranoid") and (password == "paranoid"):
+ # 2-part auth (even openssh doesn't support this)
+ self.paranoid_did_password = True
+ if self.paranoid_did_public_key:
+ return AUTH_SUCCESSFUL
+ return AUTH_PARTIALLY_SUCCESSFUL
+ if (username == "utf8") and (password == unicodey):
+ return AUTH_SUCCESSFUL
+ if (username == "non-utf8") and (password == "\xff"):
+ return AUTH_SUCCESSFUL
+ if username == "bad-server":
+ raise Exception("Ack!")
+ if username == "unresponsive-server":
+ time.sleep(5)
+ return AUTH_SUCCESSFUL
+ return AUTH_FAILED
+
+ def check_auth_publickey(self, username, key):
+ if (username == "paranoid") and (key == self.paranoid_key):
+ # 2-part auth
+ self.paranoid_did_public_key = True
+ if self.paranoid_did_password:
+ return AUTH_SUCCESSFUL
+ return AUTH_PARTIALLY_SUCCESSFUL
+ # TODO: make sure all tests incidentally using this to pass, _without
+ # sending a username oops_, get updated somehow - probably via server()
+ # default always injecting a username
+ elif key in self.allowed_keys:
+ return AUTH_SUCCESSFUL
+ return AUTH_FAILED
+
+ def check_auth_interactive(self, username, submethods):
+ if username == "commie":
+ self.username = username
+ return InteractiveQuery(
+ "password", "Please enter a password.", ("Password", False)
+ )
+ return AUTH_FAILED
+
+ def check_auth_interactive_response(self, responses):
+ if self.username == "commie":
+ if (len(responses) == 1) and (responses[0] == "cat"):
+ return AUTH_SUCCESSFUL
+ return AUTH_FAILED
+
@contextmanager
def server(
@@ -1206,13 +1347,20 @@ def server(
connect=None,
pubkeys=None,
catch_error=False,
+ transport_factory=None,
+ server_transport_factory=None,
+ defer=False,
+ skip_verify=False,
):
"""
SSH server contextmanager for testing.
+ Yields a tuple of ``(tc, ts)`` (client- and server-side `Transport`
+ objects), or ``(tc, ts, err)`` when ``catch_error==True``.
+
:param hostkey:
Host key to use for the server; if None, loads
- ``test_rsa.key``.
+ ``rsa.key``.
:param init:
Default `Transport` constructor kwargs to use for both sides.
:param server_init:
@@ -1226,6 +1374,17 @@ def server(
:param catch_error:
Whether to capture connection errors & yield from contextmanager.
Necessary for connection_time exception testing.
+ :param transport_factory:
+ Like the same-named param in SSHClient: which Transport class to use.
+ :param server_transport_factory:
+ Like ``transport_factory``, but only impacts the server transport.
+ :param bool defer:
+ Whether to defer authentication during connecting.
+
+ This is really just shorthand for ``connect={}`` which would do roughly
+ the same thing. Also: this implies skip_verify=True automatically!
+ :param bool skip_verify:
+ Whether NOT to do the default "make sure auth passed" check.
"""
if init is None:
init = {}
@@ -1234,18 +1393,27 @@ def server(
if client_init is None:
client_init = {}
if connect is None:
- connect = dict(username="slowdive", password="pygmalion")
+ # No auth at all please
+ if defer:
+ connect = dict()
+ # Default username based auth
+ else:
+ connect = dict(username="slowdive", password="pygmalion")
socks = LoopSocket()
sockc = LoopSocket()
sockc.link(socks)
- tc = Transport(sockc, **dict(init, **client_init))
- ts = Transport(socks, **dict(init, **server_init))
+ if transport_factory is None:
+ transport_factory = Transport
+ if server_transport_factory is None:
+ server_transport_factory = transport_factory
+ tc = transport_factory(sockc, **dict(init, **client_init))
+ ts = server_transport_factory(socks, **dict(init, **server_init))
if hostkey is None:
hostkey = RSAKey.from_private_key_file(_support("test_rsa.key"))
ts.add_server_key(hostkey)
event = threading.Event()
- server = NullServer(allowed_keys=pubkeys)
+ server = TestServer(allowed_keys=pubkeys)
assert not event.is_set()
assert not ts.is_active()
assert tc.get_username() is None
@@ -1273,22 +1441,15 @@ def server(
yield (tc, ts, err) if catch_error else (tc, ts)
+ if not (catch_error or skip_verify or defer):
+ assert ts.is_authenticated()
+ assert tc.is_authenticated()
+
tc.close()
ts.close()
socks.close()
sockc.close()
-
-_disable_sha2 = dict(
- disabled_algorithms=dict(keys=["rsa-sha2-256", "rsa-sha2-512"])
-)
-_disable_sha1 = dict(disabled_algorithms=dict(keys=["ssh-rsa"]))
-_disable_sha2_pubkey = dict(
- disabled_algorithms=dict(pubkeys=["rsa-sha2-256", "rsa-sha2-512"])
-)
-_disable_sha1_pubkey = dict(disabled_algorithms=dict(pubkeys=["ssh-rsa"]))
-
-
class TestSHA2SignatureKeyExchange(unittest.TestCase):
# NOTE: these all rely on the default server() hostkey being RSA
# NOTE: these rely on both sides being properly implemented re: agreed-upon
@@ -1352,7 +1513,10 @@ class TestSHA2SignatureKeyExchange(unittest.TestCase):
# the entire preferred-hostkeys structure when given an explicit key as
# a client.)
hostkey = RSAKey.from_private_key_file(_support("test_rsa.key"))
- with server(hostkey=hostkey, connect=dict(hostkey=hostkey)) as (tc, _):
+ connect = dict(
+ hostkey=hostkey, username="slowdive", password="pygmalion"
+ )
+ with server(hostkey=hostkey, connect=connect) as (tc, _):
assert tc.host_key_type == "rsa-sha2-512"
--
2.33.0

View File

@ -1,6 +1,6 @@
Name: python-paramiko Name: python-paramiko
Version: 2.11.0 Version: 2.11.0
Release: 2 Release: 3
Summary: Python SSH module Summary: Python SSH module
License: LGPLv2+ License: LGPLv2+
URL: https://github.com/paramiko/paramiko URL: https://github.com/paramiko/paramiko
@ -11,6 +11,13 @@ Source0: https://github.com/paramiko/paramiko/archive/%{version}/paramiko-
Patch6000: backport-Skip-tests-requiring-invoke.patch Patch6000: backport-Skip-tests-requiring-invoke.patch
Patch6001: 0003-remove-pytest-relaxed-dep.patch Patch6001: 0003-remove-pytest-relaxed-dep.patch
Patch6002: backport-fix-error-in-sftp-testcase.patch Patch6002: backport-fix-error-in-sftp-testcase.patch
Patch6003: backport-0001-CVE-2023-48795.patch
Patch6004: backport-0002-CVE-2023-48795.patch
Patch6005: backport-0003-CVE-2023-48795.patch
Patch6006: backport-0004-CVE-2023-48795.patch
Patch6007: backport-0005-CVE-2023-48795.patch
Patch6008: backport-0006-CVE-2023-48795.patch
Patch6009: backport-0007-CVE-2023-48795.patch
BuildArch: noarch BuildArch: noarch
@ -69,6 +76,9 @@ PYTHONPATH=%{buildroot}%{python3_sitelib} pytest-%{python3_version}
%doc html/ demos/ NEWS README.rst %doc html/ demos/ NEWS README.rst
%changelog %changelog
* Thu Jan 11 2024 zhangpan <zhangpan103@h-partners.com> - 2.11.0-3
- fix CVE-2023-48795
* Mon Oct 31 2022 chengyechun <chengyechun1@huawei.com> - 2.11.0-2 * Mon Oct 31 2022 chengyechun <chengyechun1@huawei.com> - 2.11.0-2
- Type:bugfix - Type:bugfix
- CVE:NA - CVE:NA