HTTP Smuggling 请求走私
前几天打的 defcon ctf qual
里有一题 uploooadit
,里面涉及到 HTTP Smuggling | HTTP Desync Attacks
,也就是 HTTP 走私攻击
,是一个很有趣的攻击方式了,而这个其实也和自己正在弄的协议一致性研究有所关联,于是参考了些论文和一些师傅的博客,回顾一下,并做个记录。
前置知识
HTTP/1.1
Keep alive
HTTP
持久连接( HTTP persistent connection
,也称作 HTTP keep-alive
或 HTTP connection reuse
,翻译过来可以是保持连接或者连接复用)是使用同一个 TCP
连接来发送和接收多个 HTTP
请求应答,而不是为每一个新的请求应答打开新的连接的方式。
HTTP
协议采用 请求 - 应答
模式,当使用普通模式,即非 KeepAlive
模式时,每个请求应答客户和服务器都要新建一个连接,完成 之后立即断开连接( HTTP
协议为无连接的协议),每次请求都会经过三次握手四次挥手过程,效率较低;当使用 Keep-Alive
模式时,客户端到服务器端的连接不会断开,当出现对服务器的后继请求时,客户端就会复用已建立的连接。
Http1.1
以后, Keep-Alive
已经默认支持并开启。客户端(包括但不限于浏览器)发送请求时会在 Header
中增加一个请求头 Connection: Keep-Alive
,当服务器收到附带有 Connection: Keep-Alive
的请求时,也会在响应头中添加 Keep-Alive
。这样一来,客户端和服务器之间的 HTTP
连接就会被保持,不会断开,当客户端发送另外一个请求时,就可以复用已建立的连接。
Pipline
HTTP Pipine(管线化)
是将多个 HTTP
请求批量提交的技术,并且在发送的过程中不需要等待服务器的回应,而服务器接收后,会按照先进先出的方式将响应报文与请求报文严格对应。
pipline
需通过上述所说的 keep alive
模式来完成。这个模式仅 HTTP/1.1
支持( HTTP/1.0
不支持),并且只有 GET 和 HEAD 要求可以进行管线化,而 POST 则有所限制。目前多数浏览器默认不启用该模式,但是服务器一般是支持的。
在使用 pipline
后的请求模式如下图所示:
但是这里有一个问题需要讨论,在使用 pipline
时,如果服务器端对于多个请求的理解出现问题,是否可能会出现将前一个请求的内容解析为后一个请求的情况?后续会展开讨论,这个也是引发 HTTP Smuggling
的主要原因。
Content-Length
Content-Length
, 用于描述 HTTP
消息实体的传输长度( the transfer-length of the message-body
), 用十进制数字表示的八位字节的数目,这里需要注意消息实体传输长度与实际消息实体长度的区别:
- 消息实体长度:即
Entity-length
,压缩之前的message-body
的长度 - 消息实体的传输长度:
Content-length
,压缩后的message-body
的长度。
Content-Length
表示实体内容传输长度,客户端(服务器)可以根据这个值来判断数据是否接收完成。但是如果消息中没有 Conent-Length
,那该如何来判断?客户端如何来判断数据是否接收完成呢?
静态页面或者图片:当客户端向服务器请求一个静态页面或者一张图片时,服务器可以很清楚的知道内容大小,然后通过
Content-length
消息首部字段告诉客户端 需要接收多少数据。动态页面: 如果是动态页面等时,服务器是不可能预先知道内容大小,这时就可以使用
Transfer-Encoding:chunk
模式来传输 数据了。即如果要一边产生数据,一边发给客户端,服务器就需要使用Transfer-Encoding: chunked
这样的方式来代替Content-Length
。
这里问题又来了,这两种方式都可以判断数据是否接收完成,我们知道很多 Web
服务是包含前置、后端服务器的(比如代理服务器),如果 用户-前置服务器
和 前置服务器-后端
的处理方式不同,是否会引发什么问题?
Transfer-Encoding
分块传输编码 (Chunked transfer encoding)
是 HTTP
中的一种数据传输机制,允许 HTTP
由网页服务器发送给客户端的数据可以分成多个部分。分块传输编码只在 HTTP/1.1
中提供。
通常, HTTP Response
中发送的数据是整个发送的, Content-Length
消息头字段表示数据的长度。数据的长度很重要,因为客户端需要知道哪里是应答消息的结束,以及后续应答消息的开始。然而,使用分块传输编码,数据分解成一系列数据块,并以一个或多个块发送,这样服务器可以发送数据而不需要预先知道发送内容的总大小。
如果一个 HTTP 消息
的 Transfer-Encoding
值为 chunked
,那么,消息体由数量未定的块组成,并以最后一个大小为 0 的块为结束。
其中,每一个非空的块都以该块包含数据的字节数(字节数以十六进制表示)开始,跟随一个 CRLF
,然后是数据本身,最后块 CRLF
结束。并且在一些实现中,块大小和 CRLF 之间填充有白空格 (0×20)
。
最后一块不再包含任何数据,消息最后以 CRLF 结尾。在这一个块中的内容是称为 footer
的内容,是一些附加的 Header
信息:
// Chunk 编码的格式如下: |
反向代理
我们知道,一个简单的 Web
服务结构,由客户端(浏览器)、前端页面、后端处理程序组成,而其中前端页面的存储及后端处理程序就是位于 Server
端的服务器中,用户需要使用这个服务时,由浏览器请求前端页面,渲染之后操作将数据传入到后端进行处理。
但是这样简单的结构容易出现问题,如果请求数量过多,服务器的负担过大,则会导致用户无法以正常的浏览速度和浏览效果来使用这一 Web
服务,于是便需要加入新的结构来解决这一问题,最简单的方法就是使用一个带有缓存功能的反向代理服务器,用户请求资源时,可以如果反向代理服务器中有的话,可以直接从反向代理服务器的缓存中获得,从而减少了对源站的请求和资源的消耗。下面对其具体进行介绍:
反向代理 (Reverse Proxy)
方式是指以代理服务器来接受 Internet
上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给 Internet
上请求连接的客户端,此时代理服务器对外就表现为一个服务器。(注:以下称反向代理服务器为代理服务器)
代理服务器其实不光是可以提供上述所说的缓存功能,其也可以作为源站的一个保护措施,用以保护真实的 server
端服务器,这种代理服务器和用于负载均衡的代理服务器区别就在于是否严格在防火墙内运行,以及其支持的安全机制可能有所不同。
下面为代理服务器可以起到的部分作用或功能:
- 可以起到保护网站安全的作用,因为任何来自
Internet
的请求都必须先经过代理服务器。 - 通过缓存静态资源,加速
Web
请求。 - 实现负载均衡。
对于负载均衡功能, Web
服务提供者可以在一个组织内使用多个代理服务器来平衡各 Web
服务器间的网络负载。在此模型中,可以利用代理服务器的高速缓存特性,创建一个用于负载平衡的服务器池。此时,如果 Web
服务器每天都会接收大量的请求,则可以使用代理服务器分担 Web
服务器的负载并提高网络访问效率。
对于客户机发往真正服务器的请求,代理服务器起着中间调停者的作用。代理服务器会将所请求的文档存入高速缓存。如果有不止一个代理服务器, DNS
可以采用 循环复用法
选择其 IP
地址,随机地为请求选择路由。客户机每次都使用同一个 URL
,但请求所采取的路由每次都可能经过不同的代理服务器。
可以使用多个代理服务器来处理对一个高用量内容服务器的请求,这样做的好处是内容服务器可以处理更高的负载,并且比其独自工作时更有效率。在初始启动期间,代理服务器首次从内容服务器检索文档,此后,对内容服务器的请求数会大大下降。
只有 CGI
请求和偶发的新请求必须一路直达内容服务器。其余的请求可以由代理服务器进行处理。下面对此进行举例说明:
假定对服务器的请求中有 90%
都不是 CGI
请求(这表示它们可以进行高速缓存),而且内容服务器每天都会被命中 2 百万
次。在此情况下,如果连接三个反向代理服务器,且每个代理服务器每天处理 2 百万
次命中,则每天将能够处理大约 6 百万
次命中。请求中有 10%
达到内容服务器,合计约为每个代理服务器每天 200,000
次命中,即总数仅为 600,000
,从而效率显著提高。命中次数可从大约 2 百万
次增加到 6 百万
次,而内容服务器的负载却相应地从 2 百万
次减少到 600,000
次。实际结果依具体情况而定。
CDN
内容分发网络 (Content Delivery Network)
,简称 CDN
,其目的是使用户可就近取得所需内容,解决 Internet
网络拥挤的状况,提高用户访问网站的响应速度。
其基本思路是尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定。通过在网络各处放置节点服务器所构成的在现有的互联网基础之上的一层智能虚拟网络, CDN
系统能够实时地根据网络流量和各节点的连接、负载状况以及到用户的距离和响应时间等综合信息将用户的请求重新导向离用户最近的服务节点上。
个人理解, CDN
更像是一个分布式的有着大量负载均衡反向代理服务器组成的网络,用户请求资源时,可以直接就近选择一个节点,此时用户客户端-节点-源站实则就是构成了上述所说的用户-反向代理服务器-源站的结构。
这里引用 @Esacape Plan
在 博客 中所描述的用户 A、B 通过 CDN
访问指定资源的流程:
用户 A 第一次访问流程如下图所示:
- 第 1 步访问的是加速域名,而不是源站域名。
- 第 3 步返回
CNAME
域名。 - 第 5 步返回
CNAME
域名对应的IP
地址,指向CDN
边缘层节点。 - 第 6 步请求的
URL
(或者说Referer
)仍为js.tt.com/idx.html
。 - 第 7 步请求中心层节点时,会带上第 6 步的
URL
作为参数。 - 第 8 步通过查询配置数据得到源站域名,进而向源站发起请求。这里的业务服务器即为
CDN
的源站。简单起见,省略了从DNS
服务器查询 A 记录的过程。 - 在整个过程中,URL 的域名会变化,但是
URL
的路径不会变化。
用户 A 第二次访问流程如下图:
- 由于本地
DNS
客户端拥有了加速域名的解析缓存,就不需要再查询DNS
服务器了。 - 由于
CDN
边缘层节点有了对应资源的缓存,就不需要再向上请求资源了。
用户 B 第一次访问流程如图所示:
- 由于用户 A 和用户 B 地域相差比较远,使用不同的边缘层节点,所以边缘层节点没有对应资源的缓存,需要向中心层节点请求资源。
- 中心层节点拥有该资源的缓存,所以就不需要回源了。
从上述访问过程可以看出,在使用 CDN
时,用户所发送的资源请求如果在节点中未命中,则 CDN
节点服务器则会向后端源服务器请求资源,即这里可以理解为 CDN
节点接收了客户端的请求,处理后转发给了后端源服务器,相同的,反向代理服务器也是如此,那么这里就出现了两个服务器对于客户端请求的两次理解了,那么如果这两次理解存在差异,是否会导致什么问题?
结合前面所说的 Pipline/Content-Length/Transfer-Encoding
,是否会引起对请求的理解不一致,从而导致安全问题?这其实便是 HTTP Smuggling
问题的根源所在。
HTTP Smuggling 原因
正如上面所讨论的,对于请求理解的不一致和差异,会导致安全问题,那么这种安全问题会导致什么后果呢?这里引用 @mengchen 师傅对 HTTP Smuggling
原因的描述:
当我们向代理服务器发送一个比较模糊的 HTTP 请求时,由于两者服务器的实现方式不同,可能代理服务器认为这是一个 HTTP 请求,然后将其转发给了后端的源站服务器,但源站服务器经过解析处理后,只认为其中的一部分为正常请求,剩下的那一部分,就算是走私的请求,当该部分对正常用户的请求造成了影响之后,就实现了 HTTP 走私攻击。
看下面这个图可能会理解的清晰一些,在使用 Pipline
时,如果前置服务器和后端服务器对请求的解析处理不一致,从而导致对请求的划分出现问题,则会导致将一个请求一部分解析为正常请求,剩下一部分可能会被划分到其他请求队列中,就成为了走私的请求:
而由上图可以看出,如果可以使得前置服务器和后端服务器对请求长度的理解产生偏差,则会导致走私,那么如何使得对长度理解产生偏差呢?这里就回到了前面所提到的 Content-Length
和 Transfer-Encoding
(后文中以 CL
代替 Content-length
,以 TE
代替 Transfer-Encoding
),这两种方式都可以用来表示 HTTP
请求内容的长度。
那么就可以很容易想到,如果这两者同时存在,而前置服务器以 CL
来分辨长度,后端服务器以 TE
来划分,则就能引发两个服务器对请求长度理解的差异了。或者干脆这两种只有一种存在,但是存在两个,并且前后端对其理解有歧义,可以通过精心构造从而产生混淆,引发长度理解差异。
事实是,为了避免歧义,在 rfc2616#section-4.4
中规定当这两个同时出现时,Content-Length 将被忽略:
3.If a Content-Length header field (section 14.13) is present, its decimal value in OCTETs represents both the entity-length and the transfer-length. The Content-Length header field MUST NOT be sent if these two lengths are different (i.e., if a Transfer-Encoding header field is present). If a message is received with both a Transfer-Encoding header field and a Content-Length header field, the latter MUST be ignored.
但是规范只是规范,而不是必须遵守的铁律,因此并不是所有的 Web
服务器(中间件)都严格遵循规范来进行设计,部分实现者在实现时因为各种原因有了自己的一些设计,从而导致了 HTTP Smuggling
的出现,关于这一问题的分类, @mengchen 师傅归纳得非常全面,主要分为以下几种(注:后文出现的 CL-TE
这种形式,意为两个服务器的优先处理,如 CL-TE
表示前置服务器优先处理 CL
,后置服务器优先处理 TE
):
- CL 不为 0 的 GET 请求
- CL-CL
- CL-TE
- TE-CL
- TE-TE
原因分类
这里对上述五种产生请求走私的原因进行分析,并给出一些样例,如果感兴趣的可以自己 实验理解 一下。
CL 不为 0 的 GET 请求
正如我们熟知的, GET
请求一般是没有请求体的,也即 CL
为 0 , 但是有的服务器在设计实现时,是可能运行存在请求体的,所以如果前置服务器和后端服务器存在这种对请求体的支持不一致,也是可能导致请求走私的。这一点不光是 GET
请求,实际上对于所有不携带请求体的 HTTP
请求都是存在这样的问题的。
比如如果代理服务器支持 GET
请求带有请求体,后端服务器不支持请求体,则如果客户端给代理服务器发送时,将另一个请求附在第一个请求的请求体中,代理服务器会识别成一个请求,但是后端服务则因为不支持请求体,可能会将其识别为两个请求:
GET / HTTP/1.1\r\n |
这里前置服务器支持请求体,将其解析为一个请求,后端服务器因为不支持,将其解析为两个请求:
// 第一个 |
CL-CL
构造一个包含两个 Content-Length
的包,根据规范( RFC7230 3.3.3 节 )此时应当返回 400
错误,但如果没有正确遵守规范,且服务器按不同的 CL
进行处理,这可能会导致 HTTP 走私。当然,这种场景其实并不多见。
假设有这样一个请求报文,并且前置服务器解析第一个 CL
,后端服务器解析第二个:
POST / HTTP/1.1\r\n |
那么在前置服务器中,对这个请求的理解是正常的,于将上述数据包转发给后端服务器。而后端服务器根据第二个 CL
处理,于是读取到的报文如下:
POST / HTTP/1.1\r\n |
此时,后端服务器认为自己已经读取完,并且也生成了对应的响应发送,但是可以看到由前置服务器带来的字符中还剩下一个 Get
,那么这个字符就有可能作为下一个请求的一部分,假设现在有一个正常的用户发起了一个请求:
GET /index.html HTTP/1.1\r\n |
那么后端服务器会将前一个请求中剩下的 Get
作为后一个请求的一部分,也就是变成了这样:
GetGET /index.html HTTP/1.1\r\n |
可以看到,此时已经对正常用户的请求产生了影响了,此时后端处理这个用户的请求时就会发现,这个请求中使用的 HTTP Method
为 GetGET
,很显然,会给客户端返回一个 request method not found
。
这里这种场景的话,只是导致请求失败,那么如果能够结合 CRLF
的话,可能就能进行危害更大的攻击了。
CL-TE
在这种情况中,前置服务器认为 Content-Length
优先级更高(或者根本就不支持 Transfer-Encoding
) ,后端认为 Transfer-Encoding
优先级更高。因此在发送同时带有 CL
和 TE
的请求时,可能会导致请求走私。而这种情况,其实也就是在前几天的 defcon ctf qual
中出现的。
比如下面这个例子:
POST / HTTP/1.1\r\n |
在前置服务器处理时,使用 CL
进行处理,即得到一个完整的请求体:
0\r\n |
这时是正常的,但是转发给后端服务器时,后端服务器根据 TE
来进行处理,则当其读取到 \r\n\r\n
时,会认为当前请求体已经读取完成,即读取到的请求体如下:
0\r\n |
和 CL-CL
中一样,此时后端服务器处理完成后完成响应,而剩下的 Get
将会被走私到下一个请求中,下一个请求就变成了这样:
GetGET /index.html HTTP/1.1\r\n |
TE-CL
和上一种情况类似,但是此时前置服务器认为 Transfer-Encoding
优先级更高,后端认为 Content-Length
优先级更高(或者不支持 Transfer-Encoding
)。如以下报文:
POST / HTTP/1.1\r\n |
在这个例子中,前置服务器使用 TE
进行处理,即使用 \r\n\r\n
作为结束标识,所以可以将这个报文全部读取,而在后端服务器中,使用 CL
进行处理,此时该值为 4
,则后端解析的请求体为:
12\r\n |
至于剩下的内容,则会被后端服务器解析到另一个请求中:
POST / HTTP/1.1\r\n |
TE-TE
上述已经说了 CL-CL
的情况,那如果前置和后端服务器都优先支持 Transfer-Encoding
,是否可以进行请求走私呢?
答案显然是可以的,这里我们可以对服务器的实现进行分析和 FUZZ,恶意构造出使其中一个服务器不优先支持 TE
的请求(或者无法识别到 TE
),从而使其转而使用 CL
进行解析处理。这里如果是使前置服务器产生混淆,则就变成了另外一种形式的 CL-TE
,类似的,使后端服务器混淆则变成了另一种形式的 TE-CL
。
那么如何导致这种混淆呢?这里一种比较简单的方法是使用大小写,如果前置服务器和后端服务器在解析 TE
的时候对大小写敏感,则可能会导致这样的情况,或者使用一个不符合规范的 TE
头,使得其中一个服务器解析失败,转而使用 CL
。
下面为一个样例,需要注意的是,因为产生混淆后需要让服务器以 CL
进行处理,所以这里是使用了一个 CL
,两个 TE
,下面这个样例通过混淆将 TE-TE
转为了 TE-CL
:
POST / HTTP/1.1\r\n |
对于上述请求,可以看到前后的 Transfer-Encoding
,E 的大小写不同,并且后一个 TE
中使用 cow
,因此可能导致识别的不同。前置服务器使用正常的 TE
进行解析,因此得到的请求体为完整的,而后端服务器因为对 TE
解析失败,因此使用 CL
进行解析,则只能解析到下面的请求体:
5c\r\n |
而后续其他的内容则会当做是下一个请求的内容:
GetPOST / HTTP/1.1\r\n |
利用与防御
关于上述所说的一些场景, PortSwigger 中有一些具体的实验,可以进行尝试从而加深理解,而在现实应用中,请求走私攻击又可能会导致怎么样的后果呢?
有一个现成的例子就是在 Defcon CTF 2020 Qual
中的 uploooadit
一题,有兴趣的可以 尝试复现 。另外, PortSwigger
也对其现实危害进行了归纳:
- 绕过前置服务器的安全限制
- 获取前置服务器修改过的请求字段
- 获取其他用户的请求
- 反射型
XSS
组合拳 - 将
on-site
重定向变为开放式重定向 - 缓存投毒
- 缓存欺骗
一样的,结合 PortSwigger
给出的实验会更容易理解,另外,如果不满足于这些的话,可以搭建一下 CVE-2018-8004
的环境进行 复现 。
而对于请求走私如何防御呢?
不针对特定的服务器,通用的防御措施大概有三种:
- 禁用代理服务器与后端服务器之间的 TCP 连接重用。
- 使用 HTTP/2 协议。
- 前后端使用相同的服务器。
其实,归根到底的话,这些防御措施都是在已有这样的问题的情况下如何进行防御,在条件允许的情况下,如果能够严格遵循 RFC
中的规范来设计实现服务器的话,想必可以最大程度上减少此类问题的发生。当然,这一点可能又会因为各种其他因素而变得并不容易。
总结
其实 HTTP Smuggling
并不是最近才兴起的,而是一个长久以来都存在的问题,这种因为处理的不一致性而导致的问题很多,其中不光是有请求走私这种,另外还有可能可以利用不一致性绕过 Web
应用中的 check
,或者引发其他问题,比如之前所接触的同组学长发现的利用 CDN
和源站对 Range
头的解析差异而导致可以 使用 CDN 进行 Dos 攻击 。
总得来说,互联网野草般的发展,决定了总体环境的多样化与多元化,而开发者设计和实现的不一致,也可能会导致很多问题。规范的制定在一定程度上避免了这样的问题发生,但是总归系统的实现者是一个独立的灵魂和个体,不同的机构和组织也会有自己的一些考虑,从而在规范的遵循上会有差异,这类问题往往也是无法避免的,而作为一个安全研究者,如果能够发掘一些这样的问题,从而减少一些由此带来的危害,相比会是件很意思也很有意义的事。
参考文献
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论