1. 程式人生 > 其它 >Golang網路程式設計: DNS子域名爆破

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