黑魔法

所谓黑魔法,即指在web开发中,一些看似违反直觉,但能实现特殊功能的技巧或语言特性,而这些特性往往依赖php的弱类型、动态特性或隐式转换机制,就是利用函数的逻辑漏洞来进行攻击。

全局变量

如果存在<?php include('flag.php');,可以用下面的方法得到flag

$GLOBALS

php全局变量$GLOBALS引用全局作用域中可用的全部变量,可利用这个特性看flag:

get_defined_vars()

用法:**var_dump(get_defined_vars())**

intval()函数缺陷

intval() 函数用于将其他类型的数据转化为整型数据。

1
intval(mixed $value, int $base = 10): int

$value:需要使用intval()进行转化的数据

$base:指定被转化数据采用的进制(默认为10进制)

字符串解析:

  • 若字符串的首个字符不为数字且不为空格等空白字符,则该字符串转化为零。

  • 若字符串的首个字符不为数字但为空格等空白字符,则尝试读取其余字符,若忽略到数字字符前的所有空白字符,在遇到非数字字符时停止对字符串的读取并将已读取字符串转化为数值。

    即把前面空格去掉在解析

  • 若字符串的 首个字符为数字,则尝试读取其余字符,在遇到非数字字符(除符合科学计数法格式的字符 e 或 E外)时停止对字符串的读取并将已读取字符转化为数值。

科学计数法绕过

所以当遇到下面这些情况时就可用科学计数法绕过:

1
if(intval($num) < 2020 && intval($num + 1) > 2021)
1
2
3
4
5
6
if($num==4476){
die("no no no!");
}
if(intval($num,0)==4476){
echo $flag;
}

虽然科学计数法的数值(字符串)在某些版本(如php7.0.0)无法被**intval()正确解析**,但PHP是默认得到它的,在与数值 1 进行加法运算时,$num 将被 PHP 正确解析。

进制转换绕过

1
2
3
4
5
6
7
8
9
if($num==="4476"){
die("no no no!");
}
if(intval($num,0)===4476){
echo $flag;
}
else{
echo intval($num,0);
}

这种情况,就看他转换为八进制或者十六进制来绕过。

1
2
3
0b?? : 二进制
0??? : 八进制
0X?? : 十六进制

payload:

1
num=0x117c || num=010574

再看下面这种情况:

1
2
3
4
5
6
7
8
9
if($num==4476){
die("no no no!");
}
if(preg_match("/[a-z]/i", $num)){
die("no no no!");
}
if(intval($num,0)==4476){
echo $flag;
}

这里就是正则匹配过滤了字母,所以不能用十六进制了,可以转换为八进制.

1
num=010574

小数点绕过

1
2
3
4
5
6
7
8
9
10
11
12
if($num==="114514"){
die("no no no!");
}
if(preg_match("/[a-z]/i", $num)){ //禁用字母
die("no no no!");
}
if(!strpos($num, "0")){ //禁止0开头
die("no no no!");
}
if(intval($num,0)===114514){
echo $flag;
}

像上诉这个就不能使用进制转换了,那么可以使用传值小数,intval()会帮我们转换为整型,以此达到绕过的目的。

payload:

1
num=114514.114514

preg_match()函数缺陷

preg_match()主要用于执行正则匹配,其基本语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int preg_match(string $pattern,string $subject [, array &$matches [, int $flags = 0 [, int $offset = 0 ]]] )
参数说明:

$pattern: 要搜索的模式,字符串形式。

$subject: 输入字符串。

$matches: 如果提供了参数matches,它将被填充为搜索结果。 $matches[0]将包含完整模式匹配到的文本, $matches[1] 将包含第一个捕获子组匹配到的文本,以此类推。

$flags:flags 可以被设置为以下标记值:

PREG_OFFSET_CAPTURE: 如果传递了这个标记,对于每一个出现的匹配返回时会附加字符串偏移量(相对于目标字符串的)。 注意:这会改变填充到matches参数的数组,使其每个元素成为一个由 第0个元素是匹配到的字符串,第1个元素是该匹配字符串 在目标字符串subject中的偏移量。

offset: 通常,搜索从目标字符串的开始位置开始。可选参数 offset 用于 指定从目标字符串的某个未知开始搜索(单位是字节)。

而php手册告诉我们,该函数的返回值有三种,分别为:

1
2
3
return 1; //如果匹配到
return 0; //如果没匹配到
return false; //匹配失败

安全的写法是使用 === 运算符对返回值进行比较,手册推荐用效率更快的 strpos 函数替代 preg_match 函数

数组绕过

preg_match只能处理字符串,如果传入数组会返回false,不会进入if语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php 
highlight_file(__FILE__);
include("flag.php");
$num[]=1;
if(preg_match("/^[a-zA-Z0-9]+$/",$num)){
die("error");
}
else{
echo $flag;
}
?>

//flag{php_is_not_safe}

%0a换行符绕过

**.不会匹配换行符(\n,\r)**,如:

1
2
3
4
5
6
7
<?php 
highlight_file(__FILE__);
include("flag.php");
if(preg_match('/^.*hello.*$/',$_GET['a'])&&$_GET['a']!=='hello'){
echo $flag;
}
?>

%0a不行

而在非多行模式下(即/i模式下),**$会忽略在句尾的%0a**

1
2
3
4
5
6
7
<?php 
highlight_file(__FILE__);
include("flag.php");
if(preg_match('/^hello$/',$_GET['a'])&&$_GET['a']!=='hello'){
echo $flag;
}
?>

^和$字符用来匹配字符串的开始和结束,要求我们必须是hello

回溯绕过

具体可以参考p牛的博客:https://www.leavesongs.com/PENETRATION/use-pcre-backtrack-limit-to-bypass-restrict.html

1
preg_match('/<\?.*[(`;?>].*/is', $_GET['a']); 

如果我们输入phpinfo();//aaaaa,由于**.*可以匹配任意字符,此时会进入贪婪模式,会将phpinfo();//aaaaa所有字符进行匹配**

PHP为了防止正则表达式的拒绝服务攻击(reDOS),给pcre设定了一个回溯次数上限pcre.backtrack_limit如果回溯次数大于1000000次时返回False:

1
2
$a = 'hello world'+'h'*1000000
preg_match("/hello.*world/is",$a) == False

trim()及is_numberic()函数缺陷

trim函数会过滤空格以及\n\r\t\v\x00,但不会过滤\f(%0c)(换行符)

1
2
3
4
5
6
<?php 
#highlight_file(__FILE__);
include("flag.php");
$a=" \n\r\t\v\x00 a \f";
var_dump(trim($a)); //a \f
?>

is_numberic用于检测是否是数字或数字字符串,而当数字前面有空格或\n\t\r\f\v等换行符时会被认为是数字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php 
$a="\n1";
$b="\t1";
$c="\f1";
$d="\r1";
$e="\v1";
$f=" 1";

var_dump(is_numeric($a));//bool(true)
var_dump(is_numeric($b));//bool(true)
var_dump(is_numeric($c));//bool(true)
var_dump(is_numeric($d));//bool(true)
var_dump(is_numeric($e));//bool(true)
var_dump(is_numeric($f));//bool(true)

所以**\f能突破这两个函数的限制**,下面例题:

1
2
3
4
5
6
7
8
9
<?php
highlight_file(__FILE__);
include("flag.php");
$num="\f36";
if(is_numeric($num) and $num!=='36' and trim($num)!=='36'){
echo $flag;
}
?>
//flag{php_is_not_safe}

strcmp函数缺陷

1
int strcmp ( string $str1 , string $str2 )

参数 str1第一个字符串。str2第二个字符串。如果 str1 小于 str2 返回 < 0; 如果 str1 大于 str2 返回 > 0;如果两者相等,返回 0。

php5.3之前,当这个函数接受到了不符合的类型,这个函数将发生错误,显示了报错的警告信息后,将return 0。

ereg(),eregi()函数缺陷

int ereg(string pattern, string originalstring, [array regs])函数用指定的模式搜索一个字符串中指定的字符串,如果匹配成功返回1,否则返回0,且搜索对大小写敏感

ereg()函数存在NULL截断漏洞,当传入的字符串包含%00时,只有**%00前的字符串会传入函数并执行,而后半部分不会传入函数判断。因此可以使用%00截断,连接非法字符串,从而绕过函数**

1
2
3
4
5
6
7
8
9
10
11
12
<?php
highlight_file(__FILE__);
error_reporting(0);
include("flag.php");
if (ereg ("^[a-zA-Z]+$", $_GET['c'])===FALSE) {
die('error');
}
//只有36d的人才能看到flag
if(intval(strrev($_GET['c']))==0x36d){
echo $flag;
}
?>

十六进制数 0x36d即十进制的 877,反转后是778

所以payload:

1
?c=a%00778

strlen()函数缺陷

strlen()函数用于求字符串的长度,可以用科学计数法来绕过:

1
2
3
4
5
6
7
8
9
<?php
@$a = $_GET['num'];
if(strlen($a)<4 && $a>10000){
echo $flag;
}
else{
echo "is too small";
}
?>

payload:

1
?num=1e9

strpos()函数缺陷

strpos()函数用于查找字符串在另一字符串种第一次出现的位置(区分大小写)。

1
2
3
4
5
6
7
8
<?php
if(strpos($_GET['a'],'abc') == 0 ){
echo '123';
}
else{
echo '456';
}
?>

这里传入abc会打印123,但如果传入一个数组或不传入数据或传入没有abc的值都会打印123,这时因为该函数只解析string类型的字符串,给它个数组就不知到如何解析,就会返回null,null==0,而当不传入数据或传入数据不包含abc时,就会由于找不到值而返回null。

in_array()函数缺陷

in_array()函数用来判断字符串是否存在与数组中,但是在判断的时候,会进行类型强制转换,就会出现数字比较的情况。

1
2
3
4
5
<?php
$array=[0,1,2,'3'];
var_dump(in_array('abc', $array)); // true
var_dump(in_array('1bc', $array)); // true
?>

字符串变量解析特性

PHP需要将所有参数转换为有效的变量名,因此在解析查询字符串变量时,它会做两件事:

  • 删除空白符
  • 将某些字符[+.等等转换为下划线(包括空格)

例如:

User input Decoded PHP variable name
%20foo_bar%00 foo_bar foo_bar
foo%20bar%00 foo bar foo_bar
foo%5bbar foo[bar foo_bar

1
2
3
4
5
6
7
8
9
<?php
highlight_file(__FILE__);
error_reporting(0);
include("flag.php");
$a=$_GET['a_b_c_d'];
if($a=="hello"){
echo $flag;
}
?>

需要注意的是,当php版本小于8时,GET传参请求的参数名含有非法字符.,会被转为_,但如果参数名前面有[,这个[会被直接转为_,但如果后面有.,这个.就不会被转为_.

如果有一个 WAF 规定某个参数的值必须是数字,不能包含字母时,我们就可以利用这个绕过。

如上面**[RoarCTF 2019]Easy Calc要求我们num变量必须为数字,此时我们可通过?%20num=phinfo()绕过**

参考

https://nivi4.notion.site/PHP-8b2e93df6683422f815651f12d1b97c7

https://www.cnblogs.com/murkuo/p/15388795.html

https://www.cnblogs.com/gxngxngxn/p/17410173.html