作者:御守实验室
原文链接:https://mp.weixin.qq.com/s/OHbFhqyLQlx5W2W40PRoLg
相关阅读:《从 mimikatz 学习 Windows 安全之访问控制模型(一)》

0x00 前言

上次的文章分析了mimikatz的token模块,并简单介绍了windows访问控制模型的概念。在本篇文章中,主要介绍sid相关的概念,并介绍mimikatz的sid模块,着重分析sid::patch功能的原理

0x01 SID简介

1. 安全标识符(SID)

在Windows操作系统中,系统使用安全标识符来唯一标识系统中执行各种动作的实体,每个用户有SID,计算机、用户组和服务同样也有SID,并且这些SID互不相同,这样才能保证所标识实体的唯一性

SID一般由以下组成:

  • “S”表示SID,SID始终以S开头
  • “1”表示版本,该值始终为1
  • “5”表示Windows安全权威机构
  • “21-1463437245-1224812800-863842198”是子机构值,通常用来表示并区分域
  • “1128”为相对标识符(RID),如域管理员组的RID为512

Windows也定义了一些内置的本地SID和域SID来表示一些常见的组或身份

2. AD域中的SID

在AD域中,SID同样用来唯一标识一个对象,在LDAP中对应的属性名称为objectSid

重点需要了解的是LDAP上的sIDHistory属性

(1) SIDHistory

SIDHistory是一个为支持域迁移方案而设置的属性,当一个对象从一个域迁移到另一个域时,会在新域创建一个新的SID作为该对象的objectSid,在之前域中的SID会添加到该对象的sIDHistory属性中,此时该对象将保留在原来域的SID对应的访问权限

比如此时域A有一个用户User1,其LDAP上的属性如下:

此时我们将用户User1从域A迁移到域B,那么他的LDAP属性将变为:

值得注意的是,该属性不仅在两个域之间起作用,它同样也可以用于单个域中,比如实战中我们将一个用户A的sIDHistory属性设置为域管的objectSid,那么该用户就具有域管的权限此时当User1访问域A中的资源时,系统会将目标资源的DACL与User1的sIDHistory进行匹配,也就是说User1仍具有原SID在域A的访问权限

另一个实战中常用的利用,是在金票中添加Enterprise Admins组的SID作为sIDHistory,从而实现同一域林下的跨域操作,这个将在后面关于金票的文章中阐述

(2) SID Filtering

SID Filtering简单的说就是跨林访问时目标域返回给你的服务票据中,会过滤掉非目标林中的SID,即使你添加了sIDHistory属性。SID Filtering林信任中默认开启,在单林中默认关闭

具体可以参考微软的文档和@dirkjanm的文章:

https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-pac/55fc19f2-55ba-4251-8a6a-103dd7c66280?redirectedfrom=MSDN

https://dirkjanm.io/active-directory-forest-trusts-part-one-how-does-sid-filtering-work/

0x02 mimikatz的sid模块

1. sid::lookup

该功能实现SID与对象名之间的相互转换,有三个参数:

  • /name:指定对象名,将其转换为SID
  • /sid:指定SID,将其转换为对象名
  • /system:指定查询的目标计算机

其原理是调用LookupAccountName()LookupAccountSid()来实现对象名和SID之间的相互转化,这类API底层是调用MS-LSAT协议(RPC),比如将对象名转换为SID,底层调用的是LsarLookupNames4()

2. sid::query

该功能支持通过SID或对象名来查询对象的信息,同样有三个参数,使用时指定/sam/sid/system可选

  • /sam:指定要查询对象的sAMAccountName
  • /sid:指定要查询对象的objectSid
  • /system:指定查询的目标域控(LDAP)

这个功能其原理就是直接使用LDAP查询,通过sAMAccountName查询对应的objectSid,或者通过objectSid查询对应的sAMAccountName

其核心是调用Windows一系列的LDAP操作API,主要是ldap_search_s()

3. sid::modify

该功能用于修改一个域对象的SID,可以使用的参数有三个:

  • /sam:通过sAMAccountName指定要修改SID的对象
  • /sid:通过objectSid指定要修改SID的对象
  • /new:要修改对象的新SID 使用该功能是需要先使用sid::patch功能对限制LDAP修改的函数进行patch(自然也需要先开启debug特权),需要在域控上执行

修改时的操作就很简单了,调用LDAP操作的API对域对象的objectSid进行修改,主要使用的是ldap_modify_s()

4. sid::add

该功能用来向一个域对象添加sIDHistoy属性,有两个参数:

  • /sam:通过sAMAccountName指定要修改的对象
  • /sid:通过objectSid指定要修改的对象
  • /new:要修改sIDHistory为哪个对象的SID,该参数可指定目标的sAMAccountNameobjectSid,当指定名称时会先调用LookupAccountSid将其转换为SID

使用该功能也要先执行sid::patch,修改时同样是操作LDAP通过ldap_modify_s()修改,不再赘述

5. sid::clear

该功能用来清空一个对象的sIDHistory属性

  • /sam:要清空sIDHistory的对象的sAMAccountName
  • /sid:要清空sIDHistory的对象的objectSid

原理就是使用ldap_modify_s()将目标对象sIDHistory属性修改为空

6. sid::patch

对域控LDAP修改过程中的验证函数进行patch,需要在域控上执行,该功能没有参数

patch共分为两个步骤,如果仅第一步patch成功的话,那么可以使用sid::add功能,两步都patch成功的话才可以使用sid::modify功能

0x03 sid::patch分析

sid::patch在系统版本 < Vista时,patch的是samss服务中ntdsa.dll的内存,更高版本patch的是ntds服务中ntdsai.dll的内存

整个patch过程分为两步:

  1. 第一步patch的是SampModifyLoopbackCheck()的内存
  2. 第二步patch的是ModSetAttsHelperPreProcess()的内存

我们以Windows Server 2012 R2环境为例来分析,首先我们需要找到NTDS服务所对应的进程,我们打开任务管理器选中NTDS服务,单击右键,选择“转到详细信息”就会跳转到对应进程,这里NTDS服务对应的进程是lsass.exe

1. 域控对LDAP请求的处理

大致分析一下域控对本地LDAP修改请求的过滤与处理流程,当我们修改objectSidsIDHistory时,SampModifyLoopbackCheck()会过滤我们的请求,即使绕过该函数修改objectSid时,仍会受到SysModReservedAtt()的限制

侵入式切换到lsass进程并重新加载用户态符号表:

给两个检查函数打断点

此时我们修改一个用户的描述来触发LDAP修改请求

命中断点后的调用栈如下:

SampModifyLoopbackCheck()函数中存在大量Check函数,通过动态调试发现修改sIDHistoy的请求经过该函数后便会进入返回错误代码的流程

继续调试到下一个断点

SysModReservedAtt()执行结束后,正常的修改请求不会在jne处跳转,而当修改objectSid时会在jne处跳转,进入返回错误的流程

2. Patch 1/2

当我们想要进行内存patch时,通常会寻找目标内存地址附近的一块内存的值作为标记,编写程序时首先在内存中搜索该标记并拿到标记的首地址,然后再根据偏移找到要patch的内存地址,然后再进行相应的修改操作

mimikatz正是使用这种方法,其在内存中搜索的标记在代码中有明确的体现:

我们将域控的ntdsai.dll拿回本地分析,在其中搜索标记41 be 01 00 00 00 45 89 34 24 83

这一部分内容是在函数SampModifyLoopbackCheck()函数的流程中,我们可以使用windbg本地调试对比一下patch前后的函数内容

首先我们找到lsass.exe的基址并切换到该进程上下文:

使用lm列出模块,可以看到lsass进程中加载了ntdsai.dll,表明此时我们可以访问ntdsai.dll对应的内存了

我们直接查看SampModifyLoopbackCheck()函数在内存中的反汇编

为了对比patch前后的区别,我们使用mimikatz执行sid::patch,然后再查看函数的反汇编。如下图所示,箭头所指处原本是74也就是je,而patch后直接改为ebjmp,使流程直接跳转到0x7ffc403b2660

0x7ffc403b2660处的代码之后基本没有条件检查的函数了,恢复堆栈和寄存器后就直接返回了,这样就达到了绕过检查逻辑的目的

3. Patch 2/2

同理,按照mimikatz代码中的标记搜索第二次patch的位置0f b7 8c 24 b8 00 00 00

查看ModSetAttsHelperPreProcess()处要patch的内存,patch前如下图所示

patch完成后内存如下图,其实本质是让SysModReservedAtt()函数失效,在内存中寻找到标记后偏移-6个字节,然后将验证后的跳转逻辑nop

4. 解决patch失败的问题

由于mimikatz中内存搜索的标记覆盖的windows版本不全,所以经常会出现patch失败的问题。例如在我的Windows Server 2016上,第二步patch就会失败,这种情况多半是因为mimikatz中没有该系统版本对应的内存patch标记

此时我们只需要将目标的ntdsai.dll拿下来找到目标地址

然后修改为正确的内存标记和对应的偏移地址即可,如果新增的话记得定义好版本号等信息

此时重新编译后就可以正常patch了

0x04 渗透测试中的应用

在渗透测试中的利用,一个是使用SIDHistory属性来留后门,另一个是修改域对象的SID来实现域内的“影子账户”或者跨域等操作

1. SIDHistoy后门

拿下域控后,我们将普通域用户test1的sIDHistory属性设置为域管的SID:

此时test1将具有域管权限,我们可以利用这个特性来留后门

2. 域内“影子账户”

假设我们此时拿到了域控,然后设置一个普通域用户的SID为域管的SID

此时我们这个用户仍然只是Domain Users组中的普通域成员

但该用户此时已经具有了域管的权限,例如dcsync:

并且此时也可以用该用户的账号和密码登录域控,登录成功后是administrator的session。但该操作很有可能造成域内一些访问冲突(猜测,未考证),建议在生产环境中慎用

3. 跨域

通常我们拿到一个域林下的一个子域,会通过黄金票据+SIDHistory的方式获取企业管理员权限,控制整个域林

除了这种方法,我们也可以直接修改当前子域对象的sIDHistory属性,假设我们现在拿到一个子域域控,通过信任关系发现存在一个父域,此时我们无法访问父域域控的CIFS

但我们给子域域管的sIDHistory属性设置为父域域管的SID

此时就可以访问父域域控的CIFS了:

0x05 参考


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1672/