域名系统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. openvas在centos中扫描单项的python实现

    使用gvm_cli命令来实现 先创建一个空的配置 copy_id = '085569ce-73ed-11df-83c3-002264764cea' new_config = ''' <creat ...

  2. css scoped和moudle

    scoped css 官方文档 缺点 一.如果用户在别处定义了相同的类名,也许还是会影响到组件的样式. 二.根据css样式优先级的特性,scoped这种处理会造成每个样式的权重加重了: 即理论上我们要 ...

  3. jmeter 数据库连接

    位置:右击添加>配置元件>JDBC Connection Configuration 作用:需要对数据库发起请求查询数据或者对数据库施加压力 3.设置说明 Variable Name(变量 ...

  4. Jenkins系列(1)-离线安装插件

    插件地址:http://updates.jenkins-ci.org/download/plugins/

  5. RPC方式调用远程webservice接口

    /** * 可调整调用方法与命名空间的请求 * @param wsMethod 方法名 * @param bodyMessage json请求体.toString() * @return JSONOb ...

  6. ASP.NET实现前台调用后台变量或者方法

    前台页面 <div> <%= Name %> </div> <div> <%= getName() %> </div> 后台代码 ...

  7. eclipse微服务续,Hystrix+Gateway+Config配置管理中心+Bus动态刷新配置

    Hystrix延迟和容错库 Gateway微服务网关 Config配置管理中心 Bus动态刷新配置 四.Hystrix延迟和容错库 SpringCloud默认已为Feign整合了hystrix,所以添 ...

  8. vue中小写数字转大写汉字

    numTocoggle(money){ //汉字的数字 var cnNums = new Array('零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖') ...

  9. UIPath踩坑记一UIpath中抓取数据后在tableau中无表头

    UIpath抓取数据存在Excel中(Excel 应用程序范围),且已设置表头,但是放到tableau中无表头 更换为"写入范围(工作簿)",同时属性设置必须勾选"添加标 ...

  10. 三艾云 Kubernetes 集群最佳实践

    三艾云 Kubernetes 集群最佳实践 三艾云 Kubernetes 集群最佳实践 容器是 Cloud Native 的基石,它们之间的关系不言而喻.了解容器对于学习 Cloud Native 也 ...