Golang网络编程: DNS子域名爆破

2022/5/27 1:22:38

本文主要是介绍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
...


这篇关于Golang网络编程: DNS子域名爆破的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程