新手勇闯网络安全 | PHP基础(四)
前三篇掌握了 PHP 基础语法、超全局变量和文件操作,这一篇进入安全核心:危险函数与反序列化漏洞。eval、system、include、unserialize------这些函数本身不是恶意的,但当用户输入未经校验就传入这些函数时,就会触发代码执行、命令注入、文件包含和反序列化漏洞。理解危险函数的原理,才能看懂 WebShell 是怎么来的、反序列化是怎么打穿服务器的。
一、PHP 数据类型与面向对象基础
1.1 八种基本数据类型
PHP 是弱类型语言,变量无需声明类型即可直接赋值。理解数据类型是掌握面向对象和反序列化漏洞的前提。
| 类型 | 说明 | 示例 | gettype() 返回值 |
|---|---|---|---|
| int | 整数,不带小数点 | $age = 18 |
integer |
| float | 浮点数,带小数点 | $price = 99.9 |
double |
| string | 字符串,用引号包裹 | $name = "admin" |
string |
| bool | 布尔值,true/false | $isOk = true |
boolean |
| array | 数组,存储多个值 | $list = [1, 2, 3] |
array |
| object | 对象,类的实例 | $obj = new Class() |
object |
| null | 空值,什么都没有 | $x = null |
NULL |
| resource | 资源类型,外部资源引用 | $fp = fopen() |
resource |
1.2 类与对象
类(class)是定义对象属性和方法的抽象模板,对象(object)是类通过 new 关键字实例化后的具体实体。对象同时包含数据(属性)和行为(方法)。
php
<?php
class Dog {
public $name = "柴犬"; // 属性:数据
public function bark() { // 方法:行为
return "汪汪!";
}
}
$dog = new Dog(); // 实例化:创建对象
echo $dog->name; // 访问属性:柴犬
echo $dog->bark(); // 调用方法:汪汪!
?>
面向过程强调「怎么做」(按步骤执行),面向对象强调「谁来做」(划分角色与职责)。反序列化漏洞的攻击面正是面向对象中的魔术方法。
1.3 访问修饰符
| 修饰符 | 类内部 | 子类 | 外部 | 安全意义 |
|---|---|---|---|---|
| public | 可访问 | 可访问 | 可访问 | 属性可被外部直接修改,存在安全风险 |
| private | 可访问 | 不可访问 | 不可访问 | 封装性最好,反序列化时仍可被控制 |
| protected | 可访问 | 可访问 | 不可访问 | 子类可继承,序列化时属性名带空字节前缀 |
二、PHP 危险函数详解
危险函数本身并非恶意,其危险性源于将未经严格校验的用户输入直接传递给这些函数。在 Web 安全领域,危险函数是代码执行、命令注入、文件包含和反序列化漏洞的核心触发点。
2.1 代码执行函数
代码执行函数可将字符串作为 PHP 代码动态解析执行,是 WebShell 最常用的载体。
| 函数 | 特点 | 禁用情况 |
|---|---|---|
eval() |
动态代码执行,属于 PHP 语言构造器(Language Construct) | 无法通过 disable_functions 禁用 |
assert() |
断言执行,PHP 5.x 为函数可禁用,PHP 7.2+ 改为语言构造器 | PHP 7.2+ 无法禁用,PHP 8.0 不再执行字符串参数 |
关键知识点:
eval()不是标准函数,而是语言构造器,因此无法通过php.ini的disable_functions指令禁用。这是eval()成为 WebShell 首选载体的核心原因。安全编码规范中应严格禁止将用户可控输入传入eval()。
2.2 命令执行函数
命令执行函数直接调用操作系统命令,可导致远程命令执行(RCE)漏洞。
| 函数 | 功能说明 | 返回值特性 | 风险等级 |
|---|---|---|---|
system() |
执行外部系统命令并直接输出结果 | 返回命令输出的最后一行 | 严重 |
exec() |
执行外部系统命令 | 返回命令输出的最后一行(需传入数组获取完整输出) | 严重 |
shell_exec() |
通过 Shell 环境执行命令 | 以字符串形式返回命令的完整输出 | 严重 |
passthru() |
执行外部命令并直接输出原始数据(含二进制) | 无返回值,直接输出到浏览器 | 严重 |
popen() |
打开进程文件指针,建立管道通信 | 返回文件指针,需配合 fread() 读取输出 |
高 |
proc_open() |
执行命令并建立可双向通信的进程管道 | 返回进程资源句柄,支持标准输入/输出/错误流 | 高 |
2.3 文件包含函数
文件包含函数可将指定文件的内容作为 PHP 代码执行,是本地文件包含(LFI)和远程文件包含(RFI)漏洞的触发点。
| 函数 | 特点 | 错误处理 |
|---|---|---|
include() |
包含并执行文件 | 警告,脚本继续执行 |
require() |
包含并执行文件 | 致命错误,脚本终止 |
include_once() |
只包含一次 | 警告,脚本继续执行 |
require_once() |
只包含一次 | 致命错误,脚本终止 |
文件包含漏洞分类:
| 漏洞类型 | 说明 | 利用条件 | 危害 |
|---|---|---|---|
| LFI(本地文件包含) | 包含服务器本地文件 | 无特殊配置要求 | 读取敏感文件、日志注入 GetShell |
| RFI(远程文件包含) | 包含远程服务器上的恶意文件 | allow_url_include=On(PHP 5.2 后默认关闭) |
直接包含恶意 PHP 脚本,获取服务器权限 |
2.4 反序列化函数
unserialize() 函数将序列化字符串还原为 PHP 对象。若反序列化的字符串用户可控,攻击者可构造恶意序列化数据,触发魔术方法实现远程代码执行。
php
<?php
$data = serialize($obj); // 序列化:对象→字符串
$obj2 = unserialize($data); // 反序列化:字符串→对象(触发 __wakeup)
?>
2.5 危险函数速查表
| 函数类别 | 函数名称 | 对应漏洞类型 | 危害等级 | 防御建议 |
|---|---|---|---|---|
| 代码执行 | eval() |
代码注入 / RCE | 严重 | 避免使用;如必须使用则实施严格白名单校验 |
| 代码执行 | assert() |
代码注入 / RCE | 严重 | PHP 7.2+ 已废弃为函数;避免在生产环境使用 |
| 命令执行 | system() |
命令注入 / RCE | 严重 | 使用 escapeshellarg()/escapeshellcmd() 过滤输入 |
| 命令执行 | exec() |
命令注入 / RCE | 严重 | 同上;限制可执行命令范围 |
| 命令执行 | shell_exec() |
命令注入 / RCE | 严重 | 同上;优先使用内置函数替代外部命令 |
| 命令执行 | passthru() |
命令注入 / RCE | 严重 | 同上;注意二进制输出可能绕过检测 |
| 命令执行 | popen() |
命令注入 | 高 | 严格校验命令参数;最小权限运行 |
| 命令执行 | proc_open() |
命令注入 | 高 | 同上;监控进程创建行为 |
| 文件包含 | include() |
LFI / RFI | 严重 | 关闭 allow_url_include;实施白名单校验 |
| 文件包含 | require() |
LFI / RFI | 严重 | 同上;错误处理中避免泄露路径信息 |
| 反序列化 | unserialize() |
对象注入 / RCE | 严重 | 避免反序列化用户可控输入;使用 JSON 替代 |
三、文件上传漏洞原理与防护
3.1 WebShell 工作原理
WebShell 是一段运行在目标服务器上的恶意脚本,攻击者通过 HTTP/HTTPS 协议向其发送命令,WebShell 执行命令后将结果返回。通信过程通常使用 POST 请求,参数名即为连接密码。
3.2 文件上传漏洞成因
文件上传漏洞的核心成因是:服务器对上传文件未做充分校验,导致攻击者可以上传恶意脚本文件,并通过 URL 访问执行。
常见上传绕过技巧(了解原理,用于防护):
| 绕过类型 | 原理 | 对应防护 |
|---|---|---|
| 扩展名绕过 | 使用 .php5、.phtml、.phar 等特殊后缀 |
使用白名单校验,仅允许指定后缀 |
| MIME 类型绕过 | 将 Content-Type 改为 image/jpeg |
校验文件内容(文件头魔数),不仅依赖 MIME |
| 文件头绕过 | 在 PHP 代码前添加 GIF89a |
校验文件内容完整性,不仅检查文件头 |
| 解析漏洞 | 利用 Apache AddHandler、Nginx CGI 解析漏洞 | 配置服务器正确解析规则,上传目录禁执行 |
3.3 防护方案
| 防护层级 | 具体措施 |
|---|---|
| 后缀名校验 | 使用白名单校验,仅允许指定后缀(如 jpg、png、gif),禁止黑名单方式 |
| 文件类型校验 | 校验文件内容(文件头魔数),不仅依赖 MIME 类型 |
| 文件名处理 | 上传文件重命名为随机文件名,禁止使用原始文件名 |
| 存储目录 | 上传目录禁止执行脚本,配置服务器将该目录解析为纯静态文件 |
| 权限控制 | 上传目录设置最小权限,禁止写入可执行文件 |
3.4 disable_functions 绕过原理
当目标服务器通过 php.ini 的 disable_functions 禁用了命令执行函数时,可能的绕过技术分为三大类:
| 绕过类型 | 技术原理 | 说明 |
|---|---|---|
| 函数替代类 | 利用未被禁用的同类功能函数 | 如 popen()/proc_open() 管道执行 |
| 环境劫持类 | 通过环境变量机制劫持系统函数调用 | 如 LD_PRELOAD 劫持 |
| 漏洞利用类 | 借助组件漏洞执行命令 | 如 ImageMagick 命令注入、Apache mod_cgi |
防护建议:不仅要禁用命令执行函数,还应限制 PHP 进程权限、禁用危险扩展、定期更新组件版本。
四、魔术方法详解
魔术方法是 PHP 中以两个下划线(__)开头的特殊方法,在特定条件下自动触发,无需手动调用。反序列化漏洞的核心利用面正是魔术方法的自动触发机制。
| 魔术方法 | 触发时机 | 反序列化中的作用 | 利用风险 |
|---|---|---|---|
__construct() |
new 创建对象时 | 反序列化过程不调用构造函数 | 低 |
__destruct() |
对象销毁时(脚本结束/unset) | 反序列化对象生命周期结束时自动调用 | 高 |
__wakeup() |
unserialize() 反序列化完成时 |
反序列化后立即触发,是首要利用点 | 高 |
__sleep() |
serialize() 序列化之前 |
控制哪些属性被序列化,影响 POP 链构造 | 中 |
__toString() |
对象被当作字符串使用时(echo/拼接) | 在输出或字符串操作场景中触发 | 高 |
__call() |
调用对象中不存在的方法时 | 在方法调用链(POP 链)中作为中转触发 | 中 |
__callStatic() |
调用类中不存在的静态方法时 | 静态方法调用链中触发 | 中 |
__get() |
访问对象中不存在的属性时 | 属性访问链中触发,常用于 POP 链构造 | 中 |
__set() |
设置对象中不存在的属性时 | 属性赋值链中触发 | 低 |
__isset() |
对不存在属性调用 isset()/empty() 时 |
属性检查链中触发 | 低 |
__unset() |
对不存在属性调用 unset() 时 |
属性销毁链中触发 | 低 |
关键知识点:反序列化过程中,
__construct()不会被调用,但__wakeup()和__destruct()都会被自动执行。这是反序列化漏洞能够利用的核心机制------攻击者无需调用任何方法,只需构造恶意序列化字符串,PHP 引擎会自动触发危险的魔术方法。
五、序列化与反序列化基础
5.1 序列化格式
serialize() 将 PHP 变量(包括对象)转换为可存储或传输的字符串格式,unserialize() 将字符串还原为 PHP 变量。
基本类型序列化示例:
| 类型标识 | 对应类型 | 格式示例 | 说明 |
|---|---|---|---|
i |
integer | i:60; |
整数序列化 |
d |
double | d:3.14; |
浮点数序列化 |
s |
string | s:5:"hello"; |
字符串长度+内容 |
b |
boolean | b:1; |
1=true, 0=false |
a |
array | a:2:{...} |
元素数量+键值对 |
O |
object | O:4:"User":1:{...} |
类名长度+类名+属性数量 |
N |
null | N; |
无附加数据 |
5.2 对象序列化结构解析
以一个简单对象的序列化字符串为例,逐字段解析结构:
css
O:4:"Hack":1:{s:3:"cmd";s:6:"whoami";}
| 字段 | 值 | 含义 |
|---|---|---|
| 类型标识 | O |
Object,表示这是一个对象 |
| 类名长度 | 4 |
类名 Hack 的字符长度 |
| 类名 | "Hack" |
对象所属的类名 |
| 属性数量 | 1 |
该对象包含 1 个属性 |
| 属性名类型 | s |
string,属性名为字符串类型 |
| 属性名长度 | 3 |
属性名 cmd 的长度 |
| 属性名 | "cmd" |
属性名称 |
| 属性值类型 | s |
string,属性值为字符串类型 |
| 属性值长度 | 6 |
属性值 whoami 的长度 |
| 属性值 | "whoami" |
属性的具体值 |
六、反序列化漏洞原理
6.1 漏洞本质
反序列化漏洞(PHP Object Injection)的本质是:应用信任了用户提供的序列化数据,未做校验即反序列化,导致攻击者构造的恶意对象中的魔术方法被自动执行。
漏洞公式:
反序列化漏洞 = 魔术方法中存在危险操作 + 反序列化数据用户可控 + 危险函数可执行命令
三个条件同时满足即可构成 RCE 漏洞。
6.2 攻击流程
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1. 侦察 | 识别目标存在 unserialize() 且参数用户可控 |
通过代码审计或黑盒测试发现反序列化入口 |
| 2. 构造 Payload | 本地创建恶意对象,设置危险属性,serialize() 生成字符串 |
构造包含危险操作的序列化数据 |
| 3. URL 编码 | 对 payload 进行 URL 编码,避免特殊字符被过滤 | urlencode() 处理后附加到 URL 参数 |
| 4. 发送请求 | 访问目标页面,传入编码后的 payload | 服务器接收并反序列化恶意数据 |
| 5. 获取结果 | 服务器自动触发 __wakeup → 危险函数,返回命令执行结果 |
输出命令执行结果 |
6.3 POP 链利用技术
POP(Property-Oriented Programming)链是反序列化漏洞利用的核心技术。在真实应用中,直接存在 __wakeup → system() 这种危险类的情况很少,攻击者需要利用目标应用中已存在的类的魔术方法调用链(Gadget Chain),将原本无害的对象操作串联为完整的恶意代码执行路径。
POP 链构造原理:
| 环节 | 说明 | 示例 |
|---|---|---|
| 起点(Sink) | 最终执行危险操作的方法 | __destruct 中调用 file_put_contents 写文件 |
| 中转(Gadget) | 连接各魔术方法的调用链 | __toString → __call → __get → 危险方法 |
| 属性控制 | 通过序列化字符串控制对象属性值 | 将文件名属性设为恶意路径,内容属性设为恶意代码 |
| 触发入口 | 反序列化后自动触发的第一个魔术方法 | __wakeup() 或 __destruct() |
phpggc(PHP Generic Gadget Chains)是主流的 POP 链利用工具,内置了 Laravel、Symfony、ThinkPHP、CodeIgniter、WordPress 等主流 PHP 框架和 CMS 的已验证 POP 链。
七、漏洞修复方案
7.1 反序列化漏洞修复
| 修复方法 | 实现方式 | 安全等级 | 适用场景 |
|---|---|---|---|
| 禁用反序列化 | 使用 json_decode() 替代 unserialize() |
最高 | 新开发项目,JSON 不会触发魔术方法 |
| 白名单限制 | unserialize($data, ['allowed_classes'=>['User','Product']]) |
高 | PHP 7.0+,只允许反序列化指定类 |
| 魔术方法安全 | __wakeup/__destruct 中不调用危险函数 |
中 | 已有代码重构,移除危险操作 |
| 输入校验 | 对序列化数据进行签名或加密校验 | 中 | 需要保留反序列化功能的场景 |
| WAF 防护 | 部署 WAF 拦截恶意序列化 payload | 低 | 应急防护,不能替代代码修复 |
安全修复代码示例:
php
<?php
// 安全修复方案 1:使用 JSON 替代
$data = json_decode($_GET['data'], true); // JSON 不触发魔术方法
// 安全修复方案 2:白名单限制(PHP 7.0+)
$obj = unserialize($data, [
'allowed_classes' => ['User', 'Product'] // 只允许这两个类
]);
// 安全修复方案 3:魔术方法中移除危险操作
public function __wakeup() {
// 移除 system()、exec()、file_put_contents() 等危险函数
$this->init(); // 只执行安全的初始化操作
}
?>
7.2 通用防护公式
危险函数 + 用户可控输入 + 无校验 = 远程代码执行(RCE)
最小权限 + 输入校验 + 白名单限制 + 危险函数禁用 = 安全
八、知识小结
| 知识模块 | 核心概念 | 漏洞类型 | 防御要点 |
|---|---|---|---|
| 危险函数 | eval/assert/system/exec/include/unserialize | 代码执行/命令注入/文件包含/反序列化 | 输入校验/白名单/禁用危险函数 |
| 文件上传 | 扩展名校验/MIME校验/文件头检测/目录禁执行 | 文件上传漏洞 → GetShell | 白名单后缀/文件内容检测/随机命名 |
| 面向对象 | 类/对象/属性/方法/访问修饰符 | --- | 封装性/最小权限原则 |
| 魔术方法 | __wakeup/__destruct/__toString/__call/__get |
反序列化漏洞触发点 | 魔术方法中禁止危险操作 |
| 反序列化漏洞 | POP 链/Gadget Chain/phpggc | 对象注入 → RCE | JSON 替代/白名单限制/输入签名 |
从 eval 到 unserialize,从命令执行到反序列化,PHP 危险函数的本质都是「将不可信的用户输入当作代码或命令执行」。安全的核心原则只有一条:永远不要信任用户输入,所有传入危险函数的数据必须经过严格校验。理解了原理,才能在代码审计中发现漏洞,在防护中堵住漏洞。