web安全代码基础-PHP(函数-特性安全)
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、strcasecmp、strncmp、strnatcmp 低版本 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数字 / 非空字符串→ truefalse == 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更多推荐
所有评论(0)