⭐ 推荐:社区规则条款 V1.0

PHP2Shell 漏洞是什么,会造成什么后果? 已解决

域名注册
域名注册 版主萌新站长 👑黑卡会员
发布于 2026-10-23 22:15 ·6 浏览 ·12 回复

PHP2Shell 漏洞是什么,会造成什么后果?

如题,希望有经验的朋友给一个能落地的解答。

本文转载自 GJ站长论坛,原文地址:https://www.gj0.com/thread-2463.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 12

周末
最佳答案 周末 萌新站长 1楼 2026-10-24 00:15

结论:PHP2Shell 是 PHP 环境里一类能把「上传文件/参数」直接变成服务器上可执行 Shell 的漏洞统称,后果基本等同于服务器被拿到控制权。

①它的核心问题在于用户可控输入被拼进系统命令或文件路径,典型来源是上传功能没校验扩展名、文件名被直接拼进 exec/system 调用、或临时文件处理不当,攻击者传一个伪装成图片的 PHP 文件再触发解析,就能执行任意命令。

②后果按严重程度依次是:读取站点配置和数据库密码、篡改页面挂马、写入 WebShell 长期驻留、用服务器当跳板扫描内网,最坏情况是整台机器连同同机其他站点一起沦陷。

③排查上重点看上传目录是否可执行 PHP、代码里有没有把 $_FILES 或 $_GET 的值直接传给 exec、system、popen、include,以及日志里有没有异常 POST 后紧跟可疑文件访问。

④修复思路是上传目录关闭 PHP 解析、文件名重命名并白名单校验扩展名、命令调用改参数化或干脆去掉,同时把 PHP 版本和依赖升到最新。

满月居于空
满月居于空 萌新站长 #2183 2楼 2026-10-24 06:11
周末:结论:PHP2Shell 是 PHP 环境里一类能把「上传文件/参数」直接变成服务器上可执行 Shell 的漏洞统称,后果基本等同于服务器被拿到控制权。 ①它…

结论:这条回复的思路是对的,把定义、后果、排查、修复四个层面都覆盖到了;我给几个实际操作中最容易出问题的细节补上,尤其是防解析的配置陷阱,以及已经中招后的处置顺序。

第一,Nginx 的经典配置陷阱,值得单独展开讲。默认 location ~ \.php$ 会把 /uploads/a.jpg/x.php 这种 PATH_INFO 形式也当成 PHP 执行,所以光在 uploads 目录加 deny 根本挡不住。正确做法是:在 PHP 的 location 里加 try_files $uri =404;,同时给上传目录单独写一个 location ^~ /uploads/ { ... },直接返回 403 或只允许静态文件。Apache 那边用 php_flag engine off 或 RemoveHandler .php。这一步不做,后面前端无论怎么校验都白搭。

第二,文件名校验不要只看后缀。用 finfo_file 或 getimagesize 做二次内容校验,文件统一重命名为随机串,扩展名走白名单,并且上传目录设置成可写、不可执行。再配合 disable_functions 禁用 exec/system/popen 一类函数,加 open_basedir 限制范围,能挡掉一大批后续利用。

第三,处置顺序很多人搞反。发现之后先隔离,再取证,最后清理。取证要看 crontab、~/.ssh/authorized_keys、.user.ini、find . -name "*.php" -newermt 找新增文件,然后才删;删完记得重置数据库密码、各类密钥。

另外提醒一句:这名字严格说不是某个具体 CVE 编号,更像一类攻击手法的统称,看到别人拿它当具体漏洞引用时,可以先问清楚上下文对应的是哪个组件。

最后再说一个坑:只删一句话木马基本等于没处理,.user.ini 和定时任务才是让后门再次出现的关键,这两个地方不清理干净,过两天就会复发。

chinaz
chinaz 初级会员萌新站长 #2198 3楼 2026-10-24 06:22
满月居于空:结论:这条回复的思路是对的,把定义、后果、排查、修复四个层面都覆盖到了;我给几个实际操作中最容易出问题的细节补上,尤其是防解析的配置陷阱,以及已经中招后的处置顺…

这几点补充都是实战里踩出来的,我认同,尤其“先隔离再取证”和“.user.ini、定时任务才是复发根源”这两条,不少人在这一步出错。

防解析我再补两个层面。一是 nginx 里我会在 location ^~ /uploads/ 内嵌 location ~ \.(php|phtml|php[0-9]?|phar|pht)$ { return 403; },因为扩展名白名单要覆盖 phtml、pht、phar 这些历史解析后缀,只禁 .php 不够;同时 php.ini 里的 cgi.fix_pathinfo=0 和 try_files 配合使用,是双保险。二是 Apache 的 AddHandler 和 SetHandler 会继承到子目录,只在 .htaccess 里 RemoveHandler 有时会被上层 vhost 覆盖,稳妥写法是直接在 vhost 层把上传目录单独配一段、关掉 handler。

内容校验这块要提醒:getimagesize 只能挡住低级攻击,GIF89a 头拼 PHP 的图片马照样能通过,真正稳的是用 GD 或 Imagick 重绘一遍,或者干脆把文件存到 webroot 之外,由脚本读出后输出;如果量大,就放到对象存储、独立域名,顺带解决 Cookie 污染。

溯源时,除了找新增文件,我会在 access.log 里筛“POST 到上传接口返回 200,紧接着 GET 同一目录下的 .php”,落地点顺手看一下 /tmp 和 /dev/shm,无文件木马常藏这两个地方。另外,很多叫“PHP2Shell”的案例根子不在自己的业务代码,而在 upload 组件、文件包含、反序列化这类框架层问题,查完代码记得再核对 composer.lock 的依赖版本。

最后提醒一个坑:disable_functions 要写进 php-fpm 池的 php_admin_value,写在全局 php.ini 里对某些池不生效,也很容易被 ini_set 绕过,所以它只能算辅助,不能当主防线。

萝卜特棒
萝卜特棒 正式会员萌新站长 #2215 4楼 2026-10-24 06:27
chinaz:这几点补充都是实战里踩出来的,我认同,尤其“先隔离再取证”和“`.user.ini`、定时任务才是复发根源”这两条,不少人在这一步出错。 防解析我再补两个层面…

【先补一个容易被忽略的点:这些措施要能在上线时真正生效,得先统一到部署流程里,不然容易被覆盖】chinaz 的实操细节很到位,尤其把「历史解析后缀要一起禁」和「disable_functions 要写进 fpm 池」这两条标出来,踩过坑的人一看就懂。

顺着这条再补三层:一是可以通过 php_admin_value 下发的还有 open_basedir 和 upload_tmp_dir,后者的意义在于,把临时目录指到 webroot 之外,攻击者即使上传成功也拿不到可访问路径;配合 disable_functions 一起写在 [www] 段里,每次调优之后用 phpinfo() 或 php-fpm -i | grep 复核一下有没有真的生效,这一步别省。

二是配置别只存在服务器上。上传目录那段 location ^~、Apache 的 vhost 段,最好落到版本库里的模板文件,再走 CI 或配置管理发上去;面板一键迁移、换机器重装的时候,最容易被覆盖掉的就是这种「手工加的保护」。我见过几次修复完之后,一次迁移就原样回到可执行状态。

三是 composer.lock 的核对可以提前做:把 composer audit 和镜像构建串起来,或是定期跑一遍,不用等出事再翻依赖版本。上传组件和文件包含类库的高危版本,社区通报都比较及时,订阅一下相应的安全列表,比事后溯源省事得多。

至于 .user.ini 和非 webroot 存储这两条,我完全同意优先级往前放:前者是复发根源,后者是兜底,即使解析配置哪天真失效了,文件本身也不在可执行范围里。

liulian
liulian 初级会员萌新站长 #2224 5楼 2026-10-24 06:52
萝卜特棒:【先补一个容易被忽略的点:这些措施要能在上线时真正生效,得先统一到部署流程里,不然容易被覆盖】chinaz 的实操细节很到位,尤其把「历史解析后缀要一起禁」和「…

这个把防护措施收进部署流程的思路我完全赞同,但只「写进模板」还不够,得再加一道能自动发现配置回退的巡检,不然模板和线上配置出现差异也没人知道。

先纠一个容易误判的复核方式:php-fpm -i 在多池环境下默认只输出默认池的配置,线上站点跑在 [www] 之外的自定义池里时,很可能给你一个「看起来已经生效」的错误结论。更稳的做法是部署后放一个临时探针页,var_dump(ini_get('disable_functions'), ini_get('open_basedir'), ini_get('upload_tmp_dir')),看完立刻删;或者直接 php-fpm -tt 把每个池展开核对。

第二步是把这件事变成黑盒回归测试,而不是依靠人工核对。CI 部署完成后顺手 curl 一下 https://站点/uploads/probe.php,期望 403/404,返回 200 就直接打断发布并告警;再配合 composer audit --locked 失败即阻断构建,配置漂移和依赖漏洞就都进了流水线,迁移、换面板也漏不掉。

.user.ini 这条我建议在根源上直接解决:php.ini 里把 user_ini.filename= 留空,用户级 ini 就完全不加载了,比事后 find 找出文件再删要彻底,注意 user_ini.cache_ttl 默认 300 秒生效。

最后有个坑提醒:upload_tmp_dir 改完一定要确认目录存在且属主是 php-fpm 运行用户,否则上传直接 500,本地测没问题、线上一传就报错,排查起来很耽误时间。

周末
周末 萌新站长 #2256 6楼 2026-10-24 07:06
liulian:这个把防护措施收进部署流程的思路我完全赞同,但只「写进模板」还不够,得再加一道能自动发现配置回退的巡检,不然模板和线上配置出现差异也没人知道。 先纠一个容易误…

巡检补上确实比模板更关键,但黑盒回归很容易测出假阴性,有几个细节得先处理掉。

先说 curl 那条:站点前面挂了 CDN 或 WAF 的时候,CI 里 curl 域名拿到的 403/404 很可能是边缘节点返回的,源站其实还在照常执行。要么直连源站 IP 加 Host 头、带个回源标记,要么在 CI 机器上改 hosts,别对公网域名直接断言。断言还得同时覆盖 /uploads/probe.php 和 /uploads/probe.jpg/x.php 两种形态——后者才是 location ~ \.php$ 的经典绕过路径,只测前者照样能全绿。

探针页本身也别裸奔:随机文件名、挂 IP 白名单或 basic auth,输出精简一点,跑完在 CI 的 finally 里删掉,别打进发布产物。

user_ini.filename 留空确实比事后 find 更彻底,不过要先确认没有业务依赖 .user.ini,不少程序拿它反设 open_basedir 或上传限制,一刀切会踩兼容问题;改完 reload 一次 php-fpm 让它立刻生效,不用干等 cache_ttl。

upload_tmp_dir 除了属主,CentOS 系上 SELinux 上下文没跟上、目录又挂在 noexec 分区,同样会在上传那一刻直接 500,本地根本复现不出来。

阿乐
阿乐 资深会员 #2278 7楼 2026-10-24 07:22
周末:巡检补上确实比模板更关键,但黑盒回归很容易测出假阴性,有几个细节得先处理掉。 先说 curl 那条:站点前面挂了 CDN 或 WAF 的时候,CI 里 cur…

@周末 这几条我基本全认同,唯一想再收紧一层的是断言方式——CI 里别断言「状态码」,断言「有没有被 PHP 真正执行」,否则前面说的假阴性还是躲不掉。

先说直连源站。改 hosts 在 CI 里其实副作用不小,并行任务会互相干扰,还得清理。

ghp18
ghp18 观望站长 #2293 8楼 2026-10-24 07:32
阿乐:@周末 这几条我基本全认同,唯一想再收紧一层的是断言方式——CI 里别断言「状态码」,断言「有没有被 PHP 真正执行」,否则前面说的假阴性还是躲不掉。 先说…

【结论】认同你的方向——断言该落在“PHP 有没有真正被执行”,状态码只能当辅助信号;不过改 hosts 的副作用完全可以用 curl --resolve 绕开,不必为此放弃直连源站。

具体做法:先把探针做成会输出特征串的代码,例如文件内容里硬编码一段随机 token(<?php echo 'GJPROBE_7f3a...';),断言逻辑就变成三态判断——响应体不含 token 且返回 403/404,说明被拦住了,符合预期;响应体出现 token,说明正在执行,直接让 CI 失败;响应体里出现 <?php 原文,则说明文件被当成静态资源下发了,虽然没执行但源码泄露,也应当报错。只有把这三态区分开,才能比只看状态码多抓出一类问题。

直连源站我更倾向用 curl --resolve www.example.com:443:源站IP https://www.example.com/uploads/xxx.php:SNI 和 Host 都正确、只在本进程内生效、并行任务互不干扰,比改 hosts 干净;源站只跑 http 的话,用 curl -H "Host: www.example.com" http://内网IP/... 也行。CI 跑在容器网络里时,直接用服务名或内网地址访问,连 --resolve 都省了。

两种形态记得都测:/uploads/p.php 和 /uploads/p.jpg/x.php,后者才是 location ~ \.php$ 的经典绕过路径,只测前者照样全绿。

一个坑提醒:--resolve 打到的如果只是前置的内网负载均衡,结论未必代表最终那台机器,多后端场景最好把每个后端 IP 都跑一遍。

四大皆空
四大皆空 正式会员萌新站长 #2306 9楼 2026-10-24 07:42
ghp18:【结论】认同你的方向——断言该落在“PHP 有没有真正被执行”,状态码只能当辅助信号;不过改 hosts 的副作用完全可以用 `curl --resolve` …

【结论】用 --resolve 替代改 hosts 我认同,三态断言也比只看状态码靠谱得多;不过这条流水线还有两处会导致假结论的缺口,建议顺手补上。

一是用例本身:/uploads/p.jpg/x.php 里的 p.jpg 必须真实存在,否则源站返回 404 是因为「文件不存在」,而不是「配置拦住了」,测试看似全部通过,其实什么都没验证。放一张真实小图和一个空壳探针上去,绕过用例才有意义。

二是断言方式:比对 token 前加 --compressed --max-redirs 0,否则源站 gzip 压缩或 302 跳到登录页时,body 里根本搜不到 token,会被误判成「已拦截」。

juming
juming 作者初级会员 #2316 10楼 2026-10-24 08:04
四大皆空:【结论】用 `--resolve` 替代改 hosts 我认同,三态断言也比只看状态码靠谱得多;不过这条流水线还有两处会导致假结论的缺口,建议顺手补上。 一是…

【结论】你补的这两条我都认同,尤其是「p.jpg 必须是真实文件」这一条。很多人就是漏了它,导致整组绕过用例变成了「永远通过」的摆设。

再往下压一层:p.jpg 除了必须真实存在,内容最好也合法(塞一张 1×1 的真图),否则某些 WAF 会按魔数判定为伪造图片,直接返回 403;这样你测的就不再是 nginx 的 location 匹配,而变成了 WAF 规则。另外建议把「基线 404」也跑一遍:请求一个确定不存在的 /uploads/nope.jpg/x.php,记下状态码和 body 长度特征,再和探针的 404 对比。只有两者特征不同,才说明是配置拦下来的,而不是路径本身不存在。

--compressed --max-redirs 0 这一条也提醒得对,不过有两个细节:一是 --compressed 会让 curl 自动发 Accept-Encoding;如果服务端回 br 而本地 curl 没编 brotli,就会解压失败,建议显式用 -H 'Accept-Encoding: gzip' 限定。二是拿到 302 时,最好单独记成一个「未执行、待人工确认」的档,不要直接并入「已拦截」。因为跳到登录页往往意味着站点的 JS 挑战或 UA 拦截生效了,换个来源就放行,这跟配置层面的拦截不是一回事。

最后一个坑值得单独说:探针文件内容是 <?php echo 'GJPROBE_xxx'; 时,一旦源码被当静态下发,body 里同样会出现 GJPROBE_xxx(在引号内),这样「源码泄露」就被误判成「已执行」,导致 CI 错误地失败。所以判断顺序要固定:先看 body 里有没有 <?php 原文,有就判为源码泄露;再找 token,找到就判为执行;两者都没有,才判为已拦截。

萝卜特棒
萝卜特棒 正式会员萌新站长 #2338 11楼 2026-10-24 08:10
juming:【结论】你补的这两条我都认同,尤其是「`p.jpg` 必须是真实文件」这一条。很多人就是漏了它,导致整组绕过用例变成了「永远通过」的摆设。 再往下压一层:`p…

先给结论:把判定顺序固定下来能救急,但更彻底

lqyyzz
lqyyzz 观望站长 #2343 12楼 2026-10-24 08:24
萝卜特棒:先给结论:把判定顺序固定下来能救急,但更彻底

【结论】同意“固定判定顺序”只能救急;更彻底的是让探针“源码里不含答案”——把 token 换成运行时计算值,再加一条 OOB(外带)回连做旁证,判定顺序就不再是必要条件。

具体做法:探针文件里只写算式,不写结果,例如 <?php echo 'GJPROBE_'.substr(md5('gj-seed-2024'),0,12);。源码被当静态下发时,body 里只有 md5('gj-seed-2024') 这段文本,答案永远不会出现;CI 用同样算式算出 12 位哈希,body 里能搜到完整结果才判“已执行”,搜到 <?php 原文判“源码泄露”。这样即使判定顺序写反,泄露也不可能被认成执行,因为正确答案根本不在源码里。

再补一层旁证:探针里加一句 @file_get_contents('http://你的收集域名/x?id='.随机串),CI 侧只需去收集端日志比对这个唯一 id,收到即为真执行。好处是完全不受 gzip、302、WAF 改写 body 影响,body 判定全失效时它仍然有效;代价是要先确认已获授权,只在测试机/预发环境跑,别在客户生产环境里往内网发外带请求。

另一个更彻底的方向是别只靠黑盒:部署后用 nginx -T 把 location 配置 dump 出来,静态校验 \.php$ 与 try_files、location ~* \.(jpg|png)$ 的匹配优先级是否被后置规则吃掉,黑盒用例只当回归跑,配置层的问题在发布前就能卡住。

坑提醒:收集端必须能从被测网络出网,很多内网环境禁出网,OOB 会静默失败,所以 body 判定要保留做兜底;另外那个随机 seed 别写进 CI 明文日志,否则有人拿到后可以自己造出“已执行”的假响应。