[RoarCTF 2019]Easy Calc1 wp
预测试
手工测试
这个页面实现了一个简单的计算器功能,当输入 1+1 时能正确返回执行结果 2,
但当输入 1+1&&ifconfig
之类的表达式时,会出现弹窗:
查看源码
前端页面调用了一个函数,对用户输入做了一个简单的处理。该页面提示使用了 waf ,并且可以发现有一个 calc.php 文件,并且可以通过 get 传参,参数是 num 。
访问 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.';');
}
?>
直接给出了 php 源码。
首先,代码通过 error_reporting(0)
关闭所有错误和警告的输出。
然后,代码判断是否存在 GET 参数 num 。如果 num 不存在,则显示当前脚本的源代码,即显示自身的代码。
如果 num 存在,则将获取到的值赋给变量 $str 。接下来,代码定义一个黑名单数组 $blacklist ,包含了一些需要过滤的特殊字符和字符串。
然后,代码使用 foreach 循环遍历黑名单数组 $blacklist 。在每次循环中,代码使用 preg_match() 函数对 $str 进行匹配,判断是否包含黑名单中的任何一个特殊字符或字符串。如果匹配成功,即 $str 中包含黑名单中的特殊字符或字符串,代码会输出 " what are you want to do? " 并终止脚本的执行。
如果没有匹配到任何黑名单字符,代码会使用 eval() 函数执行一个动态的 PHP 代码字符串,即将$str作为被执行的PHP代码。这段代码的作用是输出 $str 中的内容。
waf 绕过
尝试直接访问:node4.buuoj.cn:29780/calc.php?num=system('ls');
,返回结果:
假设是被上面的 php 文件中的过滤机制过滤了,那么应该输出:what are you want to do?
,这句话才对,但是出现这种画面,应该是被防火墙拦截了。
空格绕过 waf
在 num 参数前加上一个空格,即:node4.buuoj.cn:29780/calc.php? num=system('ls');
返回结果:
这样服务器会认为传入的参数是 空格num ,而不是 num 。这里用到这种绕过方式是因为假定服务器只对 num 参数做检测,而对于其他参数不做检测。
当 空格num 参数传入到后端,被 PHP 代码处理时,会被去除多余空格及特殊字符:如空格、制表符、回车换行符以及某些特殊字符等。这样一来仍然是 num 参数了。
黑名单绕过
这里可以用到无参函数 RCE 的 payload
传入参数:calc.php? num=var_dump(scandir(current(localeconv())));
查看当前目录下的文件
localeconv()
函数返回当前设置的地区的格式化信息,包括货币符号、小数点符号等。它返回一个数组,其中包含了与当前地区相关的格式化参数,该函数返回的第一个元素的值通常是小数点 “.” 。current()
函数用于获取数组中的当前元素的值。在这里,它用于获取localeconv()
函数返回的数组的第一个元素的值,即一个小数点。scandir()
函数用于获取指定目录中的文件和文件夹列表。它接受一个路径作为参数,并返回一个包含指定目录中所有文件和文件夹的数组。scandir(".")
表示获取当前目录下的文件列表。- 最后使用
var_dump()
函数将该列表输出到页面上。
但其实这里可以更简单:
calc.php? num=var_dump(scandir(chr(46)));
46 是 “.” 的 ASCII 码值,返回结果:
因为可以有参数嘛。用 chr() 函数将数字变为字符。
同理查看根目录下的文件:calc.php? num=var_dump(scandir(chr(47)));
47 是 “/” 的 ASCII 码值,返回结果:
找到一个名为 f1agg 的文件。
查看 /f1agg 文件
? num=file_get_contents(chr(47).chr(102).chr(49).chr(97).chr(103).chr(103));
file_get_contents()
函数把整个文件读入一个字符串中。
chr(47).chr(102).chr(49).chr(97).chr(103).chr(103)
分别是 ‘/’ ‘f’ ‘1’ ‘a’ ‘g’ ‘g’ 的 ASCII 值转字符,‘.’ 用作字符串连接。每一个 chr() 函数返回的结果由于是字符,所以自带了一对引号,不需要额外再加。
返回结果:
新的思路 - url 溢出
此外,舍友提出了一种新思路:
查看 phpinfo()
? num=1;eval(end(pos(get_defined_vars())))&nss=phpinfo();
get_defined_vars():返回由所有已定义变量所组成的数组,会返回 _GET
, _POST
, _COOKIE
, _FILES
全局变量的值,返回数组顺序为 get->post->cookie->files 。
current():返回数组中的当前单元,初始指向插入到数组中的第一个单元,也就是会返回 $_GET
变量的数组值。
end() : 将内部指针指向数组中的最后一个元素,并输出。即新加入的参数 nss 。
最后由 eval() 函数执行,使得 get 方式的参数 nss 生效。
这样的话就可以再利用 nss 传参了,由于代码只对 num 参数的值做了过滤,因此 nss 参数理论上可以造成任意代码执行。
上面代码的返回结果为:
可以看到成功执行了 phpinfo()
在 disable_functions 处发现禁用的函数太多了,还是没有办法任意代码执行。不过 include 函数似乎没被禁用。
查看 /etc/passwd 文件
? num=1;eval(end(pos(get_defined_vars())))&nss=include("/etc/passwd");
输出结果:
有点意思。
http 请求走私
绕过 waf 的方式还有一种 - http 请求走私。
大致原理就是使用了两个 Content-Length 头,使得前端无法识别,直接将整个包完全发给了后端。但这样还是要接受后端的黑名单过滤,所以 num 传参还是不能为所欲为。