域名系统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子域名爆破的更多相关文章

  1. PJzhang:经典子域名爆破工具subdomainsbrute

    猫宁!!! 参考链接: https://www.waitalone.cn/subdomainsbrute.html https://www.secpulse.com/archives/5900.htm ...

  2. ubuntu进行子域名爆破

    好记性不如烂笔头,此处记录一下,ubuntu进行子域名的爆破. 先记录一个在线的子域名爆破网址,无意中发现,很不错的网址,界面很干净,作者也很用心,很感谢. https://phpinfo.me/do ...

  3. 子域名爆破&C段查询&调用Bing查询同IP网站

    在线子域名爆破 <?php function domainfuzz($domain) { $ip = gethostbyname($domain); preg_match("/\d+\ ...

  4. PJzhang:子域名爆破工具wydomain(猪猪侠)

    猫宁!!! 参考链接:https://www.secpulse.com/archives/53182.html https://www.jianshu.com/p/65c85f4b7698 http: ...

  5. golang 网络编程之如何正确关闭tcp连接以及管理它的生命周期

    欢迎访问我的个人网站获取更佳阅读排版 golang 网络编程之如何正确关闭tcp连接以及管理它的生命周期 | yoko blog (https://pengrl.com/p/47401/) 本篇文章部 ...

  6. 使用python处理子域名爆破工具subdomainsbrute结果txt

    近期学习了一段时间python,结合自己的安全从业经验,越来越感觉到安全测试是一个体力活.如果没有良好的coding能力去自动化的话,无疑会把安全测试效率变得很低. 作为安全测试而言,第一步往往要通过 ...

  7. 【网络编程】TCPIP-7-域名与网络地址

    目录 前言 7. 域名与网络地址 7.1 IP 7.2 域名 7.3 DNS 7.4 IP地址与域名之间的转换 7.4.1 利用域名获取IP地址 7.4.2 利用IP地址获取域名 7.4.3 升级版的 ...

  8. 子域名爆破工具:OneForALL

    0x00 简介 OneForAll是一款功能强大的子域收集工具 0x01 下载地址 码云: https://gitee.com/shmilylty/OneForAll.git Github: http ...

  9. golang网络编程高并发

    1 golang写服务器不需要epoll吗 golang写服务器不需要在用reactor模式的epoll了,因为golang的协程非常廉价,可以并发开启成千上完个协程. 一个协程占用内存大概2KB左右 ...

  10. 无状态子域名爆破工具:ksubdomain

    概述 开源地址:https://github.com/knownsec/ksubdomain 二进制文件下载:https://github.com/knownsec/ksubdomain/releas ...

随机推荐

  1. url not set

    UrI not set 原因与处理方法 今天下午跑代码时发现,上午能跑的代码下午跑不了了.一直报 Url not set错误. 出现这个问题的主要原因,是因为代码中的@ConfigurationPro ...

  2. linux-usermod

    添加组成员 sudo usermod -aG <targetgroup> <selfuser>

  3. SQL Server之Cross apply

    1 --这样是不行的 2 select sys.dm_exec_sql_text(most_recent_sql_handle) from sys.dm_exec_connections 3 4 -- ...

  4. win10自带录屏为什么录两个小时自动关闭?如何调节使其可以时间更长?

    Windows设置->游戏->屏幕截图->录制时间: https://www.zhihu.com/question/404390297

  5. Ubuntu解决无法远程连接

    检查SSH是否安装 ssh localhost 如果没有安装,通过APT的命令安装 sudo apt install openssh-server 无法连接Ubuntu中的root用户 其他用户可以连 ...

  6. pytest之运行环境

    简介 pytest是Python最流程化的单元测试框架,它具有允许直接使用assert进行断言,而不需要使用self.assert*:可以自动寻找单测文件.类和函数,还可支持执行部分用例:Modula ...

  7. 转载C#文件下载的实现

    一.//TransmitFile实现下载     protected void Button1_Click(object sender, EventArgs e)    {        /*     ...

  8. 经典面试题:UDP和TCP的区别?

    相信测试这行的同道朋友们,经常会被问到这个问题,这里我用自己的语言总结了几点: UDP 和 TCP的区别: 连接方面:tcp面向连接,三次握手,四次挥手 udp无连接,即发送数据之前不需要建立连接 安 ...

  9. URule规则引擎

    没有规则,不成方圆: 一.背景 前段时间,在做项目重构的时候,遇到很多地方需要做很多的条件判断.当然可以用很多的if-else判断去解决,但是当时也不清楚怎么回事,就想玩点别的.于是乎,就去调研了规则 ...

  10. ElasticSearch 实现分词全文检索 - 高亮查询

    目录 ElasticSearch 实现分词全文检索 - 概述 ElasticSearch 实现分词全文检索 - ES.Kibana.IK安装 ElasticSearch 实现分词全文检索 - Rest ...