在 Web 安全领域,PHP 代码审计是识别和修复应用程序安全隐患的关键环节。由于 PHP 语言的灵活性和易用性,其代码中常存在各类可被攻击者利用的漏洞。本文将系统梳理 PHP 开发中常见的高风险漏洞类型,剖析其原理、示例及审计要点,为安全审计人员和开发人员提供参考。
一、代码执行漏洞
代码执行漏洞源于对可执行代码的函数滥用,攻击者可通过可控参数注入恶意逻辑。以下为需重点审计的函数及细节:
1. eval()
- 定义:将字符串作为 PHP 代码执行,语法为
eval(string $code) : mixed
。 - 关键特性:
- 返回值:默认返回
NULL
,若代码中有return
则返回对应值。 - 版本差异:PHP7 中解析错误会抛出
ParseError
异常;PHP5 中返回FALSE
,但后续代码继续执行。
- 返回值:默认返回
- 漏洞示例:
<?php
$code = $_GET['code']; // 可控参数
eval($code); // 注入"system('whoami');"即可执行命令
?>
- 审计要点:检查
$code
是否直接由用户输入控制,是否存在过滤绕过可能。
2. assert()
- 定义:断言检查,语法因版本而异:
- PHP5:
assert(mixed $assertion[, string $description]) : bool
($assertion
可为执行字符串)。 - PHP7:
assert(mixed $assertion[, Throwable $exception]) : bool
(兼容字符串执行以保持向下兼容)。
- PHP5:
- 配置影响:
配置项 | 默认值 | 风险场景 |
---|---|---|
zend.assertions |
1 | 设为 1 时(开发模式)会执行断言代码,存在风险 |
assert.exception |
0 | 设为 0 时仅警告,不阻断执行 |
- 漏洞示例:
<?php
$test = $_GET['test'];
assert($test); // 传入"system('ipconfig')"执行命令
?>
- 审计要点:
$assertion
参数是否可控,是否包含用户输入。
3. preg_replace()
- 定义:正则替换,语法为
preg_replace(mixed $pattern, mixed $replacement, mixed $subject [, int $limit = -1 [, int &$count ]])
。 - 风险点:
$pattern
含/e
模式时,$replacement
会被当作 PHP 代码执行(PHP7 后废弃/e
模式)。 - 漏洞示例:
<?php
$regex = $_GET['regex'];
$value = $_GET['value'];
preg_replace('/(' . $regex . ')/ei', '\\1', $value);
// payload: ?regex=.*&value={${phpinfo()}}
?>
- 审计要点:检查是否使用
/e
模式,$pattern
和$replacement
是否可控。
4. create_function()
- 定义:创建匿名函数(内部调用
eval()
),语法为create_function(string $args, string $code) : string
(PHP7.2 后废弃)。 - 风险点:
$code
参数可控时可注入恶意代码。 - 漏洞示例:
<?php
$args = $_GET['args'];
$code = $_GET['code'];
$func = create_function($args, $code); // 注入$code="system('whoami');"
$func();
?>
- 衍生场景:通过字符串拼接构造
$code
,如排序逻辑中的代码注入:
<?php
$sort_by = $_GET['sort_by'];
$sort_func = "return strnatcasecmp(\$a['$sort_by'], \$b['$sort_by']);";
usort($data, create_function('$a,$b', $sort_func));
// payload: ?sort_by=';}phpinfo();/* 闭合并注入代码
?>
5. 回调函数类
所有接受回调函数的函数均需审计,若回调函数或其参数可控则存在风险:
函数 | 定义 | 漏洞示例 |
---|---|---|
array_map() |
为数组元素应用回调:array_map(callable $callback, array $array1 [, array $...]) |
array_map($_GET['func'], ['whoami']); (func=system ) |
call_user_func() |
调用回调函数:call_user_func(callable $callback [, mixed $...]) |
call_user_func($_GET['func'], 'id'); |
call_user_func_array() |
以数组为参数调用回调:call_user_func_array(callable $callback, array $param_arr) |
call_user_func_array('system', [$_GET['cmd']]); |
array_filter() |
用回调过滤数组:array_filter(array $array [, callable $callback [, int $flag ]]) |
array_filter([$_GET['cmd']], 'system'); |
usort() |
用自定义函数排序:usort(array &$array, callable $value_compare_func) |
usort($data, $_GET['func']); (func=assert&... ) |
uasort() |
排序并保持索引关联:uasort(array &$array, callable $value_compare_func) |
同usort() ,风险一致 |
二、命令执行漏洞
直接调用系统命令的函数,参数可控时可导致命令注入,需逐个审计:
1. system()
- 定义:执行命令并输出结果,语法
system(string $command [, int &$return_var ]) : string
。 - 示例:
system($_GET['cmd']);
(传入cmd=whoami
执行)。 - 特点:直接输出命令结果,易被发现。
2. exec()
- 定义:执行命令,返回最后一行输出,语法
exec(string $command [, array &$output [, int &$return_var ]]) : string
。 - 示例:
exec($_GET['cmd'], $output); var_dump($output);
(结果存于数组)。 - 特点:不直接输出,需通过
$output
获取结果。
3. shell_exec()
- 定义:通过 shell 执行命令,返回完整输出字符串,语法
shell_exec(string $cmd) : string
。 - 示例:
echo shell_exec($_GET['cmd']);
(执行并输出完整结果)。
4. passthru()
- 定义:执行命令并显示原始输出(适合二进制数据),语法
passthru(string $command [, int &$return_var ]) : void
。 - 示例:
passthru($_GET['cmd']);
(直接输出原始结果)。
5. pcntl_exec()
- 定义:在当前进程空间执行程序,语法
pcntl_exec(string $path [, array $args [, array $envs ]]) : void
。 - 示例:
pcntl_exec('/bin/sh', ['-c', $_GET['cmd']]);
(执行 shell 命令)。 - 特点:需
pcntl
扩展,常用于子进程命令执行。
6. popen () 与 proc_open ()
- 定义:
popen(string $command, string $mode) : resource
:打开进程文件指针。proc_open(string $cmd, array $descriptorspec, array &$pipes [, string $cwd [, array $env [, array $other_options ]]])
:更复杂的进程控制。
- 示例:
<?php
$handle = popen($_GET['cmd'], 'r');
echo fread($handle, 1024);
pclose($handle);
?>
三、文件包含漏洞
通过包含文件执行代码,所有文件包含函数均需审计:
1. include () 与 include_once ()
- 定义:
include(string $filename)
:包含并执行文件,出错仅警告。include_once(string $filename)
:确保文件只被包含一次。
- 漏洞示例:
<?php
$file = $_GET['file'];
include $file; // 传入?file=../../etc/passwd 或 php://filter伪协议
?>
- 审计要点:
$filename
是否可控,是否限制协议(如禁止php://
、file://
)。
2. require () 与 require_once ()
- 定义:
require(string $filename)
:包含文件,出错终止脚本。require_once(string $filename)
:确保文件只被包含一次。
- 风险与
include
类似,但因出错终止特性,漏洞利用更易被发现。
四、文件读取漏洞
可读取文件内容的函数,需审计参数是否可控:
函数 | 定义 | 漏洞示例 |
---|---|---|
file_get_contents() |
读取文件为字符串:file_get_contents(string $filename [, int $use_include_path = 0 [, resource $context [, int $offset = 0 [, int $maxlen ]]]]) |
echo file_get_contents($_GET['file']); |
fopen() + fread() |
fopen 打开文件指针,fread 读取:fread(resource $handle, int $length) |
$f = fopen($_GET['file'], 'r'); echo fread($f, 1024); |
fgets() |
读取文件一行:fgets(resource $handle [, int $length ]) |
while(($line = fgets($f)) !== false) { echo $line; } |
fgetss() |
读取一行并过滤 HTML/PHP 标签:fgetss(resource $handle [, int $length [, string $allowable_tags ]]) |
风险同fgets() ,但过滤标签不影响文件读取 |
readfile() |
读取文件并输出:readfile(string $filename [, int $use_include_path = 0 [, resource $context ]]) |
readfile($_GET['file']); (直接输出文件内容) |
file() |
按行读取文件为数组:file(string $filename [, int $use_include_path = 0 [, resource $context ]]) |
print_r(file($_GET['file'])); |
parse_ini_file() |
解析 INI 文件为数组:parse_ini_file(string $filename [, bool $process_sections = false [, int $scanner_mode = INI_SCANNER_NORMAL ]]) |
print_r(parse_ini_file($_GET['ini'])); |
show_source() /highlight_file() |
高亮显示 PHP 文件:show_source(string $filename [, bool $return = false ]) |
show_source($_GET['phpfile']); (读取 PHP 源码) |
五、文件上传漏洞
核心审计函数为
move_uploaded_file()
:- 定义:
move_uploaded_file(string $filename, string $destination) : bool
(移动上传文件到新位置)。 - 漏洞点:
- 未验证文件类型:如允许
.php
后缀。 - 文件名可控:如
$destination = 'uploads/' . $_FILES['file']['name']
。
- 未验证文件类型:如允许
- 示例:
<?php
$dest = 'uploads/' . $_FILES['file']['name']; // 文件名可控
move_uploaded_file($_FILES['file']['tmp_name'], $dest);
?>
六、文件删除漏洞
需审计的函数:
1. unlink()
- 定义:删除文件,语法
unlink(string $filename [, resource $context ]) : bool
。 - 漏洞示例:
unlink($_GET['file']);
(传入file=../config.php
删除配置文件)。
2. session_destroy()
- 定义:销毁会话数据,语法
session_destroy() : bool
。 - 风险点:仅清空会话数据,不删除会话文件,但若会话 ID 可控,可能间接影响会话安全。
七、变量覆盖漏洞
导致变量被意外重赋值的函数及场景:
1. extract()
- 定义:从数组导入变量到当前作用域,语法
extract(array &$array [, int $flags = EXTR_OVERWRITE [, string $prefix = '' ]]) : int
。 - 关键参数:
$flags
为EXTR_OVERWRITE
(默认)时会覆盖已有变量。 - 示例:
<?php
$user = 'admin';
extract($_POST); // POST传入user=hacker 覆盖$user
echo $user; // 输出hacker
?>
2. parse_str()
- 定义:解析字符串为变量,语法
parse_str(string $encoded_string [, array &$result ])
。 - 风险点:未指定
$result
时,变量直接存入当前作用域,覆盖已有值。 - 示例:
<?php
$id = 1;
parse_str($_GET['data']); // 传入data=id=2 覆盖$id
echo $id; // 输出2
?>
3. import_request_variables()
- 定义:导入 GET/POST/Cookie 变量到全局作用域(PHP5.4 后废弃),语法
import_request_variables(string $types [, string $prefix ]) : bool
。 - 示例:
import_request_variables('g');
(导入 GET 变量,覆盖全局变量)。
4. foreach 与 $$ 可变变量
- 场景:通过
foreach
遍历用户可控数组,结合$$
可变变量覆盖全局变量。 - 示例:
<?php
$role = 'user';
foreach($_GET as $k => $v) { $$k = $v; } // 传入?role=admin 覆盖$role
echo $role; // 输出admin
?>
八、弱类型比较漏洞
PHP 弱类型特性导致的逻辑绕过,需关注以下场景:
1. md5 () 与 sha1 () 绕过
- 原理:
0E
开头的哈希值被解析为 0,如md5('s878926199a') = 0e545993274517709034328855841020
。 - 示例:
<?php
if(md5($_GET['a']) == md5($_GET['b']) && $_GET['a'] != $_GET['b']){
echo 'success';
}
// 传入a=s878926199a&b=s155964671a 绕过
?>
- 数组绕过:
md5(['x']) === md5(['y'])
结果为true
(均返回NULL
)。
2. is_numeric () 绕过
- 原理:十六进制字符串(如
0x123
)被识别为数字。 - 示例:
<?php
if(is_numeric($_GET['id'])){
$sql = "SELECT * FROM users WHERE id = {$_GET['id']}";
}
// 传入id=0x31206f722031 注入SQL:SELECT * FROM users WHERE id = 1 or 1
?>
3. in_array () 绕过
- 原理:非严格模式(
$strict = false
)下,字符串会强制转换为数字。 - 示例:
<?php
$whitelist = ['admin', 'user'];
if(in_array($_GET['role'], $whitelist)){
echo 'allowed';
}
// 传入role=0 绕过('admin'转数字为0)
?>
九、XSS 漏洞
未过滤用户输入直接输出的函数,均可能导致 XSS:
函数 | 示例 | 风险 |
---|---|---|
echo() |
echo $_GET['x']; |
直接输出 HTML/JS 代码 |
print() |
print($_GET['x']); |
同echo |
print_r() |
print_r($_GET['x']); |
输出数组时包含用户输入 |
printf() /sprintf() |
printf($_GET['x']); |
格式化字符串包含用户输入 |
die() /exit() |
die($_GET['x']); |
退出前输出用户输入 |
var_dump() |
var_dump($_GET['x']); |
打印变量时包含 HTML |
var_export() |
var_export($_GET['x']); |
输出变量结构,含用户输入 |
十、反序列化漏洞
需审计
unserialize()
函数及所有触发反序列化的场景,重点关注魔术方法:1. 核心魔术方法
方法 | 触发时机 | 利用示例 |
---|---|---|
__wakeup() |
反序列化时 | 篡改属性个数(如O:5:"Test":2:{...} )可跳过执行(PHP5<5.6.25/PHP7<7.0.10) |
__destruct() |
对象销毁时 | 若含system() 等命令执行函数,可直接触发 |
__construct() |
对象创建时 | 初始化对象时执行,若参数可控则注入代码 |
__toString() |
对象被当作字符串时 | 如echo $obj 触发,若含eval() 则执行代码 |
__call() |
调用不存在的方法时 | 如$obj->test() 触发,可执行预设逻辑 |
__callStatic() |
调用不存在的静态方法时 | 如Test::test() 触发,风险同__call() |
__get() |
访问私有属性时 | 如$obj->prop 触发,可读取 / 修改敏感属性 |
__set() |
设置私有属性时 | 如$obj->prop = 1 触发,可注入恶意值 |
__isset() |
用isset() 检测私有属性时 |
可触发自定义逻辑 |
__unset() |
用unset() 删除私有属性时 |
可触发自定义逻辑 |
__invoke() |
对象被当作函数调用时 | 如$obj() 触发,可执行命令 |
2. PHAR 反序列化
- 原理:PHAR 文件的
meta-data
以序列化格式存储,通过phar://
伪协议访问时触发反序列化。 - 示例:
file_get_contents('phar://malicious.phar')
触发meta-data
中对象的反序列化。
十一、其他高风险函数
1. basename()
:路径截断漏洞
- 函数作用:从路径中提取文件名(如
basename("/path/to/file.php")
返回file.php
)。 - 核心风险:自动过滤非 ASCII 字符(如
%ff
、\0
),导致路径绕过。- 示例:
<?php
$file = $_GET['file']; // 用户传入:../../etc/passwd%ff
$filename = basename($file); // 过滤%ff后变为 ../../etc/passwd
readfile($filename); // 读取敏感文件
?>
- 攻击逻辑:利用非 ASCII 字符截断路径过滤,访问预期外的文件。
2. curl_setopt()
:SSRF 漏洞入口
- 函数作用:设置 cURL 请求参数(如
CURLOPT_URL
控制请求 URL)。 - 核心风险:
CURLOPT_URL
可控时可发起任意协议请求。- 示例 1:读取本地文件:
<?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_GET['url']); // 可控参数
curl_exec($ch);
?>
- Payload:
?url=file:///etc/passwd
- 示例 2:攻击内网服务:
- Payload:
?url=http://192.168.1.1/admin
(探测内网地址)。
- Payload:
3. urldecode()
:二次编码绕过
- 函数作用:对 URL 编码字符串解码(如
urldecode("%3C")
转为<
)。 - 核心风险:多次解码导致过滤失效(如二次编码
%253C
→ 第一次解码%3C
→ 第二次解码<
)。- 示例:XSS 绕过过滤:
<?php
$input = urldecode($_GET['x']); // 一次解码:%253C → %3C
echo $input; // 浏览器二次解码为<,触发XSS
?>
- Payload:
?x=%253Cscript%253Ealert(1)%253C/script%253E
总结
PHP 代码审计需要结合语言特性与漏洞原理,重点关注用户可控参数的流向,以及高风险函数的使用场景。通过本文梳理的漏洞类型和审计要点,开发人员可在编码阶段规避风险,安全人员可更高效地开展审计工作,共同提升 Web 应用的安全性。在实际审计中,还需结合具体业务逻辑和框架特性,进行全面细致的检查。