我们在渗透测试的时候,DNS是我们不可或缺的,我们对于许多漏洞的探测都需要使用到DNSlog。而且DNS还能用来帮助我们隐藏CS或者信息收集时收集子域名信息等等。所以本次我们就来完成一个DNS的子域名爆破器。
首先我们还是要完成一些基础内容的学习。Go的内置包net包提供了强大的功能,可以让我们完成大多数的DNS类型记录请求。但是使用该包的缺点是我们无法指定DNS服务器,这给我们造成了很大的不便。不过该包会使用操作系统上配置的解析器。其另一个缺点便是无法对结果进行深入的检查。为了解决这个问题,我们将使用一个优秀的第三方包,,它高度模块化,是我们的首选DNS包。
go get github.com/miekg/dns
首先我们来解析DNS中的A记录,该记录了域名对应的IP地址,在我们的子域名爆破中,我们只需要查看该域名是否能解析到IP地址,如果能解析到,则说明存在该子域名,反之则不存在。
package mainimport (
"github.com/miekg/dns"
)
func main() {
var msg dns.Msg
fqdn := dns.Fqdn("stacktitan.com")
msg.SetQuestion(fqdn, dns.TypeA)
dns.Exchange(&msg, "8.8.8.8:53")
}
首先,我们引入刚刚创建的包,然后创建一个新的Msg,调用函数fqdn(string),这个中的string就是我们要查找的域名,我们使用该函数将其转换为可以与DNS服务器通信的FQDN,然后使用函数SetQuestion(string,uint16)修改Msg的内部状态,也就是在这里设定我们要对该域名查询的记录类型。最后,调用Exchange(*Msg,string)来将消息发送给我们设置的DNS服务器地址。至此,我们就完成了一个简单的DNS客户端,向谷歌的DNS服务器请求了一个域名。当然,我们还未对结果的返回进行处理,也就无法判断是否查询成功,这些我们在后面的代码中将会完成。我们现在先来看一下流量,可以发现成功向DNS服务器发送了域名的请求并得到了返回的结果。
tcpdump -i en0 -n udp port 53
从Exchange函数,也就是我们的Dns服务器的返回值为*Msg,err。返回的错误类型是可以接受的,而且在go的习惯用语中很常见。但是我们传入的也是*Msg,那么为什么返回的也是Msg呢,我们可以看10-16行的结构体,我们可以发现Msg结构体其中包含了请求,也包含了应答。这样我们可以将所有的DNS请求及相应整合在一个结构体中。且结构体Msg拥有许多使数据处理起来很容易的方法。比如使用SetQuestion()切片Question。也可以使用方法append()直接修改切片,并获得相同的结果。切片Answer保存对查询的响应,其类型为RR。
package mainimport (
"fmt"
"github.com/miekg/dns"
)
/*
type Msg struct {
Msghdr
Compress bool `json:"-"` // 如果消息为true,消息将被压缩
Question []Question // 保留question部分的RR
Answer []RR // 保留answer部分的RR
Ns []RR // 保留authority部分的RR
Extra []RR // 保留addittional部分的RR
}
*/
func main() {
var msg dns.Msg
fqdn := dns.Fqdn("image.haochen1204.com")
msg.SetQuestion(fqdn, dns.TypeA)
in, err := dns.Exchange(&msg, "8.8.8.8:53")
if err != nil {
panic(err)
}
if len(in.Answer) < 1 {
fmt.Println("No records")
return
}
for _, answer := range in.Answer {
if a, ok := answer.(*dns.A); ok {
fmt.Println(a.A)
}
}
}
如上述代码,在刚刚的代码基础上增加了应答,首先接受DNS服务器的响应结果,然后检查是否存在错误,如果存在错误则使用panic()函数停止程序,使用该函数可以快速的查看堆栈跟踪并确定错误发生的位置。然后确认切片Answer的长度至少为1,如果不是1,则说明记录没有立即返回,因为在某些情况下会存在域名无法解析的情况。
而类型RR是一个仅有两个方法的接口,我们的相应内容就是在这个接口中,但是我们却不能直接通过这个接口获得我们想要解析的IP地址。所以我们需要首先声明来将数据创建为我们需要的类型实例。我们首先遍历所有应答,然后对应答执行断言类型,来保证我们正在处理的是类型dns.A。在这里,我们会接受两个值,一个是断言的值,一个是表示断言是否成功的布尔值,检查断言是否成功后,打印存储在a.A中的IP地址。
然后,我们来一步步完成子域名枚举的工具,首先我们要知道,我们都需要哪些参数来启动我们的程序,比如目标的根域名、字典的文件名、DNS服务器的地址以及需要的线程数量。我们可以使用flag包来像python那样,使用-xxx xxx来从命令行获取用户输入的参数。
package mainimport (
"flag"
"fmt"
"os"
)
func main() {
var (
flDomain = flag.String("Domain", "", "The domain to perform guessing against.")
flWordlist = flag.String("Wordlist", "", "The wordlist to use for guessing.")
flWorkerCount = flag.Int("c", 100, "The amount of workers to use.")
flServerAddr = flag.String("server", "8.8.8.8:53", "The DNS server to use.")
)
flag.Parse()
if *flDomain == "" || *flWordlist == "" {
fmt.Println("-domain and -wordlist are required")
os.Exit(1)
}
fmt.Println(*flWorkerCount, *flServerAddr)
}
首先我们进行第一参数的步骤,在这里使用flag.xxx来从命令行接收用户输入的参数,而这里的xxx代表我们要接受的数据的数据类型。然后flag中的第一个参数为我们用户输入的参数名,-或者--都是可以识别的,第二个参数为我们的默认参数,第三个参数为说明,当我们输入-h时,所有的flag的第三个参数都会被按照一定的格式打印出来。需要注意的是,flag的返回值都是地址,所以我们需要用他们的值的时候都需要加上*。然后我们在下面判断用户是否输入了必须参数,要枚举的域名和使用的字典,如果没有则提示用户输入并且结束程序。
然后我们需要创建2个函数来执行查询,一个用来查询A记录,一个用来查询CHANME记录。两个函数均接受FQDN作为第一个参数,并接受DNS服务器的地址作为第二个参数。每个函数都返回一个字符串切片和一个错误。
package mainimport (
"errors"
"flag"
"fmt"
"os"
"github.com/miekg/dns"
)
type result struct {
IPAddress string
Hostname string
}
func lookupA(fqdn, serverAddr string) ([]string, error) {
var m dns.Msg
var ips []string
m.SetQuestion(dns.Fqdn(fqdn), dns.TypeA)
in, err := dns.Exchange(&m, serverAddr)
if err != nil {
return ips, err
}
if len(in.Answer) < 1 {
return ips, errors.New("no answer")
}
for _, answer := range in.Answer {
if a, ok := answer.(*dns.A); ok {
ips = append(ips, a.A.String())
}
}
return ips, nil
}
func lookupCNAME(fqdn, serverAddr string) ([]string, error) {
var m dns.Msg
var fqdns []string
m.SetQuestion(dns.Fqdn(fqdn), dns.TypeCNAME)
in, err := dns.Exchange(&m, serverAddr)
if err != nil {
return fqdns, err
}
if len(in.Answer) < 1 {
return fqdns, errors.New("no answer")
}
for _, answer := range in.Answer {
if c, ok := answer.(*dns.CNAME); ok {
fqdns = append(fqdns, c.Target)
}
}
return fqdns, nil
}
func lookup(fqdn, serverAddr string) []result {
var results []result
var cfqdn = fqdn //请勿修改原始信息
for {
cnames, err := lookupCNAME(cfqdn, serverAddr)
if err == nil && len(cnames) > 0 {
cfqdn = cnames[0]
continue
}
ips, err := lookupA(cfqdn, serverAddr)
if err != nil {
break // 该主机名没有DNS
}
for _, ip := range ips {
results = append(results, result{IPAddress: ip, Hostname: fqdn})
}
break // 已经处理了所有结果
}
return results
}
func main() {
var (
flDomain = flag.String("Domain", "", "The domain to perform guessing against.")
flWordlist = flag.String("Wordlist", "", "The wordlist to use for guessing.")
flWorkerCount = flag.Int("c", 100, "The amount of workers to use.")
flServerAddr = flag.String("server", "8.8.8.8:53", "The DNS server to use.")
)
flag.Parse()
if *flDomain == "" || *flWordlist == "" {
fmt.Println("-domain and -wordlist are required")
os.Exit(1)
}
fmt.Println(*flWorkerCount, *flServerAddr)
}
我们将lookupA和lookupCNAME两个函数加入到上面写好的代码中。我们可以看到这两个函数十分熟悉,都是通过用户输入的fqdn,设定查询的类型然后就进行查询,判断输出的内容是否查询成功,如果查询成功则通过for循环获取查询的记过,并将结果返回。因为我们一个域名是可以解析到多个IP的,所以这里要使用for循环循环读取查询到的ip,并且加入到字符串列表中进行返回。
除此之外,我们还要创建一个result结构体,该结构体用来存放域名与ip的对应数据。然后我们来创建一个lookup函数用来处理域名和查询到的ip信息。在这之前,我们需要知道什么是CNAME类型。我们的域名不仅仅可以解析成一个ip,还是可以解析成域名的,比如我们将a.haochen1204.com解析成b.haochen1204.com再将b.haochen1204.com解析成c.haocen1204.com,这是一个解析链,所以我们需要查询的a.haochen1204.com的ip,实际上存储在c.haochen1204.com的A记录中。
在lookup函数中,我们首先也是接受了用户要枚举的域名和DNS服务器地址。然后创建一个用来存储结果的结构体,用来将我们查询到的域名和他的(可能是多个)ip对应存放。然后进入一个死循环,首先不断的去查询目标的CNAME字段,直到查询的结果为空,则证明我们查询到了链中的最后一个域名,比如我们上面链中c.haochen1204.com,然后去查找它的A记录,将查询到的结果与我们输入的域名对应起来。比如a.haochen1204.com -> b.haochen1204.com -> c.haochen1204.com 而c.haochen1204.com解析的ip为1.1.1.1和2.2.2.2,那么则存放a.haochen1204.com -> 1.1.1.1和a.haochen1204.com -> 2.2.2.2。最后,将我们的结果返回。
然后我们来创建一个线程池,用来不断启动我们刚刚完成的lookup函数。我们在python中,使用多线程有个很头疼的问题,就是无法获取线程的返回值,而在go中,我们可以通过管道解决这个问题,顺便解决了子线程未运行完,主线程便结束的问题。
package mainimport (
"errors"
"flag"
"fmt"
"os"
"github.com/miekg/dns"
)
type result struct {
IPAddress string
Hostname string
}
type empty struct{}
func lookupA(fqdn, serverAddr string) ([]string, error) {
var m dns.Msg
var ips []string
m.SetQuestion(dns.Fqdn(fqdn), dns.TypeA)
in, err := dns.Exchange(&m, serverAddr)
if err != nil {
return ips, err
}
if len(in.Answer) < 1 {
return ips, errors.New("no answer")
}
for _, answer := range in.Answer {
if a, ok := answer.(*dns.A); ok {
ips = append(ips, a.A.String())
}
}
return ips, nil
}
func lookupCNAME(fqdn, serverAddr string) ([]string, error) {
var m dns.Msg
var fqdns []string
m.SetQuestion(dns.Fqdn(fqdn), dns.TypeCNAME)
in, err := dns.Exchange(&m, serverAddr)
if err != nil {
return fqdns, err
}
if len(in.Answer) < 1 {
return fqdns, errors.New("no answer")
}
for _, answer := range in.Answer {
if c, ok := answer.(*dns.CNAME); ok {
fqdns = append(fqdns, c.Target)
}
}
return fqdns, nil
}
func lookup(fqdn, serverAddr string) []result {
var results []result
var cfqdn = fqdn //请勿修改原始信息
for {
cnames, err := lookupCNAME(cfqdn, serverAddr)
if err == nil && len(cnames) > 0 {
cfqdn = cnames[0]
continue
}
ips, err := lookupA(cfqdn, serverAddr)
if err != nil {
break // 该主机名没有DNS
}
for _, ip := range ips {
results = append(results, result{IPAddress: ip, Hostname: fqdn})
}
break // 已经处理了所有结果
}
return results
}
func worker(tracker chan empty, fqdns chan string, gather chan []result, serverAddr string) {
for fqdn := range fqdns {
results := lookup(fqdn, serverAddr)
if len(results) > 0 {
gather <- results
}
}
var e empty
tracker <- e
}
func main() {
var (
flDomain = flag.String("Domain", "", "The domain to perform guessing against.")
flWordlist = flag.String("Wordlist", "", "The wordlist to use for guessing.")
flWorkerCount = flag.Int("c", 100, "The amount of workers to use.")
flServerAddr = flag.String("server", "8.8.8.8:53", "The DNS server to use.")
resultes []result
)
// make 创建内存并分配地址
fqdns := make(chan string, *flWorkerCount)
gather := make(chan []result)
tracker := make(chan empty)
}
如上述代码,我们创建了一个空的结构体,他用来跟踪线程的执行情况,判断其是否执行完成,为什么使用空的结构体呢,因为他的大小是0b,而我们也不需要他存储什么数据。然后我们创建work函数,首先需要用户输入的域名信息和DNS服务器地址信息。然后还需要传入三个管道,第一个管道是存放了用户要枚举的子域名数据,该线程会循环才能够该管道中读取子域名进行枚举,每次执行完一次枚举结果,便会用第二个管道来进行收集函数的执行结果(82行),最后一个管道用来判断线程的运行情况(86行),当该线程运行结束后,也就是从fqdn管道中提取不到任何子域名时,便会向管道存入一个空值。这样,在主函数中通过接受该管道的值遍可判断执行情况。
然后我们在主函数中需要添加一些内容,创建我们需要的3个管道以及我们接受最终结果的result结构体列表。通过用户提供的线程数量,将fqdns管道创建为缓冲管道,剩下的管道均为一次只能通过一个数据的管道,如果这个数据阻塞,则剩下的也会阻塞。
然后我们要从字典中读取用户要枚举的子域名,我们可以使用bufio包创建一个sacnner,该文本扫描器允许我们一次一行的读取文件。
package mainimport (
"bufio"
"errors"
"flag"
"fmt"
"os"
"github.com/miekg/dns"
)
type result struct {
IPAddress string
Hostname string
}
type empty struct{}
func lookupA(fqdn, serverAddr string) ([]string, error) {
var m dns.Msg
var ips []string
m.SetQuestion(dns.Fqdn(fqdn), dns.TypeA)
in, err := dns.Exchange(&m, serverAddr)
if err != nil {
return ips, err
}
if len(in.Answer) < 1 {
return ips, errors.New("no answer")
}
for _, answer := range in.Answer {
if a, ok := answer.(*dns.A); ok {
ips = append(ips, a.A.String())
}
}
return ips, nil
}
func lookupCNAME(fqdn, serverAddr string) ([]string, error) {
var m dns.Msg
var fqdns []string
m.SetQuestion(dns.Fqdn(fqdn), dns.TypeCNAME)
in, err := dns.Exchange(&m, serverAddr)
if err != nil {
return fqdns, err
}
if len(in.Answer) < 1 {
return fqdns, errors.New("no answer")
}
for _, answer := range in.Answer {
if c, ok := answer.(*dns.CNAME); ok {
fqdns = append(fqdns, c.Target)
}
}
return fqdns, nil
}
func lookup(fqdn, serverAddr string) []result {
var results []result
var cfqdn = fqdn //请勿修改原始信息
for {
cnames, err := lookupCNAME(cfqdn, serverAddr)
if err == nil && len(cnames) > 0 {
cfqdn = cnames[0]
continue
}
ips, err := lookupA(cfqdn, serverAddr)
if err != nil {
break // 该主机名没有DNS
}
for _, ip := range ips {
results = append(results, result{IPAddress: ip, Hostname: fqdn})
}
break // 已经处理了所有结果
}
return results
}
func worker(tracker chan empty, fqdns chan string, gather chan []result, serverAddr string) {
for fqdn := range fqdns {
results := lookup(fqdn, serverAddr)
if len(results) > 0 {
gather <- results
}
}
var e empty
tracker <- e
}
func main() {
var (
flDomain = flag.String("Domain", "", "The domain to perform guessing against.")
flWordlist = flag.String("Wordlist", "", "The wordlist to use for guessing.")
flWorkerCount = flag.Int("c", 100, "The amount of workers to use.")
flServerAddr = flag.String("server", "8.8.8.8:53", "The DNS server to use.")
resultes []result
)
flag.Parse()
if *flDomain == "" || *flWordlist == "" {
fmt.Println("-domain and -wordlist are required")
os.Exit(1)
}
// make 创建内存并分配地址
fqdns := make(chan string, *flWorkerCount)
gather := make(chan []result)
tracker := make(chan empty)
// 创建一个新的scanner
fh, err := os.Open(*flWordlist)
if err != nil {
panic(err)
}
defer fh.Close()
scanner := bufio.NewScanner(fh)
// 启动工人函数
for i := 0; i < *flWorkerCount; i++ {
go worker(tracker, fqdns, gather, *flServerAddr)
}
for scanner.Scan() {
fqdns <- fmt.Sprintf("%s.%s", scanner.Text(), *flDomain)
}
}
我们首先打开文件,然后创建扫描器。根据用户输入的线程数量,启动线程,此时我们的worker函数会等待fqdns管道传入数据,但是现在这个管道中并没有数据,所以我们在下面创建一个循环,依次利用上面的扫描器从文本中读取的子域名,并将其与根域名拼接起来发送给我们的管道。这样,我们在不断的向管道中发送数据的时候,已经运行的管道也会同时不断接收数据并启动开始运行。
package mainimport (
"bufio"
"errors"
"flag"
"fmt"
"os"
"text/tabwriter"
"github.com/miekg/dns"
)
type result struct {
IPAddress string
Hostname string
}
type empty struct{}
func lookupA(fqdn, serverAddr string) ([]string, error) {
var m dns.Msg
var ips []string
m.SetQuestion(dns.Fqdn(fqdn), dns.TypeA)
in, err := dns.Exchange(&m, serverAddr)
if err != nil {
return ips, err
}
if len(in.Answer) < 1 {
return ips, errors.New("no answer")
}
for _, answer := range in.Answer {
if a, ok := answer.(*dns.A); ok {
ips = append(ips, a.A.String())
}
}
return ips, nil
}
func lookupCNAME(fqdn, serverAddr string) ([]string, error) {
var m dns.Msg
var fqdns []string
m.SetQuestion(dns.Fqdn(fqdn), dns.TypeCNAME)
in, err := dns.Exchange(&m, serverAddr)
if err != nil {
return fqdns, err
}
if len(in.Answer) < 1 {
return fqdns, errors.New("no answer")
}
for _, answer := range in.Answer {
if c, ok := answer.(*dns.CNAME); ok {
fqdns = append(fqdns, c.Target)
}
}
return fqdns, nil
}
func lookup(fqdn, serverAddr string) []result {
var results []result
var cfqdn = fqdn //请勿修改原始信息
for {
cnames, err := lookupCNAME(cfqdn, serverAddr)
if err == nil && len(cnames) > 0 {
cfqdn = cnames[0]
continue
}
ips, err := lookupA(cfqdn, serverAddr)
if err != nil {
break // 该主机名没有DNS
}
for _, ip := range ips {
results = append(results, result{IPAddress: ip, Hostname: fqdn})
}
break // 已经处理了所有结果
}
return results
}
func worker(tracker chan empty, fqdns chan string, gather chan []result, serverAddr string) {
for fqdn := range fqdns {
results := lookup(fqdn, serverAddr)
if len(results) > 0 {
gather <- results
}
}
var e empty
tracker <- e
}
func main() {
var (
flDomain = flag.String("Domain", "", "The domain to perform guessing against.")
flWordlist = flag.String("Wordlist", "", "The wordlist to use for guessing.")
flWorkerCount = flag.Int("c", 100, "The amount of workers to use.")
flServerAddr = flag.String("server", "8.8.8.8:53", "The DNS server to use.")
results []result
)
flag.Parse()
if *flDomain == "" || *flWordlist == "" {
fmt.Println("-domain and -wordlist are required")
os.Exit(1)
}
// make 创建内存并分配地址
fqdns := make(chan string, *flWorkerCount)
gather := make(chan []result)
tracker := make(chan empty)
// 创建一个新的scanner
fh, err := os.Open(*flWordlist)
if err != nil {
panic(err)
}
defer fh.Close()
scanner := bufio.NewScanner(fh)
// 启动工人函数
for i := 0; i < *flWorkerCount; i++ {
go worker(tracker, fqdns, gather, *flServerAddr)
}
for scanner.Scan() {
fqdns <- fmt.Sprintf("%s.%s", scanner.Text(), *flDomain)
}
// 读取结果
go func() {
for r := range gather {
results = append(results, r...)
}
var e empty
tracker <- e
}()
// 关闭通道并显示结果
close(fqdns)
for i := 0; i < *flWorkerCount; i++ {
<-tracker
}
close(gather)
<-tracker
// 打印结果
w := tabwriter.NewWriter(os.Stdout, 0, 8, 4, ' ', 0)
for _, r := range results {
fmt.Fprintf(w, "%s\t%s\n", r.Hostname, r.IPAddress)
}
w.Flush()
}
然后我们在128行创建了一个线程,这个线程用来从线程的结果中读取执行的结果。我们这个线程启动时,我们会同时运行2种线程,一种是爆破的线程,会有多个,一种是接收结果的线程,只有一个,还有一个主线程,此时主线程在干嘛呢,首先都运行到这了,会等待我们的子域名在管道中全部被接收,我们在上面的循环中只是将这些子域名放到了管道中,并不意味着所有子域名都被接收了,所以这里还需要等待所有的子域名都被接收才能关闭管道。然后我们回到我们剩下的2个线程会不断运行,当fqdns管道中没有数据时,意味着线程们执行完当前的任务可以陆续结束了,也就是会通过tarcker管道发送一个空值,而此时主线程已经运行到了138行的循环中,会一次从管道中把空值取出,我们开启了多少个线程,就取多少次,等取完了,也就证明子线程都结束了,这时就可以关闭接收机过的管道了。我们遍可以对结果进行打印了。这里用到了tobwriter库,他可以让我们快速方便的按格式打印出我们想要的结果。我们循环将结果一行一行写入创建的NewWriter,最后进行刷新,即可按照一定的格式进行显示。
其实在这点内容学了一半的时候,我便也自己完成了一个DNS的子域名爆破器,但是原书作者的代码真的让我感到惊艳,尤其是关于管道的应用,通过管道,让子线程们就像一个公司的人一样工作,比如我们有100个子线程,那么就像100个人,9点上班,大家9点前陆续到位(依次开启线程)。然后9点,所有人准时到齐开始上班,打开一个总的文档(fqdns),老板一次将今天的任务放上去(123-125行),而大家从其中领取任务不断完成,每完成一个任务就将任务的结果给迟到的人力(老板吧任务都放到共享文档后才来(子域名全部放到管道中后才来,128行))。当老板发现,共享文档中没任务了(fqdns管道中没子域名了)遍把这个共享文档关了,员工发现,文档中没任务了或者文档干脆直接被关了,干完手上的任务与人力交接后,遍跟老板打卡下班。直到最后一个员工干完了活,与人力交接完成(人力并不知道是最后一员工,仍在等待下一个人的结果)。该员工到老板那打卡下班后,老板发现100个人走完了,便过去给人力电脑关机(关闭gather管道),人力知道没活了,便也打卡下班。留老板最后把结果打印出来。
星 球 免 费 福 利
转发公众号本文到朋友圈
截图到公众号后台第1、3、5名获取免费进入星球
星球的最近主题和星球内部工具一些展示 欢 迎 加 入 星 球 !
关 注 有 礼
关注下方公众号回复“666”可以领取一套精品渗透测试工具集和百度云视频链接。 还在等什么?赶紧点击下方名片关注学习吧!
推荐阅读
免责声明 由于传播、利用本公众号渗透安全团队所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,公众号渗透安全团队及作者不为此承担任何责任,一旦造成后果请自行承担!如有侵权烦请告知,我们会立即删除并致歉。谢谢!