导语:如果您的源代码是以Go语言编写的,并且它使用了单向或双向TLS身份验证,那么该程序将很容易受到CPU拒绝服务(DoS)攻击。
概述
如果您的源代码是以Go语言编写的,并且它使用了单向或双向TLS身份验证,那么该程序将很容易受到CPU拒绝服务(DoS)攻击。攻击者可以通过某种特定的方式来格式化输入内容,使得Go的crypto/x509标准库中的验证算法,在尝试验证客户端提供的TLS证书链的过程中,占用了所有可用的CPU资源。
如果要保护您的服务,请立即升级到Go 1.10.6或更高版本,或者升级至1.11.3或更高版本。
背景
42Crunch的API安全平台的后端,已经使用了微服务的架构来实现。这一微服务使用Go语言编写,通过gRPC相互通信,并且具有用于外部调用的REST API网关。为了确保安全性,我们遵循着“能用TLS就尽量用”的宗旨,广泛依赖于双向TLS身份验证。
Go在其标准库中提供本地的SSL/TLS支持,以及用于操作连接、验证、身份验证和证书的大量x509及TLS原语。这一本地支持,通过使用标准、经过检查、定期维护的TLS实现,来避免外部依赖性,并降低风险。
那么自然,42Crunch就可能会受到影响。因此,我们必须认真分析这一TLS漏洞,以确保42Crunch平台的安全性。
42Crunch安全团队发布了关于这一漏洞的分析以及详细信息。
问题
TLS链验证中的拒绝服务漏洞,最初由Netflix发现并报告,如Golang的问题跟踪器中所述:
crypto/x509包负责解析并验证X.509编码的密钥和证书。该包在处理攻击者提供的证书链时,本应该进行使用资源是否合理的判断。crypto/x509包没有限制为每个链验证执行的工作量,这可能允许攻击者制作导致CPU拒绝服务的恶意输入。Go TLS服务器接受客户端证书,TLS客户端将验证证书是否受到影响。
这里的问题之处,在于调用路径crypto/x509 Certificate.Verify()函数,该函数负责授权控制和验证证书。
漏洞详细分析
为了简化这一部分内容,并且使读者能够清楚了解,我们仅以TLS客户端连接到验证客户端证书的TLS服务器为例,展现这一过程的具体细节。
TLS服务器持续监听TLS客户端的8080端口,并根据一个受信任的证书颁发机构(CA)对客户端的证书进行验证:
caPool := x509.NewCertPool() ok := caPool.AppendCertsFromPEM(caCert) if !ok { panic(errors.New("could not add to CA pool")) } tlsConfig := &tls.Config{ ClientCAs: caPool, ClientAuth: tls.RequireAndVerifyClientCert, } //tlsConfig.BuildNameToCertificate() server := &http.Server{ Addr: ":8080", TLSConfig: tlsConfig, } server.ListenAndServeTLS(certWeb, keyWeb)
在一个标准的TLS验证方案中,TLS客户端连接到TLS服务器的8080端口,并提供其“信任链”,其中包括客户端证书、根CA证书以及所有中间CA证书。TLS服务器处理TLS握手,并验证客户端证书,检查客户端是否可信(客户端证书是否由服务器信任的CA进行签名)。下图展现了TLS握手时的简化版流程:
通过Go的crypto/x509库,我们最终进入到x509/tls/handshake_server.go:doFullHandshake(),具体如下:
... if c.config.ClientAuth >= RequestClientCert { if certMsg, ok = msg.(*certificateMsg); !ok { c.sendAlert(alertUnexpectedMessage) return unexpectedMessageError(certMsg, msg) } hs.finishedHash.Write(certMsg.marshal()) if len(certMsg.certificates) == 0 { // The client didn't actually send a certificate switch c.config.ClientAuth { case RequireAnyClientCert, RequireAndVerifyClientCert: c.sendAlert(alertBadCertificate) return errors.New("tls: client didn't provide a certificate") } } pub, err = hs.processCertsFromClient(certMsg.certificates) if err != nil { return err } msg, err = c.readHandshake() if err != nil { return err } } ...
在这里,服务器处理其收到的客户端证书,并调用函数x509/tls/handshake_server.go:processCertsFromClient()。如果必须要验证客户端证书,那么服务器会创建一个包含以下内容的VerifyOptions结构:
1、根CA池,配置为验证客户端证书的受信任CA列表,由服务器控制。
2、中间CA池,接收的中间CA列表,由客户端控制。
3、签名的客户端证书,由客户端控制。
4、其他字段,可选。
if c.config.ClientAuth >= VerifyClientCertIfGiven && len(certs) > 0 { opts := x509.VerifyOptions{ Roots: c.config.ClientCAs, CurrentTime: c.config.time(), Intermediates: x509.NewCertPool(), KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, } for _, cert := range certs[1:] { opts.Intermediates.AddCert(cert) } chains, err := certs[0].Verify(opts) if err != nil { c.sendAlert(alertBadCertificate) return nil, errors.New("tls: failed to verify client's certificate: " + err.Error()) } c.verifiedChains = chains }
要理解上述过程中哪里存在问题,我们需要首先了解证书池的组织方式,从而以有效的方式来验证证书。简而言之,证书池是一个证书列表,可以根据需要,通过三种不同的方式访问。下图展示了这一示例:池中分组的证书,可以通过索引数组(名为“Certs”)访问,并且由CN、IssuerName、SubjectKeyId进行哈希处理。
验证过程
服务器使用客户端证书上的VerifyOptions调用函数Verify()(链中的第一个证书:certs[0])。
然后,Verify()将根据提供的链,验证客户端证书。但是,必须首先使用buildChains()函数来构建并检查验证链:
var candidateChains [][]*Certificate if opts.Roots.contains(c) { candidateChains = append(candidateChains, []*Certificate{c}) } else { if candidateChains, err = c.buildChains(make(map[int][][]*Certificate), []*Certificate{c}, &opts); err != nil { return nil, err } }
buildChains()函数依次调用一些消耗CPU较高的函数,并在其找到的链的每个元素上递归调用其自身。
BuildChains()函数依赖于辅助函数findVerifiedParents(),该函数通过IssuerName或AuthorityKeyId,对证书池进行映射访问,从而标识父证书,并返回可选证书的索引,然后根据客户端控制的池,对其进行验证。
在正常情况下,IssuerName和AuthorityKeyId将会被填充,并且预计它们都是唯一的,只会返回一个证书来进行验证:
func (s *CertPool) findVerifiedParents(cert *Certificate) (parents []int, errCert *Certificate, err error) { if s == nil { return } var candidates []int if len(cert.AuthorityKeyId) > 0 { candidates = s.bySubjectKeyId[string(cert.AuthorityKeyId)] } if len(candidates) == 0 { candidates = s.byName[string(cert.RawIssuer)] } for _, c := range candidates { if err = cert.CheckSignatureFrom(s.certs[c]); err == nil { parents = append(parents, c) } else { errCert = s.certs[c] } } return }
BuildChains()函数在客户端发送到TLS服务器的整个证书链上调用以下内容:
1、根CA池(服务器端)上的findVerifiedParents(client_certificate),用于查找已验证证书的签名机构(如果它是根CA),并检查证书的AuthorityKeyId(如果不为nil)或所有可能的原始颁发者的签名(如果为nil)。
2、中间CA池(客户端提供)上的findVerifiedParents(client_certificate),用于查找已验证证书的签名机构(如果它是根CA),并检查证书AuthorityKeyId(如果不为nil)或所有可能的原始颁发者的签名(如果为nil)。
3、获取负责签名的父级中间CA。
4、使用新找到的父级中间CA,调用buildChains(),并重复前面所述的整套签名检查流程。
DoS攻击方式
针对主CPU的DoS攻击,将由buildChains()和findVerifiedParent()函数在未预期的情况下触发,其中所有中间CA证书使用相同的名称,并且具有为nil的AuthKeyId值。findVerifiedParent()函数返回与该名称匹配的所有证书,也就是整个池,然后检查所有证书的签名。完成后,再次为找到的父级证书调用buildChains()函数,直至到达根CA。其中,每一次检查都会验证中间CA池的全部证书,因此只需要一次TLS连接,就可能会消耗掉全部可用的CPU。
漏洞影响
攻击者可以构建证书链,使客户端证书验证过程消耗掉所有CPU资源,从而降低主机的响应速度。上述影响只需要使用一个连接就可以实现。根据Go调度程序的规则,只有两个CPU核心会受到影响,并以100%占用率使用,创建新的连接,会强制调度程序分配更多资源来处理签名检查,这样一来可以导致整个服务或者整个主机无响应。
安全建议
Go语言社区建议通过实施以下更改,来缓解这一漏洞:
1、将签名检查移出findVerifiedParent()证书池查找过程;
2、将签名检查的数量限定在最多100个中间CA(这是一个不切实际的信任链)。
要实现针对这一漏洞的修复,需要立即升级到Go 1.10.6或更高版本,或者1.11.3或更高版本。