Creating A Simple C2 Server Using aiohttp
This is a writeup on how you can use the aiohttp python library to create a basic web C2 server. I picked aiohttp because it is part of the aiosync framework and includes built in functionality for asynchronous web requests. For the purposes of this writeup, I will be looking at how to build your own bare bones C2 server where the post exploitation code lives on the client (rather than on the server). There are pros and cons to this approach. A pro is that this makes it easy to create a cross platform C2 server that will work regardless of the target operating system as long as the requests to the web server are formatted correctly. A con however with this approach is that all code is client side, meaning that if the client side payload is analyzed by blue teams, all the code is there. I am not advocating for this bare bones C2 server as the best approach, but wanted to use it as an example to help guide others who are looking to quickly build their own C2 servers.
This can be helpful for blue teams who want to become more familiar with how command and control servers function at the most basic level and can also help offensive engineers quickly stand up a custom web C2 server pretty quickly. Here’s where you can find baisc info on aiohttp:
Below are the steps you can follow to set up your own aiohttp C2 server. But first you will need to install aiohttp for python3 on the server (pip3 install aiohttp).
Structuring Your C2 Endpoints
A good first place to start is figuring out how you want to lay out the endpoints on your aiohttp C2 server. When I say endpoints, I mean the urls that you want the client to hit in order to check in, get instructions, and return data to. There are lots of different ways you can approach this, so I will just cover one approach. But before I do, let’s take a quick look at the overall flow of communications between your client (the host you want to control) and the server:
Basic Communications Flow:
- payload executed on the client
- client makes an initial callout to the C2 server (will hit an endpoint on the server)
- client makes a GET request for instructions (will hit an endpoint on the server)
- server returns C2 instructions from the operator
- client executes the instructions and sends the results to the C2 server (will hit an endpoint on the server)
- server displays results to the operator
- steps 3 through 6 continue repeating over some interval (i.e., a sleep variable)
Of course there are lots of other steps you can incorporate above, but these are what I would say are the basics for client-server C2 communications. Since you’ll need at least one C2 server endpoint for the communications above, here’s an example of how you can lay out the endpoints on your aiohttp C2 server. First let’s look at some example endpoints on your C2 server that you can set up for the initial check in from the client and for the request for C2 instructions from the client:
- /initialize/sequence/0 (GET request): initial checkin from the client
- /validate/status (GET request): request for C2 instructions from the client
Then we can also plan out endpoints for the client to send data to after getting C2 instructions from the /validation/status endpoint. Whereas the two endpoints above in my example are configured for GET requests, you can set up other API endpoints for the client to send data to as POST requests. Examples of some endpoints could be:
- /validate/status/1 (POST request from client): client sends screenshots here
- /validate/status/2 (POST request from client): client sends downloaded files here
- /validate/status/3 (POST request from client): client sends path information here
- /validate/status/4 (POST request from client): client sends change directory information here
I could keep going on with other post exploitation tasks that you could configure as API endpoints, but for brevity I will keep it to the items above for now.
Below is an example showing what this looks like in your aiohttp server python code:
You definitely do not have to have a lot of API endpoints…you could reduce the number of endpoints by having separate GET and POST functions for each endpoint. I chose to keep them separate for the purpose of this blog to make things easy to follow. Each route (or endpoint) above includes the http method (in the example above that is either GET or POST), the url endpoint, and then the function that corresponds to that endpoint. So logically the next thing to add in the aiohttp server python code would be the functions themselves.
A Look At The Functions
Next, I’ll walk through different parts of the C2 server code for the server.
The snippet above is the first 1/3 of the code. I set up an empty dictionary (named “cmds”) that will hold the C2 post exploitation commands for the target host that the operator will enter. Then the code starts to spell out what happens with each function.
The InitCall function (remember this is mapped to the “initialize/sequence/0” endpoint as seen in the first screenshot above) simply returns an OK response to the client without performing any other tasks. When building your client payload, you can have it hit this endpoint (i.e., ‘initialize/sequence/0’) just once upon the initial execution of the payload. Then on the server side you can use this data to track all of the unique target hosts that have had the payload executed on it.
The CheckIn function (remember this is mapped to the “validate/status” endpoint as seen in the first screenshot above) is where the process of the operator entering C2 commands for the target client host starts. I set up a while loop so that the operator can run the help function to see available commands and then select from those options, adding each command into the cmds dictionary. When the operator enters the “done” command, all of the commands entered by the operator (stored in the cmds dictionary) are returned to the client in the body of the HTTP response. Since this data from the cmds dictionary is returned to the client, I have the cmds dictionary being cleared each time this endpoint is hit by the client (client will hit this endpoint repetitively based on a sleep variable). The client will then take all of the commands from the cmds dictionary (returned in the HTTP response body) and interpret those commands to perform specific post exploitation tasks and then return the results of those commands as HTTP POST data back to the C2 server.
The screenshot above only shows the content of the help command so the next two screenshots show the rest of the commands available to the operator:
Though I wrote a client payload for MacOS that calls back to this C2 server, as you can see above these are pretty generic types of commands. All that is really happening in the code above is the operator is selecting available commands, which will be sent to the client where the processing occurs. Since the processing of the commands occurs client side you can really create payloads for any target operating system and have it communicate back.
At the start of this function I clear out the cmds dictionary. I do this because once the operator enters the commands desired to be run on the target client, the client will pick receive those commands in response to its GET request to this endpoint.
Lastly, the “done” command is what exits the operator out of this loop. “done” takes all of the commands that the operator has selected (which are stored in the cmds dictionary), converts them to a list, and returns them in list form in the web response body back to the client. At this point, the operator is out of the loop and is waiting for the client to post the command results back to the server and restart the command loop on the next check in (determined by the sleep variable).
The next functions I will group together since they are smaller and are pretty similar in terms of content:
The GetScreenshot function (remember this is mapped to the “validate/status/1” endpoint as seen in the first screenshot above) is the code that handles the target client sending a screenshot image to the server (because the operator selected the “screenshot” command). This one is pretty straightforward: the C2 server reads the HTTP POST request data (which is the screenshot jpeg image), writes out the current timestamp, saves the screenshot with the timestamp as the filename in the current directory, and returns “OK” to the client. The GetDownload function (remember this is mapped to the “validate/status/2” endpoint as seen in the first screenshot above) is pretty much doing the same thing but for any file that the operator specifies with the “download [filename]” command. The GetPath function (remember this is mapped to the “validate/status/3” endpoint as seen in the first screenshot above) is invoked when the operator enters the “pwd” command to get the present working directory on the target client. The ChangeDir function (remember this is mapped to the “validate/status/4” endpoint as seen in the first screenshot above) is invoked when the operator enters the “cd [dest_directory]” command to change the current directory on the target client. The ListDir function (remember this is mapped to the “validate/status/5” endpoint as seen in the first screenshot above) is invoked with the operator enters the “listdir” command to list directory contents on the target client.
To save time, I’ll just show the rest of the code without as much commentary, since you probably are comfortable with the code flow at this point:
One thing to note about Code Snippet 7: the EDR and antivirus searches in this code is geared towards MacOS endpoints. If you are targeting a different operating system client in your payload, then you’ll need to adjust this section as needed for your target operating system.
And below is how the server code ends (note: you’ll see the same data from the first screenshot at the top of this blog post, but I wanted to put it here to show you the order of the code within the script):
Note: You can use the port=[port] setting to change the default port (which is 8080) to the port of your choice. You can also use ssl instead and import your certificate and key (recommended!), but I will not delve into that here since the purpose of this post was to go into the basic inner workings of standing up a simple C2 server.
Protecting Your C2 Server
There are some simple things you can do to restrict access to the C2 server to only the targeted endpoints running your payload. One thing you can do is have your client payload generate an access or session token and then on the C2 server check if inbound connections have that token and if so perform the task (and if not return a 404 not found). You can also do this with a user agent string (though if you use this approach I recommend using a real valid user agent in your payload so that the traffic from your payload does not not stand out to blue teamers). An example of how to parse incoming web requests and make 200 OK or 404 Not Found decisions is below:
You could add this code at the beginning of each of the async def functions (corresponds to your C2 server endpoints) so that all endpoints undergo this check before returning data. I would recommend taking this approach for both the user agent and the token from your client payload.
Putting It All Together
I have included all of the code from my “SimpleC2 Server” aiohttp server code here in order. You can also see the code on my github here:
You can take this, modify it as needed and you have your simple C2 server that can handle multiple different target clients connecting in at different times. Here’s what the server looks like when started:
The blue input prompt comes up when the payload is executed on the target host and the target checks in with the C2 server for commands (“/validate/status” endpoint).
And here is an example of what it looks like when the operator enters commands and the results are returned to the C2 server (I whited out the systeminfo and pwd data returned):
The next step would be writing a payload for the target client that would periodically (using a sleep variable) connect to the C2 server, get C2 instructions, execute the instructions, and send the data to the C2 server. Since I focus more on MacOS than Windows I have written a payload for MacOS that connects to the “SimpleC2 Server”. But rather than share it, I will leave it to you to write your own payload. That way you can dig in and have your own customizable payload and C2 server (and have a ton of fun while doing it!). I hope you found this helpful!