2017-02-10 13:53:47
来源:blog.filippo.io 作者:胖胖秦
阅读:513次
点赞(0)
收藏
翻译:胖胖秦
预估稿费:200RMB
投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿
前言
Ticketbleed(CVE-2016-9244)是存在F5产品的TLS堆栈中的软件漏洞,它允许远程攻击者一次性提取多达31个字节的未初始化内存数据,像Heartbleed一样,它可以包含任意的敏感信息。如果您不确定是否会受到此漏洞的影响,您可以在ticketbleed.com(包含在线测试)或F5 K05121675文章中找到详细信息和缓解说明。
在这篇文章中,我们将讨论如何找到,验证和披露Ticketbleed。
JIRA RG-XXX
这要从CloudFlare Railgun产生的一个错误报告说起。rg-listener <> 原始请求失败,错误命令是"local error: unexpected message"
rg-listener <> 原始流量包被记录并显示在握手之间触发了一个TLS警告.
值得注意的是客户在Railgun 和原始服务器之前使用了一个F5的负载均横:visitor > edge > cache > rg-sender > F5 > rg-listener > F5 > origin web server
Matthew不可能在Go中使用一个基本的TLS.Dial 来复现它,所以问题似乎很棘手
Railgun的位置:Railgun通过建立永久优化的连接并对HTTP响应执行增量压缩来加速Cloudflare edge和原始网站之间的请求。
Railgun连接使用基于TLS的自定义二进制协议,两个终端都是Go程序:一个终端位于Cloudflare edge,另一个安装在客户服务器上。这意味着整个连接都要通过Go TLS栈,crypto/tls。
连接失败的错误代码是:local error: unexpected message,这意味着客户端发送了一些Railgun的Go TLS堆栈无法处理的数据。由于客户端在Railgun和我们之间运行着F5负载均衡,这也表明Go TLS栈和F5之间存在不兼容性。
但是,当我的同事Matthew试图使用crypto/tls.Dial连接到负载均衡上来重现错误时,它成功了。
深入分析PCAP
由于Matthew正坐在我对面,他知道我一直在使用Go TLS协议来实现TLS 1.3。于是我们很快完成了联合调试。
下面是我们分析的PCAP。
上图中有ClientHello和ServerHello数据包,然后马上发送ChangeCipherSpec消息。在TLS 1.2中,ChangeCipherSpec代表的意思就是”让我们开始加密吧”。只有一种情况,ChangeCipherSpec会在握手之前先发送,那就是会话复用。
事实上,通过观察ClientHello,我们可以发现Railgun客户端发送了一个Session Ticket。
Session Ticket携带着先前会话的一些加密密钥信息,来告诉服务器复用先前会话,而不是协商新的会话。
要了解有关TLS 1.2会话复用的更多信息,请阅读Cloudflare Crypto Team TLS 1.3Take的第一部分,阅读副本或Cloudflare博客上的“TLS会话复用”的帖子。
在发送ChangeCipherSpec消息之后,Railgun和Wireshark变的不知所错(HelloVerifyRequest?Umh?)。所以我们有理由确定这个问题与Session Ticket有关。
在Go中,您需要在客户端上设置ClientSessionCache来显式开启Session Ticket。我们验证Railgun开启了这个功能,并写了这个小测试:
packagemain import( "crypto/tls" ) funcmain(){ conf:=&tls.Config{ InsecureSkipVerify:true, ClientSessionCache:tls.NewLRUClientSessionCache(32), } conn,err:=tls.Dial("tcp","redacted:443",conf) iferr!=nil{ panic("failedtoconnect:"+err.Error()) } conn.Close() conn,err=tls.Dial("tcp","redacted:443",conf) iferr!=nil{ panic("failedtoresume:"+err.Error()) } conn.Close() }这足以证明错误的发生(local error: unexpected message)与Session Ticket有关。
深入分析crypto/tls
只要我们能在本地重现它,就能弄懂它。crypto/tls的错误消息缺少详细的信息,但是快速的调整允许我们精确定位错误在哪里发生。每次发生错误时,都会调用setErrorLocked记录错误,并确保所有后续操作失败。该函数通常从错误的站点调用。
我们应该在panic(err)处进行堆栈跟踪,它会告诉我们消息在哪出现异常。
diff--gita/src/crypto/tls/conn.gob/src/crypto/tls/conn.go index77fd6d3254..017350976a100644 ---a/src/crypto/tls/conn.go +++b/src/crypto/tls/conn.go @@-150,8+150,7@@typehalfConnstruct{ } func(hc*halfConn)setErrorLocked(errerror)error{ -hc.err=err -returnerr +panic(err) } //prepareCipherSpecsetstheencryptionandMACstates panic:localerror:tls:unexpectedmessage goroutine1[running]: panic(0x185340,0xc42006fae0) /Users/filippo/code/go/src/runtime/panic.go:500+0x1a1 crypto/tls.(*halfConn).setErrorLocked(0xc42007da38,0x25e6e0,0xc42006fae0,0x25eee0,0xc4200c0af0) /Users/filippo/code/go/src/crypto/tls/conn.go:153+0x4d crypto/tls.(*Conn).sendAlertLocked(0xc42007d880,0x1c390a,0xc42007da38,0x2d) /Users/filippo/code/go/src/crypto/tls/conn.go:719+0x147 crypto/tls.(*Conn).sendAlert(0xc42007d880,0xc42007990a,0x0,0x0) /Users/filippo/code/go/src/crypto/tls/conn.go:727+0x8c crypto/tls.(*Conn).readRecord(0xc42007d880,0xc400000016,0x0,0x0) /Users/filippo/code/go/src/crypto/tls/conn.go:672+0x719 crypto/tls.(*Conn).readHandshake(0xc42007d880,0xe7a37,0xc42006c3f0,0x1030e,0x0) /Users/filippo/code/go/src/crypto/tls/conn.go:928+0x8f crypto/tls.(*clientHandshakeState).doFullHandshake(0xc4200b7c10,0xc420070480,0x55) /Users/filippo/code/go/src/crypto/tls/handshake_client.go:262+0x8c crypto/tls.(*Conn).clientHandshake(0xc42007d880,0x1c3928,0xc42007d988) /Users/filippo/code/go/src/crypto/tls/handshake_client.go:228+0xfd1 crypto/tls.(*Conn).Handshake(0xc42007d880,0x0,0x0) /Users/filippo/code/go/src/crypto/tls/conn.go:1259+0x1b8 crypto/tls.DialWithDialer(0xc4200b7e40,0x1ad310,0x3,0x1af02b,0xf,0xc420092580,0x4ff80,0xc420072000,0xc42007d118) /Users/filippo/code/go/src/crypto/tls/tls.go:146+0x1f8 crypto/tls.Dial(0x1ad310,0x3,0x1af02b,0xf,0xc420092580,0xc42007ce00,0x0,0x0) /Users/filippo/code/go/src/crypto/tls/tls.go:170+0x9d让我们看看异常的消息警报会发送到哪里conn.go:672。
670caserecordTypeChangeCipherSpec: 671iftyp!=want||len(data)!=1||data[0]!=1{ 672c.in.setErrorLocked(c.sendAlert(alertUnexpectedMessage)) 673break 674} 675err:=c.in.changeCipherSpec() 676iferr!=nil{ 677c.in.setErrorLocked(c.sendAlert(err.(alert))) 678}所以异常的消息是ChangeCipherSpec。让我们检查上一级的堆栈,看看是否有线索。让我们看看handshake_client.go:262。
259func(hs*clientHandshakeState)doFullHandshake()error{ 260c:=hs.c 261 262msg,err:=c.readHandshake() 263iferr!=nil{ 264returnerr 265}这是doFullHandshake函数。等等,这里的服务器显然正在进行会话复用(在Server Hello之后立即发送一个Change Cipher Spec),而客户端正在尝试进行完整握手?
看起来情况是,客户端提供Session Ticket,服务器接受它,但是客户端并不知道并继续执行下去。
深入RFC
在这一点上,我查阅了TLS 1.2的相关信息,以了解服务器是如何表示接受Session Ticket?RFC 5077,过时的RFC 4507:
当携带一个ticket时,客户端会在TLS ClientHello中生成并包含一个Session ID. 如果服务器接收了ticket并且Session ID不为空,它必须马上返回与ClientHello相同的Session ID.
因此,客户端不应该猜测是否Session Ticket会被接受, 客户端应该发送一个Session ID并在服务器的回显中查找这个Session ID。
crypto/tls中的代码很明显的说明了这一点。
func(hs*clientHandshakeState)serverResumedSession()bool{ //IftheserverrespondedwiththesamesessionIdthenitmeansthe //sessionTicketisbeingusedtoresumeaTLSsession. returnhs.session!=nil&&hs.hello.sessionId!=nil&& bytes.Equal(hs.serverHello.sessionId,hs.hello.sessionId) }深入分析Session IDs
一定是这里出错了。让我们加入一些基于打印输出的调试。diff--gita/src/crypto/tls/handshake_client.gob/src/crypto/tls/handshake_client.go indexf789e6f888..2868802d82100644 ---a/src/crypto/tls/handshake_client.go +++b/src/crypto/tls/handshake_client.go @@-552,6+552,8@@func(hs*clientHandshakeState)establishKeys()error{ func(hs*clientHandshakeState)serverResumedSession()bool{ //IftheserverrespondedwiththesamesessionIdthenitmeansthe //sessionTicketisbeingusedtoresumeaTLSsession. +println(hex.Dump(hs.hello.sessionId)) +println(hex.Dump(hs.serverHello.sessionId)) returnhs.session!=nil&&hs.hello.sessionId!=nil&& bytes.Equal(hs.serverHello.sessionId,hs.hello.sessionId) } 00000000a8732fc4c980e2efb8e0b7dacf0d71e5|.s/...........q.| 00000000a8732fc4c980e2efb8e0b7dacf0d71e5|.s/...........q.| 0000001000000000000000000000000000000000|................|
F5服务器将Session ID填充到它的最大长度32字节,而不是当客户端发送它时再返回它。crypto / tls在Go中使用16字节会话ID。
从这里看错误就很明显了:服务器认为它告诉客户端使用Ticket而客户端认为服务器启动了新会话,于是意外就发生了。
在TLS空间中,我们发现了一些不兼容性。为了不与某些服务器实现发生冲突,ClientHellos必须小于256字节或大于512字节。
0000000079bde5a877558b9241e98945e1503125|y...wU..A..E.P1%| 0000000079bde5a877558b9241e98945e1503125|y...wU..A..E.P1%| 000000100427a84f6322de8beff9a313dd665cee|.'.Oc".......f\.|噢哦。等等。这些不是零也不是填充。那是...内存?
在这一点上,和Heartbleed的处理类似。服务器申请和客户端的会话ID一样大的缓冲区,然后总是返回32个字节的数据,在额外的字节里携带着未分配的内存数据。
深入浏览器
我最后一个疑问是:为什么之前没有发现这个漏洞?答案是:所有浏览器使用32字节的SESSION ID来协商SESSION TICKET。我和Nick Sullivan一起检查了NSS,OpenSSL和BoringSSL来确认这个问题。以BoringSSL为例。
/*GenerateasessionIDforthissessionbasedonthesessionticket.Weuse *thesessionIDmechanismfordetectingticketresumption.Thisalsofitsin *withassumptionselsewhereinOpenSSL.*/ if(!EVP_Digest(CBS_data(&ticket),CBS_len(&ticket), session->session_id,&session->session_id_length, EVP_sha256(),NULL)){ gotoerr; }BoringSSL使用SHA256作为SESSION TICKET,正好是32个字节。
(有趣的是,在TLS中,有人提到使用1字节的SESSION ID,但是没有人对它进行测试。)
至于Go,可能是客户端没有启用SESSION TICKET。
深入披露
在意识到这个问题的影响之后,我们在公司内部进行了分享,我们的支持团队会建议客户禁用SESSION TICKET,并试图联系F5。我们与F5 SIRT联系,交换PGP密钥,并提供报告和PoC。
报告已提交给开发团队,确定问题是未初始化的内存,但是仅限于Session Ticket功能。
目前还不清楚哪些数据可以通过此漏洞泄露,但是HeartBleed和Cloudflare Heartbleed Challenge告诉我们未初始化的内存是不安全的
在规划时间表时,F5团队面临着严格的发布计划。综合考虑多种因素,包括有效的缓解(禁用Session Ticket),我决定采用由Google's Project Zero发布的业界标准的披露政策:在115天之后,如果漏洞没有被修复,就会被披露。
巧合的是今天正好是计划发布修复补丁的截至日期。
我要感谢F5 SIRT的专业性,透明度和协作性,这和我们在业内经常听到的对抗性形成鲜明对比。
该漏洞已分配CVE-2016-9244。
深入互联网
当我们向F5报告问题时,我已经针对单个主机测试了该漏洞,该主机在禁用Session Ticket后很快变得不可用。这意味着漏洞具有低信度,并且没有办法再现它。这是进行互联网扫描的绝佳场合。我选择了由密歇根大学授权Censys.io的工具包:zmap和zgrab。
zmap是一种用于检测开放端口的IPv4空间扫描工具,而zgrab是一种Go工具,通过连接到这些端口并收集大量协议详细信息来进行跟踪。
我在zgrab添加对Session Ticket复用的支持,然后让zgrab发送一个31字节的会话ID,并将其与服务器返回的ID进行比较。我写了一个简单的Ticketbleed检测器。
diff--gita/ztools/ztls/handshake_client.gob/ztools/ztls/handshake_client.go indexe6c506b..af098d3100644 ---a/ztools/ztls/handshake_client.go +++b/ztools/ztls/handshake_client.go @@-161,7+161,7@@func(c*Conn)clientHandshake()error{ session,sessionCache=nil,nil hello.ticketSupported=true hello.sessionTicket=[]byte(c.config.FixedSessionTicket) -hello.sessionId=make([]byte,32) +hello.sessionId=make([]byte,32-1) if_,err:=io.ReadFull(c.config.rand(),hello.sessionId);err!=nil{ c.sendAlert(alertInternalError) returnerrors.New("tls:shortreadfromRand:"+err.Error()) @@-658,8+658,11@@func(hs*clientHandshakeState)processServerHello()(bool,error){ ifc.config.FixedSessionTicket!=nil{ c.resumption=&Resumption{ -Accepted:hs.hello.sessionId!=nil&&bytes.Equal(hs.serverHello.sessionId,hs.hello.sessionId), -SessionID:hs.serverHello.sessionId, +Accepted:hs.hello.sessionId!=nil&&bytes.Equal(hs.serverHello.sessionId,hs.hello.sessionId), +TicketBleed:len(hs.serverHello.sessionId)>len(hs.hello.sessionId)&& +bytes.Equal(hs.serverHello.sessionId[:len(hs.hello.sessionId)],hs.hello.sessionId), +ServerSessionID:hs.serverHello.sessionId, +ClientSessionID:hs.hello.sessionId, } returnfalse,FixedSessionTicketError }选择31字节的原因是我可以确保不泄露敏感信息。
然后,我从Censys网站下载最近的扫描结果,其中包括什么主机支持Session Ticket信息,并使用pv和jq完成了管道。
在11月份的Alexa top 1m列表中的前1,000个主机中有2个存在漏洞,我中断了扫描,避免泄露漏洞,并推迟到了披露日期。
在完成这篇指导时,我完成了扫描,0.1%和0.2%的主机容易受到攻击,0.4%的网站支持Session Ticket。
阅读更多
欲了解更多详情,请访问F5 K05121675文章或ticketbleed.com,在那里你会发现一个技术总结,受影响的版本,缓解指令,一个完整的时间表,扫描结果,扫描机器的IP地址,并可以进行在线测试。否则,你应该关注我的Twitter。
本文由 安全客 翻译,转载请注明“转自安全客”,并附上链接。
原文链接:https://blog.filippo.io/finding-ticketbleed/