GO-DNS-programming


需要了解的一些DNS相关的概念:

  • 完全限定域名(Fully Qualified Domain Name,FQDN), 即[hostname].[domain].[tld]
  • A记录,用来记录域名对应的 IP 地址,缺点是容易被发现服务器的真实 IP;
  • CNAME 记录,也叫别名记录,用来把域名解析到别的域名(或者说A记录)上,用于真实网站、项目运营;
  • TXT记录(利用点): 可任意填写,可为空。一般做一些验证记录时会使用此项,比如联系方式;
  • NS记录: 域名解析服务器记录,用来给子域名指定某个域名服务器,优先级高于A记录;
  • 其它还有AAAA, SOA, SRV, PTR记录等。

可以在linux上执行dig www.baidu.com命令试一下。

go内置包也提供了一些函数,比如LookupAddr。但内置包无法指定目标服务器,所以使用另一个第三方包:

  • https://pkg.go.dev/github.com/miekg/dns
  • 安装:go get github.com/miekg/dns

1. 查询A记录

package main

import (
	"fmt"
	"github.com/miekg/dns"
)

func main() {
	var msg dns.Msg
	fqdn := dns.Fqdn("stacktitan.com")
	msg.SetQuestion(fqdn, dns.TypeA)
	res, err := dns.Exchange(&msg, "8.8.8.8:53")	// Google DNS server
	if err != nil {
		panic(err)
	}
	if len(res.Answer) < 1 {
		fmt.Println("No records")
		return
	}
	for _, answer := range res.Answer {
		if a, ok := answer.(*dns.A); ok { // ok == true means answer is a type of (*dns.A)
			fmt.Println(a.A)
		}
	}
}

dns包定义了RR(Resource Record)接口

type RR interface {
	// Header returns the header of an resource record. The header contains
	// everything up to the rdata.
	Header() *RR_Header
	// String returns the text representation of the resource record.
	String() string
	// ...
}

RR是没法直接访问响应中的IP的,需要遍历响应并用断言(ok)来判断是否返回了A记录。

wireshark抓包看下:

Domain Name System (query)
    //...
    Queries
        stacktitan.com: type A, class IN
            Name: stacktitan.com
            [Name Length: 14]
            [Label Count: 2]
            Type: A (Host Address) (1)
            Class: IN (0x0001)

        
Domain Name System (response)
    //...
    Queries
    Answers
        stacktitan.com: type A, class IN, addr 34.212.50.84
            Name: stacktitan.com
            Type: A (Host Address) (1)
            Class: IN (0x0001)
            Time to live: 3600
            Data length: 4
            Address: 34.212.50.84

2. 猜解子域

lookup

将查询A记录中SetQuestion的TypeA改成TypeCNAME,就是查询CNAME。而CNAME可能是一个位置长度的链,所以要用死循环遍历,直到查询不到下一个CNAME为止。

func lookupA(strFQDN, strDnsAddr string) ([]string, error) {
	var m dns.Msg
	var strIPs []string
	m.SetQuestion(dns.Fqdn(strFQDN), dns.TypeA)
	res, err := dns.Exchange(&m, strDnsAddr)
	if err != nil {
		return strIPs, err
	}
	if len(res.Answer) < 1 {
		return strIPs, errors.New("no answer")
	}
	for _, answer := range res.Answer {
		if a, ok := answer.(*dns.A); ok {
			strIPs = append(strIPs, a.A.String())
		}
	}
	return strIPs, nil
}

func lookupCNAME(strFQDN, strDnsAddr string) ([]string, error) {
	var m dns.Msg
	var strCNAMEs []string
	m.SetQuestion(dns.Fqdn(strFQDN), dns.TypeCNAME)
	res, err := dns.Exchange(&m, strDnsAddr)
	if err != nil {
		return strCNAMEs, err
	}
	if len(res.Answer) < 1 {
		return strCNAMEs, errors.New("no answer")
	}
	for _, answer := range res.Answer {
		if c, ok := answer.(*dns.CNAME); ok {
			strCNAMEs = append(strCNAMEs, c.Target)
		}
	}
	return strCNAMEs, nil
}

// store the lookup result
type Result struct {
	IPAddress string
	Hostname  string
}

func lookup(strFQDN, strDnsAddr string) []Result {
	var results []Result
	var strFQDN_copy = strFQDN // Don't modify the original fqdn.
	for {
		strCNAMEs, err := lookupCNAME(strFQDN_copy, strDnsAddr)
		if err == nil && len(strCNAMEs) > 0 {
			strFQDN_copy = strCNAMEs[0]
			continue // We have to process the next CNAME.
		}
		strIPs, err := lookupA(strFQDN_copy, strDnsAddr)
		if err != nil {
			break // There are no A records for this hostname.
		}
		for _, ip := range strIPs {
			results = append(results, Result{IPAddress: ip, Hostname: strFQDN})
		}
		break // We have processed all the results.
	}
	return results
}

worker

工人线程涉及4个参数:

  1. chanTracker,负责线程同步的管道
  2. chanFQDNs,要查询的FQDN的管道
  3. chanResults,回传结果的管道
  4. serverAddr,DNS
type Empty struct{} // signal

// chanTracker: used for signal
func worker(chanTracker chan Empty, chanFQDNs chan string, chanResults chan []Result, serverAddr string) {
	for fqdn := range chanFQDNs {
		results := lookup(fqdn, serverAddr)
		if len(results) > 0 {
			chanResults <- results
		}
	}

	// signal
	var e Empty
	chanTracker <- e
}

main

大致分为6步:

  1. 解析命令参数,需要指定要查询的目标、字典文件、工人数量、dns ip;
  2. 申请工人池;
  3. 从字典读取子域名,给工人们发送要查询的FQDN;
  4. 从管道查收结构(单独的goroutine);
  5. 关闭管道(这里要注意下线程同步);
  6. 输出结果

一些依赖库:

  • 设置命令参数:https://golang.google.cn/pkg/flag/
  • 按行读取字典:https://golang.google.cn/pkg/bufio/#NewScanner
  • 按列输出结果:https://golang.google.cn/pkg/text/tabwriter/#NewWriter

源码:

func main() {
	// 0. parse cmdline params
	var (
		pStrFlagDomain    = flag.String("domain", "", "The domain to perform guessing against.")
		pStrFlagWordlist  = flag.String("wordlist", "", "The wordlist to use for guessing.")
		pnFlafWorkerCount = flag.Int("c", 100, "The amount of workers to use.")
		pStrFlagDnsAddr   = flag.String("server", "8.8.8.8:53", "The DNS server to use.")
	)
	flag.Parse()

	if *pStrFlagDomain == "" || *pStrFlagWordlist == "" {
		fmt.Println("-domain and -wordlist are required")
		os.Exit(1)
	}

	// 1. worker pool
	chanTracker := make(chan Empty)
	chanFQDNs := make(chan string, *pnFlafWorkerCount)
	chanResults := make(chan []Result)

	for i := 0; i < *pnFlafWorkerCount; i++ {
		go worker(chanTracker, chanFQDNs, chanResults, *pStrFlagDnsAddr)
	}

	// 2. send fqdn to worker pool
	fileWordList, err := os.Open(*pStrFlagWordlist)
	if err != nil {
		panic(err)
	}
	defer fileWordList.Close()
	scanner := bufio.NewScanner(fileWordList)
	for scanner.Scan() {
		chanFQDNs <- fmt.Sprintf("%s.%s", scanner.Text(), *pStrFlagDomain)
	}
	// Note: We could check scanner.Err() here.

	// 3. goroutine for receiving results from worker pool
	var results []Result
	go func() {
		for r := range chanResults {
			results = append(results, r...)
		}
		var e Empty
		chanTracker <- e
	}()

	// 4. close channels
	close(chanFQDNs)
	for i := 0; i < *pnFlafWorkerCount; i++ {
		<-chanTracker // recv and drop
	}
	close(chanResults)
	<-chanTracker // recv from goroutine processing results (step 3)
	close(chanTracker)

	// 5. output results
	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()
}

准备好一个域名字典(subdomains.txt):

ajax
buy
news
applications
sc
open
ra
ris
smtp
wallet
jp
ftp

开始执行:

> go run main.go -domain www.microsoft.com -wordlist subdomains.txt -c 5
news.microsoft.com            141.193.213.20
news.microsoft.com            141.193.213.21
ajax.microsoft.com            117.18.232.200
ris.microsoft.com             213.199.139.250
smtp.microsoft.com            131.107.115.212
smtp.microsoft.com            131.107.115.215
smtp.microsoft.com            131.107.115.214
smtp.microsoft.com            205.248.106.64
smtp.microsoft.com            205.248.106.30
smtp.microsoft.com            205.248.106.32
buy.microsoft.com             134.170.48.87
open.microsoft.com            40.113.200.201
jp.microsoft.com              134.170.185.46
jp.microsoft.com              134.170.188.221
ftp.microsoft.com             134.170.188.232
wallet.microsoft.com          40.118.131.126
applications.microsoft.com    20.190.144.141
applications.microsoft.com    40.126.16.161
applications.microsoft.com    40.126.16.162
applications.microsoft.com    20.190.144.142
applications.microsoft.com    20.190.144.143
ra.microsoft.com              131.107.98.31

3. DNS proxy

如果一个红队中的n个团队要测试n个网络,那也要建立n个cs teamserver。要解决的问题是如何多个团队共享一个端口并路由到各自的cs监听器。方案就是使用代理。

环境

ubuntu+ docker java(因为后面要运行cobalt strike),docker安装步骤这里就不赘述了,装完拉取java镜像:

sudo docker pull java

运行两个java容器,需要映射端口以及目录:

  • 5005x端口用来监听cs客户端的连接请求;
  • ubuntu的Downloads里有cobalt strike,映射到两个容器的/data路径下并执行。
$ sudo docker run --rm -it -p 2020:53 -p 50051:50050 -v ~/Downloads:/data java /bin/bash
root@f02ee435ac81:/data/cobaltstrike4.3# ./teamserver 172.17.0.2 starr

$ sudo docker run --rm -it -p 2021:53 -p 50052:50050 -v ~/Downloads:/data java /bin/bash
root@f02ee435ac81:/data/cobaltstrike4.3# ./teamserver 172.17.0.3 starr

ubuntu主机启动cs客户端,可以建立两个连接,分别连接至localhost的2020和2021端口即可连接容器里的teamserver。

ubuntu需要安装go,官网安装文档:

https://golang.google.cn/doc/install

实验的ip情况如下:

  • ubuntu:172.17.0.1/16,192.168.56.101/24,需要运行dns服务, 以及cs客户端;
  • docker container 1:172.17.0.2/16,运行cs teamserver;
  • docker container 2:172.17.0.3/16,运行cs teamserver;
  • windows:192.168.56.107/24

DNS server test

先编写一个返回A记录的DNS服务器,在ubuntu上进行测试。逻辑很简单,监听udp:53端口,将所有dns请求解析为127.0.0.1并返回。

创建项目:

$ go mod init GoTest/DnsServer
$ touch GoTest/DnsServer/main.go

源码

package main

import (
	"log"
	"net"

	"github.com/miekg/dns"
)

func main() {
	dns.HandleFunc(".", func(w dns.ResponseWriter, req *dns.Msg) { // dot means all requests
		var resp dns.Msg
		resp.SetReply(req) // create reply msg
		for _, q := range req.Question {
			a := dns.A{ //
				Hdr: dns.RR_Header{
					Name:   q.Name,
					Rrtype: dns.TypeA,
					Class:  dns.ClassINET,
					Ttl:    0,
				},
				A: net.ParseIP("127.0.0.1").To4(), // parse all request to 127.0.0.1
			}
			resp.Answer = append(resp.Answer, &a)
		}
		w.WriteMsg(&resp)
	})
	log.Fatal(dns.ListenAndServe(":53", "udp", nil))
}

执行:

$ go run GoTest/DnsServer/main.go
udp:53 : bind: address already in use

端口被占用,是因为systemd-resolve作为系统的dns服务正在运行,先将它kill掉:

$ netstat -unlp
...
udp	0	0	127.0.0.53:53	0.0.0.0:*	377/systemd-resolve
...
$ sudo systemctl disable --now systemd-resolved.service

用dig命令测试:

$ dig www.baidu.com
...
www.baidu.com	0	IN	A	127.0.0.1
...

在windows机器上将dns服务器改为192.168.56.101,再次测试:

C:\Windows\System32>nslookup www.baidu.com
服务器:  101.56.168.192.in-addr.arpa		逆向域名
Address:  192.168.56.101

非权威应答:
名称:    www.baidu.com
Addresses:  127.0.0.1
          127.0.0.1

main(问题遗留)

主函数为dns代理,基本逻辑就是构造Answers字段,可以回到[第一部分](#1. 查询A记录)看下抓包内容。

要实现的操作:

  1. 从数据库或配置文件里加载域名和ip端口的映射;
  2. 监听udp:53, 接收查询,从dns question中提取域名,通过映射找到上游服务器(容器);
  3. 把请求转发给上游服务器,把响应回复给目标机器。

本地配置文件proxy.config:

attacker1.com,127.0.0.1:2020
attacker2.com,127.0.0.1:2021

2020和2021端口,已经分别映射到了两个docker容器的53端口。

package main

import (
	"bufio"
	"fmt"
	"log"
	"os"
	"os/signal"
	"strings"
	"sync"
	"syscall"

	"github.com/miekg/dns"
)

// return domains and listening ip:port
func parse(filename string) (map[string]string, error) {
	fh, err := os.Open(filename)
	if err != nil {
		return nil, err
	}
	defer fh.Close()
	scanner := bufio.NewScanner(fh)
	records := make(map[string]string)
	for scanner.Scan() {
		line := scanner.Text()
		parts := strings.SplitN(line, ",", 2)
		if len(parts) < 2 {
			return records, fmt.Errorf("%s is not a valid line", line)
		}
		records[parts[0]] = parts[1]
	}
	log.Println("records set to:")
	for k, v := range records {
		fmt.Printf("%s -> %s\n", k, v)
	}
	return records, scanner.Err()
}

func main() {
	// 1. Load local proxy config
	records, err := parse("/home/starr/Documents/GoTest/DnsProxy/proxy.config")
	if err != nil {
		log.Fatalf("Error processing configuration file: %s\n", err.Error())
	}

	var recordLock sync.RWMutex //
	dns.HandleFunc(".", func(w dns.ResponseWriter, req *dns.Msg) {
		if len(req.Question) < 1 {
			dns.HandleFailed(w, req)
			return
		}

		// 2. get request domain and find the upstream server(cs teamserver)
		name := req.Question[0].Name
		parts := strings.Split(name, ".")
		if len(parts) > 1 {
			name = strings.Join(parts[len(parts)-2:], ".")
		}
		fmt.Printf("Get query domain:%s\n", name)

		recordLock.RLock()
		match, ok := records[name]
		recordLock.RUnlock()

		if !ok {
			dns.HandleFailed(w, req)
			return
		}
		fmt.Printf("	ip:%s\n", match)

        // 3. Request and get response from upstream server(docker) and write it back to target
		resp, err := dns.Exchange(req, match)
		if err != nil {
            fmt.Printf("	resp: %s\n", resp)
            log.panicln(err)
			dns.HandleFailed(w, req)
			return
		}
		if err := w.WriteMsg(resp); err != nil {
			dns.HandleFailed(w, req)
			return
		}
	})

	go func() {
		sigs := make(chan os.Signal, 1)
		signal.Notify(sigs, syscall.SIGABRT)

		for sig := range sigs {
			switch sig {
			case syscall.SIGUSR1:
				log.Println("SIGUSR1: reloading records")
				recordsUpdate, err := parse("proxy.config")
				if err != nil {
					log.Printf("Error processing configuration file: %s\n", err.Error())
				} else {
					recordLock.Lock()
					records = recordsUpdate
					recordLock.Unlock()
				}
			}
		}
	}()

	log.Fatal(dns.ListenAndServe(":53", "udp", nil))
}

不过在之后的测试中,发现cs payload请求中的req.Question[0].Name并不是所期望的域名。但这里即使在目标机器上执行nslookup attacker1.com,最终也会在exchange那里报错,,暂未解决

Get query domain1:aaa.stage.10868471.attacker1.com.
	parts:[aaa stage 10868471 attacker1 com ]
	name:com.
	ip:127.0.0.1:2020
	Exchange error

当要更新上游服务器的域名、ip映射时,可以重启代理,但也可以通过自定义信号(SIGUSR1 of linux)来热加载新的代理配置:

go func() {
    sigs := make(chan os.Signal, 1)
    signal.Notify(sigs, syscall.SIGUSR1)

    for sig := range sigs {
        switch sig {
            case syscall.SIGUSR1:
            log.Println("SIGUSR1: reloading records")
            recordsUpdate, err := parse("proxy.config")
            if err != nil {
                log.Printf("Error processing configuration file: %s\n", err.Error())
            } else {
                recordLock.Lock()
                records = recordsUpdate
                recordLock.Unlock()
            }
        }
    }
}()

CS监听器以及payload(问题遗留)

创建dns监听器,端口要填写80,最终的payload才是使用53端口。

原本DNS Beacon可以使用两种方式进行传输:

  • 使用HTTP来下载Payload,

  • 使用TXT记录来下载Payload

4.0以后的版本中,将只使用DNS TXT记录了。

之前已经启动了两个docker teamserver,这里以docker1为例,创建payload:

在这里插入图片描述

但实际测试抓包时,数据包question里的域名并不是attacker1.com,而是xxx.attacker1.com,这就比较尴尬了。。。篇幅有限,如果以后解决了再补充。

不过如果用ubuntu主机运行teamserver的话,是没有这个问题的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值