PHP 是代码审计里最”出活”的语言之一:它历史长、函数杂、又有弱类型这种反直觉的特性,开发者一不小心就写出可被利用的代码。要把 PHP 审计做好,第一步不是去读业务,而是把”哪些函数危险、哪些写法坑人”刻进肌肉记忆。这一篇就把 PHP 审计的基本功打牢。
本文所有手法仅用于你对自有或已授权的 PHP 代码做安全评估。
命令执行与代码执行
这类漏洞最致命,因为一旦可控,攻击者就能在服务器上跑任意命令。PHP 里要盯紧这些:
- 命令执行:
system、exec、shell_exec、passthru、popen、proc_open,以及反引号`包裹的表达式。只要它们的参数拼接了用户输入又没过滤,就是命令注入。 - 代码执行:
eval(直接执行字符串代码)、assert(老版本可当 eval 用)、preg_replace的/e修饰符(已废弃但老代码还有)、create_function(已移除但老系统残留)。
$name = $_GET['name'];
eval("echo 'hello $name';"); // 传入 name=;phpinfo(); 即可执行任意代码
审计时全局搜这些函数名,对每个调用点追问:参数来源是否用户可控?中间有没有 escapeshellarg / escapeshellcmd 过滤?没有就高危。
文件包含
include、require、include_once、require_once 用来引入文件。当被包含的路径拼了用户输入,就可能出现 LFI(本地文件包含) 甚至 RFI(远程文件包含,需 allow_url_include=On)。
$page = $_GET['page'];
include("pages/$page.php"); // 传入 page=../../../../etc/passwd%00 可读取系统文件
配合 PHP 的 php://filter(如 php://filter/read=convert.base64-encode/resource=)还能读源码、配合 data:// 或日志投毒可升级为代码执行。审计文件包含,重点看路径变量是否过滤了 ../ 和协议封装器。
文件操作类
文件上传、读写、删除同样高危:file_get_contents、file_put_contents、fopen、unlink、move_uploaded_file、copy。典型问题:
- 上传没校验后缀/MIME,导致传
.php马。 - 删除/读取路径拼接用户参数,造成任意文件删除/读取(比如删除安装锁文件重新安装)。
- 用
file_get_contents($url)且$url可控,触发 SSRF。
SQL 注入
虽然现在多用 PDO 预处理,但老代码和手写拼接依然普遍:mysql_query(已废弃)、mysqli_query 直接拼 SQL、PDO 用了 query 而非 prepare,或者 prepare 里把表名/字段名拼进去(预处理管不了标识符)。
$sql = "SELECT * FROM users WHERE name = '$name'"; // $name 来自 $_GET,未转义
审计 SQL 注入,先找所有 SQL 语句拼接点,再看参数是否经过 addslashes、PDO 预处理或框架的查询构造器。注意 order by、limit 后的参数预处理也管不到,常被忽略。
变量覆盖
PHP 的 $$(可变变量)、extract、parse_str(无第二参数时)、import_request_variables(已移除)都会把外部输入变成内部变量,造成变量覆盖。
foreach ($_GET as $k => $v) { $$k = $v; } // 用户可覆盖任意已存在变量,比如覆盖 $isAdmin
这类漏洞常导致越权、认证绕过:攻击者传入 isAdmin=1 之类的参数,直接覆盖掉权限判断用的变量。审计时看到 $$、extract、parse_str 要格外警惕。
弱类型比较的坑
PHP 的 == 是弱比较,会偷偷做类型转换,这是无数漏洞的根源:
'0e123' == '0e456'都为0,在md5($a) == md5($b)的校验里可构造”哈希碰撞”绕过(如两个 md5 都以0e开头的字符串)。'123abc' == 123为true,null == ''为true。in_array('abc', [0,1,2])在默认非严格模式下,'abc'转数值为0,命中0。
所以审计里看到 ==、in_array 无第三参数、switch 比较、数组键比较,都要怀疑是否可借类型转换绕过。正确做法是用 === 严格比较。
反序列化
unserialize() 会把字符串还原成对象,并在过程中自动调用魔术方法:__wakeup、__destruct、__toString、__call 等。如果反序列化的输入用户可控,攻击者可以构造恶意对象,触发这些魔术方法里的危险操作(比如 __destruct 里写了文件、调了数据库)。这就是 PHP 反序列化漏洞(POP 链)的基础。审计时看到 unserialize($_GET/$_POST/...) 必须追魔术方法的调用链。
全局搜索的实战节奏
新手审计 PHP 项目,建议这个节奏:
- 先全局搜危险函数(
eval、system、include、unserialize、$$、extract),把可疑点列成清单。 - 对每个点逆推参数来源,看是否用户可控、有无过滤。
- 再通读入口文件(
index.php、路由、配置文件),理解全局过滤和鉴权是怎么做的,判断有没有被绕过的口子。 - 最后挑高危点动态验证(用 Xdebug 或本地起环境复现)。
PHP 配置项里的安全信号
除了代码本身,PHP 的运行时配置(php.ini)也是审计的重要信号源。很多漏洞能不能被打穿,取决于配置怎么开:
register_globals:开启后会把请求变量自动注册成全局变量,极易造成变量覆盖,现代 PHP 已移除,但老系统升级不彻底时可能残留等效写法。magic_quotes_gpc:旧版用来自动给引号加斜杠,常被开发者误当成”万能防注入”,实际上对数字型注入、二次注入完全无效,还会污染数据。看到代码依赖它做防护,要警惕。allow_url_include:开启后include能加载远程地址,直接把文件包含升级为远程代码执行,生产环境必须关。allow_url_fopen:控制能否用file_get_contents访问远程地址,开着就给服务端请求伪造留了口子。display_errors:生产环境应关,开着会把路径、SQL、框架版本直接暴露给攻击者,大幅降低对方信息收集的成本。open_basedir:限制 PHP 能访问的目录,配好能挡掉一部分任意文件读取与删除。disable_functions:禁掉exec、system、eval等危险函数,是重要的纵深防御手段。session.cookie_httponly与session.cookie_secure:前者防脚本读 Cookie,后者要求 HTTPS 传输,都该开。expose_php:开着会在响应头暴露”X-Powered-By: PHP/版本”,给攻击者版本情报,应关。
审计时先读目标环境的 php.ini 和框架配置,判断这些开关是开是关,再结合代码评估”这个洞在当前配置下到底能不能利用”。同样的代码,配置不同,危害天差地别。把配置和代码放在一起看,结论才靠谱。
还有一个容易忽略的点:不要只盯着”明显危险”的函数。很多高危问题藏在看似普通的写法里,比如用 intval 转类型后仍然可被数组绕过、用 htmlspecialchars 却只转了双引号没转单引号导致属性内跨站脚本。审计要带着”这个防护是不是真的完整”的怀疑,而不是看到有过滤函数就放行。
审计中的几个实用小技巧
最后给几个能立刻用上的小技巧。第一,善用 IDE 的全局搜索和正则,比如搜 (\$_GET|\$_POST|\$_REQUEST)\[ 能快速列出所有外部输入点,再逐个追流向。第二,遇到看不懂的加密或混淆,先确认它是不是真的保护逻辑,很多时候只是障眼法。第三,把”用户可控”四个字贴在显示器上,每次看到变量来源都先问一句能不能被用户改。第四,动态验证优先于静态猜测,能在本地复现的就别只在脑子里推演,复现一次胜过猜十次。这些习惯看着小,积累起来能让你审计又快又准。
这一篇你该记住的
- PHP 审计先把危险函数清单背熟:命令/代码执行、文件包含、文件操作、SQL 拼接、变量覆盖、反序列化。
- 弱类型
==会偷偷转型,是 md5 碰撞、类型混淆绕过的根源,能用===就别用==。 $$、extract、parse_str会造成变量覆盖,常导致越权与认证绕过。unserialize可控输入 + 魔术方法 = 反序列化漏洞,要追__wakeup/__destruct等调用链。- 实战节奏:全局搜危险函数 → 逆推参数来源 → 通读入口与鉴权 → 动态验证。
下一篇我们拿最流行的 PHP 框架 ThinkPHP 开刀,看框架的路由、控制器、模板引擎里,哪些写法会埋雷,又该怎么审。