python编写的ldap查询工具不能编译,不方便在windows主机上使用,于是想着用golang去重构。golang有ldap v3库,可以满足基本的查询需求。但是,该库没有实现SDDL的解析。在其issues中有提到相关的解析文章,可以自己实现。
解析
要解析SDDL数据,需要先获取一段原始数据,做如下查询并将nTSecurityDescriptor的原始数据打印出来,如下图。
整个SDDL结构分为NtSecurityDescriptor头+Sacl头+Sacl Ace条目+Dacl头+Dacl Ace条目
后续的解析都以我在编写该文章之前的数据进行解析
NtSecurityDescriptor header
根据微软官方[MS-DTYP]: 2.4.6的描述,该部分是一个SECURITY_DESCRIPTOR结构,有如下字段。其中,Revision、Sbz1、Control、OffsetOwner、OffsetGroup、OffsetSacl、OffsetDacl这几个字段总共占20个字节。
OffsetOwner和OffsetGroup是所有者、所在组的数据偏移,OffsetSacl和OffsetDacl是Sacl header、Dacl header数据的偏移地址。根据这些偏移找到对应的数据并将数据解析赋值给变量OwnerSid、GroupSid、Sacl、Dacl
上述结构对应我们Dump出来的数据如下图。我们主要关心的是其偏移大小。OffsetOwner占4个字节,为C0 08 00 00,按照小端解析为00 00 08 C0,转换为10进制值为2240。OffsetGroup同理。
OffsetSacl的数据为14 00 00 00,转换为10进制后是20,也就是说,NtSecurityDescriptor header之后紧跟着Sacl header(偏移的起始地址为NtSecurityDescriptor header的第一个字节)。
OffsetDacl的数据为8C 00 00 00,按照小端排序为00 00 00 8C,转换为10进制之后是0x8C=140
NtSecurityDescriptor header的数据就解析完了,紧跟着的是Sacl header
Sacl 解析
Sacl这一部分的数据包含Sacl header 和多个 Sacl Ace条目。
根据微软MS-DTYP: 2.4.5中的描述,Acl结构总共8个字节,我们需要关心的是AclSize和AceCount这两个字段,顾名思义,一个是Sacl数据的大小,一个是Sacl Ace的条目数量。
我们找到原始数据中对应的字节数,如下图,就是Sacl header
这是解析之后的。Acl Size字段数据为78 00,转换后的10进制值为120。这个大小是Sacl header的大小 + 所有Sacl Ace的大小
Ace Count的值为2,也就是说Sacl有两条Ace
Sacl header之后紧跟着的是其对应的Ace数据。
先来看Ace数据结构,在微软的文档只将AceType、AceFlags、AceSize作为Ace Header的字段,而我这里是将AceMask、Extended、SID都放在了AceStruct中进行解析。
AceType代表该Ace条目的类型是允许的ACE还是拒绝的ACE,或者是系统审核的ACE、特定于对象访问允许/拒绝的ACE。
AceFlags用于指定一组特定于 ACE 类型的控制标志。此字段可以是多个值的组合
AceSize是这条Ace的数据大小
AceMask用于对对象的用户权限进行编码。访问掩码既用于对分配给主体的对象的权限进行编码,也用于在打开对象时对请求的访问权限进行编码。
Extended是Ace的扩展位,该值存在4种情况。
- 为0代表不存在扩展位(
ObjectType、InheritedObjectType不存在) - 值为1代表存在
ObjectType字段 - 为2存在
InheritedObjectType字段 - 为3则
ObjectType和InheritedObjectType都存在
SID是该ACE对应的所有者SID,可能为用户也可能为组
如下图是我已经解析好的原始数据**。**07 5A分别是Ace Type和Ace Flags字段,他们各占用1个字节。
Ace Size占用2个字节,为38 00,转换后10进制值为56,代表该条ACE条目总共有56个字节(07 5a 30 00 一直到第19行的00 00 00 00,共56个字节),下图中还有一个ACE也是56个字节(23行的07 5a 38 00到26行的01 00 00 00 00)。还记得上面Sacl header中的Acl size的值吗?这个值的计算就是两个ACE的大小(2 * 56) + Sacl header的大小(8),总共占用120个字节。
Ace Mask占用4个字节,原始数据为20 00 00 00,小端数据为0x00000020,该字段对应的权限可参考微软MS-DTYE:2.4.3
Extended也就是扩展位,值为0x03,所以有ObjectType和InheritedObjectType两个字段。
ObjectType和InheritedObjectType都是16字节大小的数据,是GUID结构,数据以4 + 2 + 2 +2 +6的格式进行分割,如下图ObjectType解析后的可读字符串为F30E3BBE-9FF0-11D1-B603-0000F80367C1。改ObjectType是一个GP-Link属性。解析后的字符串对应的权限可以在MS-ADTS: 5.1.3.2.1中查询。
SID结构的信息可以在微软Security identifiers | Microsoft Learn文档中找到,已有人实现了这一部分的解析。SID的大小不是固定的,需要做一个运算。下图中,SID的数据大小为Ace Size的值 - Ace Type大小-Ace Flags大小 - Ace Size大小 - Extended大小 - ObjectType大小 - InheritedObjectType大小,即56-1-1-2-4-16-16=12字节
Dacl解析
Sacl Ace条目解析完之后,就是Dacl header数据,Dacl header的数据大小和结构与Sacl header一样。
Dacl的数据包含了Dacl header和Dacl Aces。Dacl的数据起始地址存放在NtSecurityDescriptor header中的OffsetDacl,当时解析出来的10进制为120,手动偏移后就是下图中标记的数据,正好紧跟在Sacl Ace条目之后
Dacl header中,Acl Size按照上面的解析方法,得到的值为2100,All Count值为46
Dacl header后面的数据就是Dacl Ace条目,一共有46条。Dacl Ace的数据结构和上面说的Sacl Ace结构相同,不再赘述。
下图中我解析了前两条Ace,拿第一条来看:
Ace Size为0x0038=56
Ace Mask为0x00000010
Extended(扩展)为0x000001=ObjectType
ObjectType解析后为4C164200-20C0-11D0-A768-00AA006E0529
SID解析后为S-1-5-21-556516541-2133182263-1194801646-553
OwnerSid和GroupSid
OwnerSid和GroupSid都是SID结构,OwnerSid数据起始地址为NtSecurityDescriptor header中的OffsetOwner的值,数据大小为OffsetGroup的值(2268) - OffsetOwner的值(2240)为28个字节。GroupSid的数据为OffsetGroup的偏移到Dump数据的末尾。如下图
至此,整个SDDL原始数据我们已经手动解析完成。
GUID结构转换成字符串
我们拿上面Dacl 第一条ACE中的ObjectType数据做解析,上文说过,该结构占用16字节,解析该数据需要以4+2+2+2+6的格式进行解析
00 42 16 4c c0 20 d0 11 a7 68 00 aa 00 6e 05 29
//按照4+2+2+2+6的格式进行分割
00 42 16 4c
c0 20
d0 11
a7 68
00 aa 00 6e 05 29
该结构的前8个字节按照小端进行解析,后8个字节是按照大端进行解析的,就是
4C 16 42 00
20 C0
11 d0
a7 68
00 aa 00 6e 05 29
所以上文的ACE转换成字符串就是
4C164200-20C0-11D0-A768-00AA006E0529
代码如下
SID结构转换成字符串
幸运的是,该结构已经有现成的代码可以使用了
Golang自动化解析
我们编写如下测试代码,以管理员账户administrator连接目标dc.test.lab ldap服务器,设置过滤器(objectclass=domain),查询属性为nTSecurityDescriptor,第39 ~ 50行两个for循环是打印出解析完成后的nTSecurityDescriptor数据
运行后结果如下:
NtSecurityDescriptor header数据
Sacl数据
Dacl数据
查出了具有DS_Replication_Get_Changes权限的DAcl Ace
普通用户不能查询DCSync权限
普通用户如果想要查询哪些用户具有DCSync权限,查询的返回结果是空。原因在这篇文章中有提到,简单来说就是默认情况下查询的SDDL属性中包含Sacl和Dacl,Sacl的属性是需要鉴权的,如果查询的用户没有读取Sacl的权限,域控就不会返回任何数据(Sacl + Dacl)。如下图,使用普通用户查询域控的nTSecurityDescriptor属性,(因为普通用户没有读取Sacl的权限)域控返回的结果中不包含任何内容。
解决方法是只查询Dacl部分就行了。在python impack中可以设置controls的sdflags位为0x04
而在golang中,这篇文章提到了ldap3 controls位的设置,于是我们可以在查询请求中配置controls位为0x04,查询结果如下。此时返回的数据中就不包含Sacl数据了,OffsetSacl的值为0
参考
Process low level NtSecurityDescriptor - IT Insights Blog