跳到正文
博客园· 张善友·· 2 小时前AI 评分42

PHP 曝出 9.2 分漏洞后,C# 的“不可能三角”还成立吗

PHP 曝出 9.2 分漏洞后,我重新想了下"不可能三角"对 C# 成不成立 - 张善友

AI 导读

PHP 解释器被 AI 辅助工具扫出两个潜伏十余年的 UAF 漏洞 CVE-2026-109618 与 CVE-2026-109619(合称 PHP2Shell),CVSS v4.0 均为 9.2,无需认证即可远程执行代码。

正文

前几天 PHP 出了件大事。

两个藏了十几年的漏洞被扒了出来:CVE-2026-109618 和 CVE-2026-109619,统称 PHP2Shell,CVSS v4.0 评分双双 9.2,无需认证即可远程执行代码。更要命的是发现方式——不是安全研究员逐行审计出来的,是 AI 辅助工具扫出来的。在 C 代码里埋了十多年,全世界没人看见。

出事的代码甚至不是 PHP 语言本身,是 PHP 解释器。

每当这种时候,技术圈总会有人搬出那个经典论调:编程语言存在"不可能三角"——功能强、易用、安全,三者最多占俩。PHP 常年被当成这个论点的头号证据。

作为一个常年写 C# 的人,我第一反应是:这个说法,对 C# 成立吗?

先把事件本身拆开看,再下结论。

一、这次翻车的,不是"PHP 这门语言"

很多人对 PHP 不安全的印象,还停留在语言层面:弱类型、unserialize、历史上各种魔幻默认值。但这次 PHP2Shell 跟这些都没关系。

两个漏洞的机制都很典型:

第一个(09618)出在内存耗尽的处理路径上。请求触发 OOM 时,解释器通过 longjmp() 直接跳到退出流程,导致请求内存还没初始化完,就被 shutdown 阶段的代码拿去用了。

第二个(09619)更直白:worker 进程的全局状态在上一个请求结束后没重置干净,下一个请求进来,访问到的是已经释放的内存。

两个都是 UAF(释放后使用),都是 C 手动内存管理最经典的死法。再配上 mod_php / PHP-FPM 这种共享 worker 进程模型——一个进程反复处理成百上千个请求,请求之间的状态边界全靠人手维护——出问题只是早晚的事。

划重点:它爆在"运行时实现层",不是"语言语义层"。PHP 解释器是一坨有二十多年历史的 C 代码,这才是事故真正的主角。

这个区别,直接决定了后面 C# 部分的结论。

二、"不可能三角"不是定理,是预算表

"功能强、易用、安全不可兼得"听起来很像 CAP 定理,但它没有数学证明,也不是物理定律。

它真正说的是:在工程资源有限的前提下,你必须在三个维度之间做取舍。各家语言的牌其实都是摊开的:

Rust 选了功能强和内存安全,代价是易用性——所有权和借用检查劝退了多少人,社区自己心里都有数。

Go 选了易用和内存安全,代价是表达力——泛型姗姗来迟,错误处理靠 if err != nil 复读。

PHP 当年选了易用和生态,安全交给开发者自律。这个选择在 2005 年没问题,在 2026 年就要还债。

那 C# 选了什么?

C# 的答案是:我全都要——但要付别的钱。

PHP2Shell × C# 托管模型

三、C# 的打法:把安全的成本转移给运行时

C# 用来换内存安全的筹码,不是开发者的头发(Rust 路线),而是运行时性能。

托管模型的这几个设计,让 PHP2Shell 这类漏洞在 C# 里几乎无从下手:

对象生命周期归 GC 管。托管堆里不存在"手工释放后复用"这回事,ASP.NET Core 的请求之间没有共享的可变内存池,"请求状态重置边界"这个概念压根不存在——09619 的成因直接落空。

边界检查是默认行为。数组、Span<T>、Memory<T> 全部带运行时边界检查。恶意构造的请求打到 Kestrel(C# 写的 HTTP 服务器),最坏的结果是抛异常,而不是踩内存。09618 那种"内存没初始化完就被消费"的路径,在托管代码里构造不出来。

危险操作必须显式声明。想玩裸指针?可以,写 unsafe,打开编译开关,代码评审时所有人都看得见。安全的实现方式从"靠程序员记得检查"变成"默认安全,越界留痕"。

IL 可验证。JIT 之前可以做类型系统层面的验证,类型混淆类漏洞在托管层被结构性排除。

所以我的结论是:这个"不可能三角"对 C# 基本不适用。它不是打破了三角,而是微软用几十年的工程投入——JIT 团队、GC 团队、MSRC 安全响应、整个工具链——把三角形硬生生撑大了。

反观 PHP,与其说是语言的原罪,不如说是一个资源有限的开源社区,在维护一个二十多年历史的 C 代码库。

三角困住的不是语言,是预算。

四、先别飘,C# 的安全边界在哪

说"不适用"不等于说"免疫"。C# 的账,记在别的地方:

CLR 本身就是 C++ 写的。CoreCLR 几十万行 C++,理论攻击面和 PHP 解释器同源。漏洞密度低一两个数量级是真的,但不是零。这次 AI 从陈年 C 代码里挖洞的模式,对所有 C/C++ 实现的运行时都是警钟——CLR、JVM、V8、CPython,谁也别笑谁。

逃逸舱一打开就打回原形。P/Invoke、COM 互操作、unsafe、Unity 生态里大片非托管代码——跨过这条线,你写的就是 C。

反序列化是祖传重灾区。BinaryFormatter 的 RCE 黑历史长到微软官方宣布弃用;Newtonsoft.Json 的 TypeNameHandling 配置不当,至今仍是经典入口。

应用层的洞,语言管不了。SQL 注入、XSS、SSRF、配置错误,C# 拦不住业务代码犯错。它只能给你好的默认值:EF Core 参数化、Razor 默认编码、防伪令牌。用不用,是你的事。

供应链不分语言。NuGet 包投毒和 npm、PyPI 面对的是同一类威胁,这是生态层的问题,语言帮不上忙。

写在最后

PHP2Shell 真正的教训,不是"PHP 不行",而是:

任何语言的安全上限,最终取决于它运行时实现层的内存安全性,以及维护那层代码的资源。

PHP 爆在了用 C 写的、历史包袱沉重的解释器上。C# 把同类风险压缩到了 CLR 这一个更小、被重兵把守的面上——这是工程投入的差异,不是语言哲学的胜负。

至于"不可能三角",下次再有人拿它说事,可以回一句:

三角困不住语言,三角困住的是预算。

参考链接

事件综述

漏洞技术细节

发现过程与全球影响

.NET 侧背景

来源:博客园 · cnblogs.com