PHP 代码审计指南
在 Web 安全领域,PHP 代码审计是识别和修复应用程序安全隐患的关键环节。由于 PHP 语言的灵活性和易用性,其代码中常存在各类可被攻击者利用的漏洞。本文将系统梳理 PHP 开发中常见的高风险漏洞类型,剖析其原理、示例及审计要点,为安全审计人员和开发人员提供参考。

一、代码执行漏洞

代码执行漏洞源于对可执行代码的函数滥用,攻击者可通过可控参数注入恶意逻辑。以下为需重点审计的函数及细节:

1. eval()

  • 定义:将字符串作为 PHP 代码执行,语法为 eval(string $code) : mixed
  • 关键特性
    • 返回值:默认返回NULL,若代码中有return则返回对应值。
    • 版本差异:PHP7 中解析错误会抛出ParseError异常;PHP5 中返回FALSE,但后续代码继续执行。
  • 漏洞示例
1
2
3
4
<code class="language-php">&lt;?php
$code = $_GET[&#039;code&#039;]; // 可控参数
eval($code); // 注入&quot;system(&#039;whoami&#039;);&quot;即可执行命令
?&gt;</code>
  • 审计要点:检查$code是否直接由用户输入控制,是否存在过滤绕过可能。

2. assert()

  • 定义:断言检查,语法因版本而异:
    • PHP5:assert(mixed $assertion[, string $description]) : bool$assertion可为执行字符串)。
    • PHP7:assert(mixed $assertion[, Throwable $exception]) : bool(兼容字符串执行以保持向下兼容)。
  • 配置影响
配置项 默认值 风险场景
zend.assertions 1 设为 1 时(开发模式)会执行断言代码,存在风险
assert.exception 0 设为 0 时仅警告,不阻断执行
  • 漏洞示例
1
2
3
4
<code class="language-php">&lt;?php
$test = $_GET[&#039;test&#039;];
assert($test); // 传入&quot;system(&#039;ipconfig&#039;)&quot;执行命令
?&gt;</code>
  • 审计要点$assertion参数是否可控,是否包含用户输入。

3. preg_replace()

  • 定义:正则替换,语法为 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">&lt;?php
$regex = $_GET[&#039;regex&#039;];
$value = $_GET[&#039;value&#039;];
preg_replace(&#039;/(&#039; . $regex . &#039;)/ei&#039;, &#039;\\1&#039;, $value);
// payload: ?regex=.*&amp;value={${phpinfo()}}
?&gt;</code>
  • 审计要点:检查是否使用/e模式,$pattern$replacement是否可控。

4. create_function()

  • 定义:创建匿名函数(内部调用eval()),语法为 create_function(string $args, string $code) : string(PHP7.2 后废弃)。
  • 风险点$code参数可控时可注入恶意代码。
  • 漏洞示例
1
2
3
4
5
6
<code class="language-php">&lt;?php
$args = $_GET[&#039;args&#039;];
$code = $_GET[&#039;code&#039;];
$func = create_function($args, $code); // 注入$code=&quot;system(&#039;whoami&#039;);&quot;
$func();
?&gt;</code>
  • 衍生场景:通过字符串拼接构造$code,如排序逻辑中的代码注入:
1
2
3
4
5
6
<code class="language-php">&lt;?php
$sort_by = $_GET[&#039;sort_by&#039;];
$sort_func = &quot;return strnatcasecmp(\$a[&#039;$sort_by&#039;], \$b[&#039;$sort_by&#039;]);&quot;;
usort($data, create_function(&#039;$a,$b&#039;, $sort_func));
// payload: ?sort_by=&#039;;}phpinfo();/* 闭合并注入代码
?&gt;</code>

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 ]]]):更复杂的进程控制。
  • 示例
1
2
3
4
5
<code class="language-php">&lt;?php
$handle = popen($_GET[&#039;cmd&#039;], &#039;r&#039;);
echo fread($handle, 1024);
pclose($handle);
?&gt;</code>

三、文件包含漏洞

通过包含文件执行代码,所有文件包含函数均需审计:

1. include () 与 include_once ()

  • 定义
    • include(string $filename):包含并执行文件,出错仅警告。
    • include_once(string $filename):确保文件只被包含一次。
  • 漏洞示例
1
2
3
4
<code class="language-php">&lt;?php
$file = $_GET[&#039;file&#039;];
include $file; // 传入?file=../../etc/passwdphp://filter伪协议
?&gt;</code>
  • 审计要点$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']
  • 示例
1
2
3
4
<code class="language-php">&lt;?php
$dest = &#039;uploads/&#039; . $_FILES[&#039;file&#039;][&#039;name&#039;]; // 文件名可控
move_uploaded_file($_FILES[&#039;file&#039;][&#039;tmp_name&#039;], $dest);
?&gt;</code>

六、文件删除漏洞

需审计的函数:

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
  • 关键参数$flagsEXTR_OVERWRITE(默认)时会覆盖已有变量。
  • 示例
1
2
3
4
5
<code class="language-php">&lt;?php
$user = &#039;admin&#039;;
extract($_POST); // POST传入user=hacker 覆盖$user
echo $user; // 输出hacker
?&gt;</code>

2. parse_str()

  • 定义:解析字符串为变量,语法 parse_str(string $encoded_string [, array &$result ])
  • 风险点:未指定$result时,变量直接存入当前作用域,覆盖已有值。
  • 示例
1
2
3
4
5
<code class="language-php">&lt;?php
$id = 1;
parse_str($_GET[&#039;data&#039;]); // 传入data=id=2 覆盖$id
echo $id; // 输出2
?&gt;</code>

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遍历用户可控数组,结合$$可变变量覆盖全局变量。
  • 示例
1
2
3
4
5
<code class="language-php">&lt;?php
$role = &#039;user&#039;;
foreach($_GET as $k =&gt; $v) { $$k = $v; } // 传入?role=admin 覆盖$role
echo $role; // 输出admin
?&gt;</code>

八、弱类型比较漏洞

PHP 弱类型特性导致的逻辑绕过,需关注以下场景:

1. md5 () 与 sha1 () 绕过

  • 原理0E开头的哈希值被解析为 0,如md5('s878926199a') = 0e545993274517709034328855841020
  • 示例
1
2
3
4
5
6
<code class="language-php">&lt;?php
if(md5($_GET[&#039;a&#039;]) == md5($_GET[&#039;b&#039;]) &amp;&amp; $_GET[&#039;a&#039;] != $_GET[&#039;b&#039;]){
echo &#039;success&#039;;
}
// 传入a=s878926199a&amp;b=s155964671a 绕过
?&gt;</code>
  • 数组绕过md5(['x']) === md5(['y']) 结果为true(均返回NULL)。

2. is_numeric () 绕过

  • 原理:十六进制字符串(如0x123)被识别为数字。
  • 示例
1
2
3
4
5
6
<code class="language-php">&lt;?php
if(is_numeric($_GET[&#039;id&#039;])){
$sql = &quot;SELECT * FROM users WHERE id = {$_GET[&#039;id&#039;]}&quot;;
}
// 传入id=0x31206f722031 注入SQL:SELECT * FROM users WHERE id = 1 or 1
?&gt;</code>

3. in_array () 绕过

  • 原理:非严格模式($strict = false)下,字符串会强制转换为数字。
  • 示例
1
2
3
4
5
6
7
<code class="language-php">&lt;?php
$whitelist = [&#039;admin&#039;, &#039;user&#039;];
if(in_array($_GET[&#039;role&#039;], $whitelist)){
echo &#039;allowed&#039;;
}
// 传入role=0 绕过(&#039;admin&#039;转数字为0)
?&gt;</code>

九、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),导致路径绕过。
    • 示例
1
2
3
4
5
<code class="language-php">&lt;?php
$file = $_GET[&#039;file&#039;]; // 用户传入:../../etc/passwd%ff
$filename = basename($file); // 过滤%ff后变为 ../../etc/passwd
readfile($filename); // 读取敏感文件
?&gt;</code>
  • 攻击逻辑:利用非 ASCII 字符截断路径过滤,访问预期外的文件。

2. curl_setopt():SSRF 漏洞入口

  • 函数作用:设置 cURL 请求参数(如 CURLOPT_URL 控制请求 URL)。
  • 核心风险CURLOPT_URL 可控时可发起任意协议请求。
    • 示例 1:读取本地文件
1
2
3
4
5
<code class="language-php">&lt;?php
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_GET[&#039;url&#039;]); // 可控参数
curl_exec($ch);
?&gt;</code>
  • Payload:?url=file:///etc/passwd
  • 示例 2:攻击内网服务
    • Payload:?url=http://192.168.1.1/admin(探测内网地址)。

3. urldecode():二次编码绕过

  • 函数作用:对 URL 编码字符串解码(如 urldecode("%3C") 转为 <)。
  • 核心风险:多次解码导致过滤失效(如二次编码 %253C → 第一次解码 %3C → 第二次解码 <)。
    • 示例:XSS 绕过过滤
1
2
3
4
<code class="language-php">&lt;?php
$input = urldecode($_GET[&#039;x&#039;]); // 一次解码:%253C → %3C
echo $input; // 浏览器二次解码为&lt;,触发XSS
?&gt;</code>
  • Payload:?x=%253Cscript%253Ealert(1)%253C/script%253E

总结

PHP 代码审计需要结合语言特性与漏洞原理,重点关注用户可控参数的流向,以及高风险函数的使用场景。通过本文梳理的漏洞类型和审计要点,开发人员可在编码阶段规避风险,安全人员可更高效地开展审计工作,共同提升 Web 应用的安全性。在实际审计中,还需结合具体业务逻辑和框架特性,进行全面细致的检查。
上一篇
下一篇