扫描器开发进阶--代理基础

Socks代理服务器

本质

Socks代理服务器的本质

如果A想通过Socks服务器访问B,那么首先去连接Socks服务器的监听端口,并将B的ip地址和想连接的端口发送给Socks服务器,然后Socks服务器用拿到的IP和端口与B建立连接。到此为止A和Socks服务器之间时Socks连接,Socks服务器与B之间是TCP连接

现在Socks的认证和建立连接算是完成了,目前Socks服务器与A和B都存在一个连接

接下来Socks服务器要做的就是做内容转发,内容转发的时候,A与Socks服务器之间又变成普通的TCP连接

假设现在A和B要完成一个Hello和Hi的对话

经过之前的一番操作,A和代理服务器已经建立上连接,并且已经告诉了代理服务器要个B对话

现在A只需要将Hello发送给Socks服务器即可

Socks服务器接收到A发来的消息并将消息写入与B的连接中

B接收到Hello只会任务是代理服务器跟他说的(连接是Socks服务器与他建立的)所以对Socks服务器返回消息Hi

代理服务器接收到Hi,再将Hi返回给A

与帮人传话类似,唯一的区别就是,A知道要跟B通信,所以告诉了Socks我要和B通信,而B却只知道Socks服务器跟他说了Hello,所以回应了Socks服务器,却不知道实际上是A想要跟他说话

这个场景和之前的电话接线员基本相同,A给B打电话,先打给接线员,然后告诉接线员我要打给B,在Socks的场景中接线员并不是直接将两个电话线直接连接起来,而是口头转述,所以B顺着电话线只知道接线员,而不知道A

Socks服务器核心代码实现也很简单

监听端口

func main() {l, err := net.Listen("tcp", ":1080")if err != nil {log.Panic(err)}for {client, err := l.Accept()if err != nil {log.Panic(err)}go handleClientRequest(client)//如果有人连接执行handle}}

A连接Socsk服务器

func handleClientRequest(client net.Conn) {if client == nil {return}defer client.Close()var b [1024]byten, err := client.Read(b[:]) //从A的口中得到B的地址if err != nil {log.Println(err)return}host = net.IPv4(b[4], b[5], b[6], b[7]).String() //获取B的地址port = strconv.Itoa(int(b[n-2])<<8 | int(b[n-1])) //B的端口server, err := net.Dial("tcp", net.JoinHostPort(host, port))//与B建立连接if err != nil {log.Println(err) return}defer server.Close()//进行转发go io.Copy(server, client) //将A说的话转述Bio.Copy(client, server) //将B说的话转述给A}}

go实现完整的Socks协议

上面知识Socks的核心功能,再之前还有一个简单的认证过程。不感兴趣可以略过,go有几个好用的第三方Socks库下面会说,可直接跳到那

协议整体可分为以下几步

协议版本及认证方式

根据认证方式执行对应的认证

请求信息

进行转发

注意:这里虽然是划分了步骤,但是都是在一个TCP连接中进行,不是每一步都再建立一个TCP连接,当中有一步错误就会导致TCP连接断开,也就不能使用Socks服务器进行转发。也就是说,最先建立一个TCP连接线进行Socks服务认证,然后直接用这个TCP连接发送数据

1.协议版本及认证方式

服务器和客户端创建tcp连接之后,客户端首先需要先向服务器发送协议版本认证,认证协议之后才能进行下一步。客户端发送的数据包格式如下

VER为SOCKS5,这里应该是0x05

NMETHODS,是METHODS的长度

METHODS是客户端支持的认证方式列表,每个方法占一字节。

0x00 不需要认证

0x01 GSSAPI

0x02 用户名、密码认证

0x03 - 0x7F由IANA分配(保留)

0x80 - 0xFE为私人方法保留

0xFF 无可接受的方法

服务端返回的数据包格式如下

VER是SOCKS的版本,这里是0x05

METHOD是服务端选中的方法。如果是0xFF表示客户端提交的方法列表中没有被选中的,客户端需要关闭

1.1客户端脚本实现

这里的服务端先使用我的qv2ray,向他发送协议版本认证

func main() { buf:=make([]byte,2) conn,err:=net.Dial("tcp","172.16.95.1:1089") //建立连接 defer conn.Close() if err!=nil{ fmt.Println(err) } conn.Write([]byte{0x05,0x01,0x00}) //发送协议版本认证数据包 conn.Read(buf)//接收返回包 fmt.Println(buf) //[5,0],服务器选中认证方式为0(不需要认证) }

现在修改代理设置为需要用户名密码才能连接,在发送协议版本认证

func main() { buf:=make([]byte,2) conn,err:=net.Dial("tcp","172.16.95.1:1089") defer conn.Close() if err!=nil{ fmt.Println(err) } conn.Write([]byte{0x05,0x01,0x00}) conn.Read(buf) fmt.Println(buf) //[5,255] 证明我们发送的认证方式他不支持 }

添加认证方式到METHOD中,可以成功获取服务端支持的认证方式

func main() { buf:=make([]byte,2) conn,err:=net.Dial("tcp","172.16.95.1:1089") defer conn.Close() if err!=nil{ fmt.Println(err) } conn.Write([]byte{0x05,0x02,0x00,0x02})//给定了两种认证方式供服务器端选择 conn.Read(buf) fmt.Println(buf) //[5,2] 我们发送的认证方式列表中他支持2(账号密码认证) }

数据包如下:前面是握手包,第五个包客户端也就是我们向v2rayq发送协议版本及认证,序号7 v2rayq返回包

到此为止,我们的扫描器一个新模块就出来了,proxyfind--代理发现

我们可以向内网中常见的代理服务器端口发送版本及认证方式数据包,判断其返回包,即可知道他是否为一个Socks服务器,

参考代码:

https://github.com/zyylhn/zscan/blob/master/cmd/proxyfind.go

1.2服务器端脚本实现

服务端在第一步的活上面已经说过了,验证客户端发来的协议版本认证,并返回可用的身份认证方法。

const METHOD_CODE = 0x00const SOCKS_VERSION = 0x05func main() { Server(":7001")}func Server(addr string){ addrtcp,err:=net.ResolveTCPAddr("tcp",addr) listen,err:=net.ListenTCP("tcp",addrtcp)//监听本地端口 defer listen.Close() if err !=nil{fmt.Println(err) } for{conn,_:=listen.AcceptTCP()afterconn(conn)//连接之后进行解析数据包并发送返回包defer conn.Close() }}func afterconn(conn *net.TCPConn) { buf:=make([]byte,255) n,_:=conn.Read(buf) var h ProtocolVersion a,err:=h.Handle_handshake(buf[0:n]) if err!=nil{fmt.Println(err) } conn.Write(a) fmt.Println(a)}type ProtocolVersion struct { VER uint8 NMETHODS uint8 METHODS []uint8}func (s *ProtocolVersion) Handle_handshake(b []byte) ([]byte,error) { n := len(b) if n < 3 {return nil,errors.New("协议错误, sNMETHODS不对") } s.VER = b[0] //第一个参数为socks的版本号 s.NMETHODS = b[1] //nmethods是记录methods的长度的。nmethods的长度是1个字节 s.METHODS = b[2 : 2+s.NMETHODS] //读取指定长度信息,读取正好len(buf)长度的字节。如果字节数不是指定长度,则返回错误信息和正确的字节数 if s.VER != 0x05 {return nil,errors.New("协议错误, version版本不为5!") } if n != int(2+s.NMETHODS) {return nil,errors.New("协议错误, NMETHODS不对") } useMethod := byte(0x00) //默认不需要密码 for _, v := range s.METHODS {if v == METHOD_CODE { useMethod = METHOD_CODE} } resp := []byte{SOCKS_VERSION, useMethod} return resp,nil }

首先运行代码监听端口,然后等待客户端发送版本验证请求,验证成功后返回支持的认证方式(不使用认证)客户端在读取。至此第一步完成。

2.根据认证方式执行对应的认证

Socks5协议提供五种认证方式:

0x00 不需要认证

0x01 GSSAPI

0x02 用户名、密码认证

0x03 - 0x7F由IANA分配(保留)

0x80 - 0xFE为私人方法保留

这里只介绍用户名密码认证,无身份验证就是直接略过这步。

客户端发送

服务器根据用户名密码正确与否返回

鉴定状态0x00表示成功,0x01表示失败

2.1客户端脚本实现

func main() { var handshack ProtocolVersion buf:=make([]byte,2) conn,err:=net.Dial("tcp","172.16.95.1:7001") defer conn.Close() if err!=nil{ fmt.Println(err) } handshack.Send_handshake(conn) //发送第上面第一步协议版本及认证方式数据包 conn.Read(buf)//读取返回包 fmt.Println(buf) if len(buf)==0{ fmt.Println("未获取到协议信息和认证方式") } if buf[0]!=0x05{ fmt.Println("协议错误") } switch buf[1] { case 0x00: fmt.Println("不需要认证") case 0x02: var a Socks5AuthUPasswd a.SendAuth(conn) //发送用户名密码认证数据包 n,_:=conn.Read(buf)//获取返回包 fmt.Println(buf[:n]) if buf[1]==0x00{//判断是否认证成功,成功的话好进行下一步 fmt.Println("success") }else{fmt.Println("error")} } }//构造身份认证数据包 func (s *Socks5AuthUPasswd) SendAuth(conn net.Conn) { username:="zyy" passwd:="123" resp:=[]byte{} s.VER=0x05 s.ULEN=uint8(len(username)) s.UNAME=username s.PLEN=uint8(len(passwd)) s.PASSWD=passwd resp= append(resp, s.VER) resp=append(resp,s.ULEN) for _,i :=range []byte(s.UNAME){ resp=append(resp,i) } resp=append(resp, s.PLEN) for _,i:=range []byte(s.PASSWD){ resp=append(resp,i) } conn.Write(resp) }

接着第一步协议版本请求,拿到正确的版本和认证方式之后,调用SendAuth方法发送认证信息

2.2服务器端代码实现

func main() { Server(":7001")}func Server(addr string){ addrtcp,err:=net.ResolveTCPAddr("tcp",addr) listen,err:=net.ListenTCP("tcp",addrtcp) defer listen.Close() if err !=nil{fmt.Println(err) } for{conn,_:=listen.AcceptTCP()afterconn(conn)defer conn.Close() }}func afterconn(conn *net.TCPConn) { buf:=make([]byte,255) n,_:=conn.Read(buf) var h ProtocolVersion a,err:=h.Handle_handshake(buf[0:n]) //第一步的协议版本及身份认证方式数据包 if err!=nil{fmt.Println(err) return } conn.Write(a) n,_=conn.Read(buf) var auth Socks5AuthUPasswd stat,_:=auth.HandleAuth(buf[0:n])//判断用户名密码并构造返回包if err!=nil{conn.Close()//认证失败关闭连接} conn.Write(stat)//将结果返回给客户端}func (s *Socks5AuthUPasswd) HandleAuth(b []byte) ([]byte, error) { n := len(b) s.VER = b[0] if s.VER != 5 { return nil, errors.New("该协议不是socks5协议") } s.ULEN = b[1] s.UNAME = string(b[2 : 2+s.ULEN]) s.PLEN = b[2+s.ULEN+1] s.PASSWD = string(b[n-int(s.PLEN) : n]) log.Println(s.UNAME, s.PASSWD) resp := []byte{SOCKS_VERSION, 0x00} return resp, nil }

在返回协议版本和认证方式之后之后紧接着获取认证信息,调用HandleAuth进行判断,然后根据判断的结果决定是否继续下一步。

3.请求信息

认证结束后客户端就可以向服务器端发送请求信息,格式如下

VER是socks版本,这里是0x05

CMD是SOCK的命令码

    0x01表示CONNECT请求

    0x02表示BIND请求

    0x03表示UDP转发

RSV 0x00保留

ATYP DST.ADDR类型

          0x01 IPV4地址,DST.ADDR部分4字节长度

          0x03 域名,DST.ADDR部分第一个字节为域名长度,剩余内容都是域名

          0x04 IPV6地址,16个字节长度

DST.PORT 表示目的端口

服务端返回的数据包格式如下

VER协议版本:0x05

REP回复字段

          0x00:成功

          0x01:常规SOCKS服务故障

          0x02:规则不允许连接

          0x03:网络不可达

          0x04:主机无法访问

          0x05:拒绝连接

          0x06:连接超时

          0x07:不支持的命令

          0x08:不支持的地址类型

          0x09:到0xff未定义

RSV 保留字段

ATYP 地址类型

         IPV4 0x01

 

         域名 0x03

         IPV6 0x04

BND.ADDR 服务端绑定地址

BND.PORT 服务端绑定端口

3.1服务端脚本实现

客户端这里就不自己写代码实现了,构造数据包太麻烦了,这里使用proxychains代替客户端

func main() { Server(":1080")}func Server(addr string){ addrtcp,err:=net.ResolveTCPAddr("tcp",addr) listen,err:=net.ListenTCP("tcp",addrtcp) defer listen.Close() if err !=nil{fmt.Println(err) } for{conn,_:=listen.AcceptTCP()afterconn(conn)defer conn.Close() }}func afterconn(conn *net.TCPConn) { //版本认证 buf:=make([]byte,255) n,_:=conn.Read(buf) var h ProtocolVersion a,err:=h.Handle_handshake(buf[0:n]) if err!=nil{fmt.Println(err) } conn.Write(a)//返回认证信息 fmt.Println(a) n,_=conn.Read(buf) var r Socks5Resolution last,err:=r.LSTRequest(buf[:n])//解析请求信息if err !=nil{return} conn.Write(last)//返回相应 dstServer, err := net.DialTCP("tcp", nil, r.RAWADDR) //与B建立连接 if err != nil {log.Print(conn.RemoteAddr(), err)return } defer dstServer.Close() go io.Copy(dstServer, conn) //将A说的话转述B io.Copy(conn, dstServer) //将B说的话转述给Atype Socks5Resolution struct { VER uint8 CMD uint8 RSV uint8 ATYPuint8 DSTADDR []byte DSTPORT uint16 DSTDOMAIN string RAWADDR *net.TCPAddr}func (s *Socks5Resolution) LSTRequest(b []byte) ([]byte, error) { n := len(b) if n < 7 {return nil, errors.New("请求协议错误") } s.VER = b[0] if s.VER != SOCKS_VERSION {return nil, errors.New("该协议不是socks5协议") } s.CMD = b[1] if s.CMD != 1 {return nil, errors.New("客户端请求类型不为代理连接, 其他功能暂时不支持.") } s.RSV = b[2] //RSV保留字端,值长度为1个字节 s.ATYP = b[3] switch s.ATYP { case 1:// IP V4 address: X01s.DSTADDR = b[4 : 4+net.IPv4len] case 3:// DOMAINNAME: X03s.DSTDOMAIN = string(b[5 : n-2])ipAddr, err := net.ResolveIPAddr("ip", s.DSTDOMAIN)if err != nil { return nil, err}s.DSTADDR = ipAddr.IP case 4:// IP V6 address: X04s.DSTADDR = b[4 : 4+net.IPv6len] default:return nil, errors.New("IP地址错误") } s.DSTPORT = binary.BigEndian.Uint16(b[n-2 : n]) // DSTADDR全部换成IP地址,可以防止DNS污染和封杀 s.RAWADDR = &net.TCPAddr{IP: s.DSTADDR,Port: int(s.DSTPORT), } resp := []byte{SOCKS_VERSION, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} return resp, nil}

仅测试,实际使用还需要完善

服务器整体流程

1. 读取发送来的数据包的认证信息,确定版本之后返回身份认证方式

2. 进行身份认证(这里没有,需要可自行添加)

3. 读取客户端发送过来的请求信息

4. 与请求信息中的地址建立连接

5. 然后将两个连接中传输的内容进行转发

3.2测试

服务器端在我本机开启监听

kali执行命令:proxychains4 nc -nv 127.0.0.1 3306(设置代理为我们本机监听的端口)

这里的3306是我本机的mysql,可见我们的流量被转发了

整个过程的数据包

第一步客户端发送协议版本认证和身份认证列表

第二步服务器返回支持的认证方式

第三步发送请求信息

第四步返回请求信息相应包(里面的字段可以设置,这里图方便没有设置)

最后将内容转发回来

到此,一个Socks服务器的基本功能就完成了,至于怎么完善就看具体需求了

Go的Socks5第三方库

socks5 package - github.com/armon/go-socks5 - pkg.go.dev

#section-readme

以这个为例

1.利用第三方库启动一个socks5服务器(无用户名密码)

第三方库可以让我们快速完成上面的繁琐的步骤,只需要几行就可以开启一个socks服务器

func main() { conf := &socks5.Config{} //创建一个配置 server, err := socks5.New(conf)//利用配置启动一个server if err != nil {panic(err) } // Create SOCKS5 proxy on localhost port 8000 if err := server.ListenAndServe("tcp", "172.16.95.1:1080"); err != nil {//开启监听panic(err) }}

仅仅这样就可以了。。。。(感觉还没开始就结束了)

当然,这个是没有密码的,有时候还是需要一个账户密码认证的

2.利用第三方库启动一个socks5服务器(用户名密码验证)

想要用户名密码验证就需要看看上面Config{}的内容了

type Config struct {// AuthMethods can be provided to implement custom authentication// By default, "auth-less" mode is enabled.// For password-based auth use UserPassAuthenticator.AuthMethods []Authenticator// If provided, username/password authentication is enabled,// by appending a UserPassAuthenticator to AuthMethods. If not provided,// and AUthMethods is nil, then "auth-less" mode is enabled.Credentials CredentialStore// Resolver can be provided to do custom name resolution.// Defaults to DNSResolver if not provided.Resolver NameResolver// Rules is provided to enable custom logic around permitting// various commands. If not provided, PermitAll is used.Rules RuleSet// Rewriter can be used to transparently rewrite addresses.// This is invoked before the RuleSet is invoked.// Defaults to NoRewrite.Rewriter AddressRewriter// BindIP is used for bind or udp associateBindIP net.IP// Logger can be used to provide a custom log target.// Defaults to stdout.Logger *log.Logger// Optional function for dialing outDial func(ctx context.Context, network, addr string) (net.Conn, error)}

描述中知道AuthMethods是支持的身份验证列表,默认是不需要验证,这个Authenticator是一个接口。

type Authenticator interface {Authenticate(reader io.Reader, writer io.Writer) (*AuthContext, error)GetCode() uint8}

在这个库中实现这个接口的方法有两个,一个是NoAuthAuthenticator(不需要身份验证),还有一个UserPassAuthenticator

NoAuthAuthenticator

#L39

UserPassAuthenticator

#L52

type UserPassAuthenticator struct {Credentials CredentialStore}

这里的CredentialStore也是一个接口 

#CredentialStore

type CredentialStore interface {Valid(user, password string) bool}

StaticCredentials实现了这个接口

#L9

type StaticCredentials map[string]string

所以我们先需要创建一个StaticCredentials对象存贮用户名密码

userpass:=socks5.StaticCredentials{"zyy":""}

然后创建一个UserPassAuthenticator对象

userpass:=socks5.StaticCredentials{"zyy":""}auth:=socks5.UserPassAuthenticator{userpass}

在稍加修改一下上面的代码,就可以变成可以支持身份认证的socks服务

conf := &socks5.Config{}if Username!=""{ if Password==""{Checkerr(fmt.Errorf("You must specify a password")) }else {userpass:=socks5.StaticCredentials{Username:Password}auth:=socks5.UserPassAuthenticator{userpass}conf.AuthMethods=[]socks5.Authenticator{auth} }}server, err := socks5.New(conf)if err != nil { panic(err)}if err := server.ListenAndServe("tcp", addr); err != nil { panic(err)}

socks服务器模块也完成了示例代码

https://github.com/zyylhn/zscan/blob/master/cmd/socks5.go

Proxy

既然服务器都有第三方库,那客户端肯定也有了,并且用起来简单方便

dialer,err:=proxy.SOCKS5("tcp",代理服务器的ip端口,nil,proxy.Direct) //声明一个dialerconn,err:=dialer.Dial("tcp",B的ip和端口)

上面这两行代码会帮我们把认证部分做好,剩下直接往连接里面发送数据就好了

总结

感觉是把代理的部分整好了,其实还没有,还存在几个问题

简单的使用代理去连接一个端口是可以的,但是走其他协议呢,用了其他第三方库例如mysql我们怎么走代理呢

2. 利用代理扫描会不会出现什么问题(放心肯定会)

3. 能不能通过外部工具走代理

   

     1.proxychans

     

     2.Proxycap

     

     3.Proxyfier

看似一般的问题,有的其实很难解决,下一片将会去解决这些问题