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