域名系统(Domain Name System,缩写:DNS)是互联网的一项服务。它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。这就如同一个地址簿,根据域名来指向IP地址。
域名系统_百度百科
使用第三方包 github.com/miekg/dns
$ go get github.com/miekg/dns go: downloading github.com/miekg/dns v1.1.49 go: downloading golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 go: downloading golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c go: downloading golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 go: downloading golang.org/x/mod v0.4.2 go: downloading golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 go: added github.com/miekg/dns v1.1.49 go: added golang.org/x/mod v0.4.2 go: added golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 go: added golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c go: added golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2 go: added golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1
要得知主机在DNS层次结构中的确切位置,须要查找完全限定域名(FQDN)。通过查找称为A记录的DNS记录,将该FQDN解析为IP地址。
A记录是Address record,也就是把域名指向某个空间的IP地址。
package main import ( "fmt" "github.com/miekg/dns" ) func main() { var msg dns.Msg // 创建msg fqdn := dns.Fqdn("baidu.com") msg.SetQuestion(fqdn, dns.TypeA) _, err := dns.Exchange(&msg, "8.8.8.8:53") if err != nil { fmt.Println(err) } }
如上代码可以向指定的DNS服务器发送询问,但尚未处理应答。
dns.Fqdn将返回可以与DNS服务器交换的FQDN。SetQuestion将创建一个询问,将得到FQDN传入该函数,然后指定A记录。dns.Exchange将消息发送给提供的DNS服务器。8.8.8.8
是google运营的DNS服务器。
使用命令:sudo tcpdump -i eth0 -n udp port 53
开启tcpdump监听UDP 53端口,eth0是网卡名称。
开启监听后运行上述程序,tcpdump输出了如下结果
08:35:50.723180 IP 192.168.43.99.44249 > 8.8.8.8.53: 60658+ A? baidu.com. (27) 08:35:50.914939 IP 8.8.8.8.53 > 192.168.43.99.44249: 60658 2/0/0 A 220.181.38.251, A 220.181.38.148 (59)
可以看到有关DNS协议的详细信息。
从IP地址192.168.43.99
向发送8.8.8.8
的UDP 53端口发送包含域名询问,之后8.8.8.8
返回IP地址 220.181.38.251
和220.181.38.148
Exchange会返回一个结构体,其中包含了问询和应答,该结构体如下:
type Msg struct { MsgHdr Compress bool `json:"-"` // 如果为true Question []Question // 保留question的RR Answer []RR // 保留answer的RR Ns []RR // 保留authority的RR Extra []RR // 保留additional的RR }
如下输出了结果
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 { fmt.Println(err) return } // 如果长度小于1 则说明没有记录 if len(in.Answer) < 1 { fmt.Println("No records") return } for _, answer := range in.Answer { if res, ok := answer.(*dns.A); ok { fmt.Println(res.A) // 打印信息 } } }
输出结果
220.181.38.251 220.181.38.148
要访问应答中存储的IP地址,要执行类型声明以将数据实例创建为所需的类型。遍历所用应答,然后对其进行类型断言,以确保正在处理的类型是*dns.A
下面将实现一个猜测子域名的工具,原理是拿域名发送给DNS服务器解析,如果能解析出A记录,说明是存在这个域名的。该程序使用命令行传参。同时为了提高效率将利用并发性,以快速枚举。
首先要明确它将使用哪些参数,至少包括目标域、要猜测的子域的文件名、要使用的目标DNS服务器以及要启动的线程的数量。
func init() { flag.StringVar(&domain, "d", "", "The domain to perform guessing against.") flag.StringVar(&wordlist, "w", "", "The wordlist to use for guessing.") flag.IntVar(&count, "c", 100, "The amount of workers to use.") flag.StringVar(&server, "s", "8.8.8.8:53", "The DNS server to use.") flag.Parse() if domain == "" || server == "" { fmt.Println("-d and -w are required") os.Exit(1) } }
使用flag包对命令行传参进行解析
定义一个结构体,来表示查询结果
// 查询结果 type result struct { address string hostname string }
该工具准备查询两种主要的记录: A记录和CNAME记录,将使用单独的函数执行每个查询。
将创建两个函数执行查询,其中一个用于查询A记录,另一个用于查询CNAME记录。这两个函数均接收FQDN作为第一个参数,并接收DNS服务器地址作为第二个参数,每个函数都应返回一个字符串切片和一个错误。
如下函数负责查找A记录
func lookupA(fqdn string) ([]string, error) { var msg dns.Msg var addrs []string msg.SetQuestion(dns.Fqdn(fqdn), dns.TypeA) in, err := dns.Exchange(&msg, server) if err != nil { return addrs, err } if len(in.Answer) < 1 { return addrs, errors.New("no answer") } for _, answer := range in.Answer { if ans, ok := answer.(*dns.A); ok { addrs = append(addrs, ans.A.String()) } } return addrs, nil }
上述函数同样是发起一个问询,然后得到一个结构体。使用for-range遍历该结构体中的数据,将结果放入切片,最后返回。
CNAME 即指别名记录,也被称为规范名字。一般用来把域名解析到别的域名上,当需要将域名指向另一个域名,再由另一个域名提供 ip 地址,就需要添加 CNAME 记录。
这意味着要跟踪CNAME记录链的查询,才能最终找到有效的A记录。
func lookupCNAME(fqdn string) ([]string, error) { var msg dns.Msg var fqdns []string msg.SetQuestion(dns.Fqdn(fqdn), dns.TypeCNAME) in, err := dns.Exchange(&msg, server) if err != nil { return fqdns, err } if len(in.Answer) < 1 { return fqdns, errors.New("no answer") } for _, answer := range in.Answer { if ans, ok := answer.(*dns.CNAME); ok { fqdns = append(fqdns, ans.Target) } } return fqdns, nil }
该函数返回的是域名组成的切片,并非IP地址
如下函数负责得到最后的结果
func lookup(fqdn string) []result { var results []result var cfqdn = fqdn for { cnames, err := lookupCNAME(cfqdn) if err == nil && len(cnames) > 0 { cfqdn = cnames[0] continue } addrs, err := lookupA(cfqdn) if err != nil { break } for _, addr := range addrs { results = append(results, result{address: addr, hostname: fqdn}) } break } return results }
该函数的第一个参数是FQDN,之后要第一个变量作为其副本。
之后在一个循环中先使用lookupCNAME查找CNAME记录,如果返回了CNAME,则获取到第一个CNAME,进入到下一次循环,往下迭代查询。
如果lookipCNAME函数出错,说明已经到了CNAME的末端,可与直接查询A记录,运行到lookupA处,得到IP。最后,将存储IP的切片返回。
目前暂不考虑并发,在main中测试结果
func main() { file, _ := os.Open(wordlist) scanner := bufio.NewScanner(file) for scanner.Scan() { fqdn := fmt.Sprintf("%s.%s", scanner.Text(), domain) result := lookup(fqdn) if len(result) > 0 { fmt.Println(result) } } }
输出
$ ./main -d baidu.com -w test.txt [{112.80.248.124 a.baidu.com}] [{180.97.104.93 ab.baidu.com}] [{180.101.49.11 abc.baidu.com} {180.101.49.12 abc.baidu.com}] [{180.97.93.62 b.baidu.com} {180.97.93.61 b.baidu.com}] [{182.61.240.110 bh.baidu.com}] [{39.156.66.102 cc.baidu.com} {220.181.111.34 cc.baidu.com} {112.34.111.153 cc.baidu.com}] [{14.215.178.159 cha.baidu.com}] [{220.181.38.251 d.baidu.com} {220.181.38.148 d.baidu.com}] [{175.6.53.37 dq.baidu.com} {180.97.64.37 dq.baidu.com} {180.97.66.37 dq.baidu.com} {183.56.138.37 dq.baidu.com} {182.106.137.37 dq.baidu.com} {180.101.38.37 dq.baidu.com} {183.60.219.37 dq.baidu.com} {218.93.204.37 dq.baidu.com} {220.169.152.37 dq.baidu.com} {124.225.184.37 dq.baidu.com}] [{183.136.195.35 e.baidu.com}] [{10.58.182.14 er.baidu.com}] ...
这里使用-w
指定一个字典,-d
指定一个域名。在循环中,如果代表结果的切片不为空,那么说明对应的域名是存在的。
下面创建线程池,进行并发请求
如下定义一个工人函数
type empty struct{} func worker(tracker chan empty, fqdns chan string, gather chan []result) { for fqdn := range fqdns { results := lookup(fqdn) if len(results) > 0 { gather <- results } } var e empty tracker <- e }
事先定义了一个名为empty的空结构体,这是Go中常用的操作,相当于一个信号发送给通道,用来防止调用者提前退出。
如下修改main函数
func main() { var results []result fqdns := make(chan string, count) gather := make(chan []result) tracker := make(chan empty) // 打开字典文件 file, err := os.Open(wordlist) if err != nil { panic(err) } defer file.Close() scanner := bufio.NewScanner(file) // 调起count个goroutine for i := 0; i < count; i++ { go worker(tracker, fqdns, gather) } // 投递域名 for scanner.Scan() { fqdns <- fmt.Sprintf("%s.%s", scanner.Text(), domain) } // 合并所有结果 go func() { for result := range gather { results = append(results, result...) } var e empty tracker <- e }() close(fqdns) // 在所有worker完成之前 阻塞住主goroutine for i := 0; i < count; i++ { <-tracker } close(gather) <-tracker // 在合并完结果前 堵塞主goroutine save, _ := os.OpenFile("result.txt", os.O_CREATE|os.O_WRONLY, 0666) writer := tabwriter.NewWriter(save, 0, 8, 4, ' ', 0) for _, result := range results { fmt.Fprintf(writer, "%s\t%s\n", result.hostname, result.address) } writer.Flush() }
在main函数中,使用bufio包对文本文件进行扫描,获得每行的字符串,拼接为FQDNS,传入通道。使用循环启动count个worker线程发起请求。最后写入文件,保存扫描的结果。
完整代码
package main import ( "bufio" "errors" "flag" "fmt" "github.com/miekg/dns" "os" "text/tabwriter" ) var ( domain string // 域名 wordlist string // 猜解字典 count int // 线程数 server string // 服务器地址 ) // 查询结果 type result struct { address string hostname string } func init() { flag.StringVar(&domain, "d", "", "The domain to perform guessing against.") flag.StringVar(&wordlist, "w", "", "The wordlist to use for guessing.") flag.IntVar(&count, "c", 100, "The amount of workers to use.") flag.StringVar(&server, "s", "8.8.8.8:53", "The DNS server to use.") flag.Parse() if domain == "" || server == "" { fmt.Println("-d and -w are required") os.Exit(1) } } func lookupA(fqdn string) ([]string, error) { var msg dns.Msg var addrs []string msg.SetQuestion(dns.Fqdn(fqdn), dns.TypeA) in, err := dns.Exchange(&msg, server) if err != nil { return addrs, err } if len(in.Answer) < 1 { return addrs, errors.New("no answer") } for _, answer := range in.Answer { if ans, ok := answer.(*dns.A); ok { addrs = append(addrs, ans.A.String()) } } return addrs, nil } func lookupCNAME(fqdn string) ([]string, error) { var msg dns.Msg var fqdns []string msg.SetQuestion(dns.Fqdn(fqdn), dns.TypeCNAME) in, err := dns.Exchange(&msg, server) if err != nil { return fqdns, err } if len(in.Answer) < 1 { return fqdns, errors.New("no answer") } for _, answer := range in.Answer { if ans, ok := answer.(*dns.CNAME); ok { fqdns = append(fqdns, ans.Target) } } return fqdns, nil } func lookup(fqdn string) []result { var results []result var cfqdn = fqdn for { cnames, err := lookupCNAME(cfqdn) if err != nil && len(cnames) > 0 { cfqdn = cnames[0] continue } addrs, err := lookupA(cfqdn) if err != nil { break } for _, addr := range addrs { results = append(results, result{address: addr, hostname: fqdn}) } break } return results } type empty struct{} func worker(tracker chan empty, fqdns chan string, gather chan []result) { for fqdn := range fqdns { results := lookup(fqdn) if len(results) > 0 { fmt.Println(fqdn) gather <- results } } var e empty tracker <- e } func main() { var results []result fqdns := make(chan string, count) gather := make(chan []result) tracker := make(chan empty) // 打开字典文件 file, err := os.Open(wordlist) if err != nil { panic(err) } defer file.Close() scanner := bufio.NewScanner(file) // 调起count个goroutine for i := 0; i < count; i++ { go worker(tracker, fqdns, gather) } // 投递域名 for scanner.Scan() { fqdns <- fmt.Sprintf("%s.%s", scanner.Text(), domain) } // 合并所有结果 go func() { for result := range gather { results = append(results, result...) } var e empty tracker <- e }() close(fqdns) // 在所有worker完成之前 阻塞住主goroutine for i := 0; i < count; i++ { <-tracker } close(gather) <-tracker // 在合并完结果前 堵塞主goroutine save, _ := os.OpenFile("result.txt", os.O_CREATE|os.O_WRONLY, 0666) writer := tabwriter.NewWriter(save, 0, 8, 4, ' ', 0) for _, result := range results { fmt.Fprintf(writer, "%s\t%s\n", result.hostname, result.address) } writer.Flush() }
测试
$ ./main -d microsoft.com -w test.txt www.microsoft.com c2.microsoft.com mail1.microsoft.com mail.microsoft.com developer.microsoft.com help.microsoft.com email.microsoft.com map.microsoft.com note.microsoft.com linux.microsoft.com docs.microsoft.com login.microsoft.com mi.microsoft.com ...