前几天打的defcon ctf qual里有一题uploooadit,里面涉及到HTTP Smuggling | HTTP Desync Attacks,也就是HTTP走私攻击,是一个很有趣的攻击方式了,而这个其实也和自己正在弄的协议一致性研究有所关联,于是参考了些论文和一些师傅的博客,回顾一下,并做个记录。

前置知识

HTTP/1.1

Keep alive

HTTP持久连接(HTTP persistent connection,也称作 HTTP keep-aliveHTTP 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编码的格式如下:

Chunked-Body = *chunk
0CRLF
footer
CRLF
chunk = chunk-size [ chunk-ext ] CRLF
chunk-data CRLF

hex-no-zero = <HEX excluding “0″>

chunk-size = hex-no-zero *HEX
chunk-ext = *( “;” chunk-ext-name [ "=" chunk-ext-value ] )
chunk-ext-name = token
chunk-ext-val = token | quoted-string
chunk-data = chunk-size(OCTET)

footer = *entity-header

// 即Chunk编码由四部分组成:
// 1. 0至多个chunk块
// 2. '0' CRLF
// 3. footer
// 4. CRLF
// 而每个chunk块由:chunk-size、chunk-ext(可选)、CRLF、chunk-data、CRLF组成。

反向代理

我们知道,一个简单的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-LengthTransfer-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
Host: example.com\r\n
Content-Length: 44\r\n

GET / secret HTTP/1.1\r\n
Host: example.com\r\n
\r\n

这里前置服务器支持请求体,将其解析为一个请求,后端服务器因为不支持,将其解析为两个请求:

// 第一个
GET / HTTP/1.1\r\n
Host: example.com\r\n
Content-Length: 44\r\n

// 第二个
GET / secret HTTP/1.1\r\n
Host: example.com\r\n
\r\n

CL-CL

构造一个包含两个Content-Length的包,根据规范(RFC7230 3.3.3节)此时应当返回400错误,但如果没有正确遵守规范,且服务器按不同的CL进行处理,这可能会导致HTTP走私。当然,这种场景其实并不多见。

假设有这样一个请求报文,并且前置服务器解析第一个CL,后端服务器解析第二个:

POST / HTTP/1.1\r\n
Host: example.com\r\n
Content-Length: 10\r\n
Content-Length: 9\r\n
12345\r\n
Get

那么在前置服务器中,对这个请求的理解是正常的,于将上述数据包转发给后端服务器。而后端服务器根据第二个CL处理,于是读取到的报文如下:

POST / HTTP/1.1\r\n
Host: example.com\r\n
Content-Length: 10\r\n
Content-Length: 9\r\n
12345\r\n

此时,后端服务器认为自己已经读取完,并且也生成了对应的响应发送,但是可以看到由前置服务器带来的字符中还剩下一个Get,那么这个字符就有可能作为下一个请求的一部分,假设现在有一个正常的用户发起了一个请求:

GET /index.html HTTP/1.1\r\n
Host: example.com\r\n

那么后端服务器会将前一个请求中剩下的Get作为后一个请求的一部分,也就是变成了这样:

GetGET /index.html HTTP/1.1\r\n
Host: example.com\r\n

可以看到,此时已经对正常用户的请求产生了影响了,此时后端处理这个用户的请求时就会发现,这个请求中使用的HTTP MethodGetGET,很显然,会给客户端返回一个request method not found

这里这种场景的话,只是导致请求失败,那么如果能够结合CRLF的话,可能就能进行危害更大的攻击了。

CL-TE

在这种情况中,前置服务器认为Content-Length优先级更高(或者根本就不支持Transfer-Encoding) ,后端认为Transfer-Encoding优先级更高。因此在发送同时带有CLTE的请求时,可能会导致请求走私。而这种情况,其实也就是在前几天的defcon ctf qual中出现的。

比如下面这个例子:

POST / HTTP/1.1\r\n
Host: example.com\r\n
Content-Length: 8\r\n
Transfer-Encoding: chunked\r\n
\r\n
0\r\n
\r\n
Get

在前置服务器处理时,使用CL进行处理,即得到一个完整的请求体:

0\r\n
\r\n
Get

这时是正常的,但是转发给后端服务器时,后端服务器根据TE来进行处理,则当其读取到\r\n\r\n时,会认为当前请求体已经读取完成,即读取到的请求体如下:

0\r\n
\r\n

CL-CL中一样,此时后端服务器处理完成后完成响应,而剩下的Get将会被走私到下一个请求中,下一个请求就变成了这样:

GetGET /index.html HTTP/1.1\r\n
Host: example.com\r\n

TE-CL

和上一种情况类似,但是此时前置服务器认为Transfer-Encoding优先级更高,后端认为Content-Length优先级更高(或者不支持Transfer-Encoding)。如以下报文:

POST / HTTP/1.1\r\n
Host: example.com\r\n
Content-Length: 4\r\n
Transfer-Encoding: chunked\r\n
\r\n
12\r\n
POST / HTTP/1.1\r\n
\r\n
0\r\n
\r\n

在这个例子中,前置服务器使用TE进行处理,即使用\r\n\r\n作为结束标识,所以可以将这个报文全部读取,而在后端服务器中,使用CL进行处理,此时该值为4,则后端解析的请求体为:

12\r\n

至于剩下的内容,则会被后端服务器解析到另一个请求中:

POST / HTTP/1.1\r\n
\r\n
0\r\n
\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
Host: example.com\r\n
Content-length: 4\r\n
Transfer-Encoding: chunked\r\n
Transfer-encoding: cow\r\n
\r\n
5c\r\n
GetPOST / HTTP/1.1\r\n
Content-Type: application/x-www-form-urlencoded\r\n
Content-Length: 15\r\n
\r\n
x=1\r\n
0\r\n
\r\n

对于上述请求,可以看到前后的Transfer-Encoding,E的大小写不同,并且后一个TE中使用cow,因此可能导致识别的不同。前置服务器使用正常的TE进行解析,因此得到的请求体为完整的,而后端服务器因为对TE解析失败,因此使用CL进行解析,则只能解析到下面的请求体:

5c\r\n

而后续其他的内容则会当做是下一个请求的内容:

GetPOST / HTTP/1.1\r\n
Content-Type: application/x-www-form-urlencoded\r\n
Content-Length: 15\r\n
\r\n
x=1\r\n
0\r\n
\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攻击

总得来说,互联网野草般的发展,决定了总体环境的多样化与多元化,而开发者设计和实现的不一致,也可能会导致很多问题。规范的制定在一定程度上避免了这样的问题发生,但是总归系统的实现者是一个独立的灵魂和个体,不同的机构和组织也会有自己的一些考虑,从而在规范的遵循上会有差异,这类问题往往也是无法避免的,而作为一个安全研究者,如果能够发掘一些这样的问题,从而减少一些由此带来的危害,相比会是件很意思也很有意义的事。

参考文献