0X00 背景
最近在做渗透测试相关的工作,因工作需要准备用Cobalt Strike,老早都知道这款神器,早几年也看过官方的视频教程,但英文水平太渣当时很多都没听懂,出于各种原因后来也没怎么深入了解,所以一直都是处在大概了解的层面上。直到现在有需求了才开始研究,过程中体会也是蛮深,技术这东西真的不能只停留在知道和了解这个层面,就像学一门语言一样需要多动手去实践才能熟练运用的。
当然在深入研究某一门技术的过程中难免遇到各种各样的问题,一步一步解决这些问题才是真正学习的过程。对Cobalt strike的学习和研究中我也同样遇到很多的问题,幸得一些素不相识的师傅无私帮助,才解决掉所有的问题,这里把过程中一些问题和解决办法记录下来,以便以后查阅,同时也希望对刚接触Cobatl strike的朋友有所帮助。
0×01基础原理
基础使用和原理网上有大把的文章和教程,我这里只阐述我个人理解的几个基本点,先说stage和stager,在传统的远程控制类软件我们都是直接生成一个完整功能的客户端(其中包含了各种远控所需功能代码),比如灰鸽子(这里年龄已暴露),然后将客户端以各种方式上传至目标机器然后运行,运行后目标机器与我们控制端点对点的通讯。而Cobalt strike把这部分拆解为两部(stage和stager),stager是一个小程序,通常是手工优化的汇编指令,用于下载stage、把它注入内存中运行。stage则就是包含了很多功能的代码块,用于接受和执行我们控制端的任务并返回结果。stager通过各种方式(如http、dns、tcp等)下载stage并注入内存运行这个过程称为Payload Staging。同样Cobalt strike也提供了类似传统远控上线的方式,把功能打包好直接运行后便可以与teamserver通讯,这个称为Payload Stageless,生成Stageless的客户端可以在Attack->Package->Windows Executeable(s)下生成。这部分我也是在研究dns上线时候才算分清楚,这里需要感谢B0y1n4o4师傅的帮助
0×02 关于破戒
目前网上公布版本大多为官方试用版破戒而来且最高版为3.14(5月4号)版,我托朋找了一份3.14官方原版的来,原版的本身没有试用版那么多限制,破戒也相对容易,只需绕过license认证即可,这里在文件common/Authorization.class的构造函数中。
public Authorization() {
String str = CommonUtils.canonicalize("cobaltstrike.auth");
if (!(new File(str)).exists())
try {
File file = new File(getClass().getProtectionDomain().getCodeSource().getLocation().toURI());
if (file.getName().toLowerCase().endsWith(".jar"))
file = file.getParentFile();
str = (new File(file, "cobaltstrike.auth")).getAbsolutePath();
} catch (Exception exception) {
MudgeSanity.logException("trouble locating auth file", exception, false);
}
byte[] arrayOfByte1 = CommonUtils.readFile(str);
if (arrayOfByte1.length == 0) {
this.error = "Could not read " + str;
return;
}
AuthCrypto authCrypto = new AuthCrypto();
byte[] arrayOfByte2 = authCrypto.decrypt(arrayOfByte1);
if (arrayOfByte2.length == 0) {
this.error = authCrypto.error();
return;
}
String[] arrayOfString = CommonUtils.toArray(CommonUtils.bString(arrayOfByte2));
if (arrayOfString.length < 4) {
this.error = "auth content is only " + arrayOfString.length + " items";
return;
}
this.licensekey = arrayOfString[0];
if ("forever".equals(arrayOfString[1])) {
this.validto = arrayOfString[1];
MudgeSanity.systemDetail("valid to", "perpetual");
} else {
this.validto = "20" + arrayOfString[1];
MudgeSanity.systemDetail("valid to", CommonUtils.formatDateAny("MMMMM d, YYYY", getExpirationDate()));
}
this.watermark = CommonUtils.toNumber(arrayOfString[2], 0);
this.valid = true;
MudgeSanity.systemDetail("id", this.watermark + "");
}
这里需要把该类的`watermark`、`licensekey`、`validto`、`valid `实例域手动设置即可,如下面代码
public Authorization() {
this.valid = true;
this.validto = "forever";
this.licensekey = "Cartier";
this.watermark = 1;
MudgeSanity.systemDetail("valid to", "perpetual");
MudgeSanity.systemDetail("id", this.watermark + "");
}
剩余下其他关于listener个数限制可以参考ssooking师傅的博客查看。
Exit暗桩
剩下的一个就是Cobalt strike作者留下的一个exit的暗桩问题,这里还是请教了ssooking师傅才知道,否则我这Java渣渣不知道要找到什么时候,这里再次对ssooking师傅的帮助表示感谢。
作者在程序里留了个验证jar文件完整性的功能,如果更改了jar包的文件 这个完整性就遭到破坏,作者会在目标上线30分钟后,在此以后添加的命令任务后门加一个exit的指令,目标的beacon就自动断开了,如下图。

代码在`beacon/BeaconC2.class`
protected boolean isPaddingRequired() {
boolean bool = false;
try {
ZipFile zipFile = new ZipFile(this.appd);
Enumeration<? extends ZipEntry> enumeration = zipFile.entries();
while (enumeration.hasMoreElements()) {
ZipEntry zipEntry = enumeration.nextElement();
long l1 = CommonUtils.checksum8(zipEntry.getName());
long l2 = zipEntry.getName().length();
if (l1 == 75L && l2 == 21L) {
if (zipEntry.getCrc() != 1661186542L && zipEntry.getCrc() != 1309838793L)
bool = true;
continue;
}
if (l1 == 144L && l2 == 20L) {
if (zipEntry.getCrc() != 1701567278L && zipEntry.getCrc() != 3030496089L)
bool = true;
continue;
}
if (l1 == 62L && l2 == 26L && zipEntry.getCrc() != 2913634760L && zipEntry.getCrc() != 376142471L)
bool = true;
}
zipFile.close();
} catch (Throwable throwable) {}
return bool;
}
public BeaconC2(Profile paramProfile, BeaconData paramBeaconData, Resources paramResources) {
this.c2profile = paramProfile;
this.resources = paramResources;
this.data = paramBeaconData;
this.channel_http = new BeaconHTTP(paramProfile, this);
this.channel_dns = new BeaconDNS(paramProfile, this);
this.socks = new BeaconSocks(this);
this.appd = getClass().getProtectionDomain().getCodeSource().getLocation().getPath();
paramBeaconData.shouldPad(isPaddingRequired()); //这里调用BeaconData类的shouldPad
this.parsers.add(new MimikatzCredentials(paramResources));
this.parsers.add(new MimikatzSamDump(paramResources));
this.parsers.add(new DcSyncCredentials(paramResources));
this.parsers.add(new MimikatzDcSyncCSV(paramResources));
this.parsers.add(new ScanResults(paramResources));
this.parsers.add(new NetViewResults(paramResources));
}
再看beacon/BeaconData.class
public void shouldPad(boolean paramBoolean) {
this.shouldPad = paramBoolean;
this.when = System.currentTimeMillis() + 1800000L;
}
public void task(String paramString, byte[] paramArrayOfbyte) {
synchronized (this) {
List<byte[]> list = getQueue(paramString);
//这里判断文件完整性和beacon上线是否草果30分钟
if (this.shouldPad && System.currentTimeMillis() > this.when) {
CommandBuilder commandBuilder = new CommandBuilder();
commandBuilder.setCommand(3);
commandBuilder.addString(paramArrayOfbyte);
list.add(commandBuilder.build());
} else {
list.add(paramArrayOfbyte);
}
this.tasked.add(paramString);
}
}
破戒方法是直接更改`shouldPad`方法中的`this.shouldPad = paramBoolean;`为`this.shouldPad = false;`
0×03 CDN+反代隐藏Teamserver
这部分原理参考垃圾桶师傅的文章([点这里]),这里帮垃圾桶师傅填一个他在文章中说遇到的坑。

这里垃圾桶师傅在添加Listener的时候Host填写的是CDN的地址,在使用powershell下载`stager`运行,`stager`再去下载`stage`的时候就是直接访问cdn的地址下载,但是`malleable profile`没有配置制定stager的行为,所以无法正常回源到teamserver下载,这里只需要在profile文件中配置`http-stager`模块,像http-get一样指定好Host即可从CDN访问到teamserver下载`stage`了。
Proxy
反向代理原理这里借用垃圾桶师傅的图,我就不具体再阐述,垃圾桶师傅已经讲得很明白。

我使用的是Nginx做的反向代理,这里如果刚研究这个的朋友可能会遇到客户端上线后IP是Nginx服务器IP,走CDN的时候显为CDN节点IP的情况,这里有两个解决办法,先看看`server/ServerUtils.class`类中代码:
public static String getRemoteAddress(Profile paramProfile, Map paramMap) {
boolean bool = paramProfile.option(".http-config.trust_x_forwarded_for");
if (bool && paramMap.containsKey("X-Forwarded-For")) {
String str1 = (String)paramMap.get("X-Forwarded-For");
if (str1.indexOf(",") > -1) {
str1 = CommonUtils.strrep(str1, " ", "");
StringStack stringStack = new StringStack(str1, ",");
str1 = stringStack.shift();
}
if (CommonUtils.isIP(str1) || CommonUtils.isIPv6(str1))
return str1;
CommonUtils.print_error("remote address '" + (String)paramMap.get("X-Forwarded-For") + "' in X-Forwarded-For header is not valid.");
}
String str = (String)paramMap.get("REMOTE_ADDRESS");
return "".equals(str) ? "" : str.substring(1);
}
}
这里Cobatl Strike可以从`HttpHeader`中的`REMOTE_ADDRESS`和`X-Forwarded-For`中取得IP,我们要么在Nginx反向代理的时候设置`REMOTE_ADDRESS`值,要么在profile的配置文件中的`http-config`模块设置`trust_x_forwarded_for`值为`true`,这也是看了代码从知道有这个配置,英文渣渣表示很惭愧,官方写得很详细。
这里有个问题就是反向代理时候自定义`REMOTE_ADDRESS`时候往往无效,不知道具体啥情况,我之前在另外的机器上都有测试成功过。
0×04 DNS上线
一个未填的坑
这个坑是研究和使用Cobalt Strike来最大一个坑,至发文今日都没有解决。问题是出在使用DNS的listener不管是`beacon_dns/reverse_http`还是`beacon_dns/reverse_dns_txt`时候,若使用`staging`方式`stager`在下载`stage`注入到内存中的时候崩掉,如下图。

而若使用`beacon_dns/reverse_http`时候,选用非纯dns模式就没问题,非纯dns模式状态下stager在下载stage时候使用http方式,stage只要成功下载注入内存后便可以mode改用dns方式来通讯了,要是有师傅知道怎么回事还赐教。
DNS Listener特性
最后经B0y1n4o4师傅指点,改用stageless方式上线就没有问题了。但是在使用dns上线的时候还需要注意个问题。在添加Listener的时候`beacon_dns/reverse_http`和`beacon_dns/reverst_dns_txt`都需要填写端口信息,如下图。

如果端口使用80的情况下,上线之后的通讯优先使用http方式,若想用纯dns通讯的话就需要在上线之后首先使用`mode` 指令切换至dns、dns-txt或者dns6模式。添加listener自定一个非80的端口上线之后所以的通讯都将默认采用dns方式,且不能使用mode切换成http模式。
0×05 结语
以上均为我个人一些研究测试结论,有不到之处还请多多指正,Cobalt Strike确实是一个蛮强大的工具,还有很多内容和技术有待研究,本人也正在学习Java,争取早日通读内核代码。

江西省抚州市 1F
这破玩意儿光配置就搞了我三天,服了
北京市 B1
@ 血誓狂徒 nginx加个proxy_set_header X-Forwarded-For $remote_addr就行
陕西省安康市 2F
stageless真香,dns上线别折腾staging了
韩国 B1
@ 青瓷浅唱 确实staging坑太多,直接stageless省心
河北省 B1
@ 青瓷浅唱 stageless生成在Attack -> Package里,win exe那个选项下
北京市 3F
求问3.14原版在哪找的?我下的全是带后门的
辽宁省沈阳市 B1
@ 烟囱冒烟 3.14原版真难找,我下的几个都跑不起来
广东省广州市 4F
之前搞过CS,teamserver被CDN反代IP全乱了,后来改profile才好
澳大利亚 5F
那个exit暗桩太阴了,上线半小时突然断连还以为自己操作错了😂
福建省厦门市 B1
@ 故纸堆 exit暗桩坑死我了,断连还以为被发现了😂
湖南省衡阳市 6F
非80端口强制走dns?学到了,之前一直纳闷为啥切不了模式
上海市 7F
Java渣表示看class文件像天书,作者咋想的放这种校验
上海市 8F
cdn+反代那段没看懂,有人能说说nginx具体怎么配header吗?
台湾省高雄市 9F
感觉还行,至少比msf稳定
北京市 B1
@ 星环编织者 确实比msf稳,就是配置太复杂了
北京市 B1
@ 星环编织者 CDN反代后IP一直不对,原来是X-Forwarded-For没配对😭
湖南省长沙市 B1
@ 星环编织者 这玩意儿比msf稳是稳,就是配置能搞死人
韩国 10F
纯dns模式下传输24字节就崩,是不是payload太大了?
湖北省宜昌市 B1
@ 星尘之子 24字节就崩感觉不像是大小问题,更像stager代码有bug
北京市 11F
这破玩意儿配置起来太折磨了,搞了三天人都麻了
中国 B1
@ 绘画达人 三天算快的,我折腾了一周还没搞定监听器
北京市 12F
cdn反代后IP全成代理的了,差点以为日志炸了
泰国 B1
@ 剑师董 X-Forwarded-For头里拿到的是cdn的ip吧?nginx没把真实ip传过去
上海市 13F
stageless救我狗命,staging纯dns就是个雷区
陕西省延安市 14F
那个shouldPad改false真的绝,不然半小时必崩
山东省日照市 15F
profile里http-config设trust_x_forwarded_for=true也行
韩国 16F
纯dns传24字节就崩?怕不是网络环境问题吧
新疆克拉玛依市 17F
Java反编译看得脑壳疼,作者还整CRC校验这手
韩国 B1
@ 棉花糖花 crc校验这招太狠了,作者防破戒用心良苦🤔
湖北省武汉市 B1
@ 棉花糖花 Java渣表示看class文件像天书+1
北京市 18F
stage和stager这段讲得挺清楚,之前一直搞混
北京市 B1
@ 锦绣前程 总算搞明白stage和stager区别了,之前一直以为是一个东西
上海市 19F
dns模式为啥非80端口不能切http啊?
韩国 20F
改shouldPad为false真的管用,半小时断连问题解决了
北京市 21F
profile配置那段救了我,cdn反代后ip显示正常了
河南省濮阳市 22F
求问stageless生成在哪找?攻击菜单里没看到
湖南省邵阳市 B1
@ 像素驯兽师 stageless在Attack-Package-Windows Executeable(s)里,菜单栏往下翻
日本 23F
非纯dns模式先用http下stage再切dns也行
日本 24F
看完头大,光一个DNS上线就这么多坑🤦♂️
台湾省 25F
profile里加个http-stager块就行,作者不是写了吗
四川省达州市 26F
那个exit暗桩真够恶心的,作者防破戒这么狠
北京市 B1
@ 玄鬼探案 这个exit暗桩确实阴,作者防破戒手段够绝的
河南省郑州市 B1
@ 玄鬼探案 破戒确实容易,但exit暗桩那个真得仔细看代码
北京市 27F
所以DNS上线还是得用stageless?懂了
湖南省长沙市 B1
@ 陨落星辰 staging纯dns就是个坑,直接用stageless省心多了
青海省 B1
@ 陨落星辰 所以非80端口上线就锁死dns了?难怪切不了模式
河南省新乡市 28F
谁有3.14原版分享一下啊,找的全是带木马的
泰国 29F
这种技术文看得真累,但确实有用
印度尼西亚 30F
纯DNS模式到底啥原理啊,为啥非80端口就不能切http了?
台湾省 31F
stage和stager那段说明白了,之前看其他教程都稀里糊涂的
湖北省宜昌市 32F
这玩意儿光license认证那块就够折腾了,代码改了好几遍才成功
山东省青岛市 33F
纯DNS模式传24字节就崩,我这边也是这毛病,无解了
日本 34F
求问非纯dns模式具体咋操作?profile里要改啥不
北京市 35F
技术文章太硬核了,看一半先收藏吧
陕西省铜川市 36F
破戒那段代码看晕了,有更傻瓜式的教程吗
江苏省无锡市 37F
CDN反代那个坑我也踩过,nginx配X-Forwarded-For死活不生效
上海市 38F
大佬们都在哪找的3.14原版啊,搜了一圈全是带后门的版本😭
河北省邯郸市永年县 39F
DNS上线这么麻烦,我还是老老实实用http吧
辽宁省沈阳市 40F
纯DNS那个坑我也卡了好久
日本 41F
这破戒改得我手抖,生怕一不小心就崩了
浙江省衢州市 42F
profile里http-stager那块能不能来个示例配置啊,照着抄都怕出错
山东省滨州市 43F
Java反编译那段真劝退,CRC校验整得跟迷宫似的
台湾省台北市 B1
@ 丹霞子 CRC校验那段看得我直接放弃,Java反编译真不是人干的
浙江省绍兴市 44F
纯DNS传24字节就崩,我换了几台VPS都一样,怕不是协议层有bug?
中国 45F
之前搞过这个,确实折腾了好久,最后干脆全用stageless了
越南 46F
dns-txt模式试过吗?听说更稳一点?
浙江省湖州市 47F
那个exit暗桩太阴了,半小时突然断连差点以为自己翻车了😂
湖北省十堰市郧县 48F
纯DNS上线传24字节就崩,是不是payload编码问题?🤔
中国 49F
之前搞过这个,确实折腾了好久,最后全切stageless才稳
北京市 50F
cdn反代后IP显示正常了,多亏profile里加了trust_x_forwarded_for
陕西省西安市 51F
破戒改valid那块手抖得不行,生怕改错一行直接变砖😂
河南省新乡市 52F
灰鸽子,时代的眼泪啊。