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
...