魔改 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 的小伙计提供点经验,欢迎交流~

文章目录
  1. 1. 问题分析
  2. 2. 杀死问题的办法 —— 魔改
    1. 2.1. 0x01 UI
    2. 2.2. 0x02 Listener
    3. 2.3. 0x03 Stage
    4. 2.4. 0x05 编译 & 替换
  3. 3. 效果
  4. 4. 后话
|