<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>CVE-2026-109618 on 搬砖程序员带你飞</title>
        <link>https://lpflpf.cn/tags/cve-2026-109618/</link>
        <description>Recent content in CVE-2026-109618 on 搬砖程序员带你飞</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>zh-CN</language>
        <managingEditor>lipengfei634626165@163.com (lpflpf)</managingEditor>
        <webMaster>lipengfei634626165@163.com (lpflpf)</webMaster>
        <lastBuildDate>Fri, 23 Oct 2026 07:30:00 +0800</lastBuildDate><atom:link href="https://lpflpf.cn/tags/cve-2026-109618/index.xml" rel="self" type="application/rss+xml" /><item>
            <title>PHP 核心爆了个 9.2 分的洞：你的代码一行都还没跑，服务器已经被人进去了</title>
            <link>https://lpflpf.cn/posts/php2shell-sapi-triage-2026-10-23/</link>
            <pubDate>Fri, 23 Oct 2026 07:30:00 +0800</pubDate><author>lipengfei634626165@163.com (lpflpf)</author>
            <guid>https://lpflpf.cn/posts/php2shell-sapi-triage-2026-10-23/</guid>
            <description>&lt;img src=&#34;https://cdn.lpflpf.cn/covers/php2shell-sapi-triage-2026-10-23-v2-plain.png&#34; alt=&#34;Featured image of post PHP 核心爆了个 9.2 分的洞：你的代码一行都还没跑，服务器已经被人进去了&#34; /&gt;&lt;p&gt;这周最难受的一类人，是手里攥着十来个 PHP 入口的运维。&lt;/p&gt;&#xA;&lt;p&gt;一边是 CVSS 9.2、未认证 RCE、新闻刷了三天；另一边是现实：WordPress 站点的插件没测过兼容性，祖传的 Drupal 只在 7.4 上跑通过，vendor 目录里塞的那个 php 二进制没人敢碰。&lt;strong&gt;一天升不完，这是常态，不是懒惰。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;这次两个洞（CVE-2026-109618 和 CVE-2026-109619）不在你的应用里，在 PHP 清理失败请求的那几毫秒里。所以你代码写得再干净也躲不开——它出事前，你一行都还没跑。这篇文章要解决的是排期问题：今晚只能动三台机器，先动哪台。以及怎么确认你真修好了，而不是看起来修好了。&lt;/p&gt;&#xA;&lt;p&gt;先对个口径，省得你被各种版本的标题绕晕：9.2 是 CVSS v4.0 的分数，NVD 上 v3.1 给的是 9.8。披露乱掉那天流传过&amp;quot;10.0 分&amp;quot;的说法，那是错的。&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;补丁是 8.2.35、8.3.36、8.4.27、8.5.12 四个版本。8.1 及更早，官方不再发更新。&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;p&gt;&lt;img alt=&#34;两条泳道对比 mod_php 与 PHP-FPM 处理一次失败请求的时间顺序：mod_php 在应用代码运行之前就执行 request shutdown，FPM 在 request startup 失败时直接终止子进程；下方单独标出 109619 需要同一 worker 上的生产者与消费者两个请求&#34; loading=&#34;lazy&#34; sizes=&#34;(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px&#34; src=&#34;https://cdn.lpflpf.cn/images/posts/php2shell-sapi-timeline-2026-10-23.png&#34;&gt;&#xA;&lt;/p&gt;&#xA;&lt;h2 id=&#34;请求还没进到你的代码php-已经在清理它了&#34;&gt;请求还没进到你的代码，PHP 已经在清理它了&#xA;&lt;/h2&gt;&lt;p&gt;先立一个反直觉的前提：这两个洞的触发阶段，在你的框架、路由、中间件之前。&lt;/p&gt;&#xA;&lt;p&gt;现代 PHP 收到一个请求，先启动请求上下文，引擎自动把 body 解析出来，multipart/form-data 就是在这一环解析的，然后才调用你的应用代码。自动解析是引擎干的，不经你的手——你也没法在校验函数里拦它，因为那会儿你还没机会执行任何一行代码。&lt;/p&gt;&#xA;&lt;p&gt;109618 的触发玩法，全长在&amp;quot;引擎自己解析 body&amp;quot;这一环上。共同点一句话：看着没超你设的限额，实际上把 &lt;code&gt;memory_limit&lt;/code&gt; 吃穿了。公告点了三种情况。请求没有可信的 &lt;code&gt;Content-Length&lt;/code&gt;，解析器预判不了规模。单个 part 的 header 数量没有上限，网络上的字节可以很小，展开到内存里可以很大。空的文件控件不计入 &lt;code&gt;max_file_uploads&lt;/code&gt;，可 PHP 照样给它们建元数据数组。三条都不需要你提供一个正常表单——攻击者自己造 HTTP 请求就够了。&lt;/p&gt;&#xA;&lt;p&gt;内存耗尽以后，PHP 用 &lt;code&gt;longjmp()&lt;/code&gt; 从当前上下文里&amp;quot;跳&amp;quot;出去，这个请求的正常收尾流程就直接放弃了。问题出在这个&amp;quot;跳&amp;quot;上：有些上下文里，请求的内存已经可达、内容却还没初始化完。这时候只要有一次分配失败——不用跟那块内存有关系，任何一次失败都行——清理流程就留在无效状态上。接着 request shutdown 去读它、按它分支，攻击就成了。&lt;/p&gt;&#xA;&lt;p&gt;反常识就反在这个&amp;quot;任何一次&amp;quot;上。攻击者不需要精确命中什么，只要让这个进程在错误的时间点 OOM 一次。这种形状的洞，代码审查防不住，输入校验防不住，&amp;ldquo;我们没用上传功能&amp;quot;也防不住——出问题的不是你的判断逻辑，是引擎跳出去之后没收拾干净的那摊东西。&lt;/p&gt;&#xA;&lt;p&gt;所以先把&amp;quot;我代码写得干净&amp;quot;这个心理安慰放一边。决定你这台机器今晚急不急的，不是版本号，是它怎么跑 PHP。&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;版本号回答的是&amp;quot;有没有补丁&amp;rdquo;。SAPI 回答的是&amp;quot;你等不等得起&amp;quot;。这是两个问题。&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;h2 id=&#34;同样是-phpmod_php-是来不及防fpm-是条件成立才防不住&#34;&gt;同样是 PHP，mod_php 是来不及防，FPM 是条件成立才防不住&#xA;&lt;/h2&gt;&lt;p&gt;这一段是全文重点，也是中文报道里最容易被糊成一句话的地方。&lt;/p&gt;&#xA;&lt;p&gt;官方公告说得很直白：风险最高的是 mod_php。它已经被演示出完整利用链，一个未认证请求，在任何应用代码运行之前就能触发。原因很具体——mod_php 挂在 Apache 进程里，request startup 失败了，它照样会执行 request shutdown。唯一能拦住攻击的时机已经错过，后面的清理照跑，而 109618 要吃的就是清理这一步。&lt;/p&gt;&#xA;&lt;p&gt;PHP-FPM 走的是另一条路。请求启动失败，FPM 直接杀掉这个子进程，正常收尾根本没机会跑，这条路就断了。所以公告的原话是：源码分析支持 FPM 也属于可能受影响的 SAPI，&lt;strong&gt;但这个 SAPI 上没有已演示的完整 exploit&lt;/strong&gt;。&lt;/p&gt;&#xA;&lt;p&gt;这句话极容易被读成&amp;quot;FPM 安全&amp;quot;。不是。它只说明 109618 这一支上，FPM 少了一环被证实的链条，源码层面仍然列在受影响里。&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;官方没有演示出 FPM 上的完整利用链，但这句只能读成&amp;quot;少一环&amp;quot;，不能读成&amp;quot;安全&amp;quot;。少的那一环是 exploit，不是条件。&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;p&gt;更要紧的是第二个洞。109619 是另一套机制：PHP 内部有一批全局状态，临时指向&amp;quot;当前请求的内存&amp;quot;。某些情况下这些状态没被正确重置，或者重置完又被写了回去——请求内存都释放了，全局指针还挂着。等下一个请求在正常初始化之前去消费这块过期状态，就出事了。&lt;/p&gt;&#xA;&lt;p&gt;它的形状是&amp;quot;生产者 + 消费者&amp;quot;，而且两个请求必须落在同一个 worker 上：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;生产者&lt;/strong&gt;：一次致命错误。解析失败、编译失败、未捕获异常、&lt;code&gt;require&lt;/code&gt; 失败、执行超时，都会在配对的清理动作之前放弃操作。另外，shutdown 阶段扩展发出的非致命诊断，也能把 error 状态在清掉之后再写回去。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;消费者&lt;/strong&gt;：初始化期间的 INI 诊断——非法或弃用配置触发的 warning、notice，不用显示、不用记日志都算。还有 session upload progress，默认就开着，也不需要你调用过 &lt;code&gt;session_start()&lt;/code&gt;。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;mod_php 上，8.5 默认开启的 fatal error backtrace 这条路已经打通了完整 RCE。FPM 上验证过的是 session 生命周期那条：成功启动一次，应用执行期留下状态，再来一次致命错误，下一个请求拿 upload progress 去消费。公告专门写了一句：多 worker 和 worker 回收会降低单请求成功率，但不消除条件。&lt;/p&gt;&#xA;&lt;p&gt;所以正确的分档是三句话：&lt;/p&gt;&#xA;&lt;ul&gt;&#xA;&lt;li&gt;&lt;strong&gt;mod_php&lt;/strong&gt;：来不及防。两条 CVE 都有确认的完整链，先动它。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;FPM&lt;/strong&gt;：条件成立才防不住。109618 没有已演示的完整 exploit；109619 的 session 路径验证过，但要一次应用级操作、一次致命错误、下一个请求还得撞上同一个 worker。8.5 还要多看一眼 &lt;code&gt;fatal_error_backtraces&lt;/code&gt; 是不是默认的 1。&lt;/li&gt;&#xA;&lt;li&gt;&lt;strong&gt;其它&lt;/strong&gt;：&lt;code&gt;php-cgi&lt;/code&gt;、FrankenPHP、LiteSpeed 的 LSAPI。官方的判断是&amp;quot;很可能受影响&amp;quot;和&amp;quot;类比 FPM，很可能但未验证&amp;quot;。按同等暴露处理，别因为&amp;quot;没有 exploit 报道&amp;quot;就把它们排后面。&lt;/li&gt;&#xA;&lt;/ul&gt;&#xA;&lt;p&gt;怎么知道自己是哪一档？一条命令：&lt;code&gt;php -i | grep -E &#39;^(Loaded Configuration File|Server API)&#39;&lt;/code&gt;。看到 &lt;code&gt;Apache 2.0 Handler&lt;/code&gt; 就是 mod_php——这个 SAPI 在宝塔面板、老虚拟主机、有些年头的老项目里还很常见。看到 &lt;code&gt;FPM/FastCGI&lt;/code&gt; 才是 FPM。这一步比查版本号重要，因为版本号告诉你&amp;quot;有没有补丁&amp;quot;，SAPI 告诉你&amp;quot;等不等得起&amp;quot;。&lt;/p&gt;&#xA;&lt;h2 id=&#34;官方那套缓解措施最容易死在配了但没生效上&#34;&gt;官方那套缓解措施，最容易死在&amp;quot;配了但没生效&amp;quot;上&#xA;&lt;/h2&gt;&lt;p&gt;公告给的临时缓解，两条都带着同一个前缀要求：得用应用改不掉的方式设置，并且重启每一个 worker。这个要求看着像废话，恰恰是绝大多数人踩坑的地方。&lt;/p&gt;&#xA;&lt;p&gt;为什么&amp;quot;应用改不掉&amp;quot;是硬要求？因为 109619 的消费发生在正常初始化之前。&lt;code&gt;ini_set()&lt;/code&gt; 是应用代码里跑的，&lt;code&gt;.user.ini&lt;/code&gt; 也是请求跑起来才生效——等它们执行，消费者早过去了。&lt;/p&gt;&#xA;&lt;p&gt;所以 &lt;code&gt;session.upload_progress.enabled=0&lt;/code&gt; 必须落在 php.ini 里、FPM pool 的 &lt;code&gt;php_admin_value&lt;/code&gt; 里，或者服务器配置里。改完不重启 worker，老进程还带着原值在跑，你以为关了，其实没关。这个功能默认开着，用的人却极少——官方自己都建议长期关掉，理由很直白：它属于&amp;quot;极少被测试的攻击面&amp;quot;。&lt;/p&gt;&#xA;&lt;p&gt;&lt;code&gt;memory_limit=-1&lt;/code&gt; 这条要单独说，因为它有两个反直觉的坑。第一，它不是完整缓解。拿掉 PHP 自己的内存上限，是为了不走 &lt;code&gt;longjmp()&lt;/code&gt; 那条路；可底层分配失败的时候，PHP 照样可能跳。第二，&lt;code&gt;RLIMIT_AS&lt;/code&gt; / &lt;code&gt;RLIMIT_DATA&lt;/code&gt; 会反向把那条路打开——ulimit 让 &lt;code&gt;mmap()&lt;/code&gt; 失败，正好制造出 109618 想要的失败点。很多人觉得&amp;quot;加个 ulimit 更保险&amp;quot;，在这儿是反的。&lt;/p&gt;&#xA;&lt;p&gt;代价也得算。&lt;code&gt;memory_limit=-1&lt;/code&gt; 等于把天花板拆了，一个正常但超大的请求就能拖垮整机。所以配套必须跟上：前端限额按解码后大小、字段数、part 数、header 数、嵌套深度来卡，别忘了&amp;quot;没有可信 Content-Length&amp;quot;的请求；&lt;code&gt;post_max_size&lt;/code&gt; 没上传给 8M、有上传给 32M，&lt;code&gt;upload_max_filesize&lt;/code&gt; 压在它下面；再有就是 &lt;code&gt;pm.max_children&lt;/code&gt;。容器或 cgroup 限额只能当可用性边界，而且要保证它挂的时候只杀单个 worker——杀掉 master，整池带走。&lt;/p&gt;&#xA;&lt;p&gt;还有个配置差异值得记：&lt;code&gt;fatal_error_backtraces&lt;/code&gt; 是 PHP 8.5 才引入的，默认值 1。在 8.2、8.3、8.4 上把它写进配置，你得到的是一个 unknown directive，不是&amp;quot;关掉了&amp;quot;。在 8.5 上它恰恰是 109619 的一条已知通路，官方建议改 0。&lt;/p&gt;&#xA;&lt;p&gt;至于 &lt;code&gt;error_reporting&lt;/code&gt;、&lt;code&gt;display_errors&lt;/code&gt;、&lt;code&gt;log_errors&lt;/code&gt;、&lt;code&gt;@&lt;/code&gt; 这些&amp;quot;把报错压下去&amp;quot;的手段，对 109619 全无效。原因还是那句：消费发生在初始化前，压不压显示跟它执行不执行没关系。日志干干净净，不能当安全的证据。&lt;/p&gt;&#xA;&#xA;    &lt;blockquote&gt;&#xA;        &lt;p&gt;这一类配置的共性不是&amp;quot;难配&amp;quot;，是&amp;quot;配对了也会白配&amp;quot;：要么配在应用改不到的地方，要么压根没重启 worker。&lt;/p&gt;&#xA;&#xA;    &lt;/blockquote&gt;&#xA;&lt;p&gt;最后说个不常见的建议：&lt;code&gt;pm.max_requests=1&lt;/code&gt;，每个 worker 只处理一个请求就退休。跨请求残留状态这事从根上掐死——没有第二个请求，就没有消费者。代价是进程频繁 fork、吞吐明显掉，只适合低流量的入口：内网管理端、低频后台。这是拿性能换确定性的一档选项，不是通用解法。&lt;/p&gt;&#xA;&lt;h2 id=&#34;三个能证伪我已经修好了的检查以及这个洞为什么修了两年&#34;&gt;三个能证伪&amp;quot;我已经修好了&amp;quot;的检查，以及这个洞为什么修了两年&#xA;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;php -v&lt;/code&gt; 为什么不可信？三个具体原因：CLI 和 FPM 读的是两份 ini，容器里的 php.ini 和宿主机的也不是同一份；你升级了包但没重启 worker，老进程还在跑旧二进制；一台机器好几个 pool，你只改了其中一个。&lt;/p&gt;&#xA;&lt;p&gt;所以确认&amp;quot;修好了&amp;quot;要用证据，不能用版本号自报。三个检查，全是只读的。&lt;/p&gt;&#xA;&lt;p&gt;第一，看清你看的是哪个 SAPI、哪份 ini。&lt;code&gt;php -i | grep -E &#39;^(Loaded Configuration File|Server API)&#39;&lt;/code&gt;。FPM 环境要看 pool 的 phpinfo 或者 &lt;code&gt;php-fpm -i&lt;/code&gt;，不是终端里那个 &lt;code&gt;php -i&lt;/code&gt;。然后 &lt;code&gt;docker exec &amp;lt;容器&amp;gt; php-fpm -v&lt;/code&gt; 拿版本，&lt;code&gt;ps -o lstart -p $(pgrep -f &#39;php-fpm: pool&#39;)&lt;/code&gt; 拿 worker 的启动时间。&lt;strong&gt;只有启动时间晚于你升级的时间，这批 worker 才算换过血。&lt;/strong&gt;&lt;/p&gt;&#xA;&lt;p&gt;第二，确认缓解值真的生效在这份配置里。&lt;code&gt;php -i | grep -E &#39;session.upload_progress.enabled|memory_limit|post_max_size|fatal_error_backtraces&#39;&lt;/code&gt;，同样要在 FPM 的 phpinfo 里看。注意 8.2 到 8.4 上 &lt;code&gt;fatal_error_backtraces&lt;/code&gt; 应该是&amp;quot;不存在&amp;quot;，不是 Off——它要是显示 Off，说明你其实在 8.5 上，版本判断错了。&lt;/p&gt;&#xA;&lt;p&gt;第三，按这个顺序过一遍你名下的机器。先看 Server API 是不是 &lt;code&gt;Apache 2.0 Handler&lt;/code&gt;，是就先动；FPM 的看 109619 的前置条件——有没有对外上传、是不是 8.5、&lt;code&gt;fatal_error_backtraces&lt;/code&gt; 是不是 1；&lt;code&gt;php-cgi&lt;/code&gt;、FrankenPHP、LiteSpeed 按同等暴露处理；再核对引擎版本对不对号：8.2.35、8.3.36、8.4.27、8.5.12；最后确认 worker 全部重启过——&lt;code&gt;pm.max_requests&lt;/code&gt; 设小可以加速旧 worker 退场，这本身就是个过渡手段。&lt;/p&gt;&#xA;&lt;p&gt;至于 PHP 7.4、8.0、8.1：官方给了源码补丁，分支名形如 &lt;code&gt;PHP-7.4-CVE-2026-109618-109619&lt;/code&gt;，但那是源码，不是发行版包。没人兜底的话，选项只剩俩：自己编译，或者下线。CISA 的建议也一样直白：一时修不了的，先下线。&lt;/p&gt;&#xA;&lt;p&gt;再说一个很多报道跳过的部分：这个洞为什么放到了现在。&lt;/p&gt;&#xA;&lt;p&gt;PHP Foundation 自己在复盘里承认这是&amp;quot;结构性失败&amp;quot;。根因 2024 年就有苗头，一次外部安全审计里，有核心开发修别的问题时已经看到 OOM 处理不对劲的影子，但当时只有理论可能，没有具体触发路径。之后几年，修复方案轮流卡住：有的破坏兼容性，有的引入新的 DoS 面，有的要求大量第三方扩展跟着改。共识制下每种方案都要谈很久，「延续」和「重启」来回了好几轮。8 月 11 日，Calif 的 Ryan A 报告了同根因的无条件 RCE，修法才算真正锁定。&lt;/p&gt;&#xA;&lt;p&gt;而最后的修法，动的是引擎的 OOM 退出机制本身。8.5.12 的 NEWS 里那条只有一行，&amp;ldquo;Avoid state corruption via bailout&amp;rdquo;，公告把它解释清楚了：OOM 时不再 &lt;code&gt;longjmp()&lt;/code&gt;，改成在下一个 VM 安全点触发的延迟软退出；内存继续涨到第二个更高的阈值，就 &lt;code&gt;_exit()&lt;/code&gt; 硬退。这样 shutdown 再也见不到无效状态，PHP 限制内存的能力也保住了。&lt;/p&gt;&#xA;&lt;p&gt;&lt;img alt=&#34;两个漏洞的机制对照：109618 由 longjmp 提前退出留下未初始化的内存、在 request shutdown 时被读取；109619 由全局状态在请求结束后仍指向已释放的内存，需要同一个 worker 先产生再消费该状态&#34; loading=&#34;lazy&#34; sizes=&#34;(max-width: 767px) calc(100vw - 30px), (max-width: 1023px) 700px, (max-width: 1279px) 950px, 1232px&#34; src=&#34;https://cdn.lpflpf.cn/images/posts/php2shell-two-cves-v3-2026-10-23.png&#34;&gt;&#xA;&lt;/p&gt;&#xA;&lt;p&gt;还有一件绕不开的事，是这次的披露节奏。10 月 20 日，两个 CVE 编号在论坛和社媒上流出来，还带着错误的分值和错误的发布时间；官方把预公告和发布都提前了一天。更麻烦的是 10 月 21 日早上，比协调披露早了六个多小时，Chainguard 单方面打破禁运，发布了含修复的镜像——按 PHP Foundation 的说法，这等于提前把二进制形态的补丁递到了攻击者手里。同一天，AWS 的威胁情报团队说，他们 10 月 17 日的蜜罐里就已经看到针对这个漏洞的尝试，来自商业匿名化网络。&lt;/p&gt;&#xA;&lt;p&gt;这几个说法得放一起读。AWS 说的是&amp;quot;有针对性的尝试&amp;quot;，他们自己也不清楚这些尝试在真实系统上能不能成；CISA 在 10 月 21 日的公告里写的是&amp;quot;截至发布时未观察到利用&amp;quot;；到那时，两个 CVE 都还没进已知被利用漏洞目录。&amp;ldquo;没进 KEV&amp;quot;和&amp;quot;没人在打&amp;quot;不是一回事，别读成一个。&lt;/p&gt;&#xA;&lt;p&gt;最后留一个比 CVE 号更有用的判断。这个洞可以归成一句话：&lt;strong&gt;一个组件在异常路径上跳了出去，没把状态清干净，下一个请求撞了上来。&lt;/strong&gt; 想想你的服务：异常分支里提前 return、连接池的引用没还、缓存里的半成品没标记失效、错误处理和正常路径共用一份可变全局——这样的地方你有多少？补丁修的是 PHP 那一处；这个模式在你自己代码里一处没少。&lt;/p&gt;&#xA;&lt;p&gt;今晚要做的不是发全员通告，是打开 &lt;code&gt;php -i&lt;/code&gt;，看清楚你面前这台机器到底是 mod_php 还是 FPM。这一步三十秒，决定你今晚先动哪台。&lt;/p&gt;&#xA;</description>
        </item></channel>
</rss>
