PHP的弱比较

== 弱相等(松散比较):先把两边值强制转换成相同类型,再比较值是否相等,会自动类型转换,容易出现反直觉结果。

=== 全等(严格比较):先判断类型是否完全一致,类型不一样直接返回 false;类型相同再比较值,不会自动转类型,推荐日常使用。

<?php
// 1. 数字和字符串对比
$num = 123;
$str = "123";

echo "== 弱比较:" . var_export($num == $str, true) . PHP_EOL;   // true 自动转成数字比较
echo "=== 强比较:" . var_export($num === $str, true) . PHP_EOL; // false 类型不同 int vs string
echo PHP_EOL;

// 2. 0 和字符串、布尔值
echo '0 == "abc":' . var_export(0 == "abc", true) . PHP_EOL;     // true 字符串转数字是0
echo '0 === "abc":' . var_export(0 === "abc", true) . PHP_EOL;   // false
echo PHP_EOL;

// 3. true/false 和数字
echo 'true == 1:' . var_export(true == 1, true) . PHP_EOL;       // true
echo 'true === 1:' . var_export(true === 1, true) . PHP_EOL;     // false
echo 'false == 0:' . var_export(false == 0, true) . PHP_EOL;     // true
echo 'false === 0:' . var_export(false === 0, true) . PHP_EOL;   // false
echo PHP_EOL;

// 4. null、false、空字符串
echo 'null == false:' . var_export(null == false, true) . PHP_EOL;       // false
echo 'null == "":' . var_export(null == "", true) . PHP_EOL;             // false
echo 'null == 0:' . var_export(null == 0, true) . PHP_EOL;               // false
echo 'null === false:' . var_export(null === false, true) . PHP_EOL;     // false
echo PHP_EOL;

// 5. 空数组、null、false
$emptyArr = [];
echo '[] == false:' . var_export($emptyArr == false, true) . PHP_EOL;    // true
echo '[] === false:' . var_export($emptyArr === false, true) . PHP_EOL;  // false
echo '[] == null:' . var_export($emptyArr == null, true) . PHP_EOL;      // true
echo '[] === null:' . var_export($emptyArr === null, true) . PHP_EOL;    // false
echo PHP_EOL;

// 6. 字符串数字和数字带字母
echo '123 == "123abc":' . var_export(123 == "123abc", true) . PHP_EOL; // true,字符串截取数字部分123
echo '123 === "123abc":' . var_export(123 === "123abc", true) . PHP_EOL; // false
?>

补:var_export() 函数用于输出或返回一个变量,以字符串形式表示。

mixed var_export ( mixed $expression [, bool $return ] )
//$expression: 你要输出的变量。
//$return: 可选,如果设置为 TRUE,该函数不会执行输出结果,而且将输出结果返回给一个变量。

另外:布尔值 true 在参与数值比较时,会转换为整数 1,false会转换为整数0。

特殊规则:当比较 null 与数字时,PHP 认为 null 不等于任何数字(包括 0);null 只与 null 和未定义的变量相等。

实战实例

场景 1:验证码 / 密码校验漏洞

<?php
$input = "123abc";
$code = 123;

if ($input == $code) {
    echo "弱比较通过,校验失效!"; // 会输出,不安全
}

if ($input === $code) {
    echo "强比较通过"; // 不会执行,安全
}
?>

场景2:判断是否等于字符串0

<?php
$val = 0;   //这里的0为数字型,下面的0为字符型
if ($val == "0") {
    echo "弱比较相等"; // true
}
if ($val === "0") {
    echo "强比较相等"; // false
}
?>
修复建议

1、业务判断一律用 ===!==,避免隐式类型转换带来逻辑 bug;

2、仅在你明确知道要忽略类型时才使用 ==,极少场景;

3、判断null、布尔、数字、字符串混合比较时,严禁弱比较。

补充

1、!=:弱不等(同样自动转类型)

2、!==:严格不等(推荐)

var_dump(123 != "123");  // false
var_dump(123 !== "123"); // true
MD5对比缺陷

进行hash加密出来的字符串如存在0e开头进行弱比较的话会直接判定为true

1. 魔术哈希 0e 漏洞演示(最经典业务漏洞)

<?php
// 已知明文,md5后都是 0e 开头魔术哈希
$str1 = '240610708'; //md5 = 0e462097431906509019562988736854
$str2 = 'QNKCDZO';  //md5 = 0e830400458393077759938481653614

$md5_1 = md5($str1);
$md5_2 = md5($str2);

echo "str1={$str1} md5 = {$md5_1}\n";
echo "str2={$str2} md5 = {$md5_2}\n\n";

// 弱比较 == :两个完全不同的哈希判定相等,漏洞出现
if ($md5_1 == $md5_2) {
    echo "【危险 == 弱比较】两个不同md5判定相等,验证绕过!\n";
} else {
    echo "== 判断不相等\n";
}

// 严格比较 === :类型一致且字符完全匹配才相等,安全
if ($md5_1 === $md5_2) {
    echo "=== 判断相等\n";
} else {
    echo "【安全 === 强比较】md5字符串内容不同,判定不相等\n";
}

// 再和数字0对比
echo "\nmd5_1 == 0:" . var_export($md5_1 == 0, true) . "\n";  // true
echo "md5_1 === 0:" . var_export($md5_1 === 0, true) . "\n";// false
?>
//md5_1 == 0:true
//md5_1 === 0:false

2. MD5 碰撞缺陷演示(不同明文相同 md5)

<?php
// 一对经典MD5碰撞原文
$a1 = "\x4d\xdc\x8b\x7a\x0c\x03\x4b\xa8\x3e\x48\x9f\xcf\x13\x8a\x31\x57";
//md5: 1faa01cd2e51d69cfc49a1ad938a2bc6
$a2 = "\x4d\xdc\x8b\x7a\x0c\x03\x4b\xa8\x3e\x48\x9f\xcf\x13\x8a\x31\x58";
//md5: 8d3e28fb7fa02974830f8fa3afd88d0b

$md5_a = md5($a1);
$md5_b = md5($a2);

echo "字符串1 md5: {$md5_a}\n";
echo "字符串2 md5: {$md5_b}\n";

if ($md5_a === $md5_b) {
    echo "缺陷:两段完全不同内容,MD5值完全一样(MD5碰撞)\n";
}
?>

还有一些0e开头的字符串:

QNKCDZO
0e830400451993494058024219903391
240610708
0e462097431906509019562988736854
s878926199a
0e545993274517709034328855841020
s155964671a
0e342768416822451524974117254469
s214587387a
0e848240448830537924465865611904 
s214587387a
0e848240448830537924465865611904
s878926199a
0e545993274517709034328855841020
s1091221200a
0e940624217856561557816327384675
s1885207154a
0e509367213418206700842008763514
函数strcmp类型比较缺陷

核心漏洞原理:

1、strcmp(string $str1, string $str2) 要求两个参数都必须是字符串

2、低版本 PHP(PHP5、早期 PHP7)中,如果传入数组strcmp,函数会报错警告,但不会终止程序,返回值等于 0

3、strcmp(a,b) === 0 代表两个字符串相等,攻击者传入数组,直接绕过等值判断;

4、配合表单 / GET/POST 传参(传入数组 ?pass[]=1)即可触发漏洞。

<?php
// 预设正确密码字符串
$real_pwd = "admin123";

// 场景1:正常传字符串,strcmp正常判断
$input_str = "admin123";
$res1 = strcmp($input_str, $real_pwd);
echo "【正常传入正确字符串】strcmp返回:{$res1}\n";
if ($res1 == 0) {
    echo "正常校验:密码正确\n\n";
}

$input_wrong = "123456";
$res2 = strcmp($input_wrong, $real_pwd);
echo "【正常传入错误字符串】strcmp返回:{$res2}\n";
if ($res2 == 0) {
    echo "密码正确\n";
} else {
    echo "正常校验:密码错误\n\n";
}

// 场景2:漏洞关键点——传入数组,触发strcmp缺陷绕过
$input_arr = []; // 攻击者传入数组,如 ?pwd[]=xxx
$res3 = strcmp($input_arr, $real_pwd);
echo "【恶意传入数组】strcmp返回:{$res3}\n";
if ($res3 == 0) {
    echo "漏洞触发!数组绕过密码校验,直接登录成功\n";
} else {
    echo "密码错误\n";
}
?>

运行结果和效果:

【正常传入正确字符串】strcmp返回:0
正常校验:密码正确

【正常传入错误字符串】strcmp返回:-1
正常校验:密码错误

Warning: strcmp() expects parameter 1 to be string, array given in ...
【恶意传入数组】strcmp返回:0
漏洞触发!数组绕过密码校验,直接登录成功
实战实例
<?php
// 模拟后端逻辑,从GET获取pwd参数
$admin_pass = "secret666";
$user_input = $_GET['pwd']; // 攻击者访问:shturl.cc/Fr[]=1

if (strcmp($user_input, $admin_pass) == 0) {
    echo "欢迎管理员,登录成功(被数组绕过)";
} else {
    echo "密码错误";
}

攻击payload:

?pwd[]=abc
//$_GET['pwd'] 得到数组,strcmp 报错返回 0,判断成立,直接登录。
修复建议

1、对比前强制校验参数类型,必须是字符串:

if (!is_string($user_input)) {
    die("参数非法");
}
if (strcmp($user_input, $admin_pass) === 0) {
    // 校验通过
}

2、升级 PHP 到高版本(PHP7.4+ 传入数组会直接抛出致命错误终止程序);

3、密码校验使用 password_hash + password_verify,放弃明文字符串比对;

4、使用严格全等 === 替代字符串比较函数。

补充

同类字符串比较函数同样有坑:

1、strcasecmpstrncmpstrnatcmp 低版本 PHP 传入数组都会返回 0;

2、switch 弱类型匹配、== MD5 0e 魔术哈希、strcmp 数组绕过是 PHP 审计三大经典弱类型漏洞。

函数Bool类型比较缺陷

漏洞核心原理:

  • json_decode()unserialize() 解析数据时,会原生生成 true/false 布尔值;
  • 业务中常使用 弱比较 ==strcmp、等值判断校验数据;布尔值和数字 / 字符串弱比较会发生隐式类型转换,逻辑被绕过;
  • 两种典型场景:JSON 里 true/false 解析为 PHP bool;反序列化字符串 b:1; / b:0; 得到 true / false
  • 弱比较规则:true == 任意非0数字 / 非空字符串 → true
    • false == 0 / "" / [] / null → true
json_decode 布尔解析缺陷演示
<?php
// 业务需求:要求传入数字 123 才能通过校验
$target = 123;

// 正常传入数字JSON
$json_num = '123';
$data1 = json_decode($json_num);
var_dump($data1); // int(123)
echo "数字123 == 123:" . var_export($data1 == $target, true) . "\n\n";

// 恶意传入布尔true JSON
$json_true = 'true';
$data2 = json_decode($json_true);
var_dump($data2); // bool(true)
// 弱比较缺陷:true 和任何非零数字都相等
echo "布尔true == 123:" . var_export($data2 == $target, true) . "\n";

if ($data2 == $target) {
    echo "漏洞触发:传入true直接绕过数字校验!\n\n";
}

// 严格比较才安全
echo "布尔true === 123:" . var_export($data2 === $target, true) . "\n\n";

// 场景2:false绕过0、空字符串校验
$target_zero = 0;
$json_false = 'false';
$data3 = json_decode($json_false);
var_dump($data3); // bool(false)
echo "false == 0:" . var_export($data3 == $target_zero, true) . "\n";
if ($data3 == $target_zero) {
    echo "漏洞:false绕过0值校验\n";
}
?>

输出结果:

int(123)
数字123 == 123:true

bool(true)
布尔true == 123:true
漏洞触发:传入true直接绕过数字校验!

布尔true === 123:false

bool(false)
false == 0:true
漏洞:false绕过0值校验

实战业务漏洞示例(接口参数校验):

<?php
// 接口接收json参数
$json_input = $_POST['data'];
$allow_id = 999; // 仅允许id=999操作
$obj = json_decode($json_input);

// 危险弱比较
if ($obj->id == $allow_id) {
    echo "权限通过,执行敏感操作";
}

攻击payload:

攻击传入 JSON:{"id":true}
true == 999 成立,直接越权。
unserialize 布尔反序列化缺陷演示
<?php
$secret = "admin666";

// 1. 反序列化得到布尔true
$ser_true = 'b:1;';
$val1 = unserialize($ser_true);
var_dump($val1); // bool(true)

// 弱比较:true == 任意非空字符串
echo "true == 'admin666':" . var_export($val1 == $secret, true) . "\n";
if ($val1 == $secret) {
    echo "漏洞:布尔true绕过密码校验\n\n";
}

// 2. 布尔false对比空、0
$ser_false = 'b:0;';
$val2 = unserialize($ser_false);
var_dump($val2); // bool(false)
echo "false == '':" . var_export($val2 == "", true) . "\n";
echo "false == 0:" . var_export($val2 == 0, true) . "\n";

// 3. 搭配strcmp双重漏洞
echo "\nstrcmp(bool(true), 'admin666') 返回:" . strcmp($val1, $secret) . "\n";
if (strcmp($val1, $secret) == 0) {
    echo "双重缺陷:布尔传入strcmp返回0,校验绕过";
}
?>
//序列化布尔格式:
//b:1; → true
//b:0; → false

输出结果:

bool(true)
true == 'admin666':true
漏洞:布尔true绕过密码校验

bool(false)
false == '':true
false == 0:true

Warning: strcmp() expects parameter 1 to be string, bool given
strcmp(bool(true), 'admin666') 返回:0
双重缺陷:布尔传入strcmp返回0,校验绕过
修复建议
  • 全部使用严格全等 === 判断,杜绝隐式类型转换;
  • 解析后先校验数据类型,预期数字就用 is_int(),预期字符串用 is_string()
$obj = json_decode($json_input);
if (!is_int($obj->id) || $obj->id !== 999) {
    die("参数非法");
}
  • 禁止将未经类型校验的数据直接传入 strcmp/strcasecmp 等字符串函数;
  • 尽量避免使用 unserialize 反序列化用户可控输入,改用 json 存储数据;
  • 密码、敏感标识对比统一使用 password_verify 或完整字符串严格匹配。
函数switch 类型比较缺陷

漏洞核心原理:

1、switch 的匹配规则:会把 switch 括号里的变量强制转为 int,再和每个 case 值对比(弱类型匹配,不是严格全等 ===);

2、如果传入字符串、布尔、数组,会自动转整型,极易出现预期外匹配;

3、典型场景:case 写数字,传入带数字开头的字符串、字符串数字、true/false,都会错误命中 case。

漏洞示例

示例 1:数字 case 匹配数字开头字符串(最常见绕过)

<?php
$id = "2abc"; // 可控输入,用户传入字符串

switch ($id) {
    case 1:
        echo "匹配到 1";
        break;
    case 2:
        echo "漏洞触发:字符串'2abc'命中 case 2";
        break;
    case 3:
        echo "匹配到 3";
        break;
    default:
        echo "无匹配";
}

输出结果:

漏洞触发:字符串'2abc'命中 case 2
原因:"2abc" 转 int 等于 2,和 case 2 匹配成功。

示例 2:true 匹配所有非 0 数字 case

<?php
$val = true;

switch ($val) {
    case 0:
        echo "匹配0";
        break;
    case 999:
        echo "漏洞:true 命中任意非0数字case(999)";
        break;
    default:
        echo "默认分支";
}

输出结果:

漏洞:true 命中任意非0数字case(999)
原理:true 强转 int = 1,所有非 0 数字 case 都会和 true 弱相等。

示例 3:false 匹配 case 0

<?php
$val = false;

switch ($val) {
    case 0:
        echo "漏洞:false 转int=0,命中case 0";
        break;
    case 100:
        echo "匹配100";
        break;
}

示例 4:实战漏洞场景(后台权限判断)

<?php
// 业务逻辑:只有id=888才允许管理员操作
$user_input = $_GET['uid']; // 攻击者传入 ?uid=888admin

switch ($user_input) {
    case 888:
        echo "管理员权限,删除全部数据";
        break;
    default:
        echo "普通用户,无权限";
}

攻击payload:

?uid=888test
直接命中 case 888,越权操作。

示例 5:纯数字字符串也会误匹配

<?php
$num_str = "666";
switch ($num_str) {
    case 666:
        echo "字符串'666'匹配数字666,弱类型转换生效";
}
修复建议

方案 1:case 统一写字符串,强类型匹配

<?php
$id = "2abc";
switch ((string)$id) { // 统一转字符串
    case "1":
        echo "匹配1";
        break;
    case "2":
        echo "不会命中";
        break;
}

方案 2:放弃 switch,用 if + 严格全等 ===(推荐)

<?php
$user_input = $_GET['uid'];
$target = 888;

// 先校验类型再严格对比
if (is_int($user_input) && $user_input === $target) {
    echo "管理员权限";
} else {
    echo "无权限";
}
函数in_array数组比较缺陷

in_array($needle, $haystack, $strict)

  • 不传第 3 个参数 $strict=true:使用 == 弱比较,自动类型转换,存在绕过漏洞
  • 传入 $strict=true:使用 === 严格比较,值 + 类型完全一致才匹配

array_search 规则完全一样,第三个参数控制是否严格匹配。

漏洞示例

示例 1:数字数组匹配数字开头字符串(最常见漏洞)

<?php
$arr = [1, 2, 3, 999];

$input = "2abc";

// 不开启严格匹配,弱比较
var_dump(in_array($input, $arr));        // bool(true) 误匹配
var_dump(array_search($input, $arr));   // int(1) 返回下标,判定存在

echo PHP_EOL;

// 开启严格匹配 true,安全
var_dump(in_array($input, $arr, true));       // bool(false)
var_dump(array_search($input, $arr, true));   // bool(false)
原因:字符串 "2abc" 转数字为 2,数组里存在 2,弱比较判定相等。

示例 2:true 匹配所有非 0 数字

<?php
$allow = [10, 20, 30];
$val = true;

var_dump(in_array($val, $allow));      // true
var_dump(in_array($val, $allow, true));// false

示例 3:false 匹配 0、空字符串

<?php
$test = [0, "", "hello"];
$input = false;

var_dump(in_array($input, $test));      // true
var_dump(in_array($input, $test, true));// false

示例 4:0 匹配任意纯字母字符串

<?php
$list = [0, 5, 6];
$str = "admin";

var_dump(in_array($str, $list));      // true
var_dump(in_array($str, $list, true));// false

实战演示:

<?php
// 白名单:仅允许指定数字ID访问后台
$white_uid = [1001, 2002, 3003];
$user_id = $_GET['uid']; // 攻击者传入 ?uid=1001test

// 危险代码:未加第三个参数,弱比较
if (in_array($user_id, $white_uid)) {
    echo "白名单校验通过,进入后台(漏洞绕过)";
}

echo PHP_EOL;

// 安全代码:开启严格匹配
if (in_array($user_id, $white_uid, true)) {
    echo "白名单校验通过";
} else {
    echo "非法用户,拒绝访问";
}

示例 5:array_search 漏洞利用(获取下标做判断)

<?php
$ids = [888, 999, 666];
$input = "888xxx";

$res = array_search($input, $ids);
if ($res !== false) {
    echo "找到匹配,越权操作";
}
修复建议

1、只要业务需要精确匹配,in_array / array_search 必须带上第三个参数 true

in_array($needle, $arr, true);
array_search($needle, $arr, true);

2、提前校验参数类型,预期数字用 is_int(),预期字符串用 is_string()

3、涉及权限、ID、验证码、白名单校验,禁止使用松散比较。

===数组比较缺陷

漏洞核心原理:

1、md5(数组) / sha1(数组) 传入数组参数会触发警告,函数返回 NULL

2、两个变量都经过 md5() 处理且都传入数组,结果都等于 NULL

3、NULL === NULL 严格比较结果为 true,直接绕过全等判断;

4、哪怕用的是最严格的 ===,依然能绕过校验。

漏洞示例

基础演示:md5 传数组返回 NULL

<?php
// 1. 正常字符串md5
$str = "123456";
$md5_str = md5($str);
var_dump($md5_str); // string(32) "e10adc3949ba59abbe56e057f20f883e"

echo PHP_EOL;

// 2. 传入数组,md5报错返回NULL
$arr = [1,2,3];
$md5_arr = md5($arr);
var_dump($md5_arr); // NULL
?>
//输出会附带警告:Warning: md5() expects parameter 1 to be string, array given但函数返回值固定是 NULL。

核心漏洞:双数组 md5 后 NULL === NULL 绕过

<?php
// 业务逻辑:要求两个输入md5完全一致(使用严格===)
$input1 = $_GET['a'];
$input2 = $_GET['b'];

$md5_1 = md5($input1);
$md5_2 = md5($input2);

echo "md5(\$input1) = ";
var_dump($md5_1);
echo "md5(\$input2) = ";
var_dump($md5_2);

// 这里用的是严格全等 ===,看似安全
if ($md5_1 === $md5_2) {
    echo "\n【漏洞触发】md5结果严格相等,校验通过!";
} else {
    echo "\n校验失败";
}

攻击payload:

?a[]=test&b[]=abc
修复建议

1、提前校验参数必须为字符串,过滤数组:

$a = $_GET['a'];
$b = $_GET['b'];
if (!is_string($a) || !is_string($b)) {
    die("参数必须为字符串,禁止数组");
}
$md5_1 = md5($a);
$md5_2 = md5($b);
if ($md5_1 === $md5_2) {
    // 业务逻辑
}

2、关闭前端数组传参,接口统一接收字符串;

3、密码校验改用 password_hash() / password_verify,避免单纯哈希对比;

4、自定义判断捕获 NULL 情况,直接拦截。

补充

sha1()同理:sha1() 接收数组同样返回 NULL,同样可以用数组绕过 === 判断:

<?php
$x = $_GET['x'];
$y = $_GET['y'];

if (sha1($x) === sha1($y)) {
    echo "sha1严格比较被数组绕过";
}
//攻击payload:?x[]=a&y[]=b

更多推荐