當前位置:
首頁 > 新聞 > Golang語言TLS雙向身份驗證拒絕服務漏洞分析

Golang語言TLS雙向身份驗證拒絕服務漏洞分析

概述

如果您的源代碼是以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,

}

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)

} else {

if candidateChains, err = c.buildChains(make(map[int][][]*Certificate), []*Certificate, &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或更高版本。


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 嘶吼RoarTalk 的精彩文章:

深入分析Microsoft Outlook漏洞CVE-2018-8587
域滲透——利用GPO中的計劃任務實現遠程執行

TAG:嘶吼RoarTalk |