MS-SAMR协议深度解析:从账户枚举到句柄管理与安全模型

发布时间:2026/10/22 16:28:28
MS-SAMR协议深度解析:从账户枚举到句柄管理与安全模型 很多搞AD域运维或者安全方向的朋友一看到「MS-SAMR协议」这五个字脑子里的第一反应多半是「用户枚举」。早些年做内网安全评估的时候我也是这个印象总感觉它就是用来列账户列表的。直到后来在某个跨平台系统里做账户同步模块被一系列奇怪的权限问题折腾得焦头烂额才不得不把MS-SAMR协议从头到尾重新捋了一遍才发现自己之前的理解实在太浅了。这篇文章就是基于那次「再探」的完整记录。我会从协议定位、名字空间、句柄生命周期、安全模型、实操落盘这几个维度重新梳理一遍顺便把我在真实环境里踩过的坑和排查思路一并写出来。适合正在写账户管理工具、做安全监测规则、或者调试AD域账户同步服务的开发者参考如果你只是听说过SAMR这个词想搞明白它到底管什么这篇文章也能帮你建立一个还算完整的认知框架。1. 先给协议正个名它不只是个「用户名筛选器」1.1 协议到底管了哪些事MS-SAMR全称是Security Account Manager Remote Protocol翻译成大白话就是「远程安全账户管理协议」。它暴露在Windows系统的Server服务上走的是SMB管道里面的命名管道最常见的绑定端点是\PIPE\samr。通过这组RPC接口你可以对本地或者域内的账户数据库做几乎所有的管理操作。我这里说的「几乎所有」包括但不限于创建用户、删除用户、修改用户名、设置密码、重置密码、查询用户属性、枚举域内用户、枚举域内组、枚举别名、维护组成员关系、查询账户锁定状态、修改账户锁定阈值等等。看到没有枚举用户只是其中一小块功能。很多人只知道用SAMR去查用户名是因为在安全评估的场景里「拿到有效用户名」是后续爆破、喷洒攻击的第一步。但实际上SAMR是一个完整的账户管理面正常运维场景下的价值远大于安全测试场景。1.2 为什么叫「再探」而不是「初探」之所以叫再探是因为大部分公开资料对SAMR的描述都停留在「调用某个Opnum就能枚举用户」这个颗粒度。真正落地的时候你会发现它和AD域的其他协议是互相咬合的比如LSADLocal Security Authority Domain Policy、Netlogon甚至和底层的SMB会话认证强绑定。你单独看SAMR的协议文档很多细节根本连不起来只有把它放到域环境的上下文里才能理解为什么同一个调用在不同系统版本上的行为会不一样。还有一个原因现代Windows系统对SAMR的访问控制做了很多收紧旧的那套「拿到一个普通域用户就能枚举全量用户」的玩法在某些环境下已经不灵了。如果你不理解协议的安全模型变化照着老文章去写工具大概率会在新系统上碰一鼻子灰。2. 名字空间里的那些坑2.1 SAMR管的是「SAM数据库」不是LDAP目录很多人容易把SAMR和LDAP混在一起因为AD域里的账户信息似乎都能通过LDAP查得到。但SAMR服务的对象其实是SAM数据库也就是Windows系统里维护本地账户和域账户的那套底层存储。你可以理解为LDAP是给外部应用看的「展示层」而SAMR直接对话的是「存储层」。这意味着什么意味着SAMR接口里处理的名字格式、SID格式、权限模型和你平时在LDAP查询里看到的字符串、DN路径不是一个体系。比如在LDAP里你访问用户用CN某某,CNUsers,DCcontoso,DCcom这种可辨识的路径但在SAMR里你打开一个domain就是拿到一个SID打开一个user就是拿到一个RID一切都是SIDRID的算术关系。2.2 三个命名空间账户名、SID、RID我给自己画了一张关系图虽然这张图没法用文字画出来但核心逻辑可以讲清楚域账户的唯一标识是Domain SID Relative ID (RID)。比如S-1-5-21-****-500里的500就是内置管理员的RID。SAMR的OpenDomain调用入参是SID或者域名返回一个DomainHandle。OpenUser调用入参是DomainHandle加用户的RID或者用名字做LookupNames返回UserHandle。这里最容易踩的坑是「RID不是固定不变的」。你在一个域里删掉一个用户再重建一个同名用户新用户的RID和原来不一样。所以如果你写了一个工具依赖用户名去缓存RID一旦用户被删重建你手里的RID对应的就是另一个账户了。很多安全工具在域环境里出现误判根子往往就在这种缓存失效问题上。2.3 用户名的20字节截断问题这个属于历史遗留问题但到今天还在坑人。SAM账户名在底层存储时最大的长度限制是20个字节不是20个字符。中文环境下一个汉字按UTF-8编码通常是3个字节所以一个20字节的字段最多只能存6个中文汉字加一点后缀。很多管理工具在创建用户时如果名字超过这个限制系统会自动截断。结果就是你在管理界面看到的用户名和实际SAM数据库里存的对不上。在你用SAMR做LookupNames或者枚举的时候返回回来的名字可能就是被截断后的版本。你要是拿这个截断名字去LDAP里查大概率是查不到完整对象的因为LDAP里的sAMAccountName可能是另一套规则算出来的。所以跨协议做账户关联时一定不要拿「看起来像」的字符串直接拼接尽量用SID作为关联键。2.4 本地账户和域账户的区别SAMR不仅能管域账户也能管本地账户。关键在于你连的是一台域控还是一个普通成员服务器。如果你在普通成员服务器上访问SAMR那打开的是本机的SAM数据库如果你连的是域控打开的是整个域的账户数据库。这个差异在编程时特别容易出问题。我有一个阶段的排查经历就是用同一段代码去查询用户属性在域控上跑得好好的放到成员服务器上就报「句柄无效」。最后发现是连接对象初始化时没有正确指定目标机器类型导致SAMR内部逻辑走错了数据库实例。这个事情的教训是不要在代码里硬编码目标机器的角色每次调用前先做一次角色探测比如判断目标是不是域控再决定走哪条初始化路径。3. 状态机与句柄的生命周期管理3.1 SAMR的「打开-操作-关闭」三段式流程SAMR协议不是像HTTP那样一个请求一个响应对应到底就行了。它的核心模型是句柄Handle你需要先通过Open系列调用拿到一个句柄再拿着句柄去执行具体操作最后关闭句柄。整个生命周期可以分成三步建立上下文通过SamrConnect连接到目标SAM服务器拿到ServerHandle。逐级打开通过SamrOpenDomain打开域通过SamrOpenUser或SamrOpenGroup打开具体对象。执行操作查询属性、修改属性、枚举成员等。关闭句柄每层句柄都需要显式关闭。这个流程和我们平时操作文件很像打开文件拿到文件描述符读写关闭。但SAMR的句柄有个特点——它是有权限掩码的。你在打开用户时传入的访问掩码决定了这个句柄后续能执行哪些操作。如果当前账户权限不够打开句柄的调用就会直接失败返回AccessDenied而不是等你在执行具体操作时才报错。3.2 访问掩码的设计逻辑SAMR里常用的访问权限包括SAMR_USER_READ_GENERAL允许读取用户的通用属性。SAMR_USER_READ_LOGON允许读取与登录相关的属性比如上次登录时间、登录脚本路径。SAMR_USER_READ_ACCOUNT允许读取账户控制标志比如是否锁定、密码永不过期。SAMR_USER_CHANGE_PASSWORD允许修改用户密码。SAMR_USER_SET_PASSWORD允许重置用户密码不需要知道旧密码。SAMR_USER_SET_ATTRIBUTES允许修改用户属性。这里有一个特别容易搞错的点很多开发者为了省事直接申请一个MAXIMUM_ALLOWED权限。这个权限的意思是「把当前账户能给的权限尽量都给我」。看起来很好用但实际执行时系统是先计算当前账户的全部有效权限再从中取一个交集。如果当前账户只是一个普通域用户通过MAXIMUM_ALLOWED拿到的句柄权限可能远低于你预期的后续某些操作照样会失败。所以想让代码行为稳定最好的做法是显式声明你需要的具体权限位不要偷懒用通配。3.3 句柄泄漏是运维事故的重灾区我最早用SAMR写账户巡检脚本的时候犯过一个很低级的错误脚本循环里枚举用户拿到用户句柄后忘记调用SamrCloseHandle。结果跑了一晚上第二天发现目标域控的会话数暴涨SMB连接数一直降不下来最后只能重启Server服务才恢复。这个问题的原理是每个打开的SAMR句柄都会在服务器端占用一块内存和会话资源。如果你不主动关闭这些资源要等RPC会话超时才会被系统回收。而在一个长连接的上下文中超时时间可能很长大量未关闭句柄会像内存泄漏一样蚕食服务器资源。所以在写任何SAMR工具时务必做到句柄的「开闭配对」最好用编程语言里的资源管理机制比如Python的with块或者try/finally确保即使中间报错也能把句柄关掉。另外一个实战经验如果确实出现了句柄泄漏导致服务器端资源吃紧最快的恢复手段是重启Server服务而不是一台台去杀客户端进程。因为SAMR句柄是挂在服务端会话上的客户端进程杀掉后TCP连接断了服务端才会迟滞清理。3.4 调用序列里的Opnum和你没多大关系做协议分析的朋友可能会在意每个RPC调用对应的Opnum编号比如SamrEnumerateUsersInDomain是Opnum 39。但在实际开发中你用的通常是高级语言封装好的客户端库这些库已经把这些编号对应关系处理好了。如果你不是在做协议级逆向或者从零实现客户端不需要去背Opnum表只需要理解调用层次即可。当然了解Opnum有个好处在看网络抓包或者ETW日志时看到SAMR事件里带的Opnum值你能快速对应到具体操作。比如突然出现一大批Opnum 39那多半是有人在枚举用户这是一个有用的安全检测信号。4. 安全模型的演变与攻防视角4.1 老版本放开访问新版本越来越紧Windows系在SAMR的安全策略上做过几次明显收紧。早年间域内普通用户可以直接枚举本域的用户列表因为系统默认给Authenticated Users授予了SamrEnumerateUsersInDomain的访问权。后来域环境里的信息泄露问题太突出了系统开始要求调用者必须具备域管理员或者特定委派权限才能枚举。不过这里要注意收紧不等于完全封死。实际测试下来在某些默认配置的域环境中普通域用户依然能枚举到一批基础属性只是明细字段比如密码策略、账户控制标志里的敏感位被脱敏了。设计安全监测规则时不能想当然地认为「域内没管理员权限就调不了SAMR」你仍然需要把SAMR枚举流量当作可疑行为去监控。4.2 安全描述符决定谁能访问现代版本的SAMR在每个域对象上挂了一个安全描述符Security Descriptor里面用ACE访问控制项规定了哪些SID能对这个对象执行哪些SAMR操作。你调用的结果最终是由这个描述符和你当前令牌的组成员关系做一次交集计算得出的。这里面有一个容易忽略的细节域控上的Domain Admins组和内置Administrator账户具有很高的控制权但Server Operators、Account Operators这些中间权限组在某些操作上也拥有特殊权限。如果你写一个安全审计工具需要摸清楚当前环境的组策略是否把某些操作委派给了中间组否则你评估出来的风险面是不完整的。4.3 正常运维与恶意枚举的流量差异从防御者视角来看SAMR调用本身不是原罪关键是调用的模式和上下文。我整理过一个对照表帮助判断一个SAMR流量是正常运维还是可疑行为维度正常运维调用可疑枚举行为目标对象范围少量特定账户或组连续枚举整个域的全量对象范围很大调用频率低频、偶发、有明确业务触发高频、周期性、短时间内集中触发打开对象的模式只关注个别RID按RID递增批量读取所有对象携带的访问掩码按需申请如读取密码策略字段常常申请最大权限或大量读取类权限配合操作后续跟正常的属性修改或创建记录后续往往跟密码喷洒、登录尝试来源会话来自已知运维主机或监控系统来自陌生主机或大量不同来源IP汇聚这个表不能当硬性规则用因为正常运维批量巡检也会产生全量枚举流量。但如果一个来源突然开始大规模枚举并且紧接着出现认证失败日志那基本可以判定是攻击行为。我写监测规则时会做两级判断先看SAMR枚举行为本身再看后续短时间内是否有登录失败的聚合信号两者叠加才触发告警这样误报会小很多。4.4 跨平台系统里的SAMR调用安全边界现在很多企业的身份管理中间件需要从AD域同步账户信息到其他系统跨平台系统里SAMR调用往往扮演着「读取器」的角色。在这种架构下你给这个中间件服务账户的权限越大一旦中间件被攻破攻击者能用的权限就越大。我的建议是为同步服务单独创建一个域账户只授予它必要的SAMR读取权限比如读取用户的通用属性和账户控制标志不要直接给它域管理员权限更不要把内置管理员账户的凭据硬编码在中间件配置里。5. 实操用SAMR完成一次安全的域账户盘点5.1 场景说明假设你需要在某个内网环境里做一次域账户盘点目标是把域内所有用户、组、别名以及关键属性拉出来生成一份离线报告。要求是不能在目标机器上安装任何代理只能通过远程调用方式获取数据同时尽量少地在目标服务器上留下日志。这个场景最合适的实现方式就是用SAMR协议远程调用了。下面我分享一套我自己用的实现思路具体代码用了伪代码风格因为不同语言环境下库的API不太一样重点讲逻辑和参数。5.2 分步实现流程第一步建立SMB会话。SAMR走的是命名管道所以必须先通过SMB协议完成认证并建立会话。认证方式可以选择Kerberos或者NTLM取决于目标网络环境。域环境下优先用Kerberos因为它不需要在代码里显式传输密码的哈希且支持更细粒度的约束委派。第二步绑定SAMR管道。连接成功后通过SMB的IPC共享访问\PIPE\samr完成RPC绑定。第三步连接SAM服务器。调用SamrConnect传入目标机器名和一个访问掩码。这里要注意连接阶段的掩码选择会影响后续能打开的域句柄级别。如果你想对域做遍历至少需要MAXIMUM_ALLOWED或者SAMR_SERVER_ENUMERATE_DOMAINS这类权限。第四步枚举域。调用SamrEnumerateDomainsInSamServer拿到域列表通常只有一个默认域再调用SamrLookupDomainInSamServer把域名解析成SID。第五步打开域。调用SamrOpenDomain传入域的SID得到DomainHandle。第六步枚举用户和组。调用SamrEnumerateUsersInDomain和SamrEnumerateGroupsInDomain。这里有一个关键参数枚举索引。SAMR的枚举接口是分页的一次调用不一定返回全部对象你需要循环传入EnumerationContext枚举上下文直到系统返回ERROR_NO_MORE_ITEMS为止。第七步打开单个用户读取属性。拿到用户的RID后调用SamrOpenUser获取UserHandle再调用SamrQueryInformationUser传入不同的UserInformationClass参数可以拿到账户名、全名、账户控制标志、密码有效期等信息。第八步关闭所有句柄。5.3 关键代码片段伪代码# 伪代码示例SAMR域用户枚举核心逻辑 def enumerate_domain_users(connection, domain_sid): server_handle samr.SamrConnect(connection, access_maskSAMR_SERVER_ALL_ACCESS) domain_handle samr.SamrOpenDomain(server_handle, access_maskSAMR_DOMAIN_READ, domain_siddomain_sid) users [] enum_context 0 while True: try: batch, enum_context samr.SamrEnumerateUsersInDomain( domain_handle, enum_contextenum_context, user_account_control0, # 0 表示不过滤返回所有用户 max_count500 ) users.extend(batch) except samr.ERROR_NO_MORE_ITEMS: break for rid, name in users: user_handle samr.SamrOpenUser(domain_handle, access_maskSAMR_USER_READ_GENERAL, ridrid) info samr.SamrQueryInformationUser(user_handle, UserInformationClass1) # 通用属性 # 加工并保存 samr.SamrCloseHandle(user_handle) samr.SamrCloseHandle(domain_handle) samr.SamrCloseHandle(server_handle)这段逻辑有几个可以优化的点单次枚举的max_count不要设置太大建议500左右。太大的话如果域内用户非常多单次RPC响应会超时或者被中间设备分片容易触发rpc_s_fault。UserInformationClass的选择决定了返回字段的多少。选得太细返回数据量大网络开销高选得太粗可能漏掉你需要的字段。根据需求按需选择即可。如果只是做一次全量巡检建议在枚举完用户后再枚举一遍组和别名因为用户和组是两张表不一起拉的话会漏数据。5.4 运行结果验证跑完脚本后你可以把结果里的用户数量和组数量和域控上进行一次交叉验证。标准做法是找一台有权限的机器打开AD用户和计算机的管理界面数一下总数是否对得上。更严谨一点可以抽样几个已知用户对比脚本读到的SID、账户控制标志和域控上实际值是否一致。我碰到的常见现象是脚本读出来的用户数比管理界面里看到的少一大截。出现这种情况绝大多数是因为枚举分页逻辑写错了——enum_context没有在每次循环里更新导致你反复读取同一个分页后面的分页永远拿不到。排查的方法很简单在循环里打一条日志记录每次枚举返回的条目数和enum_context值看enum_context是否在递增。如果它一直是0那就是没有再赋值。5.5 把盘点结果变成一份可用的报告拉完数据只是第一步最终的输出应该是一份能让管理层看懂的报表。我习惯生成CSV格式包含以下字段用户名SAM账户名SID / RID全名Display Name账户控制标志如锁定、停用、密码永不过期密码有效期起始和结束时间所属组列表最后登录时间如果有权限读取的话注意最后登录时间这个字段在SAMR里不总是能拿到尤其是账户从未登录过或者权限不足时读出来会是空值。如果报表里出现大量空值不要惊慌属于正常现象。6. 常见问题与排查技巧实录6.1 OpenUser返回AccessDenied这是最典型的问题。明明账户存在打开时却报权限不足。原因基本逃不出两个一是当前认证的账户在目标域里没有对该用户对象的SAMR读取权限二是你在SamrOpenUser里传入的访问掩码超出了当前令牌允许的范围。解决方法是先用一个高权限账户测试同样的调用如果能成功说明问题在当前账户的权限配置上。如果你确实只能用低权限账户那就调整代码逻辑使用SamrLookupNames配合SamrQueryInformationUser的只读模式有时候能绕开部分权限检查但这招在严格配置下依然不灵。6.2 枚举大量对象时出现网络传输类报错比如rpc_s_faultRPC调用失败或者rpc_s_server_unavailable。这种问题多出现在域内用户数量非常大比如十万级以上且网络稳定性一般的环境里。你连续拉很多页中间某一次RPC响应因为超时或者连接被重置就断掉了。我的处理办法是在枚举循环里加入自适应退避机制。遇到这类错误时先等待几秒再重试同一个枚举上下文。如果连续重试三次还失败就把当前枚举上下文记下来退出程序下次启动时从断点续跑。很多现成的协议库没有内置断点续传能力你得自己维护这个上下文状态。6.3 非域管理员无法读取密码策略详情有些朋友想通过SAMR读取域密码策略比如密码最小长度、过期天数结果发现普通域账号读不了只有域管理员可以。这个不是代码问题是SAMR对密码策略信息做了额外的权限保护。如果你想做密码策略巡检要么用域管理员账户要么改用LDAP去读取minPwdLength这类属性LDAP的权限模型和SAMR不太一样不一定完全被限制住。6.4 句柄泄漏导致服务器端会话异常这个问题前面提过这里再补充一个排查技巧如果在事件日志里看到大量SAMR相关的事件并且伴随系统性能计数器里的SAM会话数异常飙升基本可以判断有进程在用SAMR时没有关闭句柄。你可以用系统自带的资源监视器看哪个进程持续打开命名管道再结合代码审计去定位泄漏点。最好的防御手段是在客户端做「所有句柄集中登记统一延迟关闭」的模式避免在某个异常分支里漏关。6.5 不同系统版本行为不一致同一个SAMR调用在Windows Server 2016和Windows Server 2022上的返回结果可能有差异。最典型的就是某些UserInformationClass在新版本里增加或废弃了字段。写兼容性测试时最好准备多个系统版本的测试虚拟机跑一遍全量调用把返回字段差异记录下来。这里有个实践经验优先使用文档里标注为「长期稳定」的UserInformationClass比如1、2、3这几个经典编号不要一上来就用最新的类尤其是新版本才有的类兼容性风险很大。7. 再谈一点这件事和「账户生命周期管理」的关系做完那次SAMR协议再探之后我对账户同步类工具的设计有了新的理解。很多工具喜欢把SAMR、LDAP、Graph API等接口混在一起用但没有统一抽象层的话不同接口之间的字段映射会让你痛不欲生。后来我设计同步模块时直接把SAMR定位成「底层读取器」只负责从SAM数据库拿原始数据字段标准化、校验、分发全部放到上层做。这样即使SAMR侧某个字段变了影响的也只是适配层不会影响上层业务。如果你也要做类似的跨平台系统或账户管理集成建议隔离好这层抽象。我见过太多项目把协议调用代码和业务逻辑写在一起最后改一个字段名就要翻遍全项目那感觉确实不好受。最后分享一个自己的小习惯在调试SAMR相关功能时我永远会在测试环境开一份网络抓包和一份RPC日志跑一次最小用例确认协议调用的先后顺序、句柄的打开关闭时机、以及访问掩码的实际效果然后再放到真实环境里扩大范围跑。这样可以避免很多只有在真实环境里才暴露的权限和资源问题。如果你按这个节奏来SAMR这个协议其实并没有传说中那么难缠。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询