python编写的ldap查询工具不能编译,不方便在windows主机上使用,于是想着用golang去重构。golang有ldap v3库,可以满足基本的查询需求。但是,该库没有实现SDDL的解析。在其issues中有提到相关的解析文章,可以自己实现。

解析

要解析SDDL数据,需要先获取一段原始数据,做如下查询并将nTSecurityDescriptor​的原始数据打印出来,如下图。

image-20230711223604349

整个SDDL结构分为NtSecurityDescriptor头​+Sacl头​+Sacl Ace条目​+Dacl头​+Dacl Ace条目

后续的解析都以我在编写该文章之前的数据进行解析

image-20230712064923533

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

image-20230712065629594​​

上述结构对应我们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

image-20230712070551868​​

NtSecurityDescriptor header​的数据就解析完了,紧跟着的是Sacl header

Sacl 解析

Sacl​这一部分的数据包含Sacl header​ 和多个 Sacl Ace​条目。

根据微软MS-DTYP: 2.4.5中的描述,Acl​结构总共8个字节,我们需要关心的是AclSize​和AceCount​这两个字段,顾名思义,一个是Sacl​数据的大小,一个是Sacl Ace​的条目数量。

image-20230712072508073​​

我们找到原始数据中对应的字节数,如下图,就是Sacl header

image-20230712072341341​​

这是解析之后的。​Acl Size​字段数据为78 00​,转换后的10进制值为120​。这个大小是Sacl header​的大小 + 所有Sacl Ace​的大小

Ace Count​的值为2,也就是说Sacl有两条Ace

image-20230712072928229​​

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,可能为用户也可能为组

image-20230712073818787​​

如下图是我已经解析好的原始数据**。**​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字节

image-20230712213829795​​

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

image-20230712164638669​​

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

image-20230712213917749​​

OwnerSid和GroupSid

OwnerSid​和GroupSid​都是SID结构,OwnerSid​数据起始地址为NtSecurityDescriptor header​中的OffsetOwner​的值,数据大小为OffsetGroup​的值(2268) - OffsetOwner​的值(2240)为28个字节。GroupSid​的数据为OffsetGroup​的偏移到Dump数据的末尾。如下图

image-20230712174842799​​

至此,整个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

代码如下

image-20230712180153183​​

SID结构转换成字符串

幸运的是,该结构已经有现成的代码可以使用了

image-20230712180300976​​

Golang自动化解析

我们编写如下测试代码,以管理员账户administrator​连接目标dc.test.lab​ ldap服务器,设置过滤器(objectclass=domain)​,查询属性为nTSecurityDescriptor​,第39 ~ 50行两个for循环是打印出解析完成后的nTSecurityDescriptor​数据

image-20230712214306971​​

运行后结果如下:

NtSecurityDescriptor header​数据

image-20230712214804820​​​

Sacl​数据

image-20230712214850388​​

Dacl​数据

image-20230712214923542​​

查出了具有DS_Replication_Get_Changes​权限的DAcl Ace

image-20230712215015744​​

普通用户不能查询DCSync权限

普通用户如果想要查询哪些用户具有DCSync权限,查询的返回结果是空。原因在这篇文章中有提到,简单来说就是默认情况下查询的SDDL属性中包含Sacl​和Dacl​,Sacl​的属性是需要鉴权的,如果查询的用户没有读取Sacl​的权限,域控就不会返回任何数据(Sacl + Dacl)。如下图,使用普通用户查询域控的nTSecurityDescriptor属性,(因为普通用户没有读取Sacl的权限)域控返回的结果中不包含任何内容。

image-20230713065702641​​

解决方法是只查询Dacl​部分就行了。在python impack中可以设置controls的sdflags位为0x04

image-20230713065859133​​

而在golang中,这篇文章提到了ldap3 controls位的设置,于是我们可以在查询请求中配置controls位为0x04​,查询结果如下。此时返回的数据中就不包含Sacl​数据了,OffsetSacl​的值为0

image-20230713070231088​​

参考

Process low level NtSecurityDescriptor - IT Insights Blog

Impact