PHP2Shell 深度解析:异常处理背后的运行时安全边界
近日,PHP核心曝出两项严重远程代码执行(RCE)漏洞,编号分别为CVE-2026-109618和CVE-2026-109619,被合称为PHP2Shell。PHP 官方已于2026年10月21日发布安全更新,两项漏洞的官方评分均为CVSS 4.0 9.2(严重)。
这两项漏洞涉及PHP运行时的内存状态管理。在特定条件下,未经身份认证的远程攻击者可能通过HTTP请求触发异常,造成敏感内存信息泄露,或以Web工作进程账户的权限执行代码。部分已验证路径可以在业务脚本执行之前触发,无需应用自身另有独立漏洞。
由于缺陷位于承载应用的PHP运行时中,排查范围需要覆盖网站背后的实际运行环境。PHP官方将提供Web请求服务的PHP安装纳入风险排查范围,包括PHP-FPM、Apache mod_php、php-cgi、FrankenPHP和LiteSpeed,并特别提示 mod_php的风险。不同部署的具体利用路径和成立条件仍有差异。
要理解PHP2Shell如何从一次请求异常发展为内存破坏,需要沿着请求初始化、异常退出和工作进程复用的顺序分析。下面先梳理两项漏洞的区别,再结合具体内部结构与补丁设计,解释风险如何成立,以及企业防护需要覆盖哪些边界。
两个漏洞共同挑战了什么假设
编号 | 官方定义 | 发生错误的关键关系 |
CVE-2026-109618 | RCE via OOM corruption | 内部数据尚未完整初始化,异常退出后却被当作有效状态处理 |
CVE-2026-109619 | RCE via stale global state | 请求内存已释放,较长生命周期的状态仍保留引用并被后续使用 |
两项漏洞的官方评分均为CVSS 4.0 9.2。指标同时包含网络可达、无需已有权限、攻击复杂度高和存在特定攻击条件。严重后果与条件性利用可以同时成立;具体风险需要结合部署方式、应用操作及内存布局判断。
两者共享异常处理与状态管理方面的问题,但不宜统一归类为UAF。109618的核心是OOM提前退出后,无效或未完成初始化的状态被使用;109619则明确涉及请求内存释放后继续使用旧引用。未初始化访问、无效释放与释放后使用,是需要分别辨认的内存错误。
从软件工程角度看,两者都涉及一项基本约定:程序对外公布的状态,应当与对象实际所处的状态一致。例如,一个元素已经被登记为“有效”,它的必要字段就应当能够被安全读取;一个请求的内存已经归还,后续操作就不应继续沿用原有引用。
正常执行时,这些约定可能一直得到满足。异常发生在操作中途,情况就不同了:状态已经改变,建立新状态或恢复旧状态的步骤却没有完成。后续代码仍然相信先前的登记结果,于是开始处理一个事实上并不存在的完整对象。
我们据此归纳两类漏洞的共同模式:状态先被其他代码看见,操作随后被打断,恢复或重置未完成,后续使用者继续相信失效状态。
这种模式同时包含时间顺序和资源所有权,无法只靠检查某个参数是否合法来解释。分析对象需要扩大到请求启动、错误处理、模块初始化、请求结束和进程复用的完整生命周期。
两个CVE概括了相关故障模式,具体变体可能共享触发条件。不能仅凭PHP2Shell 这个名称,就把它们画成必须依次完成的固定两步攻击。
109618:为什么内存不足能够演变为内存破坏
1. 提前退出可能跳过维持一致性的步骤
PHP的memory_limit用于限制请求的内存使用。旧的OOM处理使用longjmp()跳出当前上下文。在某些内部操作尚未完成的时刻,提前退出会留下无效请求状态,随后关闭请求的代码可能读取或使用它。
理解这个问题,关键是分清“停止当前操作”与“安全撤销当前操作”。前者改变控制流,后者还必须处理已经发生的状态变化。如果调用链中的某一层默认后续步骤总会执行,而异常退出跳过了那些步骤,原有清理逻辑就可能失去成立前提。
清理代码通常需要知道对象数量、字段类型和资源归属。它必须依据内部结构做决定。内部结构失去一致性之后,继续清理本身也可能成为一次危险操作。
2. 一个哈希表插入顺序,揭示了问题本质
Calif的原始研究给出了一个具体例子:在_zend_hash_str_add_or_update_i的相关路径中,哈希表先增加nNumUsed等计数,随后申请键所需内存,之后才完成新Bucket的字段初始化。若中途分配触发异常退出,清理代码可能按照已经增加的数量处理未完成的元素。这一先后关系也可在修复前源码中核对。
这个例子把“内存处理不当”还原成了可以检查的先后关系:清理代码依赖的有效元素数量已经增加,对应元素的字段却尚未完成初始化。
哈希表中的计数不只是统计信息,它还决定后续代码处理哪些元素。一旦计数把未完成的区域纳入有效范围,类型、引用和长度等字段就可能被按照错误语义解释。由此可能出现非法读取、错误释放或其他内存破坏;发展为代码执行,还需要进一步控制程序的内存行为和执行行为。
图中需要关注的时间窗口,位于“状态已经被登记”和“对象完全可用”之间。把同一窗口推广到其他内部组件,就能理解为什么修复工作需要覆盖多处代码,而不仅是一个上传接口。
3. 资源限制为什么没有自然挡住问题
HTTP请求大小与解析后的内存开销存在差异。输入在内存中还需要表示为字符串、数组、哈希表和其他管理结构;结构复杂度会影响这种额外开销。因此,请求在字节长度上合规,不代表解析成本也在预期范围内。
PHP通告指出,multipart的头部、字段层级和文件元数据等处理可能放大内存消耗;请求声明的长度与实际输入处理也需要分别约束。防守方不能把 post_max_size、单文件大小和memory_limit当作同一个限制来使用。
例如,空文件控件即使不计入上传文件数量,仍可能产生相应的文件元数据;嵌套字段又增加数组和哈希表的分配工作。因此,可能出现请求体没有超过 post_max_size,解析时却触及memory_limit并引发OOM的情况。这不要求整台服务器的物理内存已经耗尽。
这里形成了一项值得迁移到其他软件的审计问题:资源限制被触发时,系统会在哪个位置中断,哪些结构此时处于半完成状态,后续代码又会依据什么继续工作?限额保护的是资源边界,但实现限额的异常路径也必须具备内存安全性。
109619:请求结束后,为什么旧状态还能影响新请求
1. 请求生命周期短于工作进程生命周期
在复用工作进程的环境中,一个进程可能顺序服务很多请求。请求结束时可以释放该次请求的内存,进程中的某些全局位置却继续存在。如果它们仍引用刚被释放的对象,运行时就留下了悬空引用。
这也是“上一次请求的数据没有清干净”需要进一步解释的地方。问题不必表现为业务缓存里多了一条记录。它可能发生在解释器维护的错误、会话、对象或扩展状态中,影响的是原生代码如何理解和使用一片内存。
官方将成立条件拆成生产者和消费者:一个可达操作留下属于该请求的内部状态,内存释放后,同一worker的后续请求在正常初始化重置之前使用它,形成释放后使用,即UAF。留下状态和使用状态的操作需要共同存在。
“同一工作进程”是图中的必要边界。它不意味着任意请求之间都共享同一片内存,更不意味着不同容器之间可以直接互读。但在满足条件的进程内部,HTTP请求已经结束,也不能保证相关引用全部安全失效。
这里需要区分逻辑边界与隔离边界。HTTP将请求划分成独立消息,应用也按请求组织变量,但这不会自动为每条消息建立操作系统级内存保护。同一进程内的安全性,仍依赖解释器和扩展正确管理对象所有权。
因此,容器可以约束一个服务触及宿主机或其他服务的范围,却不能代替容器内部每次请求的状态重置。多站点或多业务共用运行时的环境,还应核对进程、身份和数据权限的实际分离程度,不能仅凭域名不同判断隔离已经成立。
2. 初始化与清理,对“已经启用”的判断可能不一致
旧版PHP启动源码中,请求变量解析php_hash_environment()先于正常模块激活 zend_activate_modules();激活完成后才设置PG(modules_activated)。请求关闭中的正常模块清理又依赖该标志。
这个顺序使“模块是否完成正常激活”和“模块是否已经在早期路径持有资源”成为两个不同问题。会话上传进度机制session.upload_progress可以在解析阶段先进入会话处理,而此时正常模块初始化尚未完成。PHP手册也明确说明,上传进度信息可以在业务脚本执行之前写入会话。
从生命周期设计看,如果组件已经取得资源,清理机制却仍把它视作尚未进入服务状态,那么异常中断就可能使资源获取和资源释放失去配对。这类不一致值得作为代码审计中的独立问题检查。
3. 为什么到“下一次初始化会重置”仍不够
重置只有发生在第一次使用之前才有效。对于初始化前就运行的输入、配置或会话处理,稍后再恢复正常状态已经来不及。官方确认的消费者包括早期配置诊断和会话上传进度处理;隐藏错误输出并不能阻止内部状态被访问。
这给修复提出了更严格的要求:既要检查请求结束时留下了什么,也要检查下一次请求最早会碰到什么。只审计常规初始化后的业务流程,容易漏掉这种跨阶段连接。
状态重置还需要覆盖清理流程的尾部。即使前面已经清空某个全局字段,后续组件如果又写入指向请求内存的信息,最后释放请求内存时仍可能留下引用。审计应检查最后一次写入与内存释放的顺序,不能只证明某个重置函数曾经被调用。
4. 部署差异决定具体链路,不能只比较产品名称
对于请求启动失败后的特定路径,mod_php仍会执行请求关闭处理;FPM会终止相应子进程。因此,某条依赖关闭过程或后续进程复用的启动阶段链路,不能原样套用到FPM。FPM在正常启动之后的应用执行路径仍需要单独分析,官方已报告其会话生命周期RCE验证。
对已验证的mod_php启动阶段路径,风险可以在业务脚本运行前成立,无需应用自身存在独立漏洞。其他路径可能依赖具体应用操作、配置或扩展。依赖应用执行某项操作,也不等于要求应用另外存在一个独立安全漏洞。
分析报告最好回答具体的生命周期问题:状态在哪个阶段产生,异常发生后进程是否存活,谁负责清理,后续使用发生在重置之前还是之后。这样的分析比一句“FPM受影响”更能指导防守,也能避免把某一路径被阻断解释为整个环境免疫。
5. 从内存错误到代码执行,仍需要额外条件
“同一worker”是跨请求路径的执行条件,不等于部署必须预先开启粘性会话,也不意味着攻击者必然能够指定工作进程。进程分配和回收会影响利用可靠性,但存在多个worker本身不能消除漏洞条件。
内存布局同样影响错误访问的结果。Heap Grooming通常指堆布局调整或堆整形,与通过大量重复分配实现堆喷的Heap Spraying不宜混为同一术语。漏洞利用可能需要让失效状态被按可控制的方式解释,进一步产生内存读取、写入或控制流影响;具体涉及的对象、回调和执行路径取决于利用链,不能统一描述成“劫持虚函数或析构回调”。
从补丁读懂修复目标:让失败发生在安全的位置
PHP的修复提交包含两项核心调整:改变OOM的退出处理,并正确重置那些可能在请求启动时被访问的全局状态。
这两项调整分别对应前面的两个问题:让中断不再任意打断脆弱的内部操作,让上一请求的状态不再以有效对象的面貌进入下一请求。
软限制与硬限制,解决的是不同层次的问题
更新后的memory_limit作为软限制,通常允许越过限制的分配先完成,再由引擎到安全点报告错误;另外通过硬限制约束允许超出的范围。某次分配将超出硬限制时,进程会立即退出。官方引入memory_hard_limit_percent描述这项边界。
其工程含义是:给维护内部一致性留出一段受限空间,同时为无法继续安全运行的情形设置终止边界。安全点并不是用户请求中的固定位置,而是引擎能够按预期处理退出的执行位置。
该图概括修复后的限制策略,不覆盖所有底层分配失败情形。它表达的设计目标是:限制资源消耗,同时避免在内部不一致的位置继续执行清理代码。
安全修复还会改变运维观察结果
某次分配将超出硬限制时,PHP会直接向标准错误流报告,并以进程退出码137终止;PHP关闭函数、析构和输出缓冲处理器都不再执行。这里的137不是HTTP状态码,也可能出现在进程因其他原因被强制终止的情形中,判断原因需要结合实际日志。
据此,升级回归不能只检查正常请求。企业还应在隔离测试环境观察内存压力下的响应、日志、任务状态和资源释放行为。业务如果依赖请求结束阶段完成某项动作,需要确认异常终止后的恢复机制由谁承担。
进程终止带来可用性代价,但让失去一致性的进程继续服务,也可能扩大安全后果。企业应将异常退出、进程拉起和业务恢复作为一条完整链路设计,而不能要求每一次失败都由同一进程继续完成所有善后工作。
回归测试需要覆盖状态迁移
对于这类修复,有价值的测试对象包括“资源已经取得但模块尚未完全激活”“元素已登记但字段尚未建立”“请求结束后下一次初始化尚未开始”等中间状态。成功完成一次业务请求,只能验证正常路径。
研发团队可在隔离环境通过分配失败注入和内存检查工具,观察这些状态遭遇失败后的行为:引用是否仍能被访问,清理是否重复执行,进程是否按设计退出。这是测试设计建议,并非本文已经完成的实验。评判标准应包括状态一致性,不能只看返回了预期错误码。
从2024年的隐患到2026年的RCE,治理问题在哪里
已知风险,需要独立于现成利用代码获得治理优先级
PHP基金会在复盘中披露:2024年处理外部审计问题时,开发者已经发现相关 OOM风险的早期迹象,但当时没有找到具体利用向量。修复方案涉及兼容性、拒绝服务和第三方扩展等取舍,决策方式及核心人员资源也使工作反复停滞。2026年的远程代码执行报告进一步明确了风险影响。
这段历史提供了一种观察基础组件漏洞的方法。组件越靠近公共运行路径,修复就越可能牵动兼容性和性能;如果治理流程又高度依赖立即可展示的攻击后果,复杂但基础性的设计缺陷就容易长期等待。
我们的判断是,对于涉及对象所有权、异常退出和状态一致性的缺陷,应建立独立跟踪与决策升级机制。即使尚无通用利用,也应明确影响假设、修复阻力、负责人和下一次评估时间,避免风险在“尚未证明”中无限延期。
AI改变了研究成本,利用成立仍取决于技术条件
Wiz报告,研究中借助模型进行多缺陷连接、内存布局适配和反复调试,并在代表性应用上验证了代码执行;同时还报告了无需先获得代码执行即可读取进程敏感内存的结果。
对防守方,这带来的变化是需要定期重审“利用难”的判断。技术前提并未消失,但完成分析、构造和适配的成本可能发生变化。曾因研究投入高而不常见的利用路径,在工具能力变化后可能需要重新评估。
企业也不宜把发现、验证、修复和部署看成同一项能力。找到异常状态,需要源码理解;证明影响,需要完整证据;选择修复方案,需要兼容性权衡;把补丁送到生产实例,还需要发布和运维能力。任何一段承接不足,都会延长风险暴露时间。
修复与处置建议
1. 尽快升级至官方安全版本
PHP官方已发布安全更新。请评估业务兼容性与扩展依赖后,尽快升级至下表对应版本或同分支的后续安全版本。
分支 | 本次修复版本 |
PHP 8.2 | 8.2.35 |
PHP 8.3 | 8.3.36 |
PHP 8.4 | 8.4.27 |
PHP 8.5 | 8.5.12 |
2. 旧版本优先迁移,必要时使用源码回移修复
PHP8.1及更早分支已结束常规上游支持,应优先迁移至受支持分支。对暂时无法迁移的系统,PHP安全团队额外提供了以下三个分支的最低限度源码回移修复:
https://github.com/php/php-src/tree/PHP-7.4-CVE-2026-109618-109619
https://github.com/php/php-src/tree/PHP-8.0-CVE-2026-109618-109619
https://github.com/php/php-src/tree/PHP-8.1-CVE-2026-109618-109619
具备源码构建能力的团队可基于对应分支编译并完成兼容性验证;其他用户应向发行版或长期支持供应商确认补丁。本次回移修复不代表旧分支恢复长期维护,仍需安排版本迁移。
3. 升级后完成三步验证
1. 核对实际Web运行版本。php-v默认只反映命令行版本,还应核对站点实际使用的PHP-FPM、Apache模块或LiteSpeed PHP版本;发行版回移补丁需结合完整包版本与修复公告确认。
2. 替换全部工作进程。按部署方式重启PHP-FPM、Apache或LiteSpeed/LSAPI工作进程;容器部署应更新镜像并替换相关副本,确认旧进程已退出、新实例实际承载请求。
3. 检查全部环境与实例。覆盖开发、测试、生产及备用节点,避免遗漏旧版本或容器副本,并完成关键业务回归。
4. 暂时无法升级时的缓解措施
对于暂时无法升级的系统,PHP官方安全公告列出了以下临时措施。这些措施只能降低特定攻击路径的风险,不能替代安全更新。
CVE-2026-109618:缓解内存耗尽(OOM)处理缺陷
取消PHP层的请求内存限额。设置memory_limit=-1,优先通过FPM池或Apache配置中的php_admin_value等方式,防止应用覆盖;同时核对池、目录及应用配置,避免重新引入有限的memory_limit。这可以移除攻击者可控的PHP内存限额触发条件,但底层内存分配失败仍可能进入漏洞路径,应配合下述资源隔离措施。
收紧请求大小与解析复杂度。官方建议,不需要文件上传的应用设置post_max_size=8M,需要上传的应用设置post_max_size=32M,并保持upload_max_filesize小于post_max_size。同时在Web服务器或代理侧限制解码后的请求大小、输入字段数、multipart分段数、请求头数量及嵌套深度;限制也须覆盖缺少有效Content-Length的请求。
通过容器或cgroup限制资源影响。将其作为可用性边界,优先采用仅终止受影响工作进程的策略。不要用RLIMIT_AS或RLIMIT_DATA替代这类限制,因为它们可能导致内存分配失败,再次触及漏洞路径。
CVE-2026-109619:缓解陈旧全局状态复用
关闭上传进度并重启全部工作进程。设置session.upload_progress.enabled=0,通过php.ini、FPM池或服务器配置,确保设置在请求解析前生效。不要依赖业务代码中的ini_set()或FPM的.user.ini。该措施只阻断已确认的会话相关路径,不覆盖其他变体。
PHP 8.5关闭致命错误回溯。设置fatal_error_backtraces=0并重启全部工作进程。该措施仅阻断已确认的致命错误回溯路径
清理过早触发诊断信息的配置。移除按请求应用、且会在正常初始化前产生诊断信息的无效或已弃用配置项。使用error_reporting、display_errors或@隐藏诊断信息,不能消除相关风险。
修复攻击者能够触发的致命错误与执行超时。排查回调、上传、导入处理及清理过程中的失败,减少留下异常请求状态的入口。
实施缓解措施后,仍应收紧访问入口、加强异常监测,并推进补丁部署。WAF和配置缓解均不能作为系统已完成修复的依据。
结语
PHP2Shell把运行时安全中的一项基础要求推到台前:请求可以失败,但失败之后的状态仍须可信;进程可以复用,但上一请求的对象不能在失效后继续被当作有效资源。
对研究团队,异常路径、生命周期交界和资源约束值得获得与正常功能同等严肃的分析。对企业,运行时依赖、补丁交付、边界防护和证据关联需要形成连续的责任链。理解这些关系,才能把一次紧急升级转化为下一次事件中更可靠的判断与响应。













