Go渗透测试笔记(五)–DNS的利用

0X00 前言

DNS用于定位Internet,并将其转化为IP地址。他可以成为攻击者手段的有效武器。因为组织通常允许协议的出站连接离开受限制的网络,并且无法充分监视其使用。

0x01 编写DNS客户端

Go本质上可以用内置的包net,支持大多数的DNS类型,内置包的好处在于简单易用的API,例如LookupAddr(addr string)返回给定IP地址的主机名列表,但是使用内置包也有缺点:无法指定目标服务器,不过,该包会使用操作系统配置的解析器。另一个缺点是:无法对结果进行深入检查

为了解决这个问题,我们使用一个优秀的由Miek Gieben编写的第三方包,即Go DNS

同样,安装命令如下:

go get github.com/miekg/dns

1. 检索A记录

编写如下代码

1
2
3
4
5
6
7
8
9
10
11
package main

import "github.com/miekg/dns"

func main() {
var msg dns.Msg
fqdn := dns.Fqdn("baidu.com")
msg.SetQuestion(fqdn,dns.TypeA)
dns.Exchange(&msg,"8.8.8.8:53")
}

FQDN 完全限定域名,该域名指定主机在DNS结构中的精确位置,然后使用一种成为A记录的DNS记录,将该FQDN地址解析为IP地址。

首先,创建一个信的Msg,然后将域转换为可以与DNS服务器交换的FQDN。接下来使用TypeA代表查找A记录

这里DNS服务器涉及到一个问题

DNS解析中的A记录、AAAA记录、CNAME记录、MX记录、NS记录、TXT记录、SRV记录、URL转发等

A记录: 将域名指向一个IPv4地址(例如:100.100.100.100),需要增加A记录

NS记录: 域名解析服务器记录,如果要将子域名指定某个域名服务器来解析,需要设置NS记录

SOA记录: SOA叫做起始授权机构记录,NS用于标识多台域名解析服务器,SOA记录用于在众多NS记录中标记哪一台是主服务器

MX记录: 建立电子邮箱服务,将指向邮件服务器地址,需要设置MX记录。建立邮箱时,一般会根据邮箱服务商提供的MX记录填写此记录

TXT记录: 可任意填写,可为空。一般做一些验证记录时会使用此项,如:做SPF(反垃圾邮件)记录

然后,调用Exchange(*Msg,string),将消息发送到提供的服务器地址,在本例中使用的是Goole的DNS服务器地址

接下来,我们使用wirshark进行数据包的分析

会得到如下的数据,通过捕获的数据包,我们可以看到通过UDP53与8.8.8.8端口连接,还可以看到与DNS信息有关的部分

可以看到在请求DNSA记录的同时,将查询先发送给8.8.8.8,然后再从8.8.8.8返回,包含以及解析的IP地址,220.181.38.251

2. 使用Msg结构体处理应答

Exchange(*Msg,string)返回的值是(*Msg error),返回的错误类型是可以接受的,那,为什么返回了(*Msg)呢?

先看一下Msg的定义

1
2
3
4
5
6
7
8
9
type Msg struct {
MsgHdr
Compress bool `json:"-"` // If true, the message will be compressed when converted to wire format.
Question []Question // Holds the RR(s) of the question section.
Answer []RR // Holds the RR(s) of the answer section.
Ns []RR // Holds the RR(s) of the authority section.
Extra []RR // Holds the RR(s) of the additional section.
}

结构体包含问询和应答,这使得我们可以将所有DNS合并为一个统一的结构体,结构体Msg拥有多种处理起来也更为容易的方法。

例如,我么可以使用SetQuestion()修改切片Question,也可以使用方法append()直接修改此切片,可以获得相同的结果。切片Answer,保存查询的情况,其类型为RR

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package main

import (
"fmt"
"github.com/miekg/dns"
)

func main() {
var msg dns.Msg
fqdn := dns.Fqdn("baidu.com")
msg.SetQuestion(fqdn,dns.TypeA)
in ,err := dns.Exchange(&msg,"8.8.8.8:53")
if err != nil {
panic(err)
}
if len(in.Answer) < 1{
fmt.Println("No records")
return
}
for _,answer := range in.Answer{
if a, ok := answer.(*dns.A); ok {
fmt.Println(a.A)
}
}
}

首先,从储存的Exchange返回的值,是否存在错误,则调用panic停止程序。可以快速确定堆栈跟踪,并确定错误发生的位置。接下来,先确认Answer的长度至少为1,如果不是,则表明没有记录,则立即返回。毕竟在某些情况下,域名无法解析。

类型RR,是一个具有两个方法的接口,并且都不允许访问应答中存储的IP地址,要访问这些地址,需要执行类型声明以将数据实例创建为所需要的类型。首先遍历所有应答,然后,对应答类型进行断言。以确保我们正在处理的类型全部是*dns.A的类型。

3. 枚举子域

现在已经可以使用Go创建一个DNS客户端了,在本节中,我们创建一个枚举的子域的工具。

当我们开始编写工具的时候,必须确定工具使用了那些参数,我们要写的工具参数,包括目标域,包含要猜测的子域文件名,要使用的DNS服务器以及要启动的工作程序数量。Go提供了一个有用的flag包,我们将使用这个包去处理命令参数。

1
2
3
4
5
6
7
8
9
10
func main() {
var(
flDomain = flag.String("domain","","要猜解的域名")
flWordlist = flag.String("wordlist","","猜解所使用的字典")
flWorkerCount = flag.Int("c",100,"所使用的线程")
flServerAddr = flag.String("Server","8.8.8.8:53","所使用的DNS服务器")
)
flag.Parse()

}

这种方式违反了 Unix法则,因为它定义了一些非可选的参数,当然此处也可以使用os.Args,但是使用flag包能更好的便捷理解

但是,此时的程序时不能编译通过的,我们会收到使用未知变量的错误。我们需要加入以下代码

1
2
3
4
if *flDomain == "" || *flWordlist == "" {
fmt.Println("-domain and -wordlist are required")
os.Exit(1)
}

为了我们的工具可以输出解析的域名以及各自的IP,我们将创建一个结构类型来储存此信息。

1
2
3
4
type result struct {
IPAddress string
Hostname string
}

此工具查询的两种主要的记录类型:A和CNAME,我们将使用单独的函数执行每个查询,每个函数只执行一种操作。

(Canonical Name)记录,(alias from one domain name to another)通常称别名指向 。
通俗点讲就是给你的域名起一个别名。比如你的域名是www.abc.com,想和你的另外一个域名www.cba.com进行绑定,应该在cname的host中填入www,在points中填入www.cba.com。这样一来当你访问www.abc.com的时候自动跳转到www.cba.com,而且浏览器上显示的域名仍然是www.abc.com。看了这个你可能会混淆解析和绑定的区别,很多站长认为将一个域名(www.abc.com)cname到另外一个域名(www.cba.com)之后就可以实现:访问www.abc.com得到www.cba.com的内容.
把cName和转向功能混为一谈了。cName只能保证www.abc.com的解析和www.cba.com同步起来,如果是共享ip的主机,不绑定还是访问不到网站内容。这就是为什么如果你设置cname到你的新浪sae域名,如果sae没有将你和他绑定你还是访问不了他的原因。

4. 查询A记录和CNAME记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
func lookupA(fqdn, serverAddr string) ([]string, error) {
var m dns.Msg
var ips []string
m.SetQuestion(dns.Fqdn(fqdn), dns.TypeA)
in, err := dns.Exchange(&m, serverAddr)
if err != nil {
return ips, err
}
if len(in.Answer) < 1 {
return ips, errors.New("no answer")
}
for _, answer := range in.Answer {
if a, ok := answer.(*dns.A); ok {
ips = append(ips, a.A.String())
}
}
return ips, nil
}

func lookupCNAME(fqdn, serverAddr string) ([]string, error) {
var m dns.Msg
var fqdns []string
m.SetQuestion(dns.Fqdn(fqdn), dns.TypeCNAME)
in, err := dns.Exchange(&m, serverAddr)
if err != nil {
return fqdns, err
}
if len(in.Answer) < 1 {
return fqdns, errors.New("no answer")
}
for _, answer := range in.Answer {
if c, ok := answer.(*dns.CNAME); ok {
fqdns = append(fqdns, c.Target)
}
}
return fqdns, nil
}

CNAME 记录一个FQDN指向另一个FQDN作为自己的别名。假如example.com组织的所有者希望通过wordpress托管来托管 wordpress网站。该服务可能有上百种ip,用于平衡其所有用户的站点,因此不可能提供单个的IP地址。wordpress可能为example.com提供一个CNAME,因此,example.comCNAME可能指向someserver.hostingcompany.org,而CNAME的A记录则指向一个IP地址,这允许example.com的所有者将其站点托管再没有IP信息的服务器上面。

因此,我们需要追踪CNAME的痕迹,才能找到最有效的A记录。

接下来我们定义lookup方法来使用CNAME追踪A记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func lookup(fqdn, serverAddr string) []result {
var results []result
var cfqdn = fqdn // Don't modify the original.
for {
cnames, err := lookupCNAME(cfqdn, serverAddr)
if err == nil && len(cnames) > 0 {
cfqdn = cnames[0]
continue // We have to process the next CNAME.
}
ips, err := lookupA(cfqdn, serverAddr)
if err != nil {
break // There are no A records for this hostname.
}
for _, ip := range ips {
results = append(results, result{IPAddress: ip, Hostname: fqdn})
}
break // We have processed all the results.
}
return results
}

通过循环一直找到解析的末尾,从而跟踪CNAME的痕迹。

5. 工人函数

我们要实现高并发,需要将工作分配给工人函数。

我们创建worker()函数,该函数使用三个通道函数,一个用于通知工人是否已经关闭通道,一个用于接受工作通道,一个用于发送结果。该函数还需要一个参数来指定要使用的DNS服务器

1
2
3
4
5
6
7
8
9
10
11
type empty struct{}
func worker(tracker chan empty, fqdns chan string, gather chan []result, serverAddr string) {
for fqdn := range fqdns {//在域通道上进行循环
results := lookup(fqdn, serverAddr)
if len(results) > 0 {
gather <- results
}
}
var e empty
tracker <- e
}

在引入函数worker()之前,我们需要定义一个名为empty的结构体,当工人完成工作时,进行跟踪记录。

在有结果的时候,发送到gather通道。 最后,当所有的工作完成的时候,在通道tracker上发送一个空结构体,表示所有工作都已完成。如果不这样做,将会使得处于竞争状态,因为调用者可能在收到结果之前退出。

之后,我们设置要传递的通道

1
2
3
4
var results []result
fqdns := make(chan string, *flWorkerCount)
gather := make(chan []result)
tracker := make(chan empty)

6. 使用 bufio包进行一个文本扫描器

打开文件之后,使用bufio包创建一个新的Scanner,该文本扫描器允许我们一行行的读

1
2
3
4
5
6
fh, err := os.Open(*flWordlist)
if err != nil {
panic(err)
}
defer fh.Close()
scanner := bufio.NewScanner(fh)

我们使用新的文本扫描器从用户提供的wordlist中抓取一行文本,并通过文本和用户提供的域结合在一起,组成信的FQDN,需要将结果发送到fqdns通道,但首先需要启动工人函数

1
2
3
4
5
6
for i := 0; i < *flWorkerCount; i++ {
go worker(tracker, fqdns, gather, *flServerAddr)
}
for scanner.Scan() {
fqdns <- fmt.Sprintf("%s.%s", scanner.Text(), *flDomain)
}

7. 收集和显示结果

首先启动一个匿名的gorountine,它将收集工人的结果

1
2
3
4
5
6
7
8
go func() {
for r := range gather {
results = append(results, r...)
}
var e empty
tracker <- e
}()

通过遍历通道gather,可以将接收到的结果,添加到切片result上,由于需要将切片附加到另一个切片上,所以需要...,之后关闭通道。

剩下的就是关闭通道,并且展现结果

1
2
3
4
5
6
close(fqdns)
for i := 0; i < *flWorkerCount; i++ {
<-tracker
}
close(gather)
<-tracker

此时结果尚未呈现给用户,我们需要将其打印

1
2
3
4
5
w := tabwriter.NewWriter(os.Stdout, 0, 8, 4, ' ', 0)
for _, r := range results {
fmt.Fprintf(w, "%s\t%s\n", r.Hostname, r.IPAddress)
}
w.Flush()

8. 完整程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
package main

import (
"bufio"
"errors"
"flag"
"fmt"
"os"
"text/tabwriter"

"github.com/miekg/dns"
)

func lookupA(fqdn, serverAddr string) ([]string, error) {
var m dns.Msg
var ips []string
m.SetQuestion(dns.Fqdn(fqdn), dns.TypeA)
in, err := dns.Exchange(&m, serverAddr)
if err != nil {
return ips, err
}
if len(in.Answer) < 1 {
return ips, errors.New("no answer")
}
for _, answer := range in.Answer {
if a, ok := answer.(*dns.A); ok {
ips = append(ips, a.A.String())
}
}
return ips, nil
}

func lookupCNAME(fqdn, serverAddr string) ([]string, error) {
var m dns.Msg
var fqdns []string
m.SetQuestion(dns.Fqdn(fqdn), dns.TypeCNAME)
in, err := dns.Exchange(&m, serverAddr)
if err != nil {
return fqdns, err
}
if len(in.Answer) < 1 {
return fqdns, errors.New("no answer")
}
for _, answer := range in.Answer {
if c, ok := answer.(*dns.CNAME); ok {
fqdns = append(fqdns, c.Target)
}
}
return fqdns, nil
}

func lookup(fqdn, serverAddr string) []result {
var results []result
var cfqdn = fqdn // Don't modify the original.
for {
cnames, err := lookupCNAME(cfqdn, serverAddr)
if err == nil && len(cnames) > 0 {
cfqdn = cnames[0]
continue // We have to process the next CNAME.
}
ips, err := lookupA(cfqdn, serverAddr)
if err != nil {
break // There are no A records for this hostname.
}
for _, ip := range ips {
results = append(results, result{IPAddress: ip, Hostname: fqdn})
}
break // We have processed all the results.
}
return results
}

func worker(tracker chan empty, fqdns chan string, gather chan []result, serverAddr string) {
for fqdn := range fqdns {
results := lookup(fqdn, serverAddr)
if len(results) > 0 {
gather <- results
}
}
var e empty
tracker <- e
}

type empty struct{}

type result struct {
IPAddress string
Hostname string
}

func main() {
var(
flDomain = flag.String("domain","","要猜解的域名")
flWordlist = flag.String("wordlist","","猜解所使用的字典")
flWorkerCount = flag.Int("c",100,"所使用的线程")
flServerAddr = flag.String("Server","8.8.8.8:53","所使用的DNS服务器")
)
flag.Parse()

if *flDomain == "" || *flWordlist == "" {
fmt.Println("-domain and -wordlist are required")
os.Exit(1)
}

var results []result

fqdns := make(chan string, *flWorkerCount)
gather := make(chan []result)
tracker := make(chan empty)

fh, err := os.Open(*flWordlist)
if err != nil {
panic(err)
}
defer fh.Close()
scanner := bufio.NewScanner(fh)

for i := 0; i < *flWorkerCount; i++ {
go worker(tracker, fqdns, gather, *flServerAddr)
}

go func() {
for r := range gather {
results = append(results, r...)
}
var e empty
tracker <- e
}()

for scanner.Scan() {
fqdns <- fmt.Sprintf("%s.%s", scanner.Text(), *flDomain)
}
// Note: We could check scanner.Err() here.

close(fqdns)
for i := 0; i < *flWorkerCount; i++ {
<-tracker
}
close(gather)
<-tracker

w := tabwriter.NewWriter(os.Stdout, 0, 8, 4, ' ', 0)
for _, r := range results {
fmt.Fprintf(w, "%s\t%s\n", r.Hostname, r.IPAddress)
}
w.Flush()
}

9. 使用

1
go run .\main.go -domain microsoft -wordlist namelist.txt -c 1000

0x02 自己编写DNS服务器

1. 实验环境搭建

在服务器上,这里选择使用ubuntu,安装好docker


先鸽一会,有点难,返回来学习