HTTP请求走私
聊HTTP请求走私之前,需要先思考一个问题:HTTP请求如何标识一个请求的结束(尤其是POST请求)
一种是通过 Content-Length
请求头 的值界定请求体的长度,另一种是在分块传输时,通过 Transfer-Encoding: chunked
请求头与请求体最后一行的 0\r\n\r\n
来标识该请求的结束(不计入请求体长度)
按照HTTP/1.1规范标准,这两种请求头同时存在时应该忽略 Content-Length
而以分块传输为准,但是对于反代链中的多个服务器而言,可能有些并不支持分块传输请求头、有些对于标准规范的实现并未足够精细,在处理一些畸形请求头时会有非预期的效果。
为了方便表述,接下来均将用于反向代理的服务器称为前端,隐藏在反代服务器之后用于提供具体业务的服务器称为后端。用
CL-TE
表示前端以Content-Length
作为请求结束界定标准、TE-CL
表示前端以Transfer-Encoding
作为请求结束界定标准。
HTTP请求走私漏洞正是由于前后端服务器界定标准不一致导致的,利用HTTP请求走私使得 一次攻击
在前端服务器识别为 一个请求
,但传送到后端服务器时其误认为这是用了pipelining,而将其识别为 两个不同的请求
。
- 更深入的细节原理,涉及到反代和后端对于消息的处理机制,这部分现在还不懂,以后懂了再单独分析
CL-TE
1 | POST /search HTTP/1.1 |
前端读取
CL
值为50,会将这一整段视为一个请求转发至后端(0
及之后的部分会被认作是该请求的请求体内容)后端接收时以
TE
作为界定标准,将0\r\n\r\n
视为一个请求的结束,将后续部分视作下一个传输过来的请求由于我们构造的后面这个请求的包结构并不完整,所以后端认为这份数据还没有接收完毕,会继续将随后到来的请求拼接进去
注意
CL
取值为50时,是截止到最后一行的最后一个字母t
的,也就是说t
后面并不存在\r\n
这对回车换行符,那么后端随后紧接而来的请求实际上会被拼接成这种样子:
1 | GET /404 HTTP/1.1 |
这就导致了后续对 /search
的访问,因为请求行被吃进了 X-Ignore
这个请求头的值中,拼接后实际变成了对 /404
的访问。
从理论上来说,我们可以发出请求走私攻击包后,紧接着发送一个正常请求,根据后者不正常的响应差异来判断漏洞存在。
在实战中我们的攻击请求和紧接着发送的正常请求之间,很可能会有其他人的某个请求刚好插在了中间,这样我们本来期待用于判断漏洞的不正常响应就会被回复给别人,影响别人正常使的同时还会导致我们误以为没洞,所以最好避开高峰期多试几次。
TE-CL
1 | POST /search HTTP/1.1 |
前端以
TE
作为界定标准,会将这一整段视为一个请求转发至后端(q=something
及之后的部分会被认作是该请求的请求体内容)前端读取
CL
值为13,认为第一个请求截止到q=something
,将后续部分视作下一个传输过来的请求由于我们构造的后面这个请求的
CL
值为15,所以后端认为这份数据还没有接收完毕,会继续在随后到来的请求中取出5个字符拼接进去后端随后紧接而来的请求实际上会被拼接成这种样子:
1 | POST /404 HTTP/1.1 |
于是就使得后续请求被截断,剩下的不完整部分会被视为无效请求丢弃,最终会得到一个不正常的响应。(同样存在前文中说的竞争问题,缓解方法一样)
TE-TE
从原理来看前后端标准一致时是不存在请求走私的,但如果一个接受畸形 TE
认为是分块传输,一个不接受畸形 TE
而按照 CL
的值作为请求结束界定标准,这种细微差异同样会导致请求走私,PortSwigger 提供了部分在实战中成功利用过的畸形头:
1 | Transfer-Encoding: xchunked |
1 | Transfer-Encoding : chunked |
1 | Transfer-Encoding: chunked |
1 | Transfer-Encoding:[tab]chunked |
1 | GET / HTTP/1.1 |
1 | X: X[\n]Transfer-Encoding: chunked |
1 | Transfer-Encoding |
- 在Burp插件中存在更多畸形
TE
头用于Fuzz,可以自动计算CL
长度和配合Turbo Intruder
光速发包,真香
利用畸形 TE
导致的差异化解析,最终还是会对应 CL-TE
或 TE-CL
的情况,就不再贴数据包了(就是改一下 TE
头)。
攻击场景
最直接的就是用来绕过前端的安全访问控制,让走私的请求直达业务逻辑后端。但是实战中可能没有这么理想化,比如后端还是会校验 client-ip
、 x-forwarded-for
或是反代加的自定义请求头,这时就需要找到一个能够回显请求体参数的地方,利用请求走私中的第二个 不完整
请求吃掉紧接而来的下一个请求,通过直接或间接的回显读到需要的请求头。
比如在一个搜索功能中,POST请求的 q
参数的内容表示搜索的字符串,这个字符串在搜索页会被 直接回显
或是存储到搜索记录中 间接回显
。
1 | POST /search HTTP/1.1 |
重点注意第二个走私请求中 CL
值被设置得偏大,且有回显的 q
参数被移到了末尾,后端随后紧接而来的请求实际上会被拼接成这种样子:
1 | POST /search HTTP/1.1 |
...
中有多少内容取决于走私请求中 CL
值的大小,建议根据需要慢慢调大,避免过大导致超时(在这个例子中我们已经读到了需要的 X-Secret-Header: 666
这个前端服务器自定义校验头),但是调大 CL
值能读到的东西最多截止到遇到 &
时(想想HTTP请求用什么符号区分不同参数?我们能回显什么参数?)
至于这个 随后紧接而来的请求
该由我们发出,还是守株待兔等着别人的访问请求进坑,就要看具体的目的是什
么了。
窃取Cookie
如果是想要打到别人的私有请求头(比如 Cookie
之类的),那就得等人进坑且需要一个存储型的间接回显点,因为一次性的直接回显会直接响应给受害者,我们是看不到的。
- 存储型间接回显点举例:搜索记录、个人简介、发布文章、发布评论、发送私信
水坑型XSS
如果实在没有存储型间接回显点的话,那就充分利用一次性直接回显这个特点,配合一个反射型XSS使其变为无条件触发的 水坑型XSS
(我自己编的名)
- 反射型XSS漏洞点可以是常规的GET或POST参数,同样也可以是像
User-Agent
头这种self触发点,因为结合请求走私我们可以实现将它强加给下一个访问的受害者
任意重定向
如果配合 Apache
和 IIS
会将无斜杠路径通过重定向方式添加斜杠的特性,就可以再次利用请求走私给下一个访问的受害者强加头部,通过重定向将其劫持到任意域名下。
1 | POST /search HTTP/1.1 |
下一个受害者的访问请求会被拼接成这种样子:
1 | GET /evil HTTP/1.1 |
Web缓存投毒
同时,对于 /a.js
的访问请求还可能被缓存下来,使得之后每个不受请求走私影响的后续请求,同样受到重定向劫持的影响,进一步造成Web缓存投毒:
1 | POST /a.js HTTP/1.1 |
Web缓存水坑
回过头来,之前没找到回显点打敏感数据的话,也可以再再次利用请求走私给下一个访问的受害者强加头部,结合Web缓存特性将其敏感数据缓存下来窃取。
1 | POST /search HTTP/1.1 |
下一个受害者的访问请求会被拼接成这种样子:
1 | GET /getapikey HTTP/1.1 |
受害者的 /getapikey
中的信息会被缓存至 /any.js
中,但是一个问题是攻击者并不知道受害者是访问的 /any.js
,所以可能需要遍历几乎所有静态文件分析= =