In the age of threat hunting, automated mass scanning, and the occasionally curious SOC, properly securing your command and control (C2) infrastructure is key to any engagement. While many setups today include a CDN Domain Front with a custom Nginx or Apache ruleset sprinkled on top, I wanted to share my recipe for success. Fully (ab)using the services provided in Microsoft’s Azure infrastructure to the absolute max – AzureC2Relay!
AzureC2Relay is an Azure Function configured HTTP(S) trigger that validates and relays Cobalt Strike beacon traffic by verifying the incoming requests based on a Cobalt Strike Malleable C2 profile. Any incoming requests that do not share the user-agent, URI paths, headers, and query parameters profiles will be redirected to a configurable decoy website. The validated C2 traffic is relayed to a team server within the same virtual network that is further restricted by a network security group. This allows the virtual machine (VM) to only expose Secure Shell (SSH).
The Azure Function needs to be provided a ruleset as a basis for validating incoming HTTPS request. This ruleset is generated locally, for each deployment, using a DotNet Core application called ‘ParseMalleable.’ ParseMalleable fetches root elements/sections from the malleable profile and transforms the key parameters necessary into a JSON object. The whole object is then serialized and base64 encoded before being placed as a configuration variable in the Azure Function resource. The Azure Function will then fetch the configuration variable ‘MalleableProfileb64’ upon runtime initialization, before proceeding to decode, deserialize, and apply the rules as incoming HTTP requests appear. This ruleset generation is all done automatically by the terraform plan, which we will touch upon later.
You can confirm that the incoming beacon traffic is being verified and forwarded to the VM’s internal IP address by going to Resource groups > (Azure Function Name) > Functions > AzureC2Relay > Monitor. All incoming requests are logged and can be streamed in real time to help troubleshoot possible bugs or configuration errors.
If you’re seeing error messages indicating that the incoming traffic is not being verified (mismatch between rules and data received), you can edit and overwrite the configuration variable ‘MalleableProfileb64’ to an empty string. This causes the Azure Function to skip validations against the profile entirely and can be useful when confirming that just basic capture and redirect indeed works.
When it comes to setting up your Cobalt Strike listener, configure it exactly the way you would for Azure Domain Fronting.
The combination of CDN domain fronting with AzureC2Relay is entirely deployed using the included terraform plan. Follow the GitHub page’s instructions to update the deployment variables in config.tf, as well as both the cobalt-strike tgz install archive (only a dummy is provided in the public repo) and your Malleable profile before applying the terraform plan. The plan itself takes about five minutes to deploy. The CDN profile usually takes another 30 minutes before becoming active. Once the terraform plan completes it will provide you with the needed SSH command, the cobalt-strike team server will be running inside an tmux session on the deployed VM
While the terraform plan deploys with a high success rate, troubleshooting steps can be found in the Github repo
If you’re looking to harden the exposed VM further, you can modify the network security group to only allow a specific set of IPs to reach the SSH service. Additionally, Azure WAF can be assigned the Azure Function to monitor and protect the endpoint.