0X00 背景
最近在做渗透测试相关的工作,因工作需要准备用Cobalt Strike,老早都知道这款神器,早几年也看过官方的视频教程,但英文水平太渣当时很多都没听懂,出于各种原因后来也没怎么深入了解,所以一直都是处在大概了解的层面上。直到现在有需求了才开始研究,过程中体会也是蛮深,技术这东西真的不能只停留在知道和了解这个层面,就像学一门语言一样需要多动手去实践才能熟练运用的。
当然在深入研究某一门技术的过程中难免遇到各种各样的问题,一步一步解决这些问题才是真正学习的过程。对Cobalt strike的学习和研究中我也同样遇到很多的问题,幸得一些素不相识的师傅无私帮助,才解决掉所有的问题,这里把过程中一些问题和解决办法记录下来,以便以后查阅,同时也希望对刚接触Cobatl strike的朋友有所帮助。
0x01基础原理
基础使用和原理网上有大把的文章和教程,我这里只阐述我个人理解的几个基本点,先说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师傅的帮助
0x02 关于破戒
目前网上公布版本大多为官方试用版破戒而来且最高版为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;`
0x03 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`时候往往无效,不知道具体啥情况,我之前在另外的机器上都有测试成功过。
0x04 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模式。
0x05 结语
以上均为我个人一些研究测试结论,有不到之处还请多多指正,Cobalt Strike确实是一个蛮强大的工具,还有很多内容和技术有待研究,本人也正在学习Java,争取早日通读内核代码。
*本文作者:Ca3tie1,转载请注明来自FreeBuf.COM