说明
该文章来源于同事lu2ker转载至此处,更多文章可参考:https://github.com/lu2ker/
文章目录
- 说明
- in_array()
- filter_var()
- class_exists()
- strpos()
- escapeshellarg()
- escapeshellcmd()
- preg_replace()
- parse_str()
- 无字母数字下划线的webshell
- str_replace()
- extract()
- htmlentities()
- CURL传输选项CURLOPT_POSTFIELDS
- file_exists的bug?or机制?
in_array()
功能: 检查数组中是否存在某个值。
定义: in_array ( mixed
$needle
, array$haystack
, bool$strict
=false
) : bool解释:大海捞针,在大海(
haystack
)中搜索针(needle
),如果没有设置strict
则使用宽松的比较。
当in_array()函数的第三个参数未设置为true时(默认为false),会进行弱比较。例如:
$exp = '2shell';
$whitelist = array(0,1,2,3,4,5);
# 此时如果调用 in_array($exp,$whitelist); 是会判定为true的。
当第三个参数设置为true时,就会进行强比较,会比较类型的。
filter_var()
函数定义如下:
功能:使用特定的过滤器过滤一个变量。
定义:filter_var( mixed $value, int $filter = FILTER_DEFAULT, array|int $options = 0) : mixed
该函数可调用指定的PHP内置的过滤器来过滤一个变量,如果符合过滤器的要求就返回原$value
。比如,如果指定的是FILTER_VALIDATE_URL
过滤器的话。FILTER_VALIDATE_URL
过滤器把$value
作为 URL 来验证,验证其是否是URL格式的字符串,但是有一个很大的问题是 这个过滤器的”宽容性很高“,类似于qwe://
这种形式的URL都可以通过它的过滤。
经过一些测试发现,形如xxx://xxx
即可验证通过该过滤器。但是如果前半部分是http://
的话,正斜杠后边不允许有特殊字符了。比如http://><"";''
是不行的但是qwe://><"";''
确实可以通过过滤的。
这里有一篇该函数相关的技术文
class_exists()
功能:检查类是否已定义。
定义:class_exists ( string
$class
, bool$autoload
= true ) : bool解释:检查指定的类是否已定义。$autoload是是否默认调用__autoload函数。
一个可以造成任意文件包含漏洞的例子。
<?php
function __autoload($classname){
echo 'I\'m working...';
include $classname;
}
$classname = $_GET['c'];
if (class_exists($classname)) {
echo 'balbala';
} else {
echo 'There is no page';
}
?>
这段代码的本意作用是:参数c获取一个classname
,class_exists()
判断其值是否是被定义好的类,如果程序中没有定义该类,就会默认调用__autoload()
将其包含到程序代码中。
这里就有一个任意文件包含漏洞,可以使用../
路径穿越来实现任意文件包含。但是,只有在PHP5~5.3(含)中才可以在class_exists()
中传入../
达到目的,高版本php中向class_exists
中传入../
是不会调用__autoload()
的。
strpos()
功能:查找字符串首次出现的位置
定义:strpos ( string
$haystack
, mixed$needle
, int$offset
= 0 ) : int解释:返回
needle
在haystack
中首次出现的数字位置。如果提供了$offset
,搜索会从字符串该字符数的起始位置开始统计。如果是负数,搜索会从字符串结尾指定字符数开始。
这个函数可能会被利用的点在于,例如下面这段代码:
<?php
$a = $_GET['a'];
$b = $_GET['b'];
if (!strpos($a, '<') && !strpos($b, '>')) {
if (strstr($a, '<') && strstr($b, '>')) {
echo 'That\'s ok.';
} else {
echo 'No No No.';
}
} else {
echo 'No No No.';
}
?>
这段代码的本意是:如果参数a
中不含有<
且参数b中不含有>
,又 如果参数a
中含有<
且参数b
中含有>
,就输出That's ok.
。其余情况都输出No No NO.
。听起来确实是自相矛盾,但是是为了更好的理解这个函数。在这段代码中传入?a=<&b=>
即可通过所有的判断。
在PHP手册中查询strstr()
函数的用法,会看到这样一句话:
但是,strpos()
函数并不能像上述案例代码那样使用。有一种特殊的情况即:<
是在$a
的首位置时,strpos()
会返回0,因为本身strpos()函数的功能就是查找字符串首次出现的位置。而!0
为true
。所以除了$a
中不存在<
这种办法可以使第一个if判定为真外,将<
写在$a
的第一个字符位置也是可以的。
escapeshellarg()
escapeshellcmd()
功能:escapeshellarg()把字符串转码为可以在 shell 命令里使用的参数。
功能:escapeshellcmd()对字符串中可能会欺骗shell命令执行任意命令的字符进行转义。此函数保证用户输入的数据在传送到
exec()
或system()
函数,或者执行操作符
之前进行转义
原理很简单,就是escapeshellarg()
为了转义字符而加上的\
又被escapeshellcmd()
转义了。具体原理和漏洞案例看这里。
但是我在Windows环境下做测试,PHP5.2.17中,原字符串中有没有单引号都“不影响”最后的结果。而大于5.2的版本,无论如何都达不到逃逸的目的。
PHP5.2.17 字符串中无单引号:
PHP5.2.17 字符串中有单引号:
PHP>5.2.17 字符串中有单引号(^
是windows中的转义字符):
preg_replace()
功能:执行一个正则表达式的搜索和替换
定义:preg_replace ( string|array
$pattern
, string|array$replacement
, string|array$subject
, int$limit
= -1 , int&$count
=null
) : string|array|null解释:搜索
subject
中匹配pattern
的部分,以replacement
进行替换。
其中以前最大的问题是 $pattern
的/e
修饰符带来的代码执行问题。但只在PHP<5.5.0有效了。
经典案例:
preg_replace('/(.*)/ei', 'strtolower("\1")', {${phpinfo()}});
PS:可以看看这个
补充一个名词:反向引用
反向引用
对一个正则表达式模式或部分模式 两边添加圆括号 将导致相关 匹配存储到一个临时缓冲区 中,所捕获的每个子匹配都按照在正则表达式模式中从左到右出现的顺序存储。缓冲区编号从 1 开始,最多可存储 99 个捕获的子表达式。每个缓冲区都可以使用 ‘\n’ 访问,其中 n 为一个标识特定缓冲区的一位或两位十进制数。
新学到了另一个问题,首先看看官方对第二个参数的部分描述:
replacement
中可以包含后向引用\\n
或$n
,语法上首选后者。 每个这样的引用将被匹配到的第 n 个捕获子组捕获到的文本替换。 n 可以是0-99,\\0
和$0
代表完整的模式匹配文本。捕获子组的序号计数方式为:代表捕获子组的左括号从左到右, 从1开始数。如果要在replacement
中使用反斜线,必须使用 4 个("\\\\"
,译注:因为这首先是 PHP 的字符串,经过转义后,是两个,再经过正则表达式引擎后才被认为是一个原文反斜线)。
**preg_replace()函数在处理字符串的时候,会自动对第二个参数的 ‘ \ ’这个字符进行转义。**那么如果在第二个参数中传入\'
就很有可能造成单引号逃逸问题了。
例如P师傅给过的这个经典的配置文件写入案例:
#index.php
<?php
if(!isset($_GET['option'])) die();
$str = addslashes($_GET['option']);
$file = file_get_contents('./config.php');
$file = preg_replace('|\$option=\'.*\';|', "\$option='$str';", $file);
file_put_contents('./config.php', $file);
#config.php
<?php
$option='test';
构造payload?option=a\';phpinfo();//
即可。
另外该题还有一个有意思的解法 也是利用了第二个参数的问题,即$n
当n为0时代表完整的模式匹配文本(什么xx意思看不懂啊=-=)好像是因为捕获子组是从1开始计数的,语法表示为$1
那么$0
就代表满足匹配的第三个参数的值了(被匹配的本身)。所以存在如下解法:
第一次:?option=;phpinfo();
结果:$option=';phpinfo();';
第二次:?option=%00 或者 ?option=$0
结果:$option='$option=';phpinfo();';'; //刚好能闭合单引号。
对比:$option='---第一次执行后的结果---';
parse_str()
功能:将字符串解析成多个变量
定义:parse_str ( string
$string
, array&$result
) : void解释:如果
string
是 URL 传递入的查询字符串(query string),则将它解析为变量并设置(or覆盖)到当前作用域(如果提供了result
则会设置到该数组里 )。
**变量覆盖问题。**这个函数的行为非常类似于注册全局变量,且它在注册变量之前不会验证变量是否已经存在。不安全的使用该函数也就是使用时没有用另外的代码判断它将要注册的变量是否已经存在。
demo案例:
同样的如果是从外部获取到的参数:
2022/6/2补充:对parse_str()规则的绕过,from PbootCMS<=3.1.2前台注⼊
由于 PHP 的变量名不能带「点」和「空格」,所以它们会被转化成下划线。用本函数带 result 参数,也会应用同样规则到数组的键名。
但是由于parse_str()底层C代码的设计问题,可能造成点或空格的绕过,在PbootCMS案例中就利用这个绕过造成了sqli漏洞,绕过demo如下:
正常处理结果:
利用’[‘字符进行绕过:
可以看到空格没有被替换为下划线了。
原因:在底层C代码的实现中,如果检测到’[‘字符会跳出 将「点」和「空格」替换为下划线的这个循环,还会将’[‘替换为下划线是因为
如果之后没有检测到’]‘字符的话,php就认为是误输入,会将当前指针指向(是’[‘后的第一个字符,因为这个时候还会考虑’[]'中包
裹字符的情况)的前一位赋值为下划线:
如果’[]'中包裹字符的最终效果:
无字母数字下划线的webshell
原理明白,不造轮子(懒)
str_replace()
功能:子字符串替换
定义:str_replace ( mixed
$search
, mixed$replace
, mixed$subject
, int&$count
= ? ) : mixed解释:该函数返回一个字符串或者数组。该字符串或数组是将
subject
中全部的search
都被replace
替换之后的结果。$count
如果被指定,它的值将被设置为替换发生的次数。
这个函数可能被利用的点在于替换,我见过有两种利用形式:①替换过滤函数插入的\
。②单次替换,双写绕过。
着重记录一下第一种利用:
function replace_bad_word($str){
global $black_list;
foreach ($black_list as $old => $new) {
strlen($old) > 2 && $str = str_replace($old,trim($new),$str);
}
return $str;
}
...
...
if(isset($_POST['msg']) && $_POST['msg'] !==''){
$msg = addslashes($_POST['msg']);
$msg = replace_bad_word(convert($msg));
$sql = "INSERT INTO users VALUES($id,'".$msg."')";
echo $sql;
$result = $conn->query($sql);
if($conn->error) die($conn->error);
}
...
以上是部分代码,为了不占篇幅就没有多贴,不过已经足够解释清楚利用方式了。
大概能明白这段代码的本意是:POST获取参数msg
,第一步addslashes
函数转义单引号、双引号、反斜线和NUL(null字符),第二步接着调用replace_bad_word
函数替换黑名单字符,比如一些敏感词之类的东西。最后再执行SQL语句。
这段代码是没毛病的。但是第二步替换的时候,**如果能够把addslashes
函数添加的\
给替换为空,不就能造成引号逃逸了吗?**现在加上一段注册全局变量的代码,这也是一段经典的变量覆盖漏洞代码:
foreach (array('_GET','_POST') as $method) {
foreach ($$method as $key => $value) {
$$key = $value;
}
}
可以使用这段代码的功能,构造任意的$black_list
,甚至构造成$black_list[\\\]=''
也是合法的。
例如:msg
传入1\' and xxxx
,在addslashes
转义后变成了1\\\' and xxx
即单引号和反斜线一起被转义了,但如果我们利用注册全局变量功能同时再传入一个参数$black_list[\\\]=''
当执行replace_bad_word
函数的时候,就会把1\\\' and xxx
中的\\\
给替换为空!造成单引号逃逸!
这种利用还有几个有同样效果的paylaod,如:
//传入参数
msg=1%00' and xxxxx
//被addslashes处理后为
msg=1\0\' and xxxxx
//做替换处理时的黑名单构造为
limit_words[\0\]=''
同样是能达到替换效果的
extract()
功能:从数组中将变量导入到当前的符号表
定义:extract ( array
&$array
, int$flags
=EXTR_OVERWRITE
, string$prefix
= “” ) : int解释:就是把一个数组注册成变量。其中
$flags
很重要:默认EXTR_OVERWRITE
:如果有冲突,覆盖已有的变量; 但是可以选择EXTR_SKIP
:如果有冲突,不覆盖已有的变量。 而prefix
仅在flags
的值是EXTR_PREFIX_SAME
,EXTR_PREFIX_ALL
,EXTR_PREFIX_INVALID
或EXTR_PREFIX_IF_EXISTS
时需要。如果附加了前缀后的结果不是合法的变量名,将不会导入到符号表中。前缀和数组键名之间会自动加上一个下划线。
与parse_str()的区别:
extract是将数组中元素分解,执行后数组的key值作为变量名,数组的value赋值给对应Key的变量,这样可以直接通过Key变量去访问,不用数组加key去访问。parse_str是根据"="来分解字符串,主要用于对url参数的解析。extract 是从数组创建变量。parse_str 是将url参数串转换成数组。功能不一样,使用对象也不一样。
手册里面也有对开发者的提示:
htmlentities()
功能:将字符转换为 HTML 转义字符
定义:htmlentities ( string
$string
, int$flags
= ENT_COMPAT | ENT_HTML401 , string$encoding
= ini_get(“default_charset”) , bool$double_encode
=true
) : string解释:关键的是
$flags
,ENT_COMPAT 会转换双引号,不转换单引号。 ENT_QUOTES 既转换双引号也转换单引号。 ENT_NOQUOTES 单/双引号都不转换
审计的时候注意flags的值。
CURL传输选项CURLOPT_POSTFIELDS
根据文档介绍:
存在这样一种情况,示例代码如下:
$url = $_GET['url'];
$username = $_GET['username'];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch,CURLOPT_SAFE_UPLOAD,0);
curl_setopt($ch,CURLOPT_POSTFIELDS,$data);
$res = curl_exec($ch);
利用示例:
算是一种SSRF任意文件读取的效果。如果目标做了一些过滤,比如过滤了常用到的伪协议file://
,还可以通过这种外部监听的方式,把文件内容发送到自己的vps上。利用条件就是
- 设置CURLOPT_POSTFIELDS
- 设置CURLOPT_SAFE_UPLOAD为false
而CURLOPT_SAFE_UPLOAD是PHP 5.5.0 中添加,默认值 FALSE。 PHP 5.6.0 改默认值为 TRUE。. PHP 7 删除了此选项, 必须使用 CURLFile interface 来上传文件
注:curl_setopt( resource $ch, int $option, mixed $value) : bool 第二个参数是int型,这意味着所有的选项实际上都是PHP内部定义的一个整型值,比如CURLOPT_URL实际上是
10002,所有选项对应的数字是多少:这里有一个网站可以查
file_exists的bug?or机制?
2022-08-19
$path2 = "C:\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\a\\..\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\aaa.txt";
if(file_exists($path2)){
echo "yes";
}else{
echo "No";
}
$path = "C:\a\b\c\d/e/f/\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\..\\..\\..\\..\\..\\..\\..\\aaa.txt";
if(file_exists($path)){
echo "yes";
}
如上两段代码,当你再增加反斜杠的话,file_exists就会返回false了。貌似和整体路径中的穿越\a\b\..\c\
有关系,构造不同的目录穿越,使得file_exists返回true的反斜杠的最大数量(一个偶数)也不一样,但一定有一个最大数量。具体原理未知,可能得看内核 C源码。