
1. 为什么我又回头研究 SAMR 协议先放个结论MS-SAMR 这个协议没死反而比很多新协议更值得深挖。你可能每天都在间接使用它却从来没意识到它的存在。比如在你电脑上敲 net user 看用户列表比如运维平台里某个按钮执行“重置用户密码”再比如安全团队扫描系统里的本地管理员账号这些动作的背后大概率都有 SAMR 的影子。MS-SAMR 全称是 Security Account Manager Remote是一组用于远程访问安全账户管理器SAM数据库的 RPC 接口。它可以直接查询和修改用户、用户组、别名也能读取和设置账户策略。很多人听到这个词是在安全相关的报告里但我不太喜欢把它只当一个“安全话题”来聊。我更愿意把它当运维基础设施看待凡是需要跟 Windows 账户数据库打交道的自动化平台绕不开它。这篇文章之所以叫“再探”是因为我最早读协议文档时也是按部就班看方法列表、看数据结构看完觉得“不过如此”。后来到真实环境里做账户基线核查、做运维自动化接口才被各种实际行为反复教育同一个枚举接口为什么在某台老系统上返回的数据怪怪的为什么我的脚本在域成员机上只列出本机用户为什么新装好的系统用管理员账号连过去还是报拒绝访问这些问题的答案大多不在协议文档的“方法定义”里而藏在系统版本演进、安全策略默认值和调用参数的具体选择里。如果只是照着网上现成代码抄一遍遇到问题就只能瞎猜。这篇内容的目的就是把我踩过的坑和一些底层逻辑讲清楚让读者少走点弯路。适合读这篇的人有三类第一类是在写自动化运维平台的开发同学尤其是要维护账户管理功能的第二类是负责系统安全基线评审、需要搞明白“SAMR 远程调用到底暴露了什么”的同事第三类是刚接触 Windows 账户体系、想弄懂 net user 这类命令背后原理的新人。下面内容不会给那种“照着敲一遍就能通”的空泛代码重点是把设计逻辑和真实环境里的坑讲明白。这里先做一个重要提醒下文所有演示都基于管理员在授权范围内维护自己系统的场景。脚本只做只读审计不涉及任何绕过身份验证的内容。真要拿到生产环境请先确认你具备足够的授权。2. 协议定位与整体设计思路2.1 协议到底负责什么先解释一个最基本的背景。Windows 上有一套本地安全体系上半部分叫 LSA负责登录验证、令牌生成、审计策略下半部分叫 SAM负责把用户、组、密码摘要存进数据库。这个分层看起来清晰但它毕竟是本地架构。一旦我们需要从远程机器去管理另一台机器的账户就得有协议把这些内部调用转移到网络上去。MS-SAMR 做的就是这件事。它定义了客户端如何远程打开服务器上的 SAM 对象、枚举域、查询用户属性、创建和删除账户、修改密码、设置域策略。这里有一个特别容易误会的点协议里的“域”概念在实际环境里有两个层面。在域控制器上SAMR 打开的是 Active Directory 域对象在普通成员服务器上打开的则是本机 SAM 数据库。所以你写的同一个程序跑在不同类型的机器上看到的数据范围完全不同。很多人会问有了 LDAP 和 Kerberos为什么还要保留 SAMR这要从兼容性讲起。AD 域里的账户管理主要走 LDAP但本地账户、本地组成员关系、某些登录相关信息的查询和修改仍然要落到 SAM 层。在跨版本、跨环境、以及一些特殊管理工具的内部实现里SAMR 是那个兜底的通用通道。协议没死一方面有历史包袱另一方面是它在底层场景里的确不可替代。2.2 从绑定到调用一条调用链的拆解一个典型 SAMR 调用的完整路径我在项目里经常要画给同事看。画完大家就明白为什么一个问题要排查那么多层。下面把完整链路列出来建立传输会话。绝大多数场景走 SMB客户端连接目标机器的 445 端口完成身份认证建立 IPC 连接。绑定 RPC 接口。在 SMB 共享上创建命名管道通常是\pipe\samr然后在管道上发起 RPC bind 请求指定 SAMR 服务对应的接口 UUID。执行业务调用。调用具体方法比如枚举域、打开域、枚举用户。释放资源断开绑定。关闭打开的句柄断开 RPC 绑定释放网络会话。这里有个容易忽略的细节SAMR 的许多调用需要先拿到上一层对象的句柄。你连上服务器后先要用 SamrConnect 拿服务器句柄想枚举用户得先打开域句柄想查某个用户还要在域句柄基础上用相对标识符RID再打开用户句柄。整个过程很像从文件系统根目录逐层进入子目录每一步都返回句柄用完必须关闭。有同事经常问为什么不直接拿着对象引用到处传原因很简单句柄是经过权限校验的引用。你在 SamrConnect 时声明想要的访问权限后续所有操作都基于这个范围系统不会每次都重新检查你“能不能做这件事”。这种设计减少了重复校验但也意味着如果你一开始把权限要小了后续调用会在更深的地方失败一开始要大了又可能触发安全告警。权限粒度怎么选要提前想清楚。2.3 版本演进与行为差异SAMR 不是一成不变的。协议规范里不同版本的方法并存最典型的是 SamrConnect 系列。老版本接口在早期系统上很常见后来随着安全需求和权限表达细化新系统逐步引入了带更多参数的新版本接口。这种演进的表面结果是“文档变厚”实际影响是同一段调用代码在不同系统上的表现会有差异。另一个重要演进是安全策略的收紧。新版系统默认禁止匿名访问 SAM并且在本地安全策略里增加了“网络访问对 SAM 的远程调用进行限制”这个选项。如果客户端请求不是来自管理员组成员目标机器会在 RPC 层直接返回拒绝访问而不会告诉你具体原因。很多自动化脚本第一次跑就会撞上这道墙。还有一种行为差异来自账户令牌过滤。部分系统对本地管理员做远程登录时会先过滤掉一部分高权限组成员身份只保留普通用户权限。这意味着即使你用管理员账号连接目标也不一定给你完整的管理权限。很多跨机器管理脚本在 A 机器上测试正常换到 B 机器就报权限不足多半就是这类差异造成的。3. 核心操作与实现细节3.1 服务器句柄与域对象的获取看完整流程的第一段连接服务器、枚举域、打开域。我刻意用“打开”而不是“查询”因为 SAMR 的对象模型面向句柄。SamrConnect / SamrConnect5建立服务器会话返回 ServerHandle。SamrEnumDomains枚举服务器可见的域列表返回域名和域 SID。SamrLookupDomain按名称查找域 SID。SamrOpenDomain基于域 SID 打开域句柄。这里最关键的参数是访问掩码DesiredAccess。SAMR 把权限拆得特别细粒度比普通文件读取权限还要细致。实际工程里常见的现象是程序员为了让代码“肯定能跑通”直接传最大权限值。这样做有两个坏处一是容易触发目标侧安全监控的“高位权限调用”告警二是权限过大客户端一旦被滥用影响范围就会扩大。我的项目习惯是连接和原型验证阶段可以先用最大权限把链路通起来确认代码本身没有写错但一旦进入生产脚本必须换成最小够用的权限。先明确业务逻辑只需要“读”还是需要“写”。只做账户清单导出用只读标志就够要做密码重置那才需要高权限位。不同版本系统对权限位具体取值的定义略有差异动手前翻一眼对应版本的枚举定义成本很低回报很高。3.2 用户枚举与分页机制拿到域句柄后最常见的操作是枚举用户列表。这里有一个全网资料讲得比较少、但在生产环境里特别重要的机制分页。SAMR 的枚举接口不是一次性把全部用户返回给你的。它的调用参数里包含类似“枚举上下文”的东西。第一次调用时客户端传初始值服务器返回一批结果同时回填一个用于继续提取后续内容的上下文值。如果客户端忽略这个回填值直接再用初始值调用拿到的可能永远是第一页数据最终导出的用户数量会明显少于实际人数。我曾接手过一个内部用户数据核对项目当时的脚本会漏掉约 15% 的账户问题就出在这里。排查时一开始怀疑过滤条件写错把日志打出来才发现每次枚举请求时上下文的值都是同一个。这说明代码里根本没处理续传逻辑。改成按返回值继续调用之后数量立刻对上了。除了分页枚举参数里通常还有过滤标志比如只枚举普通用户账户、不关心内置账户和机器账户。生产环境建议显式设置过滤条件避免把无关对象带进报表。如果你要导出完整账户列表做审计就要把过滤范围理解清楚别把“故意过滤”当成“数据缺失”。3.3 用户属性查询与常见字段解读拿到用户 RID 列表后需要先用 SamrOpenUser 打开用户句柄再用不同信息级别查询具体数据。查询时通过“信息级别”决定返回哪种复合结构这一点很像 SQL 里 select 不同字段组合的视角只是这里的结构由协议固定。查询结果里我平时最关注这几个字段用户 SID区分对象的关键标识。UserAccountControl位掩码里面藏着账户启用状态、是否锁定、是否密码永不过期等标志。PasswordLastSet上次设置密码的时间通常是 64 位整数格式需要换算。PrimaryGroupId主组 RID用于判断账户归属。AccountExpires账户过期时间很多清理任务会定期扫描这个字段。我的建议是先画一张字段对照表把协议字段和业务字段的对应关系固定下来再写解析代码。比如“账户是否禁用”在 UserAccountControl 中对应某一位代码里别用“等于”去判断整个掩码而是用“按位与”检查目标位是否被设置。否则账户上挂了其他标志位时判断结果会被干扰报表数据也就不准了。3.4 密码操作与策略设置账户管理里绕不开密码操作。SAMR 提供两类接口一类是用户本人在知道旧密码的情况下修改密码另一类是管理员直接设置新密码。后者需要的权限级别更高实际调用还会受目标侧密码策略的限制。我见过不少运维平台在密码重置功能上踩坑明明用管理员凭据调用 SAMR却被域策略拦下来返回特定错误码。原因往往是目标域开启了密码复杂性校验或者账户启用了强制密码历史。协议文档只会告诉你“服务器可能返回错误”但一个会排查的人会同步检查密码策略里是否有最小长度、复杂性、历史记录等要求把策略限制和协议错误对齐。域策略的读取和设置也有一部分由 SAMR 操作承载比如读取账户锁定阈值、密码最长使用期限。不过在域环境里这类操作通常走更贴近目录服务的通道SAMR 更多作为本地策略兜底。工程上需要先确认目标对象是域账户还是本机账户再决定调用路径避免用错通道导致权限校验失败。4. 实操写一个最小可用的账户审计脚本4.1 开发准备与依赖选择下面用一个实际可参考的例子把上面的逻辑落地。场景是一台内部服务器需要批量导出账户状态生成只读基线报表。这个需求不能改动任何账户属性但要抓出上次设置密码时间、账户是否启用等信息。我选 Python 加常用网络协议库来演示。这类库把 RPC 绑定、数据类型解码都封装好了适合做原型验证。先把依赖装上pip install impacket接着准备目标机器 IP 和账号。这里特别提醒不要拿域管理员账号去跑这种只读任务。更合理的做法是准备一个只被授权了“读取账户信息”的专用服务账号。万一脚本被误用影响可控审计日志也容易定位。下面代码里的凭据参数只是示例务必替换成你自己的专用账号。4.2 脚本设计骨架与权限位选择整个脚本分五步通过 SMB 传输封装创建 RPC 传输层。绑定 SAMR 接口。枚举域并打开域句柄。枚举用户并读取关键属性。输出结构化结果。先看连接部分的骨架from impacket.dcerpc.v5 import transport, samr def build_samr_connection(target, user, password): rpc_transport transport.SMBTransport( target, 445, r\samr, usernameuser, passwordpassword, ) dce rpc_transport.get_dce_rpc() dce.connect() dce.bind(samr.MSRPC_UUID_SAMR) return dce这段代码把 SMB 认证、管道路径、RPC 绑定都封装在一个函数里。之后调用方只关心业务逻辑不必再碰底层细节。这种拆分在实际项目里很有用以后如果要换传输方式只需要换传输对象查询逻辑基本不用动。连接建立后打开服务器句柄resp samr.hSamrConnect5(dce, r\\target_host, samr.MAXIMUM_ALLOWED) server_handle resp[ServerHandle]先把话说清楚这里用 MAXIMUM_ALLOWED 只是为了原型阶段快速打通链路缩小排查范围。真正进生产环境前必须把它改成符合“最小权限原则”的只读访问常量。在 impacket 库里各种权限常量名都能直接查到大写定义照着业务范围去挑就行这一步省不得。4.3 枚举域、用户与分页处理有了服务器句柄先枚举域拿到域名和 SIDenum_resp samr.hSamrEnumDomains(dce, server_handle, 0, 100) for item in enum_resp[Buffer][Buffer]: domain_name item[DomainInfo][Name] domain_sid item[DomainInfo][Sid] print(domain:, domain_name, domain_sid)拿到域 SID 之后可以打开域句柄继续枚举用户。我这里把枚举写成循环关键就是处理分页def page_through_users(dce, domain_handle): resume_handle 0 while True: users_resp samr.hSamrEnumUsers( dce, domain_handle, 0, samr.USER_NORMAL_ACCOUNT, resume_handle, 100, ) for u in users_resp[Buffer][Buffer]: print(u[RelativeId], u[Name]) if users_resp[ResumptionHandle]: resume_handle users_resp[ResumptionHandle] else: break这段代码的关键点在最后几行。每次调用后都去读返回的 ResumptionHandle如果有值就带进下一次调用。如果忽略它接口会认为你又要从头开始最终结果就是漏数据。这是我在真实项目里排查过很久的问题也是我觉得写 SAMR 枚举时必须放进代码模板的标准结构。4.4 用户属性读取与日期换算枚举到 RID 和账户名后需要打开用户句柄再查属性。实际操作时我建议查询完一个用户就立即关闭句柄再处理下一个避免句柄长时间占用。核心调用思路如下user_handle samr.hSamrOpenUser(dce, domain_handle, read_access, rid)[UserHandle] info_resp samr.hSamrQueryInformationUser(dce, user_handle, some_level) # 处理完成后 samr.hSamrCloseHandle(dce, user_handle)返回结构里最容易看错的是密码设置时间。协议在传输层通常把它表示成一个 64 位整数很多新人会当 Unix 时间戳去处理结果得到一个完全离谱的日期。实际上这类字段常常需要做基准日期换算转成 100 纳秒间隔后再计算。你一旦发现日期数值明显不对第一反应就应该是换算公式或起始基准用错了而不是怀疑目标机器时间错了。4.5 输出与基线做法脚本最后把数据写进 CSV或者直接存到数据表格里就能用。我真正想让读者关注的不是输出格式而是“基线”的思路首次全量导出后存成基线文件之后定期再导一次用差异对比找出新增、禁用、密码临期的账户。这个过程既能发现异常账户也是账户生命周期管理的前置步骤。很多团队做账户管理只停留在“能查”差的就是这种定期比对机制。5. 常见问题与排查技巧实录5.1 连接被拒绝权限与策略双因素症状脚本连接时报访问拒绝甚至换本机管理员也不行。排查思路先确认账号确实在目标机器管理员组里。远程登录场景下管理员组成员身份会被安全策略二次加工。检查“网络访问对 SAM 的远程调用进行限制”。这个策略如果只允许管理员普通账号必然失败即使账号是管理员同时命中更严格的共享模型配置时也可能被过滤。看目标系统版本。不同版本的默认策略不一样有些环境需要额外调整。我遇到过一个很好玩的案例同一个账号在 A 机可以登录在 B 机却无法远程查 SAM。最终发现是 B 机的限制策略没有配置好跟代码完全无关。排查这类问题最重要的不是反复改代码而是先确认策略状态。5.2 枚举漏项问题一定出在分页吗不一定。漏项除了分页还可能出在过滤条件上。SAMR 枚举接口允许按账户类型过滤如果你传入的过滤值排除了某种账户比如机器账户或禁用账户结果里自然看不到这些对象。正确的排查顺序是先看代码里的过滤参数再谈分页。另外部分工具会做慢速枚举来规避检测但在合法运维中我不建议这么干。生产环境做全量账户扫描时应当分批拉取并记录断点既降低目标负载也方便中途失败后接着跑。5.3 新旧系统返回差异对老系统调用高版本接口可能得到“接口不可用”类错误。反过来系统版本太新对老调用方式也可能更保守。经验上做到两点一是准备方法降级逻辑比如比较新的接口失败时自动尝试老版本二是把错误码完整透传到日志不要吞掉。否则你只会看到“调用失败”却不知道是接口版本不匹配还是权限不足。5.4 域控制器与成员机看到的世界不一样这是个特别容易忽略的问题。同一段代码连成员服务器枚举的是本机用户连域控制器枚举的是整个域用户。如果你的目标是“全域账户清单”却错误地连到一台成员服务器拿到的数据自然会少一大截。我在做跨机器数据核对时专门在配置表里加了一个“目标类型”字段区分域控和成员机然后把结果和已知总用户数做交叉验证。没有这个校验漏连一台机器可能很久都发现不了。5.5 部分常见错误码速查下面整理了一些常见错误码和对应的排查方向方便现场快速定位错误码典型含义排查方向0xC0000022访问被拒绝检查权限位、SAM 远程调用限制策略0xC000006A密码错误核对旧密码是否正确、是否过期0xC000006D密码不符合策略检查密码复杂性、历史、长度要求0xC0000225对象未找到检查 RID、域名、过滤条件是否匹配0x00000715密码过期/不可用查看账户策略与过期时间这张表只是起点真实环境还有更多状态码。遇到陌生错误码时建议先解析成标准 NTSTATUS 文本再顺着含义反向查策略比一句句翻译十六进制快得多。6. 从安全配置角度限制远程 SAMR 调用6.1 策略基线与推荐配置如果我不是在写脚本而是在给新系统做安全加固SAMR 相关配置一定是重点之一。最直接的策略项是“网络访问对 SAM 的远程调用进行限制”。它的用途是明确哪些主体可以通过 SAMR 远程访问安全账户对象。推荐基线是把该项设为“管理员组”既保留正常运维入口又避免任意普通账号都能枚举账户信息。另一个相关策略是本地账户的共享和安全模型项。它影响本地账户通过网络登录目标机器时的权限映射方式。在受攻击面较大的场景里通常建议按更严格的模型配置。配置完成后最好用独立账号实测一次确认管理员远程管理功能没有被影响。这类策略改起来容易但一旦改过头运维平台可能大面积报错。6.2 监控视角看哪些日志SAMR 调用本身不一定会产生独立的安全审计日志它的效果更多体现在账户枚举、权限使用和登录事件的周边。建立监控时我习惯分三层看登录层关注来自异常来源地址的登录与权限事件尤其是带管理员组成员身份的远程登录。访问层在网络层记录对 445 端口和 SAMR 管道的连接情况构建流向基线。进程层借助终端检测响应类工具检测可疑进程发起的批量远程管理调用。我做账户基线审计时会先收集两周正常流量的访问来源清单再基于清单预警突发的陌生来源批量访问。只盯单个事件很容易被正常业务流量干扰反而是“偏离基线”的信号更有价值。6.3 几个常见误区第一个误区以为把 SAMR 相关接口全部禁用就彻底安全了。实际上大量管理功能依赖它一刀切会引发大面积故障。更合理的设计是限制调用主体而不是关闭能力。第二个误区把“能枚举用户”等同于“拿到了系统权限”。枚举用户只是信息收集中的一段不等于接管系统。安全评审里如果出现这种说法其实是没把协议暴露面和真实攻击成本讲清楚。第三个误区把 SAMR 访问一律当作可疑。域内正常业务、日志采集、合规检查都会产生这类连接全部告警只会把真正的问题淹没。先分层建基线再对偏离基线的行为做响应才是可落地的策略。7. 一些个人操作体会写到这里我最想分享的其实是一件很朴素的事协议文档只是起点真正值钱的是把它放进真实环境后的适配经验。我第一次把账户管理项目从原型推到生产连续三个晚上都卡在远程受限策略上。当时一度怀疑是代码问题后来发现目标机器上放开管理侧策略后一切正常。从那以后我养成了两个习惯。第一个习惯是永远把“连接层”和“协议层”分开排查。遇到问题先确认能不能正常建立带身份验证的网络会话再看最终调用失败发生在哪一层。第二个习惯是生产脚本里绝不用最大权限去试探。虽然这样会让调试多几步但后期安全评审能省很多口舌。如果你正准备把远程账户查询能力接入自己的运维平台建议先建一个小范围试点用最少的权限把通路上每个环节的真正限制摸清楚再往大范围推。SAMR 这个协议名字里带着“老”字实际场景里它仍然是硬需求值得每个做系统和运维的人认真对待。至少对我来说把它从“看过文档”变成“真正会用”是在生产环境里踩了无数个坑之后才完成的。希望这篇再探笔记能帮你把这个过程缩短一点。