域名系统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. Python测试——安装篇总结

    测试用到的工具自己知道的有: 写脚本类:sublime  ,PyCharm,Eclipse+PyDev,目前我知道的只有这么多,大家知道的肯定还有很多,欢迎留言哈 录制脚本类:火狐自带的seleniu ...

  2. 港湾云服务器 香港新世界 节点 centos7.7 64 部署java项目

    开通云服务器 使用Xshell远程登录 XFTP连接服务器查看文件(这个步骤可有可无) yum安装jdk 在linux上使用yum安装是非常粗暴无脑的,但仍然有需要注意的点,不然会掉坑里.这里说一下步 ...

  3. 基于Dijkstra算法的郑州地铁路径规划

    需要引入geopy库 pip install geopy 安装即可 import requests from bs4 import BeautifulSoup import pandas as pd ...

  4. MySQL数据库封锁机制和事务隔离级别

    参考: 数据库技术:MySql学习笔记之事务隔离级别详解 详解MySQL 数据库隔离级别与MVCC MySQL 事务&&锁机制&&MVCC 数据库系统原理 - MySQ ...

  5. 实践课:i至诚app案例分析---江洁兰

    这个作业属于哪个课程 至诚软工实践F班 这个作业要求在哪里 作业要求 这个作业的目标 分析产品软件,找出其中的问题并进行分析,提高对产品软件bug方面的认识 学号 212106715 第一部分 找Bu ...

  6. 大数据组件对应Ranger插件的选择

    在都是开源组件的前提下,一般需要我们多关注到组件和插件的版本和类型选择. 参考 https://zhuanlan.zhihu.com/p/370263573 https://www.bookstack ...

  7. Windows右键新建菜单中添加md文件

    1.win+r------->regedit 2.找到.md文件夹,双击该文件夹后修改数据的值为".md" 3.在.md文件夹右键新建"项"------& ...

  8. springboot + mybatisplus出现was not registered for synchronization because synchronization is not active

    原因一:缺少事务注解,底层mybatisplus的接口方法有事务 原因二:该服务器被限制访问要连接的数据库 原因三:乐观锁失效 乐观锁由@version注解标注,有以下使用要求 支持的数据类型只有:i ...

  9. MySql数据库的两大引擎InnoDB和MyIsam的区别

    事务方面 InnoDB支持事务,MyISAM不支持事务.MySql的默认存储引擎为InnoDB 外键方面 InnoDB支持外键,MyISAM不支持,对一个包含外键的InnoDB表转为MYISAM会失败 ...

  10. 在Vim编辑器中查找选定文本

    按照任意选定文本查找: 1. 进入visiual模式选定文本, 按 y 键复制: 2. 按 / 键进入查找模式: 3. 按 Ctrl + r 打开vim寄存器: 4. 按 " 键将寄存器内容 ...