PHP弱类型安全

鉴于目前PHP是世界上最好的语言,PHP本身的问题也可以算作是web安全的一个方面。在PHP中的特性就是弱类型,以及内置函数对于传入参数的松散处理。

PHP弱类型简介

强类型是两个不同类型的变量不能用同一块内存存储
弱类型是两个不同类型的变量可以用同一块内存存储
PHP是弱类型语言

<?php
$var = 1;
$var = array();
$var = "string";
?>

弱类型的语言对变量的数据类型没有限制,你可以在任何地时候将变量赋值给任意的其他类型的变量,同时变量也可以转换成任意地其他类型的数据。

比如 在$a == $b的比较中

$a = null; $b = false; //为真
$a = ''; $b = 0; //同样为真

然而,php内核的开发者原本是想让程序员借由这种不需要声明的体系,更加高效的开发,所以在几乎所有内置函数以及基本结构中使用了很多松散的比较和转换,防止程序中的变量因为程序员的不规范而频繁的报错,然而这却带来了安全问题。

php内核之zval结构

在PHP中声明的变量,在ZE中都是用结构体zval来保存的
php内核中弱类型的封装

typedef struct _zval_struct zval;  

struct _zval_struct {  
    /* Variable information */  
    zvalue_value value;     /* value */  
    zend_uint refcount__gc;  
    zend_uchar type;    /* active type */  
    zend_uchar is_ref__gc;  
};  

typedef union _zvalue_value {  
    long lval;  /* long value */  
    double dval;    /* double value */  
    struct {  
        char *val;  
        int len;  
    } str;  
    HashTable *ht;  /* hash table value */  
    zend_object_value obj;  
} zvalue_value;

其中php通过type判断变量类型 存入value。

类型转换问题

zval.type决定了存储到zval.value的类型。当源代码进行一些未限制类型的比较,或数学运算的时候,可能会导致zval.type的改变,同时影响zval.value的内容改变。

比较操作符

=== 在进行比较的时候,会先判断两种字符串的类型是否相等,再比较
== 在进行比较的时候,会先将字符串类型转化成相同,再比较
如果比较一个数字和字符串或者比较涉及到数字内容的字符串,则字符串会被转换成数值并且比较按照数值来进行
比较简单,不过多举例。

Hash比较缺陷
"0e132456789"=="0e7124511451155" //true
"0e123456abc"=="0e1dddada"  //false
"0e1abc"=="0"     //true

在进行比较运算时,如果遇到了0e\d+这种字符串,就会将这种字符串解析为科学计数法。如果不满足0e\d+这种模式,就会当作字符串进行比较,所以不会相等。

十六进制转换
"0x1e240"=="123456"     //true
"0x1e240"==123456       //true
"0x1e240"=="1e240"      //false

当其中的一个字符串是0x开头的时候,PHP会将此字符串解析成为十进制然后再进行比较。

类型转换

<?php
$test=1 + "10.5"; // $test=11.5(float)
$test=1+"-1.3e3"; //$test=-1299(float)
$test=1+"bob-1.3e3";//$test=1(int)
$test=1+"2admin";//$test=3(int)
$test=1+"admin2";//$test=1(int)
?>

PHP手册:当一个字符串欸当作一个数值来取值,其结果和类型如下:如果该字符串没有包含'.','e','E'并且其数值值在整形的范围之内该字符串被当作int来取值,其他所有情况下都被作为float来取值,该字符串的开始部分决定了它的值,如果该字符串以合法的数值开始,则使用该数值,否则其值为0。

intval()函数:intval()转换的时候,会将从字符串的开始进行转换知道遇到一个非数字的字符。即使出现无法转换的字符串,intval()不会报错而是返回0。

var_dump(intval('2'))   //2
var_dump(intval('3abcd'))   //3
var_dump(intval('abcd'))    //0

bool 欺骗

当存在json_decode和unserialize的时候,部分结构会被解释成bool类型。
json_decode示例代码:

$json_str = '{"user":true,"pass":true}';
$data = json_decode($json_str,true);
if ($data['user'] == 'admin' && $data['pass']=='secirity')
{
    print_r('logined in as bool'."\n");
}

运行结果:

logined in as bool

unserialize示例代码:

$unserialize_str = 'a:2:{s:4:"user";b:1;s:4:"pass";b:1;}';
$data_unserialize = unserialize($unserialize_str);
var_dump($data_unserialize['user']);
if ($data_unserialize['user'] == 'admin' && $data_unserialize['pass']=='secirity')
{
    print_r('logined in unserialize'."\n");
}

运行结果:

bool(true) 
logined in unserialize

数字转换问题

$user_id = ($_POST['user_id']);
if ($user_id == "1")
{
    $user_id = (int)($user_id);
    #$user_id = intval($user_id);
    $qry = "SELECT * FROM `users` WHERE user_id='$user_id';";
}
$result = mysql_query($qry) or die('<pre>' . mysql_error() . '</pre>' );

可以让user_id=0.999999999999999999999即可以绕过判断,并且最终执行查询的结果却是user_id=0的数据。

<?php 
var_dump("1" == 0.9999999999999999);//false
var_dump("1" == 0.99999999999999999);//true
?>

int和intval在转换数字的时候都是就低的

print((int)'0.9999999999999');//0
print((int)'1.1');//1

intval还有个尽力模式,就是转换所有数字直到遇到非数字为止,如果采用:

if (intval($qq) === '123456')
{
    $db->query("select * from user where qq = $qq")
}

可以传入123456 union select version()进行注入。

PHP5.4.4 特殊情况

这个版本的php的一个修改导致两个数字型字符溢出导致比较相等

$ php -r 'var_dump("61529519452809720693702583126814" == "61529519452809720000000000000000");'
bool(true)

内置函数参数的松散性

内置函数的松散性说的是,调用函数时给函数传递函数无法接受的参数类型。

md5()

$array1[] = array("foo" => "bar", "bar" => "foo",);
$array2 = array("foo", "bar", "hello", "world");

var_dump(md5($array1)==var_dump($array2));  //true

PHP手册中的md5()函数的描述是string md5 ( string $str [, bool $raw_output = false ] ),md5()中的需要是一个string类型的参数。但是当你传递一个array时,md5()不会报错,只是会无法正确地求出array的md5值,并且返回NULL。这样就会导致任意2个array的md5值都会相等。

substr、sha1

如果传入给substr(),sha1的参数是数组则返回NULL

<?php
$cc=[];
var_dump(substr($cc, 123));
var_dump(sha1($cc));
var_dump(substr($cc, 123) === sha1($cc));
?>

返回

NULL
NULL
bool(true) 

strcmp()

strcmp()函数在PHP官方手册中的描述是int strcmp ( string $str1 , string $str2 ),需要给strcmp()传递2个string类型的参数。如果str1小于str2,返回-1,相等返回0,否则返回1。strcmp函数比较字符串的本质是将两个变量转换为ascii,然后进行减法运算,然后根据运算结果来决定返回值。
如果传入给出strcmp()的参数是数组则返回NULL

$array=[1,2,3];
var_dump(strcmp($array,'123')); //null,在某种意义上null也就是相当于false。

switch()

如果switch是数字类型的case的判断时,switch会将其中的参数转换为int类型。如下:

$i ="2abc";
switch ($i) {
case 0:
case 1:
case 2:
    echo "i is less than 3 but not negative";
    break;
case 3:
    echo "i is 3";
}

这个时候程序输出的是i is less than 3 but not negative,是由于switch()函数将$i进行了类型转换,转换结果为2。

in_array()

tips:Array转换整型int/浮点型float会返回元素个数;转换bool返回Array中是否有元素;转换成string返回'Array',并抛出warning。

在PHP手册中,in_array()函数的解释是bool in_array ( mixed $needle , array $haystack [, bool $strict = FALSE ] ),如果strict参数没有提供,那么in_array就会使用松散比较来判断$needle是否在$haystack中。当strince的值为true时,in_array()会比较needls的类型和haystack中的类型是否相同

$array=[0,1,2,'3'];
var_dump(in_array('abc', $array));  //true
var_dump(in_array('1bc', $array));  //true

’abc’会转换为0,’1bc’转换为1。array_search()与in_array()也是一样的问题。

Referer

PHP弱类型安全问题总结
浅谈PHP弱类型安全
php比较操作符的安全问题
PHP挑战通关攻略