如果你维护过跑在 IIS 上的 ASP.NET Web Forms 站点,大概都见过这样的访问日志:一堆正常的 GET 请求中间,夹着一条POST /default.aspx,响应 200,请求体里躺着一个长度夸张的__VIEWSTATE。绝大多数时候这只是正常回发,但凡这个站点的web.config曾经被人下载走过一次,这条看起来平平无奇的请求就可能变成整台服务器的终点站。CVE-2026-26333讲的正是这么一条链:web.config泄露 →machineKey里的validationKey与decryptionKey外流 → 攻击者本地算出合法 MAC → 伪造__VIEWSTATE→ 服务端反序列化触发 gadget →w3wp.exe进程内执行任意代码。这篇东西不打算停留在“漏洞很严重、请及时修复”这种层面,而是把每一环的技术前提、约束条件、验证手法和加固顺序完整拆一遍,写给真正要动手排查和修复的人看。不管你是刚接手一套遗留系统的运维,还是做蓝队告警运营的同学,都能从里面找到能直接落地的部分。
1. web.config 为什么会出现在攻击者的收藏夹里
web.config这个东西的特殊性在于,它不是一份普通的配置文件,而是 ASP.NET 运行时整条请求管线的行为说明书。它决定了用哪套身份验证、走哪个 HttpModule、连接哪个数据库、用什么密钥签名。更要命的是,它天生带着密钥——而且是那种“一旦拿到就能在服务端自由进出”的密钥。很多团队做安全评估时会把注意力放在 SQL 注入、上传点、越权接口上,却忽略了一件事:一个能被匿名下载的web.config,其危害等级和数据库账号密码被写在首页上基本是同一量级。
1.1 配置文件继承模型决定了它必然带密钥
ASP.NET 的配置是分层继承的:机器级的machine.config→ 站点级web.config→ 目录级web.config,逐层覆盖。<machineKey>这个节点最典型的写法是validationKey="AutoGenerate,IsolateApps",意思是“运行时自己生成,并且按应用程序隔离”。问题就出在这个AutoGenerate上——自动生成的密钥值并不会凭空消失,它被持久化在注册表的 AutoGenKey 区域里,同一台机器上的同一个应用重启之后还是同一把钥匙。
这就带来两个后果。第一,很多开发者和运维根本不知道自己的站点有密钥,更不知道密钥长什么样,因为从来没人手写过它;第二,一旦有人为了“统一管理”或者“排查问题”把实际的machineKey显式写进了web.config,这把钥匙就从注册表里搬到了一个纯文本文件里。而纯文本文件,是有可能被 HTTP 请求直接拿走的。我在实际排查中见过的最常见场景,是为了做负载均衡或者多机部署,把密钥硬编码进配置文件然后跟着代码一起提交、一起发布、一起备份,最后备份目录忘了加访问限制。
还有一层容易忽略的:<connectionStrings>和<appSettings>通常也在同一个文件里。也就是说,一份泄露的web.config往往同时交出三样东西——machineKey、数据库连接字符串、第三方 API 的密钥。攻击者甚至不需要走反序列化这条重路径,直接连数据库就能拿到数据。所以讨论这个漏洞时,不能只盯着machineKey看,得把整个文件当成一次“凭证大礼包”泄露来对待。
1.2 machineKey 节点的四个属性,改一个就换一套防护
要看懂后面的攻击链,必须先把<machineKey>的四个属性掰开。很多人复制粘贴配置的时候只知道“要写这四个”,但不知道每一个在保护什么,结果改错一个属性就直接把防护降级了。
| 属性 | 作用 | 常见取值 | 改动后的影响 |
|---|---|---|---|
validationKey | 计算 MAC(消息认证码)的密钥 | 64/128 位十六进制串 | 更换后旧 ViewState 全部失效,用户表单提交会报错 |
validation | MAC 使用的算法 | SHA1、HMACSHA256、HMACSHA512 | 从 SHA1 升到 HMACSHA256 会改变 MAC 长度,需要同步所有节点 |
decryptionKey | 加密 ViewState 的密钥 | AES 需要 64 位十六进制(32 字节) | 更换后加密的 ViewState 全部无法解密 |
decryption | 加密算法 | AES、Auto、3DES | 老版本默认Auto,实际落到 AES 或 3DES 取决于系统 |
这里有一个特别常见的坑:decryptionKey的长度必须和算法匹配。选AES就要 32 字节即 64 个十六进制字符,写成 128 个字符会直接抛配置异常,写成 32 个字符(16 字节)在部分版本上会被当成 AES-128 处理,行为不一致。而validationKey的长度在HMACSHA256下建议 64 字节即 128 个字符,太短会削弱 MAC 强度。我见过有人为了“方便记忆”把密钥写成0123456789ABCDEF重复几遍,这种可预测的密钥等于没设。
另一个容易被误操作的是compatibilityMode。ASP.NET 4.5 之后对machineKey的加密和校验逻辑做了调整,如果多台服务器上的 .NET 版本不一致,或者老代码依赖旧的加密行为,就需要显式指定兼容模式。生产环境里出现过“一台机器能登录、另一台一直提示视图状态验证失败”的问题,根因往往就是负载均衡后端版本不齐加上没配compatibilityMode。
1.3 泄露入口不止“目录遍历”这一条
提到配置文件泄露,很多人的第一反应是路径穿越。实际暴露面要宽得多,我按遇到频率从高到低列一下:
- 备份与临时文件残留:发布脚本把
web.config复制成web.config.bak、web.config.old、web.config.20240115,而 IIS 的请求过滤只拦了.config后缀,.bak和.old不在默认的隐藏段列表里,直接被静态文件处理器返回。 - 静态文件服务误配:把整个项目目录挂成了静态资源目录,或者反向代理把
/无条件转发到文件系统,导致.config被当作普通文本返回。 - 压缩包与源码包外泄:部署包里带着完整配置文件,被放在了可下载路径下。
- 源码托管误公开:仓库权限配置失误,配置文件连同真实密钥一起进了公开可见的代码库。
- 错误页与诊断信息:自定义错误页配置成
mode="Off"时,异常堆栈里可能带出物理路径,为进一步定位文件提供线索。
IIS 默认会拦截对.config的请求,这一点要给它记一功。但拦截是基于请求过滤规则的扩展名匹配,一旦文件被改名成.txt、.bak、.zip,规则就失效了。所以真正的防线不是“靠 IIS 拦”,而是“配置文件根本不放在 Web 根目录里”。这一点后面在加固部分会详细展开。
2. ViewState 的签名机制:它本来是一道门,钥匙丢了就变成后门
__VIEWSTATE这个隐藏字段,是 Web Forms 时代最典型的“把状态放在客户端”的设计。它把服务端控件树的状态序列化、Base64 编码,塞进页面的隐藏 input 里,回发时再解析回来。这个设计本身没什么问题,问题在于解析这个动作——它本质上是一次反序列化,而反序列化的输入完全来自客户端。
2.1 __VIEWSTATE 从页面状态到序列化字节流
完整走一遍流程会更清楚。页面渲染阶段,Page把控件状态收集起来交给ObjectStateFormatter做序列化,输出的字节流经过 Base64 编码写入隐藏字段。如果开启了视图状态加密,序列化后的字节还会再用decryptionKey加密一次。回发阶段反过来:Page取出隐藏字段的值,Base64 解码,校验 MAC,按需解密,然后交给ObjectStateFormatter反序列化,还原控件树。
关键在于这中间的顺序。ASP.NET 的校验逻辑是“先验 MAC,再反序列化”。MAC 的输入是validationKey加上序列化后的字节,输出一段哈希附加在数据尾部。服务端收到请求后重新计算一遍,比对不上就直接抛ViewStateException,页面报“视图状态验证失败”。所以在validationKey保密的前提下,攻击者无法构造出能通过校验的载荷,反序列化这一步根本走不到。这套机制在设计上是合理的,它的安全性完全建立在“密钥只有服务端知道”这个前提上。
2.2 MAC 校验真正挡住的是什么
很多人对 MAC 的理解有偏差,觉得它“加密”了数据。不是的。MAC 只保证完整性,不保证机密性。在默认配置下,__VIEWSTATE的内容是可以被 Base64 解码后直接看到的,只是看不懂结构而已。MAC 的作用是让服务端能够确认“这段数据是我自己生成的,没被人改过”。
这就意味着,如果攻击者知道了validationKey,他可以在本地用同样的算法、同样的密钥,对任意自己构造的字节流算出合法的 MAC。服务端收到后一比对,通过了。校验环节就此形同虚设,后面的反序列化就会老老实实地处理攻击者精心构造的字节流。这就是整条链的核心转折点:machineKey泄露不是“信息泄露”,它是“认证体系被攻破”。
加密环节也有类似的问题。如果启用了viewStateEncryptionMode="Always",攻击者还需要decryptionKey才能构造出服务端能解密的载荷。但现实情况是,很多站点出于性能考虑把加密关掉了,只保留 MAC。所以validationKey泄露往往就够了,decryptionKey泄露只是让攻击者连“猜算法”这一步都省了。
2.3 拿到 validationKey 之后,校验环节形同虚设
有一类站点会让人觉得“应该没事”:enableViewStateMac关掉了。在 .NET Framework 4.5.2 之前,这个属性是可以显式设为false的,关掉之后服务端根本不验 MAC,任何 Base64 载荷都会被送进反序列化环节。这类站点甚至连密钥泄露这一步都不需要,直接构造载荷就行。从 4.5.2 开始,微软把这个开关强制为true,即使配置文件里写了false也会被忽略。
所以评估风险时,版本信息很关键。跑在 4.5.2 之前的站点,风险等级要再往上调一档。而跑在新版本上的站点,风险就完全取决于machineKey是否外泄。这也是为什么这个漏洞的链条看起来长,实际上每一环的成立条件都很清晰、都可验证。真正麻烦的不是技术难度,而是很多存量系统根本没有能力快速做密钥轮换——因为一切换,所有在线用户的表单状态全部失效。
3. gadget 链能不能拼起来,取决于宿主进程里有什么
反序列化漏洞能不能变成 RCE,从来不是“有反序列化点”就够的。真正决定成败的是:进程里加载了哪些程序集,以及反序列化器允许还原哪些类型。这一步是整条链里变量最多的环节,也是最容易在实战和复现中被卡住的地方。
3.1 反序列化的触发点在代码的哪一层
ObjectStateFormatter内部复用了BinaryFormatter的类型解析逻辑,它会根据字节流里记录的类型信息,去Type.GetType找对应的类,然后调用其反序列化构造函数或ISerializable实现。这个“按名字找类型并实例化”的动作,就是 gadget 链的入口。攻击者不需要控制服务端代码,只要能在字节流里指定某个“在反序列化时就具有副作用”的类型,并且能让这个副作用一路传导到命令执行,链条就成立了。
所谓 gadget,本质上是 .NET 基础类库里一堆“组合起来有害”的合法类型。单个类看起来都无害:一个比较器、一个集合、一个委托包装器。但当它们互相嵌套,反序列化过程中就会触发属性 setter、ToString()调用、Equals()比较等副作用,最终调用到Process.Start。
3.2 常见 gadget 类型与它们的版本门槛
公开研究里被反复讨论的几类 gadget,各自有硬性前提:
| gadget 大类 | 依赖的程序集 | 典型适用场景 | 主要限制 |
|---|---|---|---|
| 基于集合与比较器的委托型 | 基础库mscorlib | 通用性最好,几乎不挑环境 | 需要能构造出多层嵌套的集合结构 |
基于 WPF 的ObjectDataProvider | PresentationFramework | 桌面端或引用了 WPF 的 Web 项目 | Web 站点通常没加载 WPF,容易失败 |
| 基于工作流组件 | System.Workflow.ComponentModel | 老版本 .NET 项目 | 新版本框架里该程序集默认不存在 |
| 基于文本格式化的类型 | 部分 IDE 相关程序集 | 开发机或装了特定组件的服务器 | 生产服务器上很少见 |
| 基于身份模拟的类型 | 基础库 | 用于窃取进程令牌 | 通常需要配合后续动作,不直接执行命令 |
这张表说明一个很实际的问题:网上抄来的 payload,在别人的环境里跑通,不代表在你的环境里也能跑通。评估自己的站点时,正确做法不是“试试看能不能打通”,而是“看看应用池的进程里到底加载了什么”。查看方式很直接,在服务器上抓一次w3wp.exe的模块列表,或者在进程内枚举已加载程序集,基本就能判断哪些 gadget 可用、哪些不可用。
还有一层版本因素:.NET Framework 4.8 之后,BinaryFormatter引入了序列化类型白名单相关的开关,虽然默认行为仍保持兼容,但可以通过配置收紧。而更新一代的运行时已经彻底移除了BinaryFormatter。Web Forms 跑在 .NET Framework 上,短期内不会受这个移除影响,但如果哪天做框架迁移,这条链自然就断了——这也是加固思路里值得一提的一点。
3.3 打不通的六种典型原因
复现这条链的时候,卡住是常态。我整理了几种最常见的失败原因,基本覆盖了九成以上的“明明有密钥但打不通”的情况:
- 应用池位数不匹配:32 位和 64 位进程能加载的程序集版本不同,某些 gadget 在 32 位下会解析失败。
- ViewState 加密开着但没有解密密钥:只拿到
validationKey,MAC 能过,但解密那一步直接抛异常。 - 目标类型被禁用或找不到:字节流里写的类型名在当前程序集里不存在,反序列化直接报
TypeLoadException。 - MAC 算法不匹配:配置里写的是
HMACSHA256,构造时按SHA1算的 MAC,长度都不一样,必然失败。 compatibilityMode差异:密钥派生逻辑不同,算出来的 MAC 对不上。- 站点根本没启用 ViewState:页面级别关掉了
EnableViewState,回发时压根不解析这个字段。
这几种原因里,第 4 和第 5 项最隐蔽,因为报错信息不会直接告诉你算法不一致,只会笼统地提示验证失败。排查的时候得把配置文件里的validation、decryption属性和实际构造时用的参数逐字对齐,别嫌麻烦。
4. 边界验证:怎么在不造成破坏的前提下确认链条是否成立
做安全验证最忌讳的就是“一上来就执行命令”。真实的评估场景里,需要的是能证明“这条链能走到代码执行”的最小证据,而不是真的在服务器上落地一个文件。这既是职业操守问题,也有很现实的考虑——一旦真的执行了,取证现场就被你自己破坏了。
4.1 用延时和回连替代真正的命令执行
业界通用的无害化验证思路只有两种。第一种是延时探测:让反序列化过程触发一个可控的等待,比如几百毫秒到两秒的阻塞。如果请求的响应时间出现稳定、可复现的偏移,而相同请求在换一个不带 payload 的载荷时没有这个偏移,就证明代码执行路径被打通了。这种手法的好处是几乎不留痕迹,坏处是受网络抖动影响,需要多测几次取统计结果。
第二种是外连探测:让载荷触发一次对可控域名的 DNS 查询或 HTTP 请求。DNS 查询的好处是穿透性强,很多网络环境对出站 HTTP 有策略限制,但 DNS 解析往往走内网递归。收到回连记录,就说明代码执行成功,而且能顺带确认网络出站能力。现在很多团队用自建的 DNS 回连平台来接收这类信号,比外部服务更可控,也避免把测试数据发到第三方。
需要强调的是,这两种手法都必须事先拿到书面授权,并且明确测试窗口、目标范围、回滚方案。没有授权就去别人的站点上做这类验证,性质完全变了。
4.2 验证过程中的取证盲区
有个实际问题很少有人提:做延时和回连验证的时候,服务端会记录下异常的反序列化日志,但这些日志往往散落在不同的地方。w3wp.exe进程被回收之后,内存里的痕迹就没了;IIS 日志里只留下一个 POST 请求;应用日志里可能只有一条ViewStateException。如果你是在做红蓝对抗或者应急响应,这些碎片需要拼起来才能还原全貌。
我的建议是,在验证前先把日志采集链路确认一遍:IIS 访问日志是否开启了cs-username和cs(User-Agent)字段、.NET 运行时是否开启了反序列化相关的诊断事件、EDR 或 HIDS 是否能采集到进程创建事件。没有这些基础,后面根本没法做归因。这一步听起来琐碎,但它决定了你后面能不能判断“是有人已经打过了”还是“只是我自己的测试”。
5. 蓝队排查:一条被走过的链会留下什么
假设最坏的情况已经发生,站点被人用这条链走了一遍。现在要回答两个问题:怎么判断真的被打了?打到什么程度?这两个问题要分开看,前者靠流量和日志,后者靠主机侧取证。
5.1 IIS 访问日志里的三个异常特征
IIS 日志格式默认是 W3C 扩展格式,关键字段是cs-method、cs-uri-stem、sc-status、cs(User-Agent)、time-taken。针对这条链,有几个特征值得重点盯:
- 超长的请求体。正常回发的
__VIEWSTATE长度通常在几千到几十万字符之间,取决于页面复杂度。但攻击者构造的载荷往往显著偏大,尤其是带多层嵌套集合的 gadget,长度会冲到数百 KB 甚至上 MB。可以对cs-bytes或请求体长度做基线统计,找出明显偏离分布的样本。 - 固定的 URI 集中度。攻击者需要找一个能触发 ViewState 解析的页面,通常是有表单、有回发的
.aspx,比如登录页、搜索页。这类页面在正常业务里可能是 GET 为主,如果突然出现大量 POST,值得关注。 - 响应时间分布异常。带延时验证的请求会稳定慢几百毫秒以上;带命令执行的请求如果执行了外部程序,耗时会明显偏高。把
time-taken做时间序列看,能发现成簇的尖峰。
这三个特征单独看都有误报,但叠加起来指向性就很强了。特别是“长请求体 + 固定 URI + 时间尖峰”三件套同时出现的时候。
5.2 进程与文件系统上的落脚点
流量层面确认可疑之后,下一步看主机。w3wp.exe是以应用池身份运行的,默认的应用池标识是ApplicationPoolIdentity,对应一个虚拟账户,权限不高但足够读写站点目录和访问网络。如果载荷执行了命令,通常会看到这些痕迹:
w3wp.exe派生出了不该有的子进程,比如cmd.exe、powershell.exe、certutil.exe、bitsadmin.exe。正常的 Web 应用几乎不会调用这些。- 站点目录或临时目录下出现新的可执行文件、脚本文件,创建时间集中在某个时间点。
- 应用池身份发起的异常网络连接,尤其是对非常规端口的外连。
- 注册表里出现新的自启动项,或者计划任务里多了条目。
这里有个很现实的困难:很多中小规模的环境根本没有进程创建审计,只能靠事后翻文件时间戳。所以如果现在还没出事,赶紧把进程创建事件采集补上,成本不高,价值极大。
5.3 从反序列化事件反推
.NET Framework 会写一些与序列化相关的性能计数器和事件,部分场景下能反映反序列化的调用量。更直接的办法是在代码层加钩子:在全局的Application_Error里捕获ViewStateException,把异常的堆栈和请求的特征记录下来。正常情况下这类异常的频率很低,一旦在短时间内成簇出现,很可能是在被扫描或者被尝试利用。
还有一点值得说:__VIEWSTATE校验失败时,ASP.NET 会抛异常并返回 500,页面显示“视图状态验证失败”。有些团队为了“用户体验”把这个异常吞掉了,返回一个自定义错误页。从安全角度看,这反而好——攻击者拿不到明确的成功/失败信号,探测成本变高。但如果为了排查方便把详细异常信息暴露到响应里,那就等于给攻击者送了一个校验结果反馈通道。
6. 加固顺序:先堵泄露面,再换密钥,最后降爆炸半径
修这类问题最怕上来就动密钥。密钥一换,所有在线用户的表单状态立刻失效,用户侧表现为“提交后提示出错、数据丢失”,业务方第一个找你。所以顺序很重要:先堵住泄露路径,再规划密钥轮换,最后做纵深设计降低“万一再泄露”的影响。
6.1 把 machineKey 从自动生成改成显式受控
显式配置machineKey本身不是加固,显式配置并且管好它才是。第一步是生成高质量的随机密钥。decryptionKey选 AES-256 需要 32 字节,也就是 64 个十六进制字符;validationKey选HMACSHA256建议 64 字节,128 个十六进制字符。别用在线生成器,用本地加密库生成后直接写入配置。
<system.web> <machineKey validationKey="这里放128个十六进制字符" decryptionKey="这里放64个十六进制字符" validation="HMACSHA256" decryption="AES" /> <pages viewStateEncryptionMode="Always" enableViewStateMac="true" /> </system.web>关于轮换,有一个能显著降低业务影响的做法:双密钥过渡。ASP.NET 的machineKey只支持一组密钥,但可以在负载均衡层做灰度——先给一部分节点换新密钥,观察这段时间内用户报错比例;如果错误集中在跨节点请求上,说明还有会话粘滞没处理好。另一种做法是在应用层接受两种算法:compatibilityMode配合过渡期,先不换密钥只升级算法,等所有节点同步之后再换密钥。这样每次只变动一个变量,出问题好定位。
还有一个小技巧:把machineKey放到独立的配置文件里,用configSource引进来,比如<machineKey configSource="config\keys.config" />,然后把config目录放到 Web 根目录之外。这样即使 Web 根目录被误配成静态资源,密钥文件也不在可访问路径下。同时这个做法让密钥可以独立管理、独立授权,机器之间同步起来也方便。
6.2 配置文件本身的访问控制
密钥管得再好,文件被下载走一样白搭。这一层的加固要分三个方向:
第一是位置。配置文件不放在 Web 根目录,是最彻底的方案。发布流程应该把配置项渲染到与站点目录平行的独立路径,或者放在环境变量、密钥管理服务里由启动时注入。这个改造成本不小,但对新项目来说应该作为默认要求。
第二是请求过滤。如果配置文件确实必须在站点目录内,就要确保 IIS 的请求过滤规则覆盖所有可能的变体。默认的<fileExtensions>里拦了.config,但没拦.bak、.old、.orig、.save、.swp、.tmp。把这些扩展名加进去,同时确保<hiddenSegments>里包含App_Data、bin这类敏感目录。
<system.webServer> <security> <requestFiltering> <fileExtensions allowUnlisted="true"> <add fileExtension=".config" allowed="false" /> <add fileExtension=".bak" allowed="false" /> <add fileExtension=".old" allowed="false" /> <add fileExtension=".orig" allowed="false" /> <add fileExtension=".tmp" allowed="false" /> <add fileExtension=".zip" allowed="false" /> </fileExtensions> <hiddenSegments> <add segment="App_Data" /> <add segment="bin" /> </hiddenSegments> </requestFiltering> </security> </system.webServer>第三是流程。发布脚本里加一步自动扫描,检查站点目录下有没有web.config的副本、有没有.bak结尾的文件、有没有压缩包。这一步成本极低,能避免绝大多数低级失误。我在实际工作中见过的配置泄露,超过一半都是发布流程留下的备份文件,而不是什么高明的手法。
6.3 即使密钥泄露也不该直接等于 RCE 的设计
最后这层是最值得投入的:假设密钥真的又泄露了,怎么让攻击者拿到密钥也打不出 RCE。这个思路叫降低爆炸半径。
收窄应用池权限。默认的应用池身份对站点目录有读写权限,这是很多 RCE 能立刻落地的前提。如果业务允许,把站点目录设为只读,只让特定子目录可写(比如上传目录),并且限制子目录不允许执行脚本。IIS 里对单个目录配置“处理程序映射”的禁用可以做到这一点。这样即使反序列化执行了代码,能写入的位置也极其有限。
限制出站。gadget 执行命令之后通常需要外连下载下一阶段工具。如果应用池所在主机对出站流量做了白名单,只允许业务必需的域名和端口,攻击者的后续动作会被卡住。这个措施成本不高,效果明显。
启用最小化的进程身份与隔离。不同站点用不同的应用池,避免一个站点被打了横向影响其他站点。这一点在共享服务器上尤其重要。
监控层面兜底。对w3wp.exe的异常子进程创建做告警,对站点目录下的可执行文件写入做告警,对异常的 DNS 查询做告警。这三条规则实现起来都不复杂,但能覆盖住这条链最后阶段的绝大多数动作。
还有一个容易忘的点:如果站点启用了 ViewState,但某些页面其实完全不需要,就把这些页面的EnableViewState关掉。减少解析入口,等于减少攻击面。这个改动风险很低,收益直接。
我在实际做这类加固时的体会是,最难的从来不是技术方案,而是推动改动落地。换machineKey意味着要协调发布窗口、要评估会话影响、要准备回滚;收窄应用池权限意味着要跟开发确认哪些目录需要写;限制出站意味着要跟网络团队对齐白名单。每一个动作都会遇到“这个能不能先不动”的反馈。所以真正有效的做法是把风险讲清楚:配置文件泄露不是“可能被人看到一些配置”,而是“拿到密钥的人可以在你的服务器上执行任意代码”。这句话说清楚之后,排期的优先级通常会变。至于先做哪一步,我的建议永远是先做发布流程的自动扫描——它不需要停服、不需要协调、当天就能上线,而且能挡住最常见的那批泄露路径。