CVE-2023-43660 (published 2023-09-27T22:15Z)

Warpgate is a smart SSH, HTTPS and MySQL bastion host for Linux that doesn't need special client apps. The SSH key verification for a user can be bypassed by sending an SSH key offer without a signature. This allows bypassing authentication under following conditions: 1. The attacker knows the username and a valid target name 2. The attacked knows the user's public key and 3. Only SSH public key authentication is required for the user account. This issue has been addressed in version 0.8.1. Users are advised to upgrade. There are no known workarounds for this vulnerability.

CVSS v3 base score: 8.1, HIGH (CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H)

Weakness information:
cwe_id: CWE-347
cwe_name: Improper Verification of Cryptographic Signature
description: The product does not verify, or incorrectly verifies, the cryptographic signature for data.
extended_description: 
            
            
         
         

Fix information:
repo_url: https://github.com/warp-tech/warpgate
commit_details: repo_url: https://github.com/warp-tech/warpgate
author: Eugene
author_date: 2023-09-27 21:14:41+02:00
msg: fixed GHSA-3cjp-w4cp-m9c8 - interpreting SSH public key offers as a successful authentication
repository: repo_url: https://github.com/warp-tech/warpgate
repo_name: warp-tech/warpgate
description: Smart SSH, HTTPS and MySQL bastion that requires no additional client-side software
repo_language: Rust
owner: warp-tech
file_changes: filename: Cargo.lock
change_type: ModificationType.MODIFY
diff: @@ -1473,9 +1473,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
 
 [[package]]
 name = "form_urlencoded"
-version = "1.1.0"
+version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
+checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652"
 dependencies = [
  "percent-encoding",
 ]
@@ -2057,9 +2057,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
 
 [[package]]
 name = "idna"
-version = "0.3.0"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
+checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
 dependencies = [
  "unicode-bidi",
  "unicode-normalization",
@@ -2978,9 +2978,9 @@ dependencies = [
 
 [[package]]
 name = "percent-encoding"
-version = "2.2.0"
+version = "2.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
+checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
 
 [[package]]
 name = "phf"
@@ -3625,9 +3625,9 @@ dependencies = [
 
 [[package]]
 name = "russh"
-version = "0.38.0"
+version = "0.39.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae0efcc0f4cd6c062c07e572ce4b806e3967fa029fcbfcc0aa98fb5910a37925"
+checksum = "7878311587d0353a854d5be954fbe68bdf6e77873933b484d1e45db12bb2f8cf"
 dependencies = [
  "aes",
  "aes-gcm",
@@ -5162,9 +5162,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
 
 [[package]]
 name = "url"
-version = "2.3.1"
+version = "2.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
+checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5"
 dependencies = [
  "form_urlencoded",
  "idna",
@@ -5256,7 +5256,7 @@ dependencies = [
 
 [[package]]
 name = "warpgate"
-version = "0.7.4"
+version = "0.8.0"
 dependencies = [
  "ansi_term",
  "anyhow",
@@ -5292,7 +5292,7 @@ dependencies = [
 
 [[package]]
 name = "warpgate-admin"
-version = "0.7.4"
+version = "0.8.0"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -5321,7 +5321,7 @@ dependencies = [
 
 [[package]]
 name = "warpgate-common"
-version = "0.7.4"
+version = "0.8.0"
 dependencies = [
  "anyhow",
  "argon2",
@@ -5357,7 +5357,7 @@ dependencies = [
 
 [[package]]
 name = "warpgate-core"
-version = "0.7.4"
+version = "0.8.0"
 dependencies = [
  "anyhow",
  "argon2",
@@ -5397,7 +5397,7 @@ dependencies = [
 
 [[package]]
 name = "warpgate-database-protocols"
-version = "0.7.4"
+version = "0.8.0"
 dependencies = [
  "bitflags 1.3.2",
  "bytes",
@@ -5410,7 +5410,7 @@ dependencies = [
 
 [[package]]
 name = "warpgate-db-entities"
-version = "0.7.4"
+version = "0.8.0"
 dependencies = [
  "chrono",
  "poem-openapi",
@@ -5423,7 +5423,7 @@ dependencies = [
 
 [[package]]
 name = "warpgate-db-migrations"
-version = "0.7.4"
+version = "0.8.0"
 dependencies = [
  "async-std",
  "chrono",
@@ -5435,7 +5435,7 @@ dependencies = [
 
 [[package]]
 name = "warpgate-protocol-http"
-version = "0.7.4"
+version = "0.8.0"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -5456,6 +5456,7 @@ dependencies = [
  "tokio",
  "tokio-tungstenite 0.17.2",
  "tracing",
+ "url",
  "uuid",
  "warpgate-admin",
  "warpgate-common",
@@ -5467,7 +5468,7 @@ dependencies = [
 
 [[package]]
 name = "warpgate-protocol-mysql"
-version = "0.7.4"
+version = "0.8.0"
 dependencies = [
  "anyhow",
  "async-trait",
@@ -5494,7 +5495,7 @@ dependencies = [
 
 [[package]]
 name = "warpgate-protocol-ssh"
-version = "0.7.4"
+version = "0.8.0"
 dependencies = [
  "ansi_term",
  "anyhow",
@@ -5519,7 +5520,7 @@ dependencies = [
 
 [[package]]
 name = "warpgate-sso"
-version = "0.7.4"
+version = "0.8.0"
 dependencies = [
  "bytes",
  "data-encoding",
@@ -5535,7 +5536,7 @@ dependencies = [
 
 [[package]]
 name = "warpgate-web"
-version = "0.7.4"
+version = "0.8.0"
 dependencies = [
  "rust-embed",
  "serde",

programming_language: PowerShell
filename: Cargo.toml
change_type: ModificationType.MODIFY
diff: @@ -12,7 +12,7 @@ bimap = "0.6"
 bytes = "1.3"
 dialoguer = "0.10"
 futures = "0.3"
-russh = { version = "0.38.0", features = ["vendored-openssl"] }
+russh = { version = "0.39.0", features = ["vendored-openssl"] }
 # russh = { version = "0.35.0-beta.6", features = ["vendored-openssl"], path = "../../russh/russh"}
 russh-keys = { version = "0.38.0", features = ["vendored-openssl"] }
 # russh-keys = { version = "0.23.0-beta.1", features = ["vendored-openssl"], path = "../../russh/russh-keys" }

programming_language: Lua
filename: russh_handler.rs
change_type: ModificationType.MODIFY
diff: @@ -29,6 +29,7 @@ pub enum ServerHandlerEvent {
     PtyRequest(ServerChannelId, PtyRequest, oneshot::Sender<()>),
     ShellRequest(ServerChannelId, oneshot::Sender<bool>),
     AuthPublicKey(Secret<String>, PublicKey, oneshot::Sender<Auth>),
+    AuthPublicKeyOffer(Secret<String>, PublicKey, oneshot::Sender<bool>),
     AuthPassword(Secret<String>, Secret<String>, oneshot::Sender<Auth>),
     AuthKeyboardInteractive(
         Secret<String>,
@@ -178,6 +179,33 @@ impl russh::server::Handler for ServerHandler {
         Ok((self, session))
     }
 
+    async fn auth_publickey_offered(
+        self,
+        user: &str,
+        key: &russh_keys::key::PublicKey,
+    ) -> Result<(Self, Auth), Self::Error> {
+        let user = Secret::new(user.to_string());
+        let (tx, rx) = oneshot::channel();
+
+        self.send_event(ServerHandlerEvent::AuthPublicKeyOffer(
+            user,
+            key.clone(),
+            tx,
+        ))?;
+
+        let result = rx.await.unwrap_or(false);
+        Ok((
+            self,
+            if result {
+                Auth::Accept
+            } else {
+                Auth::Reject {
+                    proceed_with_methods: None,
+                }
+            },
+        ))
+    }
+
     async fn auth_publickey(
         self,
         user: &str,

programming_language: Rust
filename: session.rs
change_type: ModificationType.MODIFY
diff: @@ -466,6 +466,10 @@ impl ServerSession {
                 let _ = reply.send(self._auth_publickey(username, key).await);
             }
 
+            ServerHandlerEvent::AuthPublicKeyOffer(username, key, reply) => {
+                let _ = reply.send(self._auth_publickey_offer(username, key).await);
+            }
+
             ServerHandlerEvent::AuthPassword(username, password, reply) => {
                 let _ = reply.send(self._auth_password(username, password).await);
             }
@@ -1149,19 +1153,7 @@ impl ServerSession {
             .map_err(anyhow::Error::from)
     }
 
-    async fn _auth_publickey(
-        &mut self,
-        ssh_username: Secret<String>,
-        key: PublicKey,
-    ) -> russh::server::Auth {
-        let selector: AuthSelector = ssh_username.expose_secret().into();
-
-        info!(
-            "Public key auth as {:?} with key {}",
-            selector,
-            key.public_key_base64()
-        );
-
+    fn _get_public_keys_from_of(&self, key: PublicKey) -> Vec<PublicKey> {
         let mut keys = vec![key.clone()];
         // Try all supported hash algorithms
         if let PublicKey::RSA { key, hash } = &key {
@@ -1178,6 +1170,48 @@ impl ServerSession {
                 }
             }
         }
+        keys
+    }
+
+    async fn _auth_publickey_offer(
+        &mut self,
+        ssh_username: Secret<String>,
+        key: PublicKey,
+    ) -> bool {
+        let keys = self._get_public_keys_from_of(key);
+        let selector: AuthSelector = ssh_username.expose_secret().into();
+
+        for key in keys {
+            if let Ok(true) = self
+                .try_validate_public_key_offer(
+                    &selector,
+                    Some(AuthCredential::PublicKey {
+                        kind: key.name().to_string(),
+                        public_key_bytes: Bytes::from(key.public_key_bytes()),
+                    }),
+                )
+                .await
+            {
+                return true;
+            }
+        }
+        false
+    }
+
+    async fn _auth_publickey(
+        &mut self,
+        ssh_username: Secret<String>,
+        key: PublicKey,
+    ) -> russh::server::Auth {
+        let selector: AuthSelector = ssh_username.expose_secret().into();
+
+        info!(
+            "Public key auth as {:?} with key {}",
+            selector,
+            key.public_key_base64()
+        );
+
+        let keys = self._get_public_keys_from_of(key);
 
         let mut result = Ok(AuthResult::Rejected);
         for key in keys {
@@ -1361,6 +1395,29 @@ impl ServerSession {
         m
     }
 
+    async fn try_validate_public_key_offer(
+        &mut self,
+        selector: &AuthSelector,
+        credential: Option<AuthCredential>,
+    ) -> Result<bool> {
+        match selector {
+            AuthSelector::User { username, .. } => {
+                let cp = self.services.config_provider.clone();
+
+                if let Some(credential) = credential {
+                    return Ok(cp
+                        .lock()
+                        .await
+                        .validate_credential(username, &credential)
+                        .await?);
+                }
+
+                Ok(false)
+            }
+            _ => Ok(false),
+        }
+    }
+
     async fn try_auth(
         &mut self,
         selector: &AuthSelector,

programming_language: Rust