Golang网络编程: DNS子域名爆破
作者:互联网
域名系统(Domain Name System,缩写:DNS)是互联网的一项服务。它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问互联网。这就如同一个地址簿,根据域名来指向IP地址。
实现DNS客户端
使用第三方包 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
检索A记录
要得知主机在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记录
将创建两个函数执行查询,其中一个用于查询A记录,另一个用于查询CNAME记录。这两个函数均接收FQDN作为第一个参数,并接收DNS服务器地址作为第二个参数,每个函数都应返回一个字符串切片和一个错误。
查找A记录
如下函数负责查找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记录
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
...
标签:baidu,DNS,err,编程,fqdn,Golang,dns,com,string 来源: https://www.cnblogs.com/N3ptune/p/16311719.html