PHP2Shell 三更:哪些打法能落地?附带防御规则
原创
messfree
messfree
MessFreeSecurity
2026年10月22日 12:15
北京
去阅读
一个只写了 <?php echo \"OK\"; ?> 的页面,没有登录、没有数据库、没有 session_start(),也可能被人用几个 POST 请求打成 shell。你写的代码没问题,不代表服务器没问题。
这不是段子。10 月 21 日,PHP 官方修了 CVE-2026-109618 和 CVE-2026-109619,CVSS 9.2;公开 PoC 在 lab 里把从 multipart 表单到命令执行的链路走通了,全程不用上传 webshell。
但先别急着转发“PHP 全完了”。CVSS 9.2 的分数很吓人,但分数不等于实战成功率。我的判断很直接:洞是真的,但没那么神;利用链也是真的,但没那么好复现。这条链很挑环境,地址泄漏也还没做稳。更值得看的是,洞在 PHP 引擎处理请求体和内存的那一层,不在框架里;Web 安全的战场,正在往下沉。
这条链很挑环境
公开链一句话能概括:换 worker、漏堆地址、漏 libphp 基址、伪造 HashTable 调 zif_system。全程只用 multipart POST,单次命令大约 14 字节。细节前两篇讲过,这里不重复。
| 它挑什么 | 公开链的情况 | 对防守方意味着什么 |
|---|---|---|
| 运行方式 | Apache prefork + mod\_php | PHP-FPM 的 worker 生命周期不同,不能直接套这条链;但官方通告确认过 FPM 侧另有路径 |
display_errors |
完整链需要 On;Off 下只完成第一阶段堆泄漏 | 生产多数是 Off;但 Off 不是安全边界:第二阶段的 libphp 泄漏和命令执行还没接上 |
| 内存和请求体 | lab 是 memory_limit=8M、请求体上限 64M |
默认 128M 内存加 8M 请求体,按 lab 的形状打不进来;把上传配置放得很宽的站点要小心 |
| worker 数量 | 单 worker | A/B 两个请求要落在同一个进程;仓库没给出多 worker 的同等验证 |
| 构建 | PHP 8.4.26 的特定构建 | 偏移跟版本、编译选项有关,换个发行版要重新校准;公开 exp 不是通用一键工具 |
所以,看到公开 PoC 就以为“所有 PHP 站点一键 RCE”,不对;觉得“默认配置打不了”就拖着不升,也不对。这张表说明的是,公开链现在更像一个 lab 复现,不是武器化 EXP。
漏地址这一步还没做稳
更麻烦的是,泄漏地址还没泛化。公开 exp 依赖 A/B 请求的字段顺序、大小、worker 历史,以及 Zend MM 的页分配;chunk 偏移和 libphp 偏移都是按 PHP 8.4.26 这套构建和请求形状校准的,远程模式默认用 lab 偏移,换应用、换构建就要重测。
display_errors=On 时,lab 里 ASLR 开启大约 9/10,失败靠重试;Off 时只完成 stage-1 堆泄漏,stage-2 读 libphp 和 stage-3 还没接上。要读真正的 HashTable,得让泄漏窗口和真实结构落在同一页,这需要三段式堆整形,不是调一个参数能解决的。
所以,窄的根因不只是配置,而是泄漏还绑在具体堆布局上。公开 exp 的窄,不是因为防守方做对了什么,而是因为攻击方还没把地址泄漏做成通用原语。看到公开 exp 别以为所有站点都能一键打;但也不能因为自己的环境不一样,就赌它打不中。
先看 mod\_php 老部署
最该先看 Apache mod\_php 老部署:prefork、单 worker、display_errors 开着、上传配置放得很宽。FPM 不能因为公开链没直接套上就放心;PHP 8.1 及更早已经 EOL,是否有回移要问供应商。
入口、主机、配置
-
入口:请求体里的
PHP_SESSION_UPLOAD_PROGRESS、响应里异常长的PHPSESSID、PHP 在 POST 阶段的 OOM 日志。 -
主机:
www-data下出现sh、bash、whoami、uname,父进程是apache2、httpd或php-fpm。 -
配置:不用上传进度就关
session.upload_progress;请求体和上传大小按业务收紧;ASLR 不要关;display_errors关掉,log_errors留着。
先做这几件事
- 盘点 PHP 版本、SAPI 和 MPM,升级到 8.5.12、8.4.27、8.3.36、8.2.35 或供应商回移版,重启 Apache/FPM。
- 不用上传进度条就关掉
session.upload_progress;要用,就限制请求体和上传大小。 - 上入口和主机告警,把请求、响应、PHP 错误日志和进程树关联起来。不要把
display_errors=Off或一条 WAF 规则当成修复。
最后聊两句:底层、AI 和一场能力秀
这次漏洞出在 PHP 引擎处理请求体和内存的那一层。公开链要拼堆布局、漏地址、伪造 HashTable,都是二进制利用的手艺。漏洞挖掘正在往底层走:语言运行时、内存分配器、JIT、浏览器引擎、内核,会越来越多地成为入口。AI 把“找 bug”的成本打下来,但没把利用、修复、推动升级的成本打下来;同一套能力,攻击者也能用。
这次事件的背景也很有意思。公开报道里,Calif 用 OpenAI 的 GPT-5.6 Sol 发现了其中一个缺陷,Wiz 独立发现并报告;从发现、协调、修复到对外发布,OpenAI 几乎全程在场,但不是唯一的发现者。它牵头协调了约两个月,对外称为“防御者之窗”,还发布了版本检查工具、Codex 技能和 10 亿美元计划。
这些动作本身没问题,漏洞是真的,补丁也是真的。但传播节奏很像一次精心安排的能力展示:发现、协调、修复、博文、基金、工具,全挤在同一周;Wiz 的“影响 70% 互联网”标题很抓眼,统计口径却没解释清楚;披露还因为 CVE 信息提前流传、Chainguard 提前发布修复镜像而被迫提前,PHP 公开指责对方破坏协调披露。
我不认为漏洞是编的,也不认为补丁是演的。但说这次事件完全没有品牌叙事的考虑,我是不信的。说得直白一点,我不排除这是一次借真实漏洞做的能力营销:漏洞是真的,补丁是真的,但叙事被用到了极致。 我的立场很简单:这个洞值得修,但不值得封神。它的技术含量在运行时内存管理,不在“一键 RCE”;CVSS 9.2 是严重性评分,“七成互联网”是传播语言,都不是实战成功率。技术事实和公关叙事可以同时为真。判断它是不是纯炒作,不看发布会,看后续:方法论能不能复现,修复能不能持续,小站点有没有真的被覆盖。
还有一件事,这几天的国内安全公众号,我看到的讨论大多没提:ThinkPHP、Discuz(DZ)这些国内部署量很大的 PHP 应用,只要跑在未修复的 PHP 上,同样在这条链的覆盖范围里。漏洞不在框架里,但框架跑在 PHP 上;升级 ThinkPHP 或 Discuz,不等于升级了 PHP。可公开传播里,不少厂商公众号更多在转 OpenAI、GPT-5.6 和“防御者之窗”,国内的应用栈反而没人提。节奏是别人带的,风险是自己扛的。
洞是真的,但没那么神;补丁不能拖。先升级,再谈规则。
附:规则和配置怎么落地
公开规则不少,但不是每条都适合直接上生产。下面按“先看规则、再上网络侧、再补主机侧、最后调配置”的顺序整理,规则都先当告警用。
1. 公开规则先筛一遍
| 来源 | 能用的 | 要注意的 |
|---|---|---|
| Corelight / Suricata 3000650 | 请求体里出现 PHP_SESSION_UPLOAD_PROGRESS,覆盖面最广 |
没限制 Content-Type、没阈值,误报高;只告警 |
| Corelight 3000651 / 3000652 | PHP 版本头、phpinfo 的版本发现 | 这是资产发现,不是攻击检测;发行版回移会误判 |
| Snort 67360 | multipart + 标记,5 秒 10 次 | 单请求、慢速会漏;正常上传进度条会误报 |
| Snort 67375 | 响应 Cookie 带 NUL | 强特征;写死 PHPSESSID,换 cookie 名或编码会漏 |
| Snort 67359 | 序列化整数碰撞样本 | 只覆盖特定样本,换组整数就绕过 |
| SigmaHQ 通用 webshell 规则 | 父进程 web server + 子进程侦察工具 | 没覆盖 php-fpm 父进程和 sh/bash,需要补 |
| ikow ModSecurity 9900100 | 标记阻断 | 用原始 body,Content-Type 大小写敏感;正常上传进度会误伤 |
| ikow ModSecurity 9900104 / 9900105 | OOM、session 异常 | display_errors=Off 时响应里没有,只能查日志 |
| ikow ModSecurity 9900106 | 封 chunked multipart | 不建议直接上线,正常流式上传也会用 |
| ikow OpenResty Lua | 请求体标记 | 只扫前 64KB、Content-Type 区分大小写、临时文件只读第一段 |
| Cloudflare 托管 WAF | 托管规则 | 只公开 ID,表达式不可调;注意 body 检查上限 |
2. 网络侧:先上三条告警
第一条,请求体标记。 ModSecurity 用解析后的字段名,别直接匹配原始 body;Content-Type 要忽略大小写;先 pass,log。
# 仅告警。需要 SecRequestBodyAccess On;依赖 multipart 解析。 SecRule REQUEST_METHOD \"@streq POST\" \\ \"id:1001001,phase:2,pass,log,t:none,severity:'WARNING',msg:'PHP2Shell heuristic: upload-progress multipart field',tag:'php2shell-heuristic',chain\" SecRule REQUEST_HEADERS:Content-Type \"@rx (?i)^multipart/form-data(?:[[:space:]]*;|$)\" \"t:none,chain\" SecRule ARGS_POST_NAMES \"@streq PHP_SESSION_UPLOAD_PROGRESS\" \"t:none\"
Suricata 可以在 Corelight 3000650 的基础上加 multipart 和阈值,减少噪声:
alert http $EXTERNAL_NET any -> $HOME_NET any (msg:\"PHP2Shell upload progress marker\"; flow:established,to_server; http.method; content:\"POST\"; http.content_type; content:\"multipart/form-data\"; nocase; http.request_body; content:\"PHP_SESSION_UPLOAD_PROGRESS\"; fast_pattern; threshold: type limit, track by_src, count 1, seconds 30; classtype:web-application-attack; sid:1009201; rev:1;)
Snort 3 直接用社区规则 67360。正常上传进度条会误报,先 alert,不要直接 drop。
第二条,响应侧异常 Cookie。 Snort 67375 已经覆盖 PHPSESSID 加 NUL。Suricata 可以写:
alert http $HOME_NET any -> $EXTERNAL_NET any (msg:\"PHP2Shell encoded NUL in PHPSESSID response\"; flow:established,to_client; http.response_header; content:\"PHPSESSID=\"; fast_pattern; pcre:\"/PHPSESSID=[^;\\r\
]*%00%00/\"; classtype:web-application-attack; sid:1009202; rev:1;)
正常 PHPSESSID 很短;超过 1KB、含大量百分号编码、含 NUL,都值得看。不要为了“防泄漏”去剥离 Set-Cookie,会把业务会话打坏。
第三条,OOM 和 session 异常。 日志或响应里看:
-
Allowed memory size ... exhausted -
Session ID is too long or contains illegal characters -
Failed to read session data -
Session object destruction failed
单独出现可能是业务问题,和上传进度字段、异常 Cookie、worker 崩溃一起看,才有判断价值。
另外两条补充:Corelight 3000651 / 3000652 用来盘版本和 phpinfo,不是攻击检测;Snort 67359 是特定样本特征,保留但不指望它覆盖所有变体。
3. 主机侧:盯进程树,不要只看告警
SigmaHQ 的通用规则抓父进程 web server、子进程 whoami/uname 等,但没覆盖 php-fpm 和 sh/bash。补一条:
title: PHP2Shell PHP Worker Spawns Shell Or Recon Utility id: 8f3c2a1e-7b64-4d2a-9c5e-1a2b3c4d5e6f status: experimental logsource: product: linux category: process_creation detection: parent_web: ParentImage|contains: - '/php-fpm' - '/apache2' - '/httpd' child_shell: Image|endswith: - '/sh' - '/bash' - '/dash' child_recon: Image|endswith: - '/whoami' - '/id' - '/uname' - '/cat' - '/curl' - '/wget' - '/nc' condition: parent_web and (child_shell or child_recon) falsepositives: - Web applications that legitimately shell out to system tools level: high
业务本来就会调系统命令的站点会吵;和请求日志、异常 Cookie、OOM 日志关联。没有进程创建日志(auditd、eBPF、Falco)这条规则就是摆设。
文件和外连也要看:公开链当前版本不要求写目标文件,但命令执行后可能落脚本、写 proof、下载二阶段。盯 www-data 对 /tmp、/dev/shm、webroot 的异常写入,以及出站连接。容器环境可以再收紧:只读根文件系统、最小镜像、去掉不必要的 shell 和包管理器。
4. OpenResty:别只扫前 64KB
公开 Lua 只读前 64KB,Content-Type 区分大小写,临时文件只读第一段。建议改成下面这个形状,先 log-only:
local marker = 'name=\"PHP_SESSION_UPLOAD_PROGRESS\"' local limit = 16 * 1024 * 1024 local function is_multipart() local ct = ngx.req.get_headers()[\"content-type\"] return type(ct) == \"string\" and ct:lower():find(\"multipart/form-data\", 1, true) ~= nil end local function scan_body() ngx.req.read_body() local body = ngx.req.get_body_data() if body then return body:sub(1, limit):find(marker, 1, true) ~= nil, #body > limit end local path = ngx.req.get_body_file() if not path then return false, true end local fh = io.open(path, \"rb\") if not fh then return false, true end local read, tail, hit = 0, \"\", false while read < limit do local part = fh:read(math.min(8192, limit - read)) if not part then break end read = read + #part local window = tail .. part if window:find(marker, 1, true) then hit = true break end tail = window:sub(-(#marker - 1)) end local incomplete = (not hit) and (read == limit and fh:read(1) ~= nil) fh:close() return hit, incomplete end if ngx.req.get_method() == \"POST\" and is_multipart() then local hit, incomplete = scan_body() if hit then ngx.log(ngx.WARN, \"PHP2Shell heuristic: upload progress; uri=\", ngx.var.request_uri) elseif incomplete then ngx.log(ngx.WARN, \"PHP2Shell heuristic: body scan incomplete; uri=\", ngx.var.request_uri) end end
放到 PHP 转发 location 的 access_by_lua_file。扫描不完整要单独记日志,否则漏检看起来像没攻击。
5. 配置:哪些真的影响堆喷射和地址泄露
| 配置 | 影响 | 建议 |
|---|---|---|
session.upload_progress.enabled |
决定 RINIT 前的 session 初始化路径 | 业务不用就设 0,这是最直接的一条 |
post_max_size / upload_max_filesize |
限制 OOM 和堆喷射可用的请求体大小 | 按业务最大上传设置,不要无限 |
memory_limit |
OOM 阈值 | 按业务设置;太低反而帮攻击者,不要照抄 lab 的 8M |
max_input_vars |
限制字段数量 | 按表单需要设置;只影响一条触发路径,不能挡住整条链 |
display_errors / log_errors |
警告输出和泄漏辅助 | Off + On;是卫生习惯,不是修复 |
ASLR kernel.randomize_va_space |
地址随机化 | 保持 2;不要用 setarch -R 启动 Apache |
Apache LimitRequestBody |
入口请求体上限 | 默认 0 是不限;设成业务上限,记录 413 |
Nginx client_max_body_size |
入口请求体上限 | 默认 1m;按业务设置,记录 413 |
ModSecurity SecRequestBodyLimit / SecRequestBodyNoFilesLimit / LimitAction |
WAF 能看多少 body | 默认约 12.5MB / 128KB;普通字段超过 128KB 后规则可能看不到;确认 Reject 还是 ProcessPartial |
Suricata request-body-limit / request-body-inspect-window |
IDS 能看多少请求体 | 默认约 100KB / 4KB;调到覆盖业务上传,回放验证 |
OpenResty max_body_read |
Lua 扫描长度 | 公开示例 64KB;调到业务上限,临时文件分块读 |
worker 数量 / KeepAlive |
A/B 是否落同一进程 | 单 worker 是公开链条件,不是防御;不要为了防攻击改单 worker |
堆喷射这条线,最有用的是:关掉不需要的 session.upload_progress,每一层都限制请求体,WAF/IDS 的 body 上限要能看见大字段。ModSecurity 的 SecRequestBodyNoFilesLimit 默认只有 128KB 量级,而公开链用十几 MB 的普通字段做 OOM;规则要么看不到,要么被引擎拒绝,观察期别把“没报”当成“没攻击”。
另外,ModSecurity v3 在 DetectionOnly 下会自动把 SecRequestBodyLimitAction 设为 ProcessPartial,超出上限的字段不会被检查。观察期更要留意这个盲区。
地址泄漏这条线,最有用的是:ASLR 保持开启,display_errors=Off 加 log_errors=On,响应侧盯异常长的 PHPSESSID。不要剥离 Set-Cookie,会打坏会话;也不要靠“我的构建不一样”来赌安全。
不建议做的“防御”:把 Apache 改单 worker、关 ASLR、把 memory_limit 调到 8M、一刀切封 chunked multipart、全封文件名里的 <?php、只加 WAF 不升级。
6. 上线顺序
- 先观察:ModSecurity 用
DetectionOnly或pass,log,Suricata/Snort 先 alert,OpenResty 先 log-only,Sigma 只告警。跑 24 到 72 小时,收集误报。 - 加例外:正常使用上传进度条的业务、大文件上传端点,逐条加白名单,不要全局关规则。
- 再拦截:只对高置信度组合拦截,比如“上传进度字段 + 大请求体”“异常 Cookie + OOM 日志”“PHP 父进程拉起 shell”。响应侧规则保持告警。
- 做验证:用授权流量回放,确认规则能命中,确认 403/413 没有打崩业务,确认 WAF/IDS 的 body 上限足够。
- 看日志:
modsec_audit.log、Suricatafast.log、Snortalert、PHP error log、Apache error log、进程树和容器运行时事件。
规则语法检查:
snort -T -c /path/to/snort.lua -R snort3.rules suricata -T -S suricata.rules -c /path/to/suricata.yaml luac -p openresty_php2shell.lua
ModSecurity 和 OpenResty 还需要在对应测试环境做配置加载和授权流量回放。规则 ID 先查冲突;上面的 100xxxx 只是示例。
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论