GHSA-5xvq-cp9x-6p6r
Russh: Pre-auth remote panic via all-zero Curve25519 peer public value (encode_mpint OOB)
Summary
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 < s.len() && s[i] == 0 { i += 1 } // i advances to s.len() for all-zero input if s[i] & 0x80 != 0 { // line 482 — index out of bounds: s[s.len()] ... ``` On Curve25519, `scalar * MontgomeryPoint([0;32])` yields `MontgomeryPoint([0;32])` (the identity element), so the all-zero shared secret is attacker-controlled. RFC 7748 §6 requires implementations to detect and reject all-zero / low-order peer public values and shared secrets; russh does not. The client path (`compute_shared_secret`, curve25519.rs:110-142) has the same chain but is reached only after the server host-key signature is verified, so it requires a malicious server that can sign its own host key (same root cause, lower severity). ## PoC The PoC is a standalone `examples/` binary that links the **unmodified** real russh 0.62.2 crate and reproduces over a real TCP connection. It runs an ATTACK case (all-zero `Q_C` → panic) and a CONTROL case (random `Q_C` → completes kex), proving the panic is caused specifically by the all-zero value. ### One-line reproducer ```bash # Drop the .rs below into russh/examples/ of a checkout of # Eugeny/russh @ c4be19f1915c (tag v0.62.2), then: cargo +stable build --release --example e2e_t13_zero_curve25519 RUST_BACKTRACE=1 ./target/release/examples/e2e_t13_zero_curve25519 ``` ### `russh/examples/e2e_t13_zero_curve25519.rs` ```rust // End-to-end PoC: a pre-auth all-zero
Affected packages
| Ecosystem | Package | Affected versions | Fixed versions |
|---|---|---|---|
| rust | russh | — | 0.62.4 |
Remediation: Upgrade to 0.62.4 or later.
References
Includes data from the GitHub Advisory Database, licensed under CC-BY 4.0.