CVE-2023-48795
Description
The SSH transport protocol with certain OpenSSH extensions, found in OpenSSH before 9.6 and other products, allows remote attackers to bypass integrity checks such that some packets are omitted (from the extension negotiation message), and a client and server may consequently end up with a connection for which some security features have been downgraded or disabled, aka a Terrapin attack. This occurs because the SSH Binary Packet Protocol (BPP), implemented by these extensions, mishandles the handshake phase and mishandles use of sequence numbers. For example, there is an effective attack against SSH's use of ChaCha20-Poly1305 (and CBC with Encrypt-then-MAC). The bypass occurs in chacha20-poly1305@openssh.com and (if CBC is used) the -etm@openssh.com MAC algorithms. This also affects Maverick Synergy Java SSH API before 3.1.0-SNAPSHOT, Dropbear through 2022.83, Ssh before 5.1.1 in Erlang/OTP, PuTTY before 0.80, AsyncSSH before 2.14.2, golang.org/x/crypto before 0.17.0, libssh before 0.10.6, libssh2 through 1.11.0, Thorn Tech SFTP Gateway before 3.4.6, Tera Term before 5.1, Paramiko before 3.4.0, jsch before 0.2.15, SFTPGo before 2.5.6, Netgate pfSense Plus through 23.09.1, Netgate pfSense CE through 2.7.2, HPN-SSH through 18.2.0, ProFTPD before 1.3.8b (and before 1.3.9rc2), ORYX CycloneSSH before 2.3.4, NetSarang XShell 7 before Build 0144, CrushFTP before 10.6.0, ConnectBot SSH library before 2.2.22, Apache MINA sshd through 2.11.0, sshj through 0.37.0, TinySSH through 20230101, trilead-ssh2 6401, LANCOM LCOS and LANconfig, FileZilla before 3.66.4, Nova before 11.8, PKIX-SSH before 14.4, SecureCRT before 9.4.3, Transmit5 before 5.10.4, Win32-OpenSSH before 9.5.0.0p1-Beta, WinSCP before 6.2.2, Bitvise SSH Server before 9.32, Bitvise SSH Client before 9.33, KiTTY through 0.76.1.13, the net-ssh gem 7.2.0 for Ruby, the mscdex ssh2 module before 1.15.0 for Node.js, the thrussh library before 0.35.1 for Rust, and the Russh crate before 0.40.2 for Rust.
Affected products
1- OpenSSH/OpenSSHdescription
Patches
68e972c5e94b4Added test for the Terrapin vulnerability (CVE-2023-48795) (#227).
4 files changed · +49 −5
README.md+1 −0 modified@@ -184,6 +184,7 @@ For convenience, a web front-end on top of the command-line tool is available at - In Ubuntu 22.04 client policy, moved host key types `sk-ssh-ed25519@openssh.com` and `ssh-ed25519` to the end of all certificate types. - Re-organized option host key types for OpenSSH 9.2 server policy to correspond with updated Debian 12 hardening guide. - Dropped support for Python 3.7 (EOL was reached in June 2023). + - Added test for the Terrapin message prefix truncation vulnerability ([CVE-2023-48795](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2023-48795)). ### v3.0.0 (2023-09-07) - Results from concurrent scans against multiple hosts are no longer improperly combined; bug discovered by [Adam Russell](https://github.com/thecliguy).
src/ssh_audit/ssh2_kexdb.py+6 −2 modified@@ -71,6 +71,8 @@ class SSH2_KexDB: # pylint: disable=too-few-public-methods INFO_REMOVED_IN_OPENSSH69 = 'removed in OpenSSH 6.9: https://www.openssh.com/txt/release-6.9' INFO_REMOVED_IN_OPENSSH70 = 'removed in OpenSSH 7.0: https://www.openssh.com/txt/release-7.0' INFO_WITHDRAWN_PQ_ALG = 'the sntrup4591761 algorithm was withdrawn, as it may not provide strong post-quantum security' + INFO_EXTENSION_NEGOTIATION = 'pseudo-algorithm that denotes the peer supports RFC8308 extensions' + INFO_STRICT_KEX = 'pseudo-algorithm that denotes the peer supports a stricter key exchange method as a counter-measure to the Terrapin attack (CVE-2023-48795)' # Maintains a dictionary per calling thread that yields its own copy of MASTER_DB. This prevents results from one thread polluting the results of another thread. DB_PER_THREAD: Dict[int, Dict[str, Dict[str, List[List[Optional[str]]]]]] = {} @@ -154,8 +156,10 @@ class SSH2_KexDB: # pylint: disable=too-few-public-methods 'ecdh-sha2-wiRIU8TKjMZ418sMqlqtvQ==': [[], [FAIL_UNPROVEN]], # sect283k1 'ecdh-sha2-zD/b3hu/71952ArpUG4OjQ==': [[], [FAIL_UNPROVEN, FAIL_SMALL_ECC_MODULUS]], # sect233k1 'ecmqv-sha2': [[], [FAIL_UNPROVEN]], - 'ext-info-c': [[]], # Extension negotiation (RFC 8308) - 'ext-info-s': [[]], # Extension negotiation (RFC 8308) + 'ext-info-c': [[], [], [], [INFO_EXTENSION_NEGOTIATION]], # Extension negotiation (RFC 8308) + 'ext-info-s': [[], [], [], [INFO_EXTENSION_NEGOTIATION]], # Extension negotiation (RFC 8308) + 'kex-strict-c-v00@openssh.com': [[], [], [], [INFO_STRICT_KEX]], # Strict KEX marker (countermeasure for CVE-2023-48795). + 'kex-strict-s-v00@openssh.com': [[], [], [], [INFO_STRICT_KEX]], # Strict KEX marker (countermeasure for CVE-2023-48795). # The GSS kex algorithms get special wildcard handling, since they include variable base64 data after their standard prefixes. 'gss-13.3.132.0.10-sha256-*': [[], [FAIL_UNKNOWN]],
src/ssh_audit/ssh_audit.py+41 −2 modified@@ -447,7 +447,7 @@ def output_info(out: OutputBuffer, software: Optional['Software'], client_audit: out.sep() -def post_process_findings(banner: Optional[Banner], algs: Algorithms) -> List[str]: +def post_process_findings(banner: Optional[Banner], algs: Algorithms, client_audit: bool) -> List[str]: '''Perform post-processing on scan results before reporting them to the user. Returns a list of algorithms that should not be recommended''' @@ -466,6 +466,45 @@ def post_process_findings(banner: Optional[Banner], algs: Algorithms) -> List[st # Ensure that this algorithm doesn't appear in the recommendations section since the user cannot control this OpenSSH bug. algorithm_recommendation_suppress_list.append('diffie-hellman-group-exchange-sha256') + # Check for the Terrapin vulnerability (CVE-2023-48795), and mark the vulnerable algorithms. + if algs.ssh2kex is not None and \ + ((client_audit and 'kex-strict-c-v00@openssh.com' not in algs.ssh2kex.kex_algorithms) or (not client_audit and 'kex-strict-s-v00@openssh.com' not in algs.ssh2kex.kex_algorithms)): # Strict KEX marker is not present. + + def add_terrapin_warning(db: Dict[str, Dict[str, List[List[Optional[str]]]]], category: str, algorithm_name: str) -> None: + while len(db[category][algorithm_name]) < 3: + db[category][algorithm_name].append([]) + + db[category][algorithm_name][2].append("vulnerable to the Terrapin attack (CVE-2023-48795), allowing message prefix truncation") + + db = SSH2_KexDB.get_db() + + # Without the strict KEX marker, these algorithms are always vulnerable. + add_terrapin_warning(db, "enc", "chacha20-poly1305") + add_terrapin_warning(db, "enc", "chacha20-poly1305@openssh.com") + + cbc_ciphers = [] + etm_macs = [] + + # Find the list of CBC ciphers the peer supports. + ciphers_supported = algs.ssh2kex.client.encryption if client_audit else algs.ssh2kex.server.encryption + for cipher in ciphers_supported: + if cipher.endswith("-cbc"): + cbc_ciphers.append(cipher) + + # Find the list of ETM MACs the peer supports. + macs_supported = algs.ssh2kex.client.mac if client_audit else algs.ssh2kex.server.mac + for mac in macs_supported: + if mac.endswith("-etm@openssh.com"): + etm_macs.append(mac) + + # If at least one CBC cipher and at least one ETM MAC is supported, mark them all as vulnerable. + if len(cbc_ciphers) > 0 and len(etm_macs) > 0: + for cipher in cbc_ciphers: + add_terrapin_warning(db, "enc", cipher) + + for mac in etm_macs: + add_terrapin_warning(db, "mac", mac) + return algorithm_recommendation_suppress_list @@ -478,7 +517,7 @@ def output(out: OutputBuffer, aconf: AuditConf, banner: Optional[Banner], header algs = Algorithms(pkm, kex) # Perform post-processing on the findings to make final adjustments before outputting the results. - algorithm_recommendation_suppress_list = post_process_findings(banner, algs) + algorithm_recommendation_suppress_list = post_process_findings(banner, algs, client_audit) with out: if print_target:
test/test_ssh2.py+1 −1 modified@@ -164,7 +164,7 @@ def test_ssh2_server_simple(self, output_spy, virtual_socket): self.audit(out, self._conf()) out.write() lines = output_spy.flush() - assert len(lines) == 70 + assert len(lines) == 83 def test_ssh2_server_invalid_first_packet(self, output_spy, virtual_socket): vsocket = virtual_socket
5c8b534f6e97Implement kex-strict from OpenSSH
4 files changed · +88 −35
src/main/java/com/trilead/ssh2/transport/KexManager.java+25 −2 modified@@ -103,6 +103,9 @@ public class KexManager /** RFC 8308 Section 2 */ private static final String EXT_INFO_C = "ext-info-c"; + private static final String KEX_STRICT_C_OPENSSH = "kex-strict-c-v00@openssh.com"; + private static final String KEX_STRICT_S_OPENSSH = "kex-strict-s-v00@openssh.com"; + private KexState kxs; private int kexCount = 0; private KeyMaterial km; @@ -193,6 +196,19 @@ private boolean compareFirstOfNameList(String[] a, String[] b) return (a[0].equals(b[0])); } + private boolean containsAlgo(String[] algos, String targetAlgo) + { + if (algos == null || targetAlgo == null) + return false; + + for (String algo : algos) { + if (targetAlgo.equals(algo)) + return true; + } + + return false; + } + private boolean isGuessOK(KexParameters cpar, KexParameters spar) { if (cpar == null || spar == null) @@ -214,6 +230,8 @@ private NegotiatedParameters mergeKexParameters(KexParameters client, KexParamet { np.kex_algo = getFirstMatch(client.kex_algorithms, server.kex_algorithms); + np.isStrictKex = containsAlgo(server.kex_algorithms, KEX_STRICT_S_OPENSSH); + log.log(20, "kex_algo=" + np.kex_algo); np.server_host_key_algo = getFirstMatch(client.server_host_key_algorithms, @@ -304,13 +322,14 @@ public synchronized void initiateKEX(CryptoWishList cwl, DHGexParameters dhgex) */ private static void addExtraKexAlgorithms(CryptoWishList cwl) { String[] oldKexAlgorithms = cwl.kexAlgorithms; - List<String> kexAlgorithms = new ArrayList<>(oldKexAlgorithms.length + 1); + List<String> kexAlgorithms = new ArrayList<>(oldKexAlgorithms.length + 2); for (String algo : oldKexAlgorithms) { - if (!algo.equals(EXT_INFO_C)) + if (!algo.equals(EXT_INFO_C) && !algo.equals(KEX_STRICT_C_OPENSSH)) kexAlgorithms.add(algo); } kexAlgorithms.add(EXT_INFO_C); + kexAlgorithms.add(KEX_STRICT_C_OPENSSH); cwl.kexAlgorithms = kexAlgorithms.toArray(new String[0]); } @@ -746,4 +765,8 @@ public synchronized void handleMessage(byte[] msg, int msglen) throws IOExceptio throw new IllegalStateException("Unkown KEX method! (" + kxs.np.kex_algo + ")"); } + + public boolean isStrictKex() { + return kxs.np.isStrictKex; + } }
src/main/java/com/trilead/ssh2/transport/NegotiatedParameters.java+1 −0 modified@@ -9,6 +9,7 @@ public class NegotiatedParameters { public boolean guessOK; + public boolean isStrictKex; public String kex_algo; public String server_host_key_algo; public String enc_algo_client_to_server;
src/main/java/com/trilead/ssh2/transport/TransportConnection.java+14 −0 modified@@ -361,4 +361,18 @@ public void startCompression() { can_recv_compress = true; can_send_compress = true; } + + /** + * Resets the send sequence number for MAC calculation. + */ + public void resetSendSequenceNumber() { + send_seq_number = 0; + } + + /** + * Resets the receive sequence number for MAC calculation. + */ + public void resetReceiveSequenceNumber() { + recv_seq_number = 0; + } }
src/main/java/com/trilead/ssh2/transport/TransportManager.java+48 −33 modified@@ -122,10 +122,11 @@ public void run() int port; Socket sock; - Object connectionSemaphore = new Object(); + private final Object connectionSemaphore = new Object(); boolean flagKexOngoing = false; boolean connectionClosed = false; + boolean firstKexFinished = false; Throwable reasonClosedCause = null; @@ -404,6 +405,8 @@ public void sendKexMessage(byte[] msg) throws IOException } public void kexFinished() { + firstKexFinished = true; + synchronized (connectionSemaphore) { flagKexOngoing = false; @@ -419,11 +422,15 @@ public void forceKeyExchange(CryptoWishList cwl, DHGexParameters dhgex) throws I public void changeRecvCipher(BlockCipher bc, MAC mac) { tc.changeRecvCipher(bc, mac); + if (km.isStrictKex()) + tc.resetReceiveSequenceNumber(); } public void changeSendCipher(BlockCipher bc, MAC mac) { tc.changeSendCipher(bc, mac); + if (km.isStrictKex()) + tc.resetSendSequenceNumber(); } /** @@ -531,38 +538,6 @@ public void receiveLoop() throws IOException int type = msg[0] & 0xff; - if (type == Packets.SSH_MSG_IGNORE) - continue; - - if (type == Packets.SSH_MSG_DEBUG) - { - if (log.isEnabled()) - { - TypesReader tr = new TypesReader(msg, 0, msglen); - tr.readByte(); - tr.readBoolean(); - StringBuffer debugMessageBuffer = new StringBuffer(); - debugMessageBuffer.append(tr.readString("UTF-8")); - - for (int i = 0; i < debugMessageBuffer.length(); i++) - { - char c = debugMessageBuffer.charAt(i); - - if ((c >= 32) && (c <= 126)) - continue; - debugMessageBuffer.setCharAt(i, '\uFFFD'); - } - - log.log(50, "DEBUG Message from remote: '" + debugMessageBuffer.toString() + "'"); - } - continue; - } - - if (type == Packets.SSH_MSG_UNIMPLEMENTED) - { - throw new IOException("Peer sent UNIMPLEMENTED message, that should not happen."); - } - if (type == Packets.SSH_MSG_DISCONNECT) { TypesReader tr = new TypesReader(msg, 0, msglen); @@ -615,6 +590,46 @@ public void receiveLoop() throws IOException continue; } + /* + * Any other packet should not be used when kex-strict is enabled. + */ + if (!firstKexFinished && km.isStrictKex()) + { + throw new IOException("Unexpected packet received when kex-strict enabled"); + } + + if (type == Packets.SSH_MSG_IGNORE) + continue; + + if (type == Packets.SSH_MSG_DEBUG) + { + if (log.isEnabled()) + { + TypesReader tr = new TypesReader(msg, 0, msglen); + tr.readByte(); + tr.readBoolean(); + StringBuffer debugMessageBuffer = new StringBuffer(); + debugMessageBuffer.append(tr.readString("UTF-8")); + + for (int i = 0; i < debugMessageBuffer.length(); i++) + { + char c = debugMessageBuffer.charAt(i); + + if ((c >= 32) && (c <= 126)) + continue; + debugMessageBuffer.setCharAt(i, '\uFFFD'); + } + + log.log(50, "DEBUG Message from remote: '" + debugMessageBuffer.toString() + "'"); + } + continue; + } + + if (type == Packets.SSH_MSG_UNIMPLEMENTED) + { + throw new IOException("Peer sent UNIMPLEMENTED message, that should not happen."); + } + if (type == Packets.SSH_MSG_USERAUTH_SUCCESS) { tc.startCompression(); }
97b223f8891blib: add strict key exchange mode support
3 files changed · +59 −9
lib/protocol/kex.js+45 −6 modified@@ -232,13 +232,37 @@ function handleKexInit(self, payload) { clientList = localKex; remoteExtInfoEnabled = (serverList.indexOf('ext-info-s') !== -1); } + if (self._strictMode === undefined) { + if (self._server) { + self._strictMode = + (clientList.indexOf('kex-strict-c-v00@openssh.com') !== -1); + } else { + self._strictMode = + (serverList.indexOf('kex-strict-s-v00@openssh.com') !== -1); + } + // Note: We check for seqno of 1 instead of 0 since we increment before + // calling the packet handler + if (self._strictMode) { + debug && debug('Handshake: strict KEX mode enabled'); + if (self._decipher.inSeqno !== 1) { + if (debug) + debug('Handshake: KEXINIT not first packet in strict KEX mode'); + return doFatalError( + self, + 'Handshake failed: KEXINIT not first packet in strict KEX mode', + 'handshake', + DISCONNECT_REASON.KEY_EXCHANGE_FAILED + ); + } + } + } // Check for agreeable key exchange algorithm for (i = 0; i < clientList.length && serverList.indexOf(clientList[i]) === -1; ++i); if (i === clientList.length) { // No suitable match found! - debug && debug('Handshake: No matching key exchange algorithm'); + debug && debug('Handshake: no matching key exchange algorithm'); return doFatalError( self, 'Handshake failed: no matching key exchange algorithm', @@ -1236,6 +1260,8 @@ const createKeyExchange = (() => { 'Inbound: NEWKEYS' ); this._receivedNEWKEYS = true; + if (this._protocol._strictMode) + this._protocol._decipher.inSeqno = 0; ++this._step; return this.finish(!this._protocol._server && !this._hostVerified); @@ -1756,11 +1782,20 @@ function onKEXPayload(state, payload) { payload = this._packetRW.read.read(payload); const type = payload[0]; + + if (!this._strictMode) { + switch (type) { + case MESSAGE.IGNORE: + case MESSAGE.UNIMPLEMENTED: + case MESSAGE.DEBUG: + if (!MESSAGE_HANDLERS) + MESSAGE_HANDLERS = require('./handlers.js'); + return MESSAGE_HANDLERS[type](this, payload); + } + } + switch (type) { case MESSAGE.DISCONNECT: - case MESSAGE.IGNORE: - case MESSAGE.UNIMPLEMENTED: - case MESSAGE.DEBUG: if (!MESSAGE_HANDLERS) MESSAGE_HANDLERS = require('./handlers.js'); return MESSAGE_HANDLERS[type](this, payload); @@ -1776,6 +1811,8 @@ function onKEXPayload(state, payload) { state.firstPacket = false; return handleKexInit(this, payload); default: + // Ensure packet is either an algorithm negotiation or KEX + // algorithm-specific packet if (type < 20 || type > 49) { return doFatalError( this, @@ -1824,6 +1861,8 @@ function trySendNEWKEYS(kex) { kex._protocol._packetRW.write.finalize(packet, true) ); kex._sentNEWKEYS = true; + if (kex._protocol._strictMode) + kex._protocol._cipher.outSeqno = 0; } } @@ -1832,7 +1871,7 @@ module.exports = { kexinit, onKEXPayload, DEFAULT_KEXINIT_CLIENT: new KexInit({ - kex: DEFAULT_KEX.concat(['ext-info-c']), + kex: DEFAULT_KEX.concat(['ext-info-c', 'kex-strict-c-v00@openssh.com']), serverHostKey: DEFAULT_SERVER_HOST_KEY, cs: { cipher: DEFAULT_CIPHER, @@ -1848,7 +1887,7 @@ module.exports = { }, }), DEFAULT_KEXINIT_SERVER: new KexInit({ - kex: DEFAULT_KEX, + kex: DEFAULT_KEX.concat(['kex-strict-s-v00@openssh.com']), serverHostKey: DEFAULT_SERVER_HOST_KEY, cs: { cipher: DEFAULT_CIPHER,
lib/protocol/Protocol.js+9 −2 modified@@ -218,11 +218,18 @@ class Protocol { if (typeof offer !== 'object' || offer === null) { offer = (this._server ? DEFAULT_KEXINIT_SERVER : DEFAULT_KEXINIT_CLIENT); } else if (offer.constructor !== KexInit) { - if (!this._server) - offer.kex = offer.kex.concat(['ext-info-c']); + if (this._server) { + offer.kex = offer.kex.concat(['kex-strict-s-v00@openssh.com']); + } else { + offer.kex = offer.kex.concat([ + 'ext-info-c', + 'kex-strict-c-v00@openssh.com', + ]); + } offer = new KexInit(offer); } this._kex = undefined; + this._strictMode = undefined; this._kexinit = undefined; this._offer = offer; this._cipher = new NullCipher(0, this._onWrite);
lib/server.js+5 −1 modified@@ -294,7 +294,11 @@ class Server extends EventEmitter { } const algorithms = { - kex: generateAlgorithmList(cfgAlgos.kex, DEFAULT_KEX, SUPPORTED_KEX), + kex: generateAlgorithmList( + cfgAlgos.kex, + DEFAULT_KEX, + SUPPORTED_KEX + ).concat(['kex-strict-s-v00@openssh.com']), serverHostKey: hostKeyAlgoOrder, cs: { cipher: generateAlgorithmList(
9 files changed · +186 −15
russh/src/client/encrypted.rs+16 −1 modified@@ -14,6 +14,7 @@ // use std::cell::RefCell; use std::convert::TryInto; +use std::num::Wrapping; use log::{debug, error, info, trace, warn}; use russh_cryptovec::CryptoVec; @@ -26,7 +27,8 @@ use crate::negotiation::{Named, Select}; use crate::parsing::{ChannelOpenConfirmation, ChannelType, OpenChannelMessage}; use crate::session::{Encrypted, EncryptedState, Kex, KexInit}; use crate::{ - auth, msg, negotiation, Channel, ChannelId, ChannelMsg, ChannelOpenFailure, ChannelParams, Sig, + auth, msg, negotiation, strict_kex_violation, Channel, ChannelId, ChannelMsg, + ChannelOpenFailure, ChannelParams, Sig, }; thread_local! { @@ -37,6 +39,7 @@ impl Session { pub(crate) async fn client_read_encrypted<H: Handler>( mut self, mut client: H, + seqn: &mut Wrapping<u32>, buf: &[u8], ) -> Result<(H, Self), H::Error> { #[allow(clippy::indexing_slicing)] // length checked @@ -65,6 +68,12 @@ impl Session { }; if let Some(kexinit) = kexinit { + if let Some(ref algo) = kexinit.algo { + if self.common.strict_kex && !algo.strict_kex { + return Err(strict_kex_violation(msg::KEXINIT, 0).into()); + } + } + let dhdone = kexinit.client_parse( self.common.config.as_ref(), &mut *self.common.cipher.local_to_remote, @@ -100,6 +109,7 @@ impl Session { .local_to_remote .write(&[msg::NEWKEYS], &mut self.common.write_buffer); self.flush()?; + self.common.maybe_reset_seqn(); Ok((client, self)) } else { error!("Wrong packet received"); @@ -125,6 +135,11 @@ impl Session { self.pending_len = 0; self.common.newkeys(newkeys); self.flush()?; + + if self.common.strict_kex { + *seqn = Wrapping(0); + } + return Ok((client, self)); } Some(Kex::Init(k)) => {
russh/src/client/mod.rs+36 −5 modified@@ -77,6 +77,7 @@ use std::cell::RefCell; use std::collections::HashMap; +use std::num::Wrapping; use std::pin::Pin; use std::sync::Arc; @@ -104,7 +105,8 @@ use crate::session::{CommonSession, EncryptedState, Exchange, Kex, KexDhDone, Ke use crate::ssh_read::SshRead; use crate::sshbuffer::{SSHBuffer, SshId}; use crate::{ - auth, msg, negotiation, timeout, ChannelId, ChannelOpenFailure, Disconnect, Limits, Sig, + auth, msg, negotiation, strict_kex_violation, timeout, ChannelId, ChannelOpenFailure, + Disconnect, Limits, Sig, }; mod encrypted; @@ -128,6 +130,8 @@ pub struct Session { inbound_channel_receiver: Receiver<Msg>, } +const STRICT_KEX_MSG_ORDER: &[u8] = &[msg::KEXINIT, msg::KEX_ECDH_REPLY, msg::NEWKEYS]; + impl Drop for Session { fn drop(&mut self) { debug!("drop session") @@ -693,6 +697,7 @@ where wants_reply: false, disconnected: false, buffer: CryptoVec::new(), + strict_kex: false, }, session_receiver, session_sender, @@ -784,7 +789,7 @@ impl Session { self.send_keepalive(true); } r = &mut reading => { - let (stream_read, buffer, mut opening_cipher) = match r { + let (stream_read, mut buffer, mut opening_cipher) = match r { Ok((_, stream_read, buffer, opening_cipher)) => (stream_read, buffer, opening_cipher), Err(e) => return Err(e.into()) }; @@ -813,8 +818,8 @@ impl Session { #[allow(clippy::indexing_slicing)] // length checked if buf[0] == crate::msg::DISCONNECT { break; - } else if buf[0] > 4 { - let (h, s) = reply(self, handler, &mut encrypted_signal, buf).await?; + } else { + let (h, s) = reply(self, handler, &mut encrypted_signal, &mut buffer.seqn, buf).await?; handler = h; self = s; } @@ -1176,8 +1181,24 @@ async fn reply<H: Handler>( mut session: Session, mut handler: H, sender: &mut Option<tokio::sync::oneshot::Sender<()>>, + seqn: &mut Wrapping<u32>, buf: &[u8], ) -> Result<(H, Session), H::Error> { + if let Some(message_type) = buf.first() { + if session.common.strict_kex && session.common.encrypted.is_none() { + let seqno = seqn.0 - 1; // was incremented after read() + if let Some(expected) = STRICT_KEX_MSG_ORDER.get(seqno as usize) { + if message_type != expected { + return Err(strict_kex_violation(*message_type, seqno as usize).into()); + } + } + } + + if [msg::IGNORE, msg::UNIMPLEMENTED, msg::DEBUG].contains(message_type) { + return Ok((handler, session)); + } + } + match session.common.kex.take() { Some(Kex::Init(kexinit)) => { if kexinit.algo.is_some() @@ -1191,6 +1212,11 @@ async fn reply<H: Handler>( &mut session.common.write_buffer, )?; + // seqno has already been incremented after read() + if done.names.strict_kex && seqn.0 != 1 { + return Err(strict_kex_violation(msg::KEXINIT, seqn.0 as usize - 1).into()); + } + if done.kex.skip_exchange() { session.common.encrypted( initial_encrypted_state(&session), @@ -1216,13 +1242,15 @@ async fn reply<H: Handler>( // We've sent ECDH_INIT, waiting for ECDH_REPLY let (kex, h) = kexdhdone.server_key_check(false, handler, buf).await?; handler = h; + session.common.strict_kex = session.common.strict_kex || kex.names.strict_kex; session.common.kex = Some(Kex::Keys(kex)); session .common .cipher .local_to_remote .write(&[msg::NEWKEYS], &mut session.common.write_buffer); session.flush()?; + session.common.maybe_reset_seqn(); Ok((handler, session)) } else { error!("Wrong packet received"); @@ -1241,13 +1269,16 @@ async fn reply<H: Handler>( .common .encrypted(initial_encrypted_state(&session), newkeys); // Ok, NEWKEYS received, now encrypted. + if session.common.strict_kex { + *seqn = Wrapping(0); + } Ok((handler, session)) } Some(kex) => { session.common.kex = Some(kex); Ok((handler, session)) } - None => session.client_read_encrypted(handler, buf).await, + None => session.client_read_encrypted(handler, seqn, buf).await, } }
russh/src/kex/mod.rs+4 −0 modified@@ -99,6 +99,10 @@ pub const NONE: Name = Name("none"); pub const EXTENSION_SUPPORT_AS_CLIENT: Name = Name("ext-info-c"); /// `ext-info-s` pub const EXTENSION_SUPPORT_AS_SERVER: Name = Name("ext-info-s"); +/// `kex-strict-c-v00@openssh.com` +pub const EXTENSION_OPENSSH_STRICT_KEX_AS_CLIENT: Name = Name("kex-strict-c-v00@openssh.com"); +/// `kex-strict-s-v00@openssh.com` +pub const EXTENSION_OPENSSH_STRICT_KEX_AS_SERVER: Name = Name("kex-strict-s-v00@openssh.com"); const _CURVE25519: Curve25519KexType = Curve25519KexType {}; const _DH_G1_SHA1: DhGroup1Sha1KexType = DhGroup1Sha1KexType {};
russh/src/lib.rs+18 −0 modified@@ -96,6 +96,7 @@ use std::fmt::{Debug, Display, Formatter}; +use log::debug; use parsing::ChannelOpenConfirmation; pub use russh_cryptovec::CryptoVec; use thiserror::Error; @@ -285,6 +286,23 @@ pub enum Error { #[error(transparent)] Elapsed(#[from] tokio::time::error::Elapsed), + + #[error("Violation detected during strict key exchange, message {message_type} at seq no {sequence_number}")] + StrictKeyExchangeViolation { + message_type: u8, + sequence_number: usize, + }, +} + +pub(crate) fn strict_kex_violation(message_type: u8, sequence_number: usize) -> crate::Error { + debug!( + "strict kex violated at sequence no. {:?}, message type: {:?}", + sequence_number, message_type + ); + crate::Error::StrictKeyExchangeViolation { + message_type, + sequence_number, + } } #[derive(Debug, Error)]
russh/src/negotiation.rs+47 −5 modified@@ -23,6 +23,7 @@ use russh_keys::key::{KeyPair, PublicKey}; use crate::cipher::CIPHERS; use crate::compression::*; +use crate::kex::{EXTENSION_OPENSSH_STRICT_KEX_AS_CLIENT, EXTENSION_OPENSSH_STRICT_KEX_AS_SERVER}; use crate::{cipher, kex, mac, msg, Error}; #[derive(Debug)] @@ -35,6 +36,7 @@ pub struct Names { pub server_compression: Compression, pub client_compression: Compression, pub ignore_guessed: bool, + pub strict_kex: bool, } /// Lists of preferred algorithms. This is normally hard-coded into implementations. @@ -56,6 +58,10 @@ const SAFE_KEX_ORDER: &[kex::Name] = &[ kex::CURVE25519, kex::CURVE25519_PRE_RFC_8731, kex::DH_G14_SHA256, + kex::EXTENSION_SUPPORT_AS_CLIENT, + kex::EXTENSION_SUPPORT_AS_SERVER, + kex::EXTENSION_OPENSSH_STRICT_KEX_AS_CLIENT, + kex::EXTENSION_OPENSSH_STRICT_KEX_AS_SERVER, ]; const CIPHER_ORDER: &[cipher::Name] = &[ @@ -143,7 +149,9 @@ impl Named for KeyPair { } } -pub trait Select { +pub(crate) trait Select { + fn is_server() -> bool; + fn select<S: AsRef<str> + Copy>(a: &[S], b: &[u8]) -> Option<(bool, S)>; fn read_kex(buffer: &[u8], pref: &Preferred) -> Result<Names, Error> { @@ -160,6 +168,24 @@ pub trait Select { return Err(Error::NoCommonKexAlgo); }; + let strict_kex_requested = pref.kex.contains(if Self::is_server() { + &EXTENSION_OPENSSH_STRICT_KEX_AS_SERVER + } else { + &EXTENSION_OPENSSH_STRICT_KEX_AS_CLIENT + }); + let strict_kex_provided = Self::select( + &[if Self::is_server() { + EXTENSION_OPENSSH_STRICT_KEX_AS_CLIENT + } else { + EXTENSION_OPENSSH_STRICT_KEX_AS_SERVER + }], + kex_string, + ) + .is_some(); + if strict_kex_requested && strict_kex_provided { + debug!("strict kex enabled") + } + let key_string = r.read_string()?; let (key_both_first, key_algorithm) = if let Some(x) = Self::select(pref.key, key_string) { x @@ -238,6 +264,7 @@ pub trait Select { server_compression, // Ignore the next packet if (1) it follows and (2) it's not the correct guess. ignore_guessed: fol && !(kex_both_first && key_both_first), + strict_kex: strict_kex_requested && strict_kex_provided, }) } _ => Err(Error::KexInit), @@ -249,6 +276,10 @@ pub struct Server; pub struct Client; impl Select for Server { + fn is_server() -> bool { + true + } + fn select<S: AsRef<str> + Copy>(server_list: &[S], client_list: &[u8]) -> Option<(bool, S)> { let mut both_first_choice = true; for c in client_list.split(|&x| x == b',') { @@ -264,6 +295,10 @@ impl Select for Server { } impl Select for Client { + fn is_server() -> bool { + false + } + fn select<S: AsRef<str> + Copy>(client_list: &[S], server_list: &[u8]) -> Option<(bool, S)> { let mut both_first_choice = true; for &c in client_list { @@ -287,11 +322,18 @@ pub fn write_kex(prefs: &Preferred, buf: &mut CryptoVec, as_server: bool) -> Res buf.extend(&cookie); // cookie buf.extend_list(prefs.kex.iter().filter(|k| { - **k != if as_server { - crate::kex::EXTENSION_SUPPORT_AS_CLIENT + !(if as_server { + [ + crate::kex::EXTENSION_SUPPORT_AS_CLIENT, + crate::kex::EXTENSION_OPENSSH_STRICT_KEX_AS_CLIENT, + ] } else { - crate::kex::EXTENSION_SUPPORT_AS_SERVER - } + [ + crate::kex::EXTENSION_SUPPORT_AS_SERVER, + crate::kex::EXTENSION_OPENSSH_STRICT_KEX_AS_SERVER, + ] + }) + .contains(*k) })); // kex algo buf.extend_list(prefs.key.iter());
russh/src/server/encrypted.rs+18 −0 modified@@ -34,6 +34,7 @@ impl Session { pub(crate) async fn server_read_encrypted<H: Handler + Send>( mut self, mut handler: H, + seqn: &mut Wrapping<u32>, buf: &[u8], ) -> Result<(H, Self), H::Error> { #[allow(clippy::indexing_slicing)] // length checked @@ -70,6 +71,9 @@ impl Session { &mut self.common.write_buffer, )?); } + if let Some(Kex::Dh(KexDh { ref names, .. })) = enc.rekey { + self.common.strict_kex = self.common.strict_kex || names.strict_kex; + } self.flush()?; return Ok((handler, self)); } @@ -82,6 +86,10 @@ impl Session { buf, &mut self.common.write_buffer, )?); + if let Some(Kex::Keys(_)) = enc.rekey { + // just sent NEWKEYS + self.common.maybe_reset_seqn(); + } self.flush()?; return Ok((handler, self)); } @@ -103,11 +111,21 @@ impl Session { self.pending_reads = pending; self.pending_len = 0; self.common.newkeys(newkeys); + if self.common.strict_kex { + *seqn = Wrapping(0); + } self.flush()?; return Ok((handler, self)); } Some(Kex::Init(k)) => { + if let Some(ref algo) = k.algo { + if self.common.strict_kex && !algo.strict_kex { + return Err(strict_kex_violation(msg::KEXINIT, 0).into()); + } + } + enc.rekey = Some(Kex::Init(k)); + self.pending_len += buf.len() as u32; if self.pending_len > 2 * self.target_window_size { return Err(Error::Pending.into());
russh/src/server/mod.rs+35 −1 modified@@ -112,6 +112,7 @@ use std; use std::collections::HashMap; +use std::num::Wrapping; use std::pin::Pin; use std::sync::Arc; use std::task::{Context, Poll}; @@ -805,14 +806,33 @@ async fn read_ssh_id<R: AsyncRead + Unpin>( wants_reply: false, disconnected: false, buffer: CryptoVec::new(), + strict_kex: false, }) } +const STRICT_KEX_MSG_ORDER: &[u8] = &[msg::KEXINIT, msg::KEX_ECDH_INIT, msg::NEWKEYS]; + async fn reply<H: Handler + Send>( mut session: Session, handler: H, + seqn: &mut Wrapping<u32>, buf: &[u8], ) -> Result<(H, Session), H::Error> { + if let Some(message_type) = buf.first() { + if session.common.strict_kex && session.common.encrypted.is_none() { + let seqno = seqn.0 - 1; // was incremented after read() + if let Some(expected) = STRICT_KEX_MSG_ORDER.get(seqno as usize) { + if message_type != expected { + return Err(strict_kex_violation(*message_type, seqno as usize).into()); + } + } + } + + if [msg::IGNORE, msg::UNIMPLEMENTED, msg::DEBUG].contains(message_type) { + return Ok((handler, session)); + } + } + // Handle key exchange/re-exchange. if session.common.encrypted.is_none() { match session.common.kex.take() { @@ -824,6 +844,13 @@ async fn reply<H: Handler + Send>( buf, &mut session.common.write_buffer, )?); + if let Some(Kex::Dh(KexDh { ref names, .. })) = session.common.kex { + session.common.strict_kex = names.strict_kex; + } + // seqno has already been incremented after read() + if session.common.strict_kex && seqn.0 != 1 { + return Err(strict_kex_violation(msg::KEXINIT, seqn.0 as usize - 1).into()); + } return Ok((handler, session)); } else { // Else, i.e. if the other side has not started @@ -839,6 +866,10 @@ async fn reply<H: Handler + Send>( buf, &mut session.common.write_buffer, )?); + if let Some(Kex::Keys(_)) = session.common.kex { + // just sent NEWKEYS + session.common.maybe_reset_seqn(); + } return Ok((handler, session)); } Some(Kex::Keys(newkeys)) => { @@ -854,6 +885,9 @@ async fn reply<H: Handler + Send>( newkeys, ); session.maybe_send_ext_info(); + if session.common.strict_kex { + *seqn = Wrapping(0); + } return Ok((handler, session)); } Some(kex) => { @@ -864,6 +898,6 @@ async fn reply<H: Handler + Send>( } Ok((handler, session)) } else { - Ok(session.server_read_encrypted(handler, buf).await?) + Ok(session.server_read_encrypted(handler, seqn, buf).await?) } }
russh/src/server/session.rs+3 −3 modified@@ -360,7 +360,7 @@ impl Session { while !self.common.disconnected { tokio::select! { r = &mut reading => { - let (stream_read, buffer, mut opening_cipher) = match r { + let (stream_read, mut buffer, mut opening_cipher) = match r { Ok((_, stream_read, buffer, opening_cipher)) => (stream_read, buffer, opening_cipher), Err(e) => return Err(e.into()) }; @@ -390,10 +390,10 @@ impl Session { debug!("break"); is_reading = Some((stream_read, buffer, opening_cipher)); break; - } else if buf[0] > 4 { + } else { std::mem::swap(&mut opening_cipher, &mut self.common.cipher.remote_to_local); // TODO it'd be cleaner to just pass cipher to reply() - match reply(self, handler, buf).await { + match reply(self, handler, &mut buffer.seqn, buf).await { Ok((h, s)) => { handler = h; self = s;
russh/src/session.rs+9 −0 modified@@ -63,6 +63,7 @@ pub(crate) struct CommonSession<Config> { pub wants_reply: bool, pub disconnected: bool, pub buffer: CryptoVec, + pub strict_kex: bool, } impl<C> CommonSession<C> { @@ -74,6 +75,7 @@ impl<C> CommonSession<C> { enc.client_mac = newkeys.names.client_mac; enc.server_mac = newkeys.names.server_mac; self.cipher = newkeys.cipher; + self.strict_kex = self.strict_kex || newkeys.names.strict_kex; } } @@ -99,6 +101,7 @@ impl<C> CommonSession<C> { decompress: crate::compression::Decompress::None, }); self.cipher = newkeys.cipher; + self.strict_kex = newkeys.names.strict_kex; } /// Send a disconnect message. @@ -127,6 +130,12 @@ impl<C> CommonSession<C> { enc.byte(channel, msg) } } + + pub(crate) fn maybe_reset_seqn(&mut self) { + if self.strict_kex { + self.write_buffer.seqn = Wrapping(0); + } + } } impl Encrypted {
7279fbd6ef4dStrict KEX 対応 (CVE-2023-48795)
3 files changed · +22 −5
ttssh2/ttxssh/kex.c+4 −4 modified@@ -146,8 +146,8 @@ void SSH2_update_kex_myproposal(PTInstVar pvar) // �L�[�č쐬�̏ꍇ�ɂ́A�ڑ����� pvar->settings ����g�ݗ��Ă�ꂽ myproposal ������������B // pvar->settings �� �ڑ����� myproposal ���쐬�����Ƃ��̒l����ς���Ă��Ȃ��ۏ��Ȃ��B // �ēx�g�ݗ��Ă�̂ł͂Ȃ������� myproposal �����������邱�Ƃɂ����B - int pos = strlen(myproposal[PROPOSAL_KEX_ALGS]) - strlen(",ext-info-c"); - if (strcmp(myproposal[PROPOSAL_KEX_ALGS] + pos, ",ext-info-c") == 0) { + int pos = strlen(myproposal[PROPOSAL_KEX_ALGS]) - strlen(",ext-info-c,kex-strict-c-v00@openssh.com"); + if (strcmp(myproposal[PROPOSAL_KEX_ALGS] + pos, ",ext-info-c,kex-strict-c-v00@openssh.com") == 0) { myproposal[PROPOSAL_KEX_ALGS][pos] = '\0'; } } @@ -163,8 +163,8 @@ void SSH2_update_kex_myproposal(PTInstVar pvar) strncat_s(buf, sizeof(buf), ",", _TRUNCATE); } - // RFC 8308 Extension Negotiation - strncat_s(buf, sizeof(buf), "ext-info-c", _TRUNCATE); + // Enables RFC 8308 Extension Negotiation & Strict KEX mode (for CVE-2023-48795) + strncat_s(buf, sizeof(buf), "ext-info-c,kex-strict-c-v00@openssh.com", _TRUNCATE); myproposal[PROPOSAL_KEX_ALGS] = buf; }
ttssh2/ttxssh/ssh.c+17 −1 modified@@ -2997,6 +2997,7 @@ void SSH_init(PTInstVar pvar) pvar->use_subsystem = FALSE; pvar->nosession = FALSE; pvar->server_sig_algs = NULL; + pvar->server_strict_kex = FALSE; } @@ -4813,7 +4814,7 @@ static BOOL handle_SSH2_kexinit(PTInstVar pvar) if (pvar->kex_status == KEX_FLAG_KEXDONE) { pvar->kex_status = KEX_FLAG_REKEYING; - // �L�[�č쐬���� myproposal ���� ",ext-info-c" ���폜���� + // �L�[�č쐬���� myproposal ���� ",ext-info-c,kex-strict-c-v00@openssh.com" ���폜���� // �X�V����̂� KEX �݂̂ł悢 SSH2_update_kex_myproposal(pvar); @@ -4878,6 +4879,13 @@ static BOOL handle_SSH2_kexinit(PTInstVar pvar) goto error; } + // �T�[�o�[����Strict KEX�ɑΉ����Ă��邩�̊m�F + choose_SSH2_proposal(buf, "kex-strict-s-v00@openssh.com", tmp, sizeof(tmp)); + if (tmp[0] != '\0') { + pvar->server_strict_kex = TRUE; + logprintf(LOG_LEVEL_INFO, "Server supports strict kex. Strict kex will be enabled."); + } + // �z�X�g���A���S���Y�� switch (get_namelist_from_payload(pvar, buf, sizeof(buf), &size)) { case GetPayloadError: @@ -5644,6 +5652,10 @@ static void ssh2_send_newkeys(PTInstVar pvar) pvar->kex_status |= KEX_FLAG_NEWKEYS_SENT; + if (pvar->server_strict_kex) { + pvar->ssh_state.sender_sequence_number = 0; + } + // SSH2_MSG_NEWKEYS �����Ɏ���Ă�����KEX�͊����B���̏����Ɉڂ�B if (pvar->kex_status & KEX_FLAG_NEWKEYS_RECEIVED) { if ((pvar->kex_status & KEX_FLAG_REKEYING)) { @@ -6238,6 +6250,10 @@ static BOOL handle_SSH2_newkeys(PTInstVar pvar) pvar->ssh2_keys[MODE_IN].comp.enabled = 1; enable_recv_compression(pvar); + if (pvar->server_strict_kex) { + pvar->ssh_state.receiver_sequence_number = 0; + } + SSH2_dispatch_add_message(SSH2_MSG_EXT_INFO); // SSH2_MSG_NEWKEYS �����ɑ����Ă�����KEX�͊����B���̏����Ɉڂ�B
ttssh2/ttxssh/ttxssh.h+1 −0 modified@@ -363,6 +363,7 @@ typedef struct _TInstVar { } recv; char *server_sig_algs; + BOOL server_strict_kex; char UIMsg[MAX_UIMSG]; } TInstVar;
9d2ee975ef9fssh: implement strict KEX protocol changes
3 files changed · +388 −9
ssh/handshake.go+52 −4 modified@@ -35,6 +35,16 @@ type keyingTransport interface { // direction will be effected if a msgNewKeys message is sent // or received. prepareKeyChange(*algorithms, *kexResult) error + + // setStrictMode sets the strict KEX mode, notably triggering + // sequence number resets on sending or receiving msgNewKeys. + // If the sequence number is already > 1 when setStrictMode + // is called, an error is returned. + setStrictMode() error + + // setInitialKEXDone indicates to the transport that the initial key exchange + // was completed + setInitialKEXDone() } // handshakeTransport implements rekeying on top of a keyingTransport @@ -100,6 +110,10 @@ type handshakeTransport struct { // The session ID or nil if first kex did not complete yet. sessionID []byte + + // strictMode indicates if the other side of the handshake indicated + // that we should be following the strict KEX protocol restrictions. + strictMode bool } type pendingKex struct { @@ -209,7 +223,10 @@ func (t *handshakeTransport) readLoop() { close(t.incoming) break } - if p[0] == msgIgnore || p[0] == msgDebug { + // If this is the first kex, and strict KEX mode is enabled, + // we don't ignore any messages, as they may be used to manipulate + // the packet sequence numbers. + if !(t.sessionID == nil && t.strictMode) && (p[0] == msgIgnore || p[0] == msgDebug) { continue } t.incoming <- p @@ -441,6 +458,11 @@ func (t *handshakeTransport) readOnePacket(first bool) ([]byte, error) { return successPacket, nil } +const ( + kexStrictClient = "kex-strict-c-v00@openssh.com" + kexStrictServer = "kex-strict-s-v00@openssh.com" +) + // sendKexInit sends a key change message. func (t *handshakeTransport) sendKexInit() error { t.mu.Lock() @@ -454,7 +476,6 @@ func (t *handshakeTransport) sendKexInit() error { } msg := &kexInitMsg{ - KexAlgos: t.config.KeyExchanges, CiphersClientServer: t.config.Ciphers, CiphersServerClient: t.config.Ciphers, MACsClientServer: t.config.MACs, @@ -464,6 +485,13 @@ func (t *handshakeTransport) sendKexInit() error { } io.ReadFull(rand.Reader, msg.Cookie[:]) + // We mutate the KexAlgos slice, in order to add the kex-strict extension algorithm, + // and possibly to add the ext-info extension algorithm. Since the slice may be the + // user owned KeyExchanges, we create our own slice in order to avoid using user + // owned memory by mistake. + msg.KexAlgos = make([]string, 0, len(t.config.KeyExchanges)+2) // room for kex-strict and ext-info + msg.KexAlgos = append(msg.KexAlgos, t.config.KeyExchanges...) + isServer := len(t.hostKeys) > 0 if isServer { for _, k := range t.hostKeys { @@ -488,17 +516,24 @@ func (t *handshakeTransport) sendKexInit() error { msg.ServerHostKeyAlgos = append(msg.ServerHostKeyAlgos, keyFormat) } } + + if t.sessionID == nil { + msg.KexAlgos = append(msg.KexAlgos, kexStrictServer) + } } else { msg.ServerHostKeyAlgos = t.hostKeyAlgorithms // As a client we opt in to receiving SSH_MSG_EXT_INFO so we know what // algorithms the server supports for public key authentication. See RFC // 8308, Section 2.1. + // + // We also send the strict KEX mode extension algorithm, in order to opt + // into the strict KEX mode. if firstKeyExchange := t.sessionID == nil; firstKeyExchange { - msg.KexAlgos = make([]string, 0, len(t.config.KeyExchanges)+1) - msg.KexAlgos = append(msg.KexAlgos, t.config.KeyExchanges...) msg.KexAlgos = append(msg.KexAlgos, "ext-info-c") + msg.KexAlgos = append(msg.KexAlgos, kexStrictClient) } + } packet := Marshal(msg) @@ -604,6 +639,13 @@ func (t *handshakeTransport) enterKeyExchange(otherInitPacket []byte) error { return err } + if t.sessionID == nil && ((isClient && contains(serverInit.KexAlgos, kexStrictServer)) || (!isClient && contains(clientInit.KexAlgos, kexStrictClient))) { + t.strictMode = true + if err := t.conn.setStrictMode(); err != nil { + return err + } + } + // We don't send FirstKexFollows, but we handle receiving it. // // RFC 4253 section 7 defines the kex and the agreement method for @@ -679,6 +721,12 @@ func (t *handshakeTransport) enterKeyExchange(otherInitPacket []byte) error { return unexpectedMessageError(msgNewKeys, packet[0]) } + if firstKeyExchange { + // Indicates to the transport that the first key exchange is completed + // after receiving SSH_MSG_NEWKEYS. + t.conn.setInitialKEXDone() + } + return nil }
ssh/handshake_test.go+309 −0 modified@@ -395,6 +395,10 @@ func (n *errorKeyingTransport) readPacket() ([]byte, error) { return n.packetConn.readPacket() } +func (n *errorKeyingTransport) setStrictMode() error { return nil } + +func (n *errorKeyingTransport) setInitialKEXDone() {} + func TestHandshakeErrorHandlingRead(t *testing.T) { for i := 0; i < 20; i++ { testHandshakeErrorHandlingN(t, i, -1, false) @@ -710,3 +714,308 @@ func TestPickIncompatibleHostKeyAlgo(t *testing.T) { t.Fatal("incompatible signer returned") } } + +func TestStrictKEXResetSeqFirstKEX(t *testing.T) { + if runtime.GOOS == "plan9" { + t.Skip("see golang.org/issue/7237") + } + + checker := &syncChecker{ + waitCall: make(chan int, 10), + called: make(chan int, 10), + } + + checker.waitCall <- 1 + trC, trS, err := handshakePair(&ClientConfig{HostKeyCallback: checker.Check}, "addr", false) + if err != nil { + t.Fatalf("handshakePair: %v", err) + } + <-checker.called + + t.Cleanup(func() { + trC.Close() + trS.Close() + }) + + // Throw away the msgExtInfo packet sent during the handshake by the server + _, err = trC.readPacket() + if err != nil { + t.Fatalf("readPacket failed: %s", err) + } + + // close the handshake transports before checking the sequence number to + // avoid races. + trC.Close() + trS.Close() + + // check that the sequence number counters. We reset after msgNewKeys, but + // then the server immediately writes msgExtInfo, and we close the + // transports so we expect read 2, write 0 on the client and read 1, write 1 + // on the server. + if trC.conn.(*transport).reader.seqNum != 2 || trC.conn.(*transport).writer.seqNum != 0 || + trS.conn.(*transport).reader.seqNum != 1 || trS.conn.(*transport).writer.seqNum != 1 { + t.Errorf( + "unexpected sequence counters:\nclient: reader %d (expected 2), writer %d (expected 0)\nserver: reader %d (expected 1), writer %d (expected 1)", + trC.conn.(*transport).reader.seqNum, + trC.conn.(*transport).writer.seqNum, + trS.conn.(*transport).reader.seqNum, + trS.conn.(*transport).writer.seqNum, + ) + } +} + +func TestStrictKEXResetSeqSuccessiveKEX(t *testing.T) { + if runtime.GOOS == "plan9" { + t.Skip("see golang.org/issue/7237") + } + + checker := &syncChecker{ + waitCall: make(chan int, 10), + called: make(chan int, 10), + } + + checker.waitCall <- 1 + trC, trS, err := handshakePair(&ClientConfig{HostKeyCallback: checker.Check}, "addr", false) + if err != nil { + t.Fatalf("handshakePair: %v", err) + } + <-checker.called + + t.Cleanup(func() { + trC.Close() + trS.Close() + }) + + // Throw away the msgExtInfo packet sent during the handshake by the server + _, err = trC.readPacket() + if err != nil { + t.Fatalf("readPacket failed: %s", err) + } + + // write and read five packets on either side to bump the sequence numbers + for i := 0; i < 5; i++ { + if err := trC.writePacket([]byte{msgRequestSuccess}); err != nil { + t.Fatalf("writePacket failed: %s", err) + } + if _, err := trS.readPacket(); err != nil { + t.Fatalf("readPacket failed: %s", err) + } + if err := trS.writePacket([]byte{msgRequestSuccess}); err != nil { + t.Fatalf("writePacket failed: %s", err) + } + if _, err := trC.readPacket(); err != nil { + t.Fatalf("readPacket failed: %s", err) + } + } + + // Request a key exchange, which should cause the sequence numbers to reset + checker.waitCall <- 1 + trC.requestKeyExchange() + <-checker.called + + // write a packet on the client, and then read it, to verify the key change has actually happened, since + // the HostKeyCallback is called _during_ the handshake, so isn't actually indicative of the handshake + // finishing. + dummyPacket := []byte{99} + if err := trS.writePacket(dummyPacket); err != nil { + t.Fatalf("writePacket failed: %s", err) + } + if p, err := trC.readPacket(); err != nil { + t.Fatalf("readPacket failed: %s", err) + } else if !bytes.Equal(p, dummyPacket) { + t.Fatalf("unexpected packet: got %x, want %x", p, dummyPacket) + } + + // close the handshake transports before checking the sequence number to + // avoid races. + trC.Close() + trS.Close() + + if trC.conn.(*transport).reader.seqNum != 2 || trC.conn.(*transport).writer.seqNum != 0 || + trS.conn.(*transport).reader.seqNum != 1 || trS.conn.(*transport).writer.seqNum != 1 { + t.Errorf( + "unexpected sequence counters:\nclient: reader %d (expected 2), writer %d (expected 0)\nserver: reader %d (expected 1), writer %d (expected 1)", + trC.conn.(*transport).reader.seqNum, + trC.conn.(*transport).writer.seqNum, + trS.conn.(*transport).reader.seqNum, + trS.conn.(*transport).writer.seqNum, + ) + } +} + +func TestSeqNumIncrease(t *testing.T) { + if runtime.GOOS == "plan9" { + t.Skip("see golang.org/issue/7237") + } + + checker := &syncChecker{ + waitCall: make(chan int, 10), + called: make(chan int, 10), + } + + checker.waitCall <- 1 + trC, trS, err := handshakePair(&ClientConfig{HostKeyCallback: checker.Check}, "addr", false) + if err != nil { + t.Fatalf("handshakePair: %v", err) + } + <-checker.called + + t.Cleanup(func() { + trC.Close() + trS.Close() + }) + + // Throw away the msgExtInfo packet sent during the handshake by the server + _, err = trC.readPacket() + if err != nil { + t.Fatalf("readPacket failed: %s", err) + } + + // write and read five packets on either side to bump the sequence numbers + for i := 0; i < 5; i++ { + if err := trC.writePacket([]byte{msgRequestSuccess}); err != nil { + t.Fatalf("writePacket failed: %s", err) + } + if _, err := trS.readPacket(); err != nil { + t.Fatalf("readPacket failed: %s", err) + } + if err := trS.writePacket([]byte{msgRequestSuccess}); err != nil { + t.Fatalf("writePacket failed: %s", err) + } + if _, err := trC.readPacket(); err != nil { + t.Fatalf("readPacket failed: %s", err) + } + } + + // close the handshake transports before checking the sequence number to + // avoid races. + trC.Close() + trS.Close() + + if trC.conn.(*transport).reader.seqNum != 7 || trC.conn.(*transport).writer.seqNum != 5 || + trS.conn.(*transport).reader.seqNum != 6 || trS.conn.(*transport).writer.seqNum != 6 { + t.Errorf( + "unexpected sequence counters:\nclient: reader %d (expected 7), writer %d (expected 5)\nserver: reader %d (expected 6), writer %d (expected 6)", + trC.conn.(*transport).reader.seqNum, + trC.conn.(*transport).writer.seqNum, + trS.conn.(*transport).reader.seqNum, + trS.conn.(*transport).writer.seqNum, + ) + } +} + +func TestStrictKEXUnexpectedMsg(t *testing.T) { + if runtime.GOOS == "plan9" { + t.Skip("see golang.org/issue/7237") + } + + // Check that unexpected messages during the handshake cause failure + _, _, err := handshakePair(&ClientConfig{HostKeyCallback: func(hostname string, remote net.Addr, key PublicKey) error { return nil }}, "addr", true) + if err == nil { + t.Fatal("handshake should fail when there are unexpected messages during the handshake") + } + + trC, trS, err := handshakePair(&ClientConfig{HostKeyCallback: func(hostname string, remote net.Addr, key PublicKey) error { return nil }}, "addr", false) + if err != nil { + t.Fatalf("handshake failed: %s", err) + } + + // Check that ignore/debug pacekts are still ignored outside of the handshake + if err := trC.writePacket([]byte{msgIgnore}); err != nil { + t.Fatalf("writePacket failed: %s", err) + } + if err := trC.writePacket([]byte{msgDebug}); err != nil { + t.Fatalf("writePacket failed: %s", err) + } + dummyPacket := []byte{99} + if err := trC.writePacket(dummyPacket); err != nil { + t.Fatalf("writePacket failed: %s", err) + } + + if p, err := trS.readPacket(); err != nil { + t.Fatalf("readPacket failed: %s", err) + } else if !bytes.Equal(p, dummyPacket) { + t.Fatalf("unexpected packet: got %x, want %x", p, dummyPacket) + } +} + +func TestStrictKEXMixed(t *testing.T) { + // Test that we still support a mixed connection, where one side sends kex-strict but the other + // side doesn't. + + a, b, err := netPipe() + if err != nil { + t.Fatalf("netPipe failed: %s", err) + } + + var trC, trS keyingTransport + + trC = newTransport(a, rand.Reader, true) + trS = newTransport(b, rand.Reader, false) + trS = addNoiseTransport(trS) + + clientConf := &ClientConfig{HostKeyCallback: func(hostname string, remote net.Addr, key PublicKey) error { return nil }} + clientConf.SetDefaults() + + v := []byte("version") + client := newClientTransport(trC, v, v, clientConf, "addr", a.RemoteAddr()) + + serverConf := &ServerConfig{} + serverConf.AddHostKey(testSigners["ecdsa"]) + serverConf.AddHostKey(testSigners["rsa"]) + serverConf.SetDefaults() + + transport := newHandshakeTransport(trS, &serverConf.Config, []byte("version"), []byte("version")) + transport.hostKeys = serverConf.hostKeys + transport.publicKeyAuthAlgorithms = serverConf.PublicKeyAuthAlgorithms + + readOneFailure := make(chan error, 1) + go func() { + if _, err := transport.readOnePacket(true); err != nil { + readOneFailure <- err + } + }() + + // Basically sendKexInit, but without the kex-strict extension algorithm + msg := &kexInitMsg{ + KexAlgos: transport.config.KeyExchanges, + CiphersClientServer: transport.config.Ciphers, + CiphersServerClient: transport.config.Ciphers, + MACsClientServer: transport.config.MACs, + MACsServerClient: transport.config.MACs, + CompressionClientServer: supportedCompressions, + CompressionServerClient: supportedCompressions, + ServerHostKeyAlgos: []string{KeyAlgoRSASHA256, KeyAlgoRSASHA512, KeyAlgoRSA}, + } + packet := Marshal(msg) + // writePacket destroys the contents, so save a copy. + packetCopy := make([]byte, len(packet)) + copy(packetCopy, packet) + if err := transport.pushPacket(packetCopy); err != nil { + t.Fatalf("pushPacket: %s", err) + } + transport.sentInitMsg = msg + transport.sentInitPacket = packet + + if err := transport.getWriteError(); err != nil { + t.Fatalf("getWriteError failed: %s", err) + } + var request *pendingKex + select { + case err = <-readOneFailure: + t.Fatalf("server readOnePacket failed: %s", err) + case request = <-transport.startKex: + break + } + + // We expect the following calls to fail if the side which does not support + // kex-strict sends unexpected/ignored packets during the handshake, even if + // the other side does support kex-strict. + + if err := transport.enterKeyExchange(request.otherInit); err != nil { + t.Fatalf("enterKeyExchange failed: %s", err) + } + if err := client.waitSession(); err != nil { + t.Fatalf("client.waitSession: %v", err) + } +}
ssh/transport.go+27 −5 modified@@ -49,6 +49,9 @@ type transport struct { rand io.Reader isClient bool io.Closer + + strictMode bool + initialKEXDone bool } // packetCipher represents a combination of SSH encryption/MAC @@ -74,6 +77,18 @@ type connectionState struct { pendingKeyChange chan packetCipher } +func (t *transport) setStrictMode() error { + if t.reader.seqNum != 1 { + return errors.New("ssh: sequence number != 1 when strict KEX mode requested") + } + t.strictMode = true + return nil +} + +func (t *transport) setInitialKEXDone() { + t.initialKEXDone = true +} + // prepareKeyChange sets up key material for a keychange. The key changes in // both directions are triggered by reading and writing a msgNewKey packet // respectively. @@ -112,11 +127,12 @@ func (t *transport) printPacket(p []byte, write bool) { // Read and decrypt next packet. func (t *transport) readPacket() (p []byte, err error) { for { - p, err = t.reader.readPacket(t.bufReader) + p, err = t.reader.readPacket(t.bufReader, t.strictMode) if err != nil { break } - if len(p) == 0 || (p[0] != msgIgnore && p[0] != msgDebug) { + // in strict mode we pass through DEBUG and IGNORE packets only during the initial KEX + if len(p) == 0 || (t.strictMode && !t.initialKEXDone) || (p[0] != msgIgnore && p[0] != msgDebug) { break } } @@ -127,7 +143,7 @@ func (t *transport) readPacket() (p []byte, err error) { return p, err } -func (s *connectionState) readPacket(r *bufio.Reader) ([]byte, error) { +func (s *connectionState) readPacket(r *bufio.Reader, strictMode bool) ([]byte, error) { packet, err := s.packetCipher.readCipherPacket(s.seqNum, r) s.seqNum++ if err == nil && len(packet) == 0 { @@ -140,6 +156,9 @@ func (s *connectionState) readPacket(r *bufio.Reader) ([]byte, error) { select { case cipher := <-s.pendingKeyChange: s.packetCipher = cipher + if strictMode { + s.seqNum = 0 + } default: return nil, errors.New("ssh: got bogus newkeys message") } @@ -170,10 +189,10 @@ func (t *transport) writePacket(packet []byte) error { if debugTransport { t.printPacket(packet, true) } - return t.writer.writePacket(t.bufWriter, t.rand, packet) + return t.writer.writePacket(t.bufWriter, t.rand, packet, t.strictMode) } -func (s *connectionState) writePacket(w *bufio.Writer, rand io.Reader, packet []byte) error { +func (s *connectionState) writePacket(w *bufio.Writer, rand io.Reader, packet []byte, strictMode bool) error { changeKeys := len(packet) > 0 && packet[0] == msgNewKeys err := s.packetCipher.writeCipherPacket(s.seqNum, w, rand, packet) @@ -188,6 +207,9 @@ func (s *connectionState) writePacket(w *bufio.Writer, rand io.Reader, packet [] select { case cipher := <-s.pendingKeyChange: s.packetCipher = cipher + if strictMode { + s.seqNum = 0 + } default: panic("ssh: no key material for msgNewKeys") }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
179- git.libssh.org/projects/libssh.git/commit/nvdPatch
- github.com/TeraTermProject/teraterm/commit/7279fbd6ef4d0c8bdd6a90af4ada2899d786eec0nvdPatch
- github.com/connectbot/sshlib/commit/5c8b534f6e97db7ac0e0e579331213aa25c173abnvdPatch
- github.com/erlang/otp/blob/d1b43dc0f1361d2ad67601169e90a7fc50bb0369/lib/ssh/doc/src/notes.xmlnvdPatch
- github.com/golang/crypto/commit/9d2ee975ef9fe627bf0a6f01c1f69e8ef1d4f05dnvdPatch
- github.com/jtesta/ssh-audit/commit/8e972c5e94b460379fe0c7d20209c16df81538a5nvdPatch
- github.com/mkj/dropbear/blob/17657c36cce6df7716d5ff151ec09a665382d5dd/CHANGESnvdPatch
- github.com/mscdex/ssh2/commit/97b223f8891b96d6fc054df5ab1d5a1a545da2a3nvdPatch
- github.com/net-ssh/net-ssh/blob/2e65064a52d73396bfc3806c9196fc8108f33cd8/CHANGES.txtnvdPatch
- github.com/openssh/openssh-portable/commits/masternvdPatch
- nest.pijul.com/pijul/thrussh/changes/D6H7OWTTMHHX6BTB3B6MNBOBX2L66CBL4LGSEUSAI2MCRCJDQFRQCnvdPatch
- www.terrapin-attack.comnvdExploit
- www.vicarius.io/vsociety/posts/cve-2023-48795-detect-openssh-vulnerabilitnvdExploitThird Party Advisory
- www.vicarius.io/vsociety/posts/cve-2023-48795-mitigate-openssh-vulnerabilitynvdExploitThird Party Advisory
- packetstormsecurity.com/files/176280/Terrapin-SSH-Connection-Weakening.htmlnvdThird Party AdvisoryVDB Entry
- seclists.org/fulldisclosure/2024/Mar/21nvdMailing ListThird Party Advisory
- access.redhat.com/security/cve/cve-2023-48795nvdThird Party Advisory
- github.com/advisories/GHSA-45x7-px36-x8w8nvdThird Party AdvisoryADVISORY
- github.com/connectbot/sshlib/compare/2.2.21...2.2.22nvdThird Party Advisory
- lists.debian.org/debian-lts-announce/2024/01/msg00013.htmlnvdMailing ListThird Party Advisory
- lists.debian.org/debian-lts-announce/2024/01/msg00014.htmlnvdMailing ListThird Party Advisory
- lists.debian.org/debian-lts-announce/2024/04/msg00016.htmlnvdMailing ListThird Party Advisory
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/33XHJUB6ROFUOH2OQNENFROTVH6MHSHA/nvdMailing ListThird Party Advisory
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/3CAYYW35MUTNO65RVAELICTNZZFMT2XS/nvdMailing ListThird Party Advisory
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/3JIMLVBDWOP4FUPXPTB4PGHHIOMGFLQE/nvdMailing ListThird Party Advisory
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/3YQLUQWLIHDB5QCXQEX7HXHAWMOKPP5O/nvdMailing ListThird Party Advisory
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/6Y74KVCPEPT4MVU3LHDWCNNOXOE5ZLUR/nvdMailing ListThird Party Advisory
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/APYIXIQOVDCRWLHTGB4VYMAUIAQLKYJ3/nvdMailing ListThird Party Advisory
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/BL5KTLOSLH2KHRN4HCXJPK3JUVLDGEL6/nvdMailing ListThird Party Advisory
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/C3AFMZ6MH2UHHOPIWT5YLSFV3D2VB3AC/nvdMailing ListThird Party Advisory
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/CHHITS4PUOZAKFIUBQAQZC7JWXMOYE4B/nvdMailing ListThird Party Advisory
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/F7EYCFQCTSGJXWO3ZZ44MGKFC5HA7G3Y/nvdMailing ListThird Party Advisory
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/HYEDEXIKFKTUJIN43RG4B7T5ZS6MHUSP/nvdMailing ListThird Party Advisory
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/I724O3LSRCPO4WNVIXTZCT4VVRMXMMSG/nvdMailing ListThird Party Advisory
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/KEOTKBUPZXHE3F352JBYNTSNRXYLWD6P/nvdMailing ListThird Party Advisory
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/KMZCVGUGJZZVDPCVDA7TEB22VUCNEXDD/nvdMailing ListThird Party Advisory
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/L5Y6MNNVAPIJSXJERQ6PKZVCIUXSNJK7/nvdMailing ListThird Party Advisory
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/LZQVUHWVWRH73YBXUQJOD6CKHDQBU3DM/nvdMailing ListThird Party Advisory
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/MKQRBF3DWMWPH36LBCOBUTSIZRTPEZXB/nvdVendor Advisory
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/QI3EHAHABFQK7OABNCSF5GMYP6TONTI7/nvdMailing ListThird Party Advisory
- psirt.global.sonicwall.com/vuln-detail/SNWLID-2024-0002nvdThird Party Advisory
- security-tracker.debian.org/tracker/CVE-2023-48795nvdVendor Advisory
- security-tracker.debian.org/tracker/source-package/libssh2nvdVendor Advisory
- security-tracker.debian.org/tracker/source-package/proftpd-dfsgnvdVendor Advisory
- security.gentoo.org/glsa/202312-16nvdThird Party Advisory
- security.gentoo.org/glsa/202312-17nvdThird Party Advisory
- security.netapp.com/advisory/ntap-20240105-0004/nvdThird Party Advisory
- support.apple.com/kb/HT214084nvdThird Party Advisory
- thorntech.com/cve-2023-48795-and-sftp-gateway/nvdThird Party Advisory
- ubuntu.com/security/CVE-2023-48795nvdVendor Advisory
- www.lancom-systems.de/service-support/allgemeine-sicherheitshinweisenvdVendor Advisory
- www.openwall.com/lists/oss-security/2023/12/18/3nvdMailing List
- www.openwall.com/lists/oss-security/2023/12/19/5nvdMailing List
- www.openwall.com/lists/oss-security/2023/12/20/3nvdMailing ListMitigation
- www.openwall.com/lists/oss-security/2024/03/06/3nvdMailing List
- www.openwall.com/lists/oss-security/2024/04/17/8nvdMailing List
- arstechnica.com/security/2023/12/hackers-can-break-ssh-channel-integrity-using-novel-data-corruption-attack/nvdPress/Media Coverage
- bugs.gentoo.org/920280nvdIssue Tracking
- bugzilla.redhat.com/show_bug.cginvdIssue Tracking
- bugzilla.suse.com/show_bug.cginvdIssue Tracking
- crates.io/crates/thrussh/versionsnvdRelease Notes
- filezilla-project.org/versions.phpnvdRelease Notes
- forum.netgate.com/topic/184941/terrapin-ssh-attacknvdIssue Tracking
- github.com/NixOS/nixpkgs/pull/275249nvdRelease Notes
- github.com/PowerShell/Win32-OpenSSH/issues/2189nvdIssue Tracking
- github.com/PowerShell/Win32-OpenSSH/releases/tag/v9.5.0.0p1-BetanvdRelease Notes
- github.com/TeraTermProject/teraterm/releases/tag/v5.1nvdRelease Notes
- github.com/apache/mina-sshd/issues/445nvdIssue Tracking
- github.com/cyd01/KiTTY/issues/520nvdIssue Tracking
- github.com/drakkan/sftpgo/releases/tag/v2.5.6nvdRelease Notes
- github.com/erlang/otp/releases/tag/OTP-26.2.1nvdRelease Notes
- github.com/hierynomus/sshj/issues/916nvdIssue Tracking
- github.com/janmojzis/tinyssh/issues/81nvdIssue Tracking
- github.com/libssh2/libssh2/pull/1291nvdMitigation
- github.com/mwiede/jsch/compare/jsch-0.2.14...jsch-0.2.15nvdProduct
- github.com/mwiede/jsch/issues/457nvdIssue Tracking
- github.com/mwiede/jsch/pull/461nvdRelease Notes
- github.com/paramiko/paramiko/issues/2337nvdIssue Tracking
- github.com/proftpd/proftpd/blob/0a7ea9b0ba9fcdf368374a226370d08f10397d99/RELEASE_NOTESnvdRelease Notes
- github.com/proftpd/proftpd/blob/d21e7a2e47e9b38f709bec58e3fa711f759ad0e1/RELEASE_NOTESnvdRelease Notes
- github.com/proftpd/proftpd/blob/master/RELEASE_NOTESnvdRelease Notes
- github.com/proftpd/proftpd/issues/456nvdIssue Tracking
- github.com/ronf/asyncssh/blob/develop/docs/changes.rstnvdRelease Notes
- github.com/ssh-mitm/ssh-mitm/issues/165nvdIssue Tracking
- github.com/warp-tech/russh/releases/tag/v0.40.2nvdRelease Notes
- groups.google.com/g/golang-announce/c/-n5WqVC18LQnvdMailing List
- groups.google.com/g/golang-announce/c/qA3XtxvMUygnvdMailing List
- help.panic.com/releasenotes/transmit5/nvdRelease Notes
- jadaptive.com/important-java-ssh-security-update-new-ssh-vulnerability-discovered-cve-2023-48795/nvdPress/Media Coverage
- lists.debian.org/debian-lts-announce/2023/12/msg00017.htmlnvdMailing List
- matt.ucc.asn.au/dropbear/CHANGESnvdRelease Notes
- news.ycombinator.com/itemnvdIssue Tracking
- news.ycombinator.com/itemnvdIssue Tracking
- news.ycombinator.com/itemnvdIssue Tracking
- nova.app/releases/nvdRelease Notes
- oryx-embedded.com/download/nvdRelease Notes
- roumenpetrov.info/secsh/nvdRelease Notes
- security-tracker.debian.org/tracker/source-package/trilead-ssh2nvdIssue Tracking
- twitter.com/TrueSkrillor/status/1736774389725565005nvdPress/Media Coverage
- winscp.net/eng/docs/historynvdRelease Notes
- www.bitvise.com/ssh-client-version-historynvdRelease Notes
- www.bitvise.com/ssh-server-version-historynvdRelease Notes
- www.chiark.greenend.org.uk/~sgtatham/putty/changes.htmlnvdRelease Notes
- www.crushftp.com/crush10wiki/Wiki.jspnvdRelease Notes
- www.debian.org/security/2023/dsa-5586nvdIssue Tracking
- www.debian.org/security/2023/dsa-5588nvdIssue Tracking
- www.freebsd.org/security/advisories/FreeBSD-SA-23:19.openssh.ascnvdRelease Notes
- www.netsarang.com/en/xshell-update-history/nvdRelease Notes
- www.openssh.com/openbsd.htmlnvdRelease Notes
- www.openssh.com/txt/release-9.6nvdRelease Notes
- www.openwall.com/lists/oss-security/2023/12/18/2nvdMailing List
- www.openwall.com/lists/oss-security/2023/12/20/3nvdMailing ListMitigation
- www.paramiko.org/changelog.htmlnvdRelease Notes
- www.reddit.com/r/sysadmin/comments/18idv52/cve202348795_why_is_this_cve_still_undisclosed/nvdIssue Tracking
- www.suse.com/c/suse-addresses-the-ssh-v2-protocol-terrapin-attack-aka-cve-2023-48795/nvdPress/Media Coverage
- www.theregister.com/2023/12/20/terrapin_attack_sshnvdPress/Media Coverage
- www.vandyke.com/products/securecrt/history.txtnvdRelease Notes
- arstechnica.com/security/2023/12/hackers-can-break-ssh-channel-integrity-using-novel-data-corruption-attackghsa
- cert-portal.siemens.com/productcert/html/ssa-082556.htmlnvd
- cert-portal.siemens.com/productcert/html/ssa-364175.htmlnvd
- cert-portal.siemens.com/productcert/html/ssa-769027.htmlnvd
- cert-portal.siemens.com/productcert/html/ssa-794697.htmlnvd
- cert-portal.siemens.com/productcert/html/ssa-915275.htmlnvd
- github.com/paramiko/paramiko/issues/2337ghsa
- github.com/warp-tech/russh/commit/1aa340a7df1d5be1c0f4a9e247aade76dfdd2951ghsa
- github.com/warp-tech/russh/security/advisories/GHSA-45x7-px36-x8w8ghsa
- go.dev/cl/550715ghsa
- go.dev/issue/64784ghsa
- help.panic.com/releasenotes/transmit5ghsa
- jadaptive.com/important-java-ssh-security-update-new-ssh-vulnerability-discovered-cve-2023-48795ghsa
- lists.debian.org/debian-lts-announce/2024/09/msg00042.htmlnvd
- lists.debian.org/debian-lts-announce/2024/11/msg00032.htmlnvd
- lists.debian.org/debian-lts-announce/2025/04/msg00028.htmlnvd
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/33XHJUB6ROFUOH2OQNENFROTVH6MHSHAghsa
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/3CAYYW35MUTNO65RVAELICTNZZFMT2XSghsa
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/3JIMLVBDWOP4FUPXPTB4PGHHIOMGFLQEghsa
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/3YQLUQWLIHDB5QCXQEX7HXHAWMOKPP5Oghsa
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/6Y74KVCPEPT4MVU3LHDWCNNOXOE5ZLURghsa
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/APYIXIQOVDCRWLHTGB4VYMAUIAQLKYJ3ghsa
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/BL5KTLOSLH2KHRN4HCXJPK3JUVLDGEL6ghsa
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/C3AFMZ6MH2UHHOPIWT5YLSFV3D2VB3ACghsa
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/CHHITS4PUOZAKFIUBQAQZC7JWXMOYE4Bghsa
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/F7EYCFQCTSGJXWO3ZZ44MGKFC5HA7G3Yghsa
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/HYEDEXIKFKTUJIN43RG4B7T5ZS6MHUSPghsa
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/I724O3LSRCPO4WNVIXTZCT4VVRMXMMSGghsa
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/KEOTKBUPZXHE3F352JBYNTSNRXYLWD6Pghsa
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/KMZCVGUGJZZVDPCVDA7TEB22VUCNEXDDghsa
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/L5Y6MNNVAPIJSXJERQ6PKZVCIUXSNJK7ghsa
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/LZQVUHWVWRH73YBXUQJOD6CKHDQBU3DMghsa
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/MKQRBF3DWMWPH36LBCOBUTSIZRTPEZXBghsa
- lists.fedoraproject.org/archives/list/package-announce%40lists.fedoraproject.org/message/QI3EHAHABFQK7OABNCSF5GMYP6TONTI7ghsa
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/33XHJUB6ROFUOH2OQNENFROTVH6MHSHAghsa
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/33XHJUB6ROFUOH2OQNENFROTVH6MHSHA/nvd
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/3JIMLVBDWOP4FUPXPTB4PGHHIOMGFLQEghsa
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/3JIMLVBDWOP4FUPXPTB4PGHHIOMGFLQE/nvd
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/3YQLUQWLIHDB5QCXQEX7HXHAWMOKPP5Oghsa
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/3YQLUQWLIHDB5QCXQEX7HXHAWMOKPP5O/nvd
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/6Y74KVCPEPT4MVU3LHDWCNNOXOE5ZLURghsa
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/6Y74KVCPEPT4MVU3LHDWCNNOXOE5ZLUR/nvd
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/APYIXIQOVDCRWLHTGB4VYMAUIAQLKYJ3ghsa
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/APYIXIQOVDCRWLHTGB4VYMAUIAQLKYJ3/nvd
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/C3AFMZ6MH2UHHOPIWT5YLSFV3D2VB3ACghsa
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/C3AFMZ6MH2UHHOPIWT5YLSFV3D2VB3AC/nvd
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/HYEDEXIKFKTUJIN43RG4B7T5ZS6MHUSPghsa
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/HYEDEXIKFKTUJIN43RG4B7T5ZS6MHUSP/nvd
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/I724O3LSRCPO4WNVIXTZCT4VVRMXMMSGghsa
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/I724O3LSRCPO4WNVIXTZCT4VVRMXMMSG/nvd
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/L5Y6MNNVAPIJSXJERQ6PKZVCIUXSNJK7ghsa
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/L5Y6MNNVAPIJSXJERQ6PKZVCIUXSNJK7/nvd
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/LZQVUHWVWRH73YBXUQJOD6CKHDQBU3DMghsa
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/LZQVUHWVWRH73YBXUQJOD6CKHDQBU3DM/nvd
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/MKQRBF3DWMWPH36LBCOBUTSIZRTPEZXBghsa
- lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/MKQRBF3DWMWPH36LBCOBUTSIZRTPEZXB/nvd
- nvd.nist.gov/vuln/detail/CVE-2023-48795ghsa
- security.netapp.com/advisory/ntap-20240105-0004ghsa
- thorntech.com/cve-2023-48795-and-sftp-gatewayghsa
- www.netsarang.com/en/xshell-update-historyghsa
- www.reddit.com/r/sysadmin/comments/18idv52/cve202348795_why_is_this_cve_still_undisclosedghsa
- www.suse.com/c/suse-addresses-the-ssh-v2-protocol-terrapin-attack-aka-cve-2023-48795ghsa
News mentions
0No linked articles in our index yet.