资讯详情

machineKey泄露与ViewState反序列化RCE剖析

📅 2026/11/5 13:15:34 | 华诺云谱 👁 阅读
machineKey泄露与ViewState反序列化RCE剖析
去年冬天处理过一起应急起因很朴素某公司的业务系统被扫出了web.config备份文件值班的同学看了眼返回内容觉得就是个配置文件随手归档了。三个月后服务器上多了一个陌生的服务账号横向移动痕迹铺了一地。回溯时才发现那份配置里躺着明文的machineKey等于把整个站点的 ViewState 签名钥匙拱手送人。CVE-2026-26333 讲的正是这条链web.config 泄露 → machineKey 暴露 → ViewState 反序列化 RCE。三个环节单拎出来都不算新鲜串起来却能一步从信息泄露跨到服务器沦陷。这篇就把整条链拆开揉碎从 ViewState 的底层机制、配置文件的泄露面、密钥材料的提取验证、gadget chain 的选择约束一直讲到自查方法和加固动作。不管你是做渗透测试、甲方安全运营还是维护着一堆老 Web Forms 系统的开发看完应该能对自己的资产有个大致判断。1. 先把整条链的因果逻辑捋直1.1 ViewState 为什么天生带着执行的基因ASP.NET Web Forms 是 HTTP 无状态协议的产物页面上的控件状态、下拉框选中项、表格排序这些信息服务端不可能每次都重新算一遍。于是框架设计了一套状态回传机制把页面的控件树序列化成一坨字符串塞进隐藏字段__VIEWSTATE随表单一起发给浏览器下次提交时浏览器再原样带回来服务端反序列化还原成对象树页面就记得之前的状态了。这里的关键在于服务端反序列化的内容是客户端可控的。保存时用的是ObjectStateFormatter早期是LosFormatter还原时用的是同一套反序列化器。.NET 的类型化反序列化有个众所周知的特性——只要类型可被解析、构造函数或属性 setter 能被触发就可能在还原过程中执行非预期逻辑。这就是所谓 gadget chain 的温床。框架当然不是没做防护。从早期版本开始就有 MAC消息认证码机制序列化完成后再给整段数据算一个签名一起发给客户端。客户端下次提交回来服务端先验签签名对不上就直接抛异常连反序列化的门都进不去。所以正常路径下攻击者改一个字节 ViewState 就废了。问题就在于这个签名用的密钥——它来自machineKey配置节。拿到密钥的人可以给自己构造的任何负载重新签一个合法签名服务端的验签环节反而变成了一道准入许可。这就解释了为什么这条链的杀伤力这么大不是绕过某个漏洞而是拿到了合法签发权。1.2 machineKey 的角色整个信任体系里唯一的锚点machineKey不是一个只服务于 ViewState 的小配置它是 ASP.NET 应用级密码学的总开关。展开看大概管四件事ViewState 签名与加密validationKey用于 MACdecryptionKey用于加密当ViewStateEncryptionMode为 Always 时启用。Forms 身份验证票据开启 Forms 认证时认证 Cookie 里的票据是用 machineKey 加密签名的。拿到密钥就能伪造任意用户的登录态包括管理员。角色与个性化 Cookie角色管理器、Profile 相关的 Cookie 同样走这套密钥。部分场景下的匿名标识与防篡改令牌。也就是说密钥泄露的后果远不止 RCE 一条。攻击者可以先伪造一个管理员身份的认证 Cookie 大大方方登录进去再慢慢找地方落地全程不触发任何密码爆破告警。很多应急案例里攻击者其实走的是这条路ViewState 反序列化只是备选。还有个容易被忽略的点machineKey在多机部署里必须保持一致。单机环境下框架会自动生成密钥重启也不变存在注册表或配置里但一旦上了负载均衡两台机器的自动密钥不同用户在两台之间跳转就会不断掉线、ViewState 报错。运维为了让系统稳定最省事的做法就是手工生成一对固定的validationKey和decryptionKey明明白白写进所有节点的web.config。这一步操作本身没毛病但它把密钥从框架内部状态变成了一个可以被读走的文本文件。泄露的种子就在这时候埋下了。1.3 这条链为什么被评为高危而不是中危常规的信息泄露漏洞比如泄露了数据库连接串攻击者还得自己找入口、找注入点、找能写文件的地方中间隔着好几道坎。而 machineKey 泄露是一条直通执行的路密钥 → 签名 → 反序列化 → 命令执行中间没有任何需要额外条件的环节除了目标站点确实在用 Web Forms 并且反序列化器可被触发这两点在老系统里几乎是默认成立的。更麻烦的是它的隐蔽性。整个过程只表现为一次普通的 POST 请求__VIEWSTATE字段长度比平常大一些状态码 200响应体正常。没有 SQL 注入那种报错特征没有文件上传那种 multipart 边界异常WAF 规则库里也很少有专门针对 ViewState 的检测项。等到主机侧出现异常进程往往已经是几天后的事了。2. web.config 到底从哪些缝里漏出去2.1 六类高频泄露通道按踩坑概率排我在实际排查里见过的泄露路径大致可以归成这么几类按出现频率从高到低排泄露通道典型文件名/路径触发条件危险度备份文件残留web.config.bak、.old、.zip、.rar、web.config~运维改配置前手工备份忘了删极高编辑器临时文件.web.config.swp、web.config.save直接在服务器上用编辑器改过配置高静态服务器误配站点根目录被当成静态资源目录换过 Web 服务器、或加了反向代理没写规则高发布包未清理publish.zip、deploy/、backup/CI 产物直接丢在站点目录下高任意文件读取其他组件的路径遍历漏洞存在其它可读文件的漏洞中高源码仓库泄露公开仓库、制品库配置被提交进版本控制高第一类最普遍。改配置前先cp web.config web.config.bak这是很多人的肌肉记忆改完验证没问题就忘了清理。而这些备份文件的后缀不在 Web 服务器的默认拦截列表里——web.config本身被保护得好好的web.config.bak却可以随便下载这种保护了正门忘了后窗的情况太常见了。第二类更有意思。有人在服务器上直接用 vim 编辑web.config中途断开连接或者被 kill留下一个.web.config.swp。这个交换文件里包含完整的文件内容片段而且是二进制格式很多扫描器不认但稍微处理一下就能捞到明文。第三类在多技术栈混布的环境里高发。原来的老系统跑在 IIS 上前面加了一层 Nginx 做统一入口配置里写了个location / { proxy_pass ... }或者干脆root指到了站点目录。IIS 那边有请求筛选帮你挡.configNginx 这边可没有一个 GET 请求就把文件读走了。这种问题在做了前后端分离改造但没改干净的老项目里尤其多。2.2 三个五分钟能做完的验证动作自查不需要复杂工具浏览器和 curl 就够。三个动作按顺序做第一直接请求配置文件本身。访问https://目标域名/web.config正常结果应该是 404 或者 403。如果返回 200 且内容是 XML那就不用往下看了直接按事故处理。这里有个坑有些 WAF 或者自定义错误页会把 404 也渲染成 200 的空页面光看状态码会误判必须看响应体的实际内容。第二撞一遍常见备份后缀。用一个精简字典跑不要用大字典——这类探测的请求量一旦上去日志里非常显眼而且可能触发防护设备。我的习惯是只测十几个高频后缀.bak、.old、.orig、.save、.swp、.tmp、.txt、.zip、.rar、.7z、.gz、.1、.2、~。覆盖面已经够大了。第三看目录列表和发布产物。访问站点根目录和几个常见的二级目录看有没有开目录浏览。开了的话翻一翻有没有publish、deploy、backup、logs这类目录。这一步经常有意外收获——我就见过一个站点根目录下明晃晃躺着一个2023-08-15发布包.zip解压出来就是完整的源码加配置。注意所有验证动作都必须在你拥有授权的资产上做。给客户做评估时探测频率控制在每秒一两个请求别把对方的监控打爆也别在业务高峰做。3. ViewState 的结构与签名校验的底层逻辑3.1 把一个__VIEWSTATE拆开看拿一个正常页面的 ViewStateBase64 解码之后你能看到很明显的分层结构。最前面是一个标志字节接着是签名长度字段然后是签名值本身最后面才是真正的对象图序列化数据。签名值的长度由validation属性指定的算法决定用 SHA1 就是 20 字节用 HMACSHA256 就是 32 字节。这个长度是个很好的判断依据——解码后数一下前面那段的字节数就能大致反推出目标用的是哪种签名算法。如果开启了 ViewState 加密中间还会多出一段密文那就得先解出明文才能看到序列化结构。序列化数据本身用的是ObjectStateFormatter的格式可以看到一堆类型标记、字符串长度前缀、以及控件属性的键值对。对于做安全的人来说这段数据的价值在于它完整暴露了服务端会去实例化哪些类型、调用哪些 setter。gadget chain 的构造思路本质上就是在合法的类型图里塞进一个会被反序列化器触发的恶意对象。顺带说一句页面上还有个__VIEWSTATEGENERATOR隐藏字段它是页面类型的指纹。当 MAC 校验开启且没配置ViewStateUserKey时这个值也需要对得上否则服务端可能直接判定 ViewState 无效。很多人第一次尝试伪造时会卡在这里——明明密钥对了、签名对了还是报ViewState 验证失败八成就是 generator 没对上。3.2 校验模式与算法组合决定了攻击的难度系数不同的配置组合攻击者需要拿到的材料完全不同配置项常见取值含义攻击者需要什么EnableViewStateMactrue4.5.2 起强制校验 MAC必须拿到validationKeyViewStateEncryptionModeAuto / Always / Never是否加密负载Always 时还需decryptionKeyvalidationSHA1 / HMACSHA256 / AES / 3DES签名算法算法不影响可行性只影响构造参数decryptionAuto / AES / 3DES加密算法同上ViewStateUserKey未设置 / 已设置绑定用户标识已设置时需额外知道绑定值这里有个关键的历史节点从 .NET Framework 4.5.2 开始框架不再允许把EnableViewStateMac关掉配置里写false会直接报错。这个改动堵死了关闭校验这条最省事的路把攻击门槛抬高到了必须拿到密钥。所以在今天的环境里看到有人讨论关闭 MAC 就能打的方案基本可以判断那是很老的资料了。另一个经常被误解的点是加密和签名的关系。很多人以为开了加密就安全了其实加密只防看不防改——真正阻止伪造的是签名。加密的作用是让攻击者看不到 ViewState 内部结构增加分析成本但如果签名密钥已经泄露加密密钥往往也在同一个配置节里一起被拿走了。密文可以用decryptionKey解开改完再加密回去照样能过。至于ViewStateUserKey这是个挺有效的额外防线。它把 ViewState 和具体用户绑定签名时把用户标识也算进去。即便攻击者拿到密钥不知道当前会话绑定的具体值也签不出能通过校验的数据。可惜的是这个属性需要开发在页面生命周期里显式设置老系统里配的人不多。4. 全链路还原从密钥文本到服务端执行4.1 阶段一从泄露的配置里提取可用的密钥材料拿到web.config之后第一件事是找system.web下面的machineKey节点。典型长这样machineKey validationKeyA1B2C3...(40或64位十六进制) decryptionKeyD4E5F6...(48或64位十六进制) validationHMACSHA256 decryptionAES /这里有两个细节必须核对否则后面全是白费功夫。第一密钥长度必须和算法匹配。SHA1 的 validationKey 要求 40 个十六进制字符20 字节HMACSHA256 要求 64 个32 字节。AES 的 decryptionKey 要求 64 个字符32 字节3DES 要求 48 个24 字节。如果长度对不上要么是配置本身有问题框架在某些情况下会容忍但行为诡异要么是文件被截断或者读到了占位值。我遇到过一次泄露出来的是AutoGenerate,IsolateApps这种写法看着像配了密钥其实是让框架自己生成——这种就没法直接用得另找路径。第二确认这是站点实际生效的配置。一个站点可能有多层配置根目录的web.config、子目录的web.config、machine.config。配置是继承的子目录的会覆盖父目录的。如果目标应用在某个子目录下根目录的密钥可能根本不生效。判断方法是看站点的物理路径结构以及有没有location节点做了局部覆盖。验证密钥是否真的有效最可靠的办法是本地搭一个最小复现环境装一个对应版本的 .NET Framework起一个 Web Forms 页面把泄露的 machineKey 原样贴进去用工具生成一段带签名的 ViewState看目标站点是否接受。接受的标志是不报ViewState MAC 验证失败的错误。这个验证动作本身是无害的只要你不把恶意负载塞进去。4.2 阶段二构造一个服务端愿意接受的 ViewState构造环节涉及三个输入密钥材料、目标页面信息、以及要执行的负载。页面信息这块容易被低估。至少需要确认目标页面路径以及它对应的__VIEWSTATEGENERATOR值可以从正常请求的响应里提取目标页面是否要求__EVENTVALIDATION启用事件验证时这个字段也得配平但通常伪造 ViewState 走的是页面初始化阶段不一定需要页面的 ViewState 最大长度限制maxPageStateFieldLength超过会被拆分或者拒绝有了这些就能用业界公开的 .NET 反序列化工具生成 ViewState。工具的典型调用方式大概是这样参数含义写在注释里# 伪代码示意仅用于授权测试环境 生成工具 \ --plugin ViewState \ --key 替换为泄露的 validationKey \ --generator 替换为页面的 VIEWSTATEGENERATOR \ --path /目标页面.aspx \ --payload 选定的 gadget chain \ --output viewstate.txt生成出来的是一段 Base64 字符串直接替换掉 POST 请求里的__VIEWSTATE值。这一步在实际操作中要极其克制——先在自建的复现环境里跑通确认链路成立再考虑是否在授权范围内对生产做最小化验证比如用一个只做延时或者只写一个标记文件的负载绝对不要上反弹或者批量执行的东西。4.3 阶段三gadget chain 的选择决定了你能不能打通.NET 反序列化的利用链不止一条选择依据主要是目标的环境配置。常见的几类第一类基于类型混淆的链。利用某些泛型集合或委托类型在反序列化时的类型检查缺陷把数据伪装成方法调用最终落到Process.Start上。这类链的优点是所需的程序集少很多环境默认就有缺点是特征比较明显部分 WAF 会针对特定类型名字做拦截。第二类基于 XAML 对象实例化的链。依赖System.Xaml或 WPF 相关程序集通过对象图的属性设置触发方法调用。功能强、可定制性高但对目标是否加载了相应程序集有要求。做过前后端分离的精简部署环境往往把 WPF 相关的东西裁掉了这类链就打不通。第三类基于程序集加载绕过的链。当目标环境禁用了某些危险类型比如通过TypeFilterLevel或者白名单限制可以尝试用动态加载的方式绕过检查。这类链在 .NET Framework 4.x 的某些版本上需要额外条件成功率不稳定。实际选择的时候我的思路是先做环境指纹看目标站点的错误页、看静态资源的框架版本特征、看响应头。确定了大版本之后按程序集可用性 → 是否需要绕过 → WAF 特征的顺序筛一遍一般能剩下两三条候选。挨个在复现环境试试通了再谈别的。4.4 阶段四投递与落地坑最多的环节投递看起来简单——一个 POST 请求而已。但实际打的时候坑一个接一个请求体大小。ViewState 加 gadget 序列化之后Base64 长度轻松上万字符再加上其它表单字段很容易超过默认的maxRequestLength默认 4MB一般够用或者反向代理的请求体限制。遇到过 Nginx 默认client_max_body_size 1m把请求直接 413 掉的。字段配对。除了__VIEWSTATE还得带上__VIEWSTATEGENERATOR、__EVENTVALIDATION如果页面有、以及页面上的其它隐藏字段和必需的按钮字段。少一个都可能让页面在解析 ViewState 之前就先失败了。Cookie 与会话。如果配置了ViewStateUserKey必须用同一个会话的 Cookie。这时候前面提到的伪造 Forms 认证票据就派上用场了——先伪造一个管理员身份拿到会话再在这个会话里投递 ViewState。响应判断。成功执行的标志不是页面返回 200而是命令真的跑了。所以负载要设计成有明确副作用的形式写一个特定名字的文件、发起一次可控的外连、或者做一个固定时长的延时。用延时最安全但要注意服务端的执行超时限制。清理。授权测试结束后现场留下的测试文件、临时账号、被改动的配置都要清理干净并且留一份完整的操作记录。这是职业操守问题也是保护自己的方式。5. 自查与排查怎么确认自己有没有中招5.1 流量侧的线索往往比主机侧更早出现如果站点前面有全流量设备或者 WAF 日志从这几个维度筛观察点异常特征可能含义备份文件探测短时间内大量 404路径集中在.bak/.zip攻击者在找配置文件配置文件请求出现/web.config且返回 200已经泄露立即处置ViewState 长度单次 POST 的__VIEWSTATE显著超长可能是伪造投递请求频率同一 IP 对同一页面高频 POST可能是 payload 调试响应异常500 错误里含反序列化相关堆栈校验或反序列化阶段出问题第一条和第二条是最有价值的早期信号。很多攻击链在真正投递之前会有一段明显的扫描期。如果监控能覆盖到配置文件被成功请求这个事件就能在 RCE 发生之前拦住。第三条要注意误报。有些页面本身 ViewState 就很大数据表格、树形控件需要先建立基线看的是相对基线的突增而不是绝对值。5.2 主机侧的三板斧一旦怀疑已经落地主机侧的检查按这个顺序做第一看进程树。Web 工作进程w3wp.exe派生出cmd.exe、powershell.exe、net.exe、whoami.exe这类子进程是反序列化 RCE 最典型的痕迹。正常的 Web 应用极少会去调这些。如果有 EDR 或者进程审计日志直接按父子关系筛。第二看站点目录的文件变化。反序列化 RCE 常见的后续动作是落 webshell。按修改时间排序翻一遍站点目录下的.aspx、.ashx、.asmx、.svc文件看看有没有陌生的、文件名随机的、内容混淆的。特别注意bin目录和上传目录。第三看配置和账号。检查machineKey是否被人改过对比版本控制或者备份检查是否新增了本地用户、是否给 IIS 应用池账号提了权、是否加了计划任务或者服务。注意应急时先固定证据再动手。直接重启应用池或者删文件会把内存里的 payload、临时文件、连接记录一起清掉后面复盘就没依据了。6. 加固方案把这条链从根上掐断6.1 密钥层不要让它以明文形态躺在磁盘上最直接的动作是把machineKey节点加密。.NET 提供了配置节加密的能力可以把敏感节点用 RSA 密钥容器加密后写回配置文件运行时由框架自动解密应用代码完全不用改。命令大概是这样在目标服务器上执行需要管理员权限# 加密指定站点的 machineKey 配置节 aspnet_regiis -pef system.web/machineKey 站点物理路径 # 需要时回滚 aspnet_regiis -pdf system.web/machineKey 站点物理路径加密之后即便配置文件再次被读走攻击者拿到的也是密文。但这里有个坑必须提醒RSA 密钥容器的权限要配对。默认情况下只有执行加密的那个账号能解密如果应用池用的是另一个身份运行时会报无法解密配置节整个站点起不来。正确做法是加密后给应用池账号授予密钥容器的读取权限然后再重启验证。更彻底的做法是把密钥托管到外部的密钥管理服务应用启动时拉取。这对老系统改造成本较高但如果有多套系统共用一套密钥值得考虑。另外两个原则validationKey 和 decryptionKey 必须不同不要图省事用同一个值定期轮换轮换会导致所有用户登出、所有在途 ViewState 失效要安排在维护窗口并且所有节点同步更新否则负载均衡下会出现随机报错。6.2 访问控制层让配置文件根本没有被请求的路径这一层解决的是泄露源头比加密更根本。Web 服务器侧统一拦截。IIS 的请求筛选默认就挡.config但要确认没被人手工放开。反向代理场景必须单独写规则Nginx 里加一段location ~* \.(config|bak|old|orig|save|swp|zip|rar|7z)$ { deny all; return 404; }返回 404 而不是 403是为了不暴露这里确实有文件但被拦了这个信息。发布流程瘦身。打包脚本里加一步清理把备份文件、日志、发布包、.git目录、.vs目录统统排除。这一步做在 CI 里比靠人自觉可靠得多。我见过的最离谱的一次是站点根目录下同时躺着源码压缩包、数据库备份和配置备份三个文件加起来把整个系统扒得底朝天。目录浏览关掉。这是基础配置但总有人为了方便查看上传的文件把它打开。备份文件换个地方放。改配置前备份是好习惯但备份目录不要放在站点根目录下放到 web 根之外或者干脆用版本控制管理配置文件。6.3 运行时层把反序列化的攻击面压到最小升级框架版本。4.5.2 以上强制 MAC这一步能挡掉关闭校验这条最简单的路。有条件的直接上更新的技术栈新框架里根本没有 ViewState 这套机制反序列化面小得多。配置ViewStateUserKey。在全局的Application_PreRequestHandlerExecute或者页面的Page_Init里把用户标识绑上去让 ViewState 和会话强绑定。即便密钥泄露攻击者也签不出能过校验的数据。关闭详细错误。自定义错误页打开把customErrors设成On或RemoteOnly。堆栈信息对攻击者来说是最好的环境指纹。限制应用池权限。应用池账号用最小权限的虚拟账号不要用管理员或者高权限域账号跑。这样即便 RCE 成功攻击者的起点也很低横向移动的难度大很多。WAF 规则补充。针对 ViewState 的检测可以从长度突增、Base64 内容中的特定序列化特征、以及异常高频的 POST 入手。规则不用追求精准能提高攻击成本就有价值。7. 几个踩过的坑和实操心得第一条心得关于排查顺序。遇到疑似 machineKey 泄露的告警很多人的第一反应是查 ViewState 相关的日志但实际最有价值的入口是配置文件的访问日志。攻击者从拿到配置文件到真正投递 payload中间往往隔着几天甚至几周要做环境探测、要调试 payload。这段时间窗口就是你的机会。所以日常监控里web.config及其变体被成功请求应该是一条独立的、高优先级的告警规则。第二条关于修复的边界。换了 machineKey 不等于事件结束。密钥泄露意味着攻击者可能已经伪造过身份登录进来也可能已经在系统里落了东西。正确的顺序是先隔离、固定证据、排查是否存在落地痕迹确认干净之后再轮换密钥。反过来做的话密钥一换之前的伪造登录记录就失去了分析价值。第三条关于验证负载的设计。在授权测试里验证 RCE 是否成立最稳妥的负载不是反弹连接而是写一个内容固定的标记文件再加一次固定时长延时。写文件的好处是可以在事后明确证明执行成功了延时则用于快速判断链路是否通畅。两者结合既不依赖外连条件也不会在目标上留下可执行的残留。第四条关于误判。不是所有__VIEWSTATE变长都是攻击。有些页面在特定操作下比如展开树节点、翻页ViewState 本来就会膨胀。建立基线很重要采集一周的正常流量算出每个页面 ViewState 长度的分位数超过 P99 三倍以上的才值得看。最后分享一个实用的小工具思路。写一个脚本定期扫描自己名下所有站点的根目录和常见子目录检查是否存在备份文件后缀、是否开启了目录浏览、web.config是否可被直接请求。这个脚本用不了二十行跑一次几分钟但能挡住这类问题里最常见的那八成。我自己就是这么干的每周跑一次报告直接发到群里谁的项目谁认领。运维同学一开始嫌烦后来有一次真扫出来一个web.config.20240315.bak从那以后就没人抱怨了。我个人在实际操作中的体会是这类漏洞的可怕之处不在于技术复杂度——它的每个环节都是公开知识工具也现成——而在于它藏在看起来无害的位置。一个备份文件、一段被复制粘贴的配置、一次先这样回头再改的临时操作单看都是小事。安全工作的价值很多时候就是在这些小事变成大事之前把它们捞出来。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑