PHP代码审计中常见的一种技巧:弱类型,英文名叫type Juggling,基本的概念如下:
PHP 8 以前,在使用
==比较或任何有弱类型转换的情况时,字符串都会先被转换成数字,再和数字进行比较。
使用==比较
因此,当一个字符串跟数字进行松散比较时(loose comparison),常常会有意想不到的结果,例如:"1 and 1=1" == 1 ,在PHP中是成立的。
下面是弱类型比较的速查表

图片参考自:https://www.php.net/manual/en/types.comparisons.php
看到标红的了么?"php" == 0居然也是成立的。这是由于,字符串会先被转换成数字,之后再和数字值进行比较。
但事情还更怪:即便是两个字符串相比较,只要它俩看起来都像字符串(如"0x123"这种表示十六进制的表示,或者"0e123"这种科学计数法的表示),PHP也会将其转换成数字再进行比较。因此下面的关系是成立的
- TRUE: "0e12345" == "0e54321"
- TRUE: "0e12345" == "0"
- TRUE: "0xF" == "15" [php 7.2.24 下为False]
下面,我们再进一步:两个字符串,既然有可能在它们本身并不相同的情况下,在松散比较下相等,那么你从它们涉及的0e或0x中想到了什么?没错,e是属于a-f的,可以用来表示16进制,而md5()函数的返回,不就是16进制的字符串么?
所以有以下结论:对于两个字符串,我们可以在它们本身并不相同的情况下,使它们的md5值相等。
下面几个字符串的md5值在松散比较下,都是相等的。
QNKCDZO
0e830400451993494058024219903391
s878926199a
0e545993274517709034328855841020
s155964671a
0e342768416822451524974117254469
此外,还有输入数组让正则匹配出错返回false的方法,也值得一试,当然,不扯远了。
sha1()、md5()函数无法处理数组类型,将报错并返回false

毕竟对于正则返回==0这种的判断,就属于黑名单嘛。白名单才是永远的神!
自动弱类型转换
in_array
in_array(search,array,type)
| 参数 | 描述 |
|---|---|
| search | 必需。规定要在数组搜索的值。 |
| array | 必需。规定要搜索的数组。 |
| type | 可选。如果设置该参数为 true,则检查搜索的数据与数组的值的类型是否相同。 |
默认情况下的_in_array_函数,是存在弱类型转换的问题滴
<?php
$whitelist = array(1, 2, 3); //白名单,只允许查询1、2、3
$id = $_GET['id']; // ?id=1' or 1=1 --
if (in_array($id, $whitelist)) { // 未设置第三参数为True
// ("1' or 1=1 --" == 1) -> TRUE
...
$sql = "select * from users where userid = '". $id ."'";
$r = $db->query($sql);
...
} else {
die("你想搞事");
}
?>
SQL注入!
array_search
array_search(value, array, strict)
技术细节:如果在数组中找到指定的键值,则返回对应的键名,否则返回 FALSE。
| 参数 | 描述 |
|---|---|
| value | 必需。规定需要搜素的键值。 |
| array | 必需。规定被搜索的数组。 |
| strict | 可选。如果该参数被设置为 TRUE,则函数在数组中搜索数据类型和值都一致的元素。可能的值: + true + false - 默认 如果设置为 true,则在数组中检查给定值的类型,数字 5 和字符串 5 是不同的(参见实例 2)。 |
下面这段示例代码,演示了
<?php
$id = "2 and" ;
$whitelist = array(1, 2);
if (array_search($id, $whitelist)) { // 未设置第三参数为True
echo "你通过了";
//SQL注入、命令执行等敏感操作
} else {
die("你想搞事");
}
?>
switch case …
switch进去的参数,也会进行类型转换,例如下面我就让"2 and 1=1;--"进到了case 2的分支
<?php
$o = "2 and 1=1;--";
switch ($o) {
case 1:
echo "fail";
break;
case 2:
echo "success"; //结果输出success;
break;
default:
echo "nothing";
}
?>
strcmp
strcmp() 函数比较两个字符串。
strcmp() 函数是二进制安全的,且对大小写敏感。
**返回值: **
0 - 如果两个字符串相等
<0 - 如果 string1 小于 string2
0 - 如果 string1 大于 string2
<?php //php 7.2.24
$array=[1, 2, 3];
var_dump(strcmp($array, '123')); //NULL
?>
这里的strcmp函数,实际上是将两个变量转换成ascii 然后做数学减法,返回一个int的差值。
也就是说键入'a'和'a'进行比较得到的结果就是0。
也就是说,我们可以让这个函数出错从而使它恒真,绕过函数的检查。
🤡1
下面是“既大于0,又不大于0”的一种现象,要注意的就是第三行,为什么'1e-1000' == 0是成立的
<?php
var_dump(intval('1e-1000') > 0); //大于0 bool(true)
var_dump('1e-1000' == 0); // 等于零 bool(true)
?>
🤡2
这大概是由于第一个is_numeric直接赋值给变量了,并没有经过and,这也间接体现了运算符优先级的重要性。
<?php //php 7.2.24
$f = is_numeric(123) and is_numeric();
var_dump($f); //输出 bool(true)
?>
# 再试试这一手,就能理解了吧
<?php //php 7.2.24
$a = 1 and 0;
$b = (1 and 0);
var_dump($a); //输出 int(1)
var_dump($b); //输出 bool(false)
?>
最佳实践
- 默认情况,统一使用
=== - 哈希比较,使用
hash_equals() - 如果涉及敏感函数的使用,最好进行显式的类型转换,如使用
strcmp()、switch case结构体之前,请用强制类型转换,统一比较的类型,demo如下
(int)"0e23812" === (int)"0e48394832"
- 使用
in_array()、array_search()这几个函数时,请在对应参数位置声明true,采用严格比较
补充知识点
关于HTTP请求
HTTP请求无法直接传入数字,传入的一般只是字符串,也可以传入数组。但有时候可以,比如
利用中间件解析JSON
POST /?a=123 HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
b=456&c[key]=value
利用代码中已有的json_decode()
<?php
...
$arr = json_decode($_GET['param'], true); //当第二参数为 TRUE 时,将返回 array
if ( $arr["key"] == 1 ){
echo $flag;
} else {
die();
}
?>
时序攻击
Timing Attack
通常,字符串比较的实现基于移位匹配,此时只要在匹配中碰到任何不匹配,则直接退出,返回比较结果。
下面这两次比较的时间,是不相等的,这种攻击的原理类似盲注。
"f447b20a7fcbf53a5d5be013ea0b15af" == "f447b20a7fcbf53a5d5be013ea0bXXXX"
"f447b20a7fcbf53a5d5be013ea0b15af" == "f447bXXXXXXXXXXXXXXXXXXXXXXXXXXX"
对于不同的输入,代码循环的次数不一样,导致执行耗时也是不一样的,当然由于现代计算机性能的提升,普通开发者很难体会到耗时的差异。不过这种差别的确是存在的。
而采用hash_equals()就不一样
hash_equals($1, $2)
(PHP 5 >= 5.6.0, PHP 7)
hash_equals — 可防止时序攻击的字符串比较。
- 比较两个字符串,无论它们是否相等,本函数的时间消耗是恒定的。
- 非常重要的一点是,用户提供的字符串必须是第二个参数。