文章前言
HTTP请求走私的\\”复兴\\”导致了我们现代应用程序部署中的破坏性漏洞,通过边缘服务器验证走私的HTTP请求可能会导致严重后果,包括伪造的内部标头、访问内部管理端点以及各种特权升级机会
HTTP/2(或HTTP/3)是解决我们面临的请求走私问题的一个很有前途的解决方案,但对HTTP/1.1的支持不会很快消失,与此同时我们仍然会收到HTTP/1.1的更多惊喜
在这篇文章中,我演示了如何通过明文(h2c)连接将HTTP/1.1连接升级到鲜为人知的HTTP/2,从而绕过反向代理访问控制并直接向后端服务器提供长期、无限制的HTTP流量
HTTP/1.1升级代理
为了了解此漏洞,让我们先回顾一下HTTP/1.1升级的行为以及代理如何实现升级的,Upgrade头最常用于将HTTP连接升级为长期WebSocket连接,代理通过保持原始客户端连接活动并简单地将TCP流量代理到后端服务器来支持这种行为,此时代理不再具有内容意识,也不再能够强制执行访问控制规则
让我们来检查h2c升级过程,它从客户端启动HTTP/1.1升级请求开始,一旦收到成功的101\\”交换协议\\”响应,客户端就重新使用连接并根据新协商的协议(在本例中为h2c)传输数据,下图说明了这种行为:
在从后端web服务器接收到101响应后代理保持持久的TCP连接,不再监视内容,引用NGINX WebSocket文档:
“A WebSocket application keeps a long?running connection open between the client and the server, facilitating the development of real?time applications. […] NGINX supports WebSocket by allowing a tunnel to be set up between a client and a backend server.
https://www.nginx.com/blog/websocket-nginx/
在Mikhail Egorov(@0ang3el)的WebSocket走私研究中,他证明了当升级到WebSocket连接时,通过触发后端问题,代理将连接升级到TCP隧道时他可以保持与后端的流水线HTTP/1.1连接,这允许请求被走私,从而规避代理服务器的访问控制
尽管这种形式的请求走私不会导致套接字中毒(也称为HTTP去同步)攻击,但它仍然允许您绕过重要的边缘服务器访问控制,在使用WebSocket支持测试服务时,这是一个很好的补充
但是如果我们不需要欺骗后端,只需要通过设计维护一个基于HTTP的TCP隧道呢?这就是h2c升级发挥作用的地方,我决定调查h2c实现的行为看看是否可以找到更灵活的走私选项
H2C规范
通常HTTP/2协议的使用是通过TLS应用层协议否定扩展(TLS-ALPN)进行协商的,它由字符串\\”h2\\”标识,这发生在我们发送第一个HTTP请求之前,然而HTTP/2也可以通过HTTP/1.1升级头启动,由字符串\\”h2c\\”标识,用于明文通信,下面是一个请求示例:
GET / HTTP/1.1
Host: www.example.com
Upgrade: h2c
HTTP2-Settings: AAMAAABkAARAAAAAAAIAAAAA
Connection: Upgrade, HTTP2-Settings
HTTP2的hop-by-hop header设置包含Base64编码的HTTP/2连接参数,根据规范仅允许在明文连接上进行h2c升级,并且不应转发HTTP2设置标头(RFC 7540第3.2.1节)
阅读说明书后,我提出了三个问题:
-
如果边缘代理正在执行TLS终止并且我在HTTP消息中发送h2c升级请求,后端服务器如何知道我们正在尝试通过TLS进行h2c升级?
-
如果边缘代理不知道h2c,它会转发客户端的h2c升级请求吗?
-
如果边缘代理成功地将我的h2c升级转发到后端服务器并且该服务器接受了该升级,我可以绕过提供的TCP隧道中的代理限制吗?
cURL和其他HTTP/2客户端不允许您通过TLS执行h2c升级,因为这违反了规范,因此使用hyper-2HTTP2库我创建了一个自定义客户端来测试
概念演示
我配置了一个NGINX服务器,在端口443上使用TLS终止,在/endpoint上使用类似WebSocket的proxy_pass连接到支持h2c升级的后端服务,我还为NGINX服务器配置了访问控制,该访问控制阻止了对/flag端点的所有请求,如下配置所示:
server {
listen 443 ssl;
server_name localhost;
ssl_certificate /usr/local/nginx/conf/cert.pem;
ssl_certificate_key /usr/local/nginx/conf/privkey.pem;
location / {
proxy_pass http://backend:9999;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
}
location /flag {
deny all;
}
对于后端服务器,我创建了一个支持h2c升级的简单Golang服务器:
// Lightly modified example from: https://github.com/thrawn01/h2c-golang-example
package main
import (
\\\"fmt\\\"
\\\"golang.org/x/net/http2\\\"
\\\"golang.org/x/net/http2/h2c\\\"
\\\"net\\\"
\\\"net/http\\\"
\\\"os\\\"
)
func checkErr(err error, msg string) {
if err == nil {
return
}
fmt.Printf(\\\"ERROR: %s: %s\\\\n\\\", msg, err)
os.Exit(1)
}
func main() {
h2s := &http2.Server{}
handler := http.NewServeMux()
handler.HandleFunc(\\\"/\\\", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, \\\"Hello, %v, http: %v\\\", r.URL.Path, r.TLS == nil)
})
handler.HandleFunc(\\\"/flag\\\", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, \\\"You got the flag!\\\");
})
server := &http.Server{
Addr: \\\"0.0.0.0:9999\\\",
Handler: h2c.NewHandler(handler, h2s),
}
fmt.Printf(\\\"Listening [0.0.0.0:9999]...\\\\n\\\")
checkErr(server.ListenAndServe(), \\\"while listening\\\")
}
按照预期直接向/endpoint的代理发送请求成功,请求/flag失败
该行为如下图所示
现在使用我的自定义客户端h2cSuggler通过TLS启动升级,我们能够成功访问受限制的端点(-x指定代理):
https://github.com/BishopFox/h2csmuggler
该行为如下图所示:
让我们来分析一下刚刚发生的事情:
-
h2cSuggler向NGINX反向代理上的端点发送HTTP/1.1升级请求
-
代理将升级和连接头转发到后端,后端以\\”101交换协议\\”响应,并准备接收HTTP2通信
-
从后端接收到101响应后,代理将连接\\”升级\\”到非托管TCP隧道
-
收到来自代理的101响应后,h2cSuggler重用现有连接并与服务器交换HTTP/2初始化帧其中包括服务器对HTTP/1.1 h2c升级中请求的端点(/endpoint)的响应
-
使用HTTP/2多路复用,h2cSuggler发送对受限/标志的附加请求
-
不再监视TCP隧道中的通信的代理将请求转发到后端服务器
-
服务器使用标志进行响应
如上所示,我们成功绕过了代理的访问控制来访问私有端点(在这里试用该工具和Docker演示)
根据规范,代理将始终期望通过TLS-ALPN进行h2协议协商,因此我们可以使用h2cSuggler通过TLS上的HTTP/1.1启动h2c连接
我们也可以在一些明文通道上执行此攻击,只要代理不支持h2c升级并简单地将客户端的h2c升级请求转发到后端,这种攻击也可能在非加密信道上成功
通过一个单独的实验,我确认了在使用多层代理的情况下,这种技术仍然有效,假设所有代理都成功地传递了必要的报头,您可以执行原始攻击,它将沿着每个代理创建的一系列中间TCP隧道传递数据
通过这种类型的请求走私(\\”隧道走私\\”)您可以通过HTTP/2复用发送任意数量的请求,此外正如我们从先前的研究中所知,HTTP请求走私会导致各种各样的攻击,包括:伪造内部标头、访问受限制的管理端点,有时还会导致主机标头SSRF允许通过网络进一步移动
但我知道你在想什么:“NGINX配置似乎太具体了,什么时候会这样?”
以下是不安全的HAProxy、Traefik和Nuster配置(尽可能通用和无害),它们默认转发所需的h2c标头:
HAProxy/Nuster
mode http
frontend fe
bind *.8080
default_backend be1
backend be1
server s1 backend:80
Traefik
http:
routers:
to-test:
rule: \\\"PathPrefix (`/`)\\\"
service: test
services:
test:
loadBalancer:
servers:
- url: http://backend:80
注意:Traefik在代理连接字符串上不包含HTTP-2-Settings,这可能会导致某些h2c实现上的攻击失败
支持h2c的后端服务器如何?
由于其降低带宽的能力,h2c成为低延迟网络内(即微服务)通信的有力候选,并避免了TLS的管理和(有争议但经常引用的)性能开销
因此流行的web框架通常支持配置选项以启用h2c升级支持,也就是说支持很少是开箱即用的默认设置
假设前端代理配置不安全,微服务中使用h2c可能会增加成功攻击的可能性
安全建议
要减轻代理服务器上h2c走私的风险,请执行以下操作:
-
需要WebSocket支持:仅允许HTTP/1.1升级标头的值WebSocket(例如,upgrade:WebSocket)
-
不需要WebSocket支持:不转发升级标头
哪些服务受默认影响(且不受影响)?
要使h2c走私成功需要将Upgrade标头(有时还有Connection标头)从边缘服务器成功转发到支持h2c升级的后端服务器,此配置可以发生在任何反向代理、WAF或负载平衡器上
默认情况下以下服务会在代理传递期间转发Upgrade和Connection标头,从而实现h2c的开箱即用:
-
HAProxy
-
Traefik
-
Nuster
默认情况下,这些服务在代理传递过程中不会转发升级和连接标头,但可以以不安全的方式进行配置(通过传递未过滤的升级和连接头)
-
AWS ALB/CLB
-
NGINX
-
Apache
-
Squid
-
Varnish
-
Kong
-
Envoy
-
Apache Traffic Server
HAProxy/Nuster的修复示例:
如果只允许WebSocket升级:
http-request replace-value Upgrade (.*) websocket
如果不允许升级:
http-request del-header Upgrade
Traefik补救示例:
此中间件配置将替换或删除传入请求中出现的升级标头:
http:
routers:
routerA:
middlewares:
- \\\"testHeader\\\"
<span style=\\\"color:#c2282e;\\\">…omitted for brevity…</span>
middlewares:
testHeader:
headers:
customRequestHeaders:
Upgrade: \\\"\\\" # \\\"\\\" removes the header; set to “websocket” to hardcode the value
文末小结
请求走私和其他代理绕过漏洞凸显了影响现代web应用程序架构的一个日益严重的问题:过度依赖边缘服务器访问控制来获得安全保证。
在许多方面通过请求走私或请求伪造攻击的任意用户控制的请求已经成为现代RPC驱动的微服务架构的“劫持指令指针”。维护纵深防御策略,降低架构中走私报头的重要性并准备识别和拒绝后端的可疑请求,将有助于降低未来攻击技术的影响
工具演示:https://github.com/BishopFox/h2csmuggler
原创文章,作者:七芒星实验室,如若转载,请注明出处:https://www.sudun.com/ask/34125.html