Published: 2024-06-XX

Introduction

In June 2024, a significant vulnerability was discovered in the way some Go (golang) SSH servers handle public key authentication. Tracked as CVE-2024-45337, this issue impacts applications and libraries that misuse the ServerConfig.PublicKeyCallback API—potentially allowing attackers to bypass authorization checks.

This article provides a clear, step-by-step explanation of the vulnerability, demonstrates how it can be exploited with code snippets, reviews the root cause, and gives actionable guidance for secure implementations.

What’s the Problem? Simplified

The vulnerability arises when applications trust the keys passed to the PublicKeyCallback function during SSH public key authentication, without confirming which key the client actually used.

SSH protocol allows a client to ask, “Is this public key acceptable?” multiple times, yet only proves control of one private key. An application that “remembers” keys presented to the callback and later assumes any of them are valid for authorization might give access to someone who doesn’t actually own the private key.

Summing up:
- An attacker can trick the server into making authorization decisions based on a public key for which they don't have the private key, simply by the way SSH authentication callbacks work.

The Code Flow (Vulnerable Example)

Let’s take a look at a simplified vulnerable server implementation using the x/crypto/ssh Go library:

import (
    "golang.org/x/crypto/ssh"
    "log"
    "net"
)

var authorizedKeys = map[string]string{
    "alice": "<Alice's SSH public key here>",
    "bob":   "<Bob's SSH public key here>",
}

func main() {
    config := &ssh.ServerConfig{
        PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
            username := conn.User()
            if authorizedKeys[username] == string(ssh.MarshalAuthorizedKey(key)) {
                // Here's the vulnerability! Just storing 'key' for later use.
                return &ssh.Permissions{
                    Extensions: map[string]string{
                        "used_pubkey": string(ssh.MarshalAuthorizedKey(key)),
                    },
                }, nil
            }
            return nil, fmt.Errorf("unknown key for %s", username)
        },
    }
    // ... listen and accept connections ...
}

Memorizes (stores) the key from the callback for later authorization.

- May later make decisions based on this stored key, not knowing if the client actually controlled its private key.

Attacker continues and authenticates using Key A (the one they do own).

5. Later, application authorizes actions based on Key B, thinking that’s how the client authenticated, even though the attacker never had its private key.

Here’s a Python-style pseudocode of the attack for illustration

# Attacker (using Paramiko SSH library, for example)
import paramiko

client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())

# Step 1: Use key A to authenticate
key_a = paramiko.RSAKey.from_private_key_file('attacker_a.pem')
client.connect('target.host', username='alice', pkey=key_a)

# Step 2: During handshake, offer key B and then key A
# (Depending on the SSH library, you may control the offered order)

# Step 3: Wait for success, then try to perform restricted actions

Deeper Dive: Why Did This Happen?

The root of the vulnerability is misinterpreting the behavior of PublicKeyCallback.

The Go SSH docs for PublicKeyCallback say

> A call to this function does not guarantee that the key offered is actually used for authentication.

The SSH protocol allows clients to ask about keys they don't own just to see if they’re accepted. Only once the client proves control of the private key should the server trust that specific key for any authorization.

Apps remember all keys presented to the callback, not just the one that passed the final proof.

- Apps make authorization decisions later, using the most-recent or last key seen, regardless of what the client proved.

Mitigation: What’s Been Fixed? What Should You Do?

### In Go 1.22.4 / x/crypto/ssh v.31.

Mitigation patch

The Go team made a change so that when public key authentication succeeds, the callback is called again with the actual key used.
This means, after the connection is established, the last key your callback saw is correct (if public key auth succeeded).

BUT:
If another auth method is used (e.g. password), that last key might _not_ be the right one.
So, you still should not trust just the callback’s key outside the callback.

Store authentication state in the callback’s Extensions field, and only trust the ServerConn.Permissions associated with the successfully authenticated session.

Example pattern

config := &ssh.ServerConfig{
    PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
        username := conn.User()
        if isKeyAllowedForUser(username, key) {
            // Store only state you want trusted in the Permissions Extensions
            return &ssh.Permissions{
                Extensions: map[string]string{
                    "authenticated_user": username,
                    "authenticated_key":  string(ssh.MarshalAuthorizedKey(key)),
                },
            }, nil
        }
        return nil, fmt.Errorf("unauthorized")
    },
}

// Later, after handshake:
permissions := conn.Permissions
user := permissions.Extensions["authenticated_user"]
key := permissions.Extensions["authenticated_key"]

Gotchas: Third-Party Libraries

Some popular Go SSH libraries and middleware share the Permissions object across multiple authentication attempts. That means you can’t trust its Extensions field unless you know the library is patched and safe.

Developers:

References and Further Reading

- Go Security Advisory: GO-2024-2592
- X/crypto commit log: Partial mitigation commit
- CVE Entry: CVE-2024-45337 at Mitre
- SSH Protocol Details: RFC 4252 - SSH Authentication Protocol

Conclusion

CVE-2024-45337 is a classic case of subtle security footgun—your code did what you thought, but the protocol didn’t work how you expected.

Always record authentication state inside the callback, using Permissions.Extensions.

- Only use the state from the final, established connection’s Permissions field for authorization decisions.

Update your dependencies!
Keep SSH security tight, and share this post with your fellow Go developers and security teams.

Timeline

Published on: 12/12/2024 02:02:07 UTC
Last modified on: 12/12/2024 21:15:08 UTC