Please enable Javascript to view the contents

被动扫描器HTTP(S)代理初探

 ·  ☕  6 分钟

如何代理 http(s) 流量

HTTP 代理的协议基于 HTTP,因此 HTTP 代理本身就是一个 HTTP 的服务,而其工作原理本质上就是中间人(MITM) ,即读取当前客户端的 HTTP 请求,从代理发送出去并获得响应,然后将响应返回给客户端。

image

使用 netcat -lv -p 8881 监听 127.0.0.1:8881

  • 访问 http://**.** nc 的数据包为:
GET http://hyuga.co/ HTTP/1.1
Host: hyuga.co
Proxy-Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://hyuga.co/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
...
  • 访问 https://**.** nc 的数据包为:
CONNECT github.com:443 HTTP/1.1
Host: github.com:443
Proxy-Connection: keep-alive
User-Agent: Mozilla/5.0

golang 实现 http(s) 代理服务器

使用隧道直接双向复制 https 数据

 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
64
65
66
67
68
69
70
71
72
73
package main

import (
	"io"
	"log"
	"net"
	"net/http"
)

const addr = "localhost:8881"

func main() {
	log.Printf("HTTPProxy is runing on %s \n", addr)
	log.Fatalln(http.ListenAndServe(addr, http.HandlerFunc(handler)))
}

func handler(rw http.ResponseWriter, req *http.Request) {
	// http && https
	if req.Method != "CONNECT" {
		// 处理http
		handleHTTP(rw, req)
	} else {
		// 处理https
		handleHTTPS(rw, req)
	}
}

func handleHTTP(rw http.ResponseWriter, req *http.Request) {
	transport := http.DefaultTransport
	// 新建一个请求outReq
	outReq := new(http.Request)
	// 复制客户端请求到outReq上
	*outReq = *req // 复制请求
	// outReq 请求放到传送上
	res, err := transport.RoundTrip(outReq)
	if err != nil {
		rw.WriteHeader(http.StatusBadGateway)
		rw.Write([]byte(err.Error()))
		return
	}
	// 回写 http 头
	for key, value := range res.Header {
		for _, v := range value {
			rw.Header().Add(key, v)
		}
	}
	// 回写状态码
	rw.WriteHeader(res.StatusCode)
	// 回写 body
	io.Copy(rw, res.Body)
	res.Body.Close()
}

func handleHTTPS(rw http.ResponseWriter, req *http.Request) {
	host := req.URL.Host
	hij, ok := rw.(http.Hijacker)
	if !ok {
		log.Printf("HTTP Server does not support hijacking")
	}
	client, _, err := hij.Hijack()
	if err != nil {
		return
	}
	// 连接远程
	server, err := net.Dial("tcp", host)
	if err != nil {
		return
	}
	client.Write([]byte("HTTP/1.0 200 Connection Established\r\n\r\n"))
	// 直通双向复制
	go io.Copy(server, client)
	go io.Copy(client, server)
}

但作为被动扫描器的代理,https 协议的请求数据都是加密的,大部分的网站都无法进行漏洞检测了,所以被动扫描器的代理还需要对 https 流量进行加解密。

HTTP & HTTPS 的区别

  • HTTP 协议以明文方式发送内容,不提供任何方式的数据加密,如果攻击者截取了Web浏览器和网站服务器之间的传输报文,就可以直接读懂其中的信息,因此,HTTP协议不适合传输一些敏感信息,比如:信用卡号、密码等支付信息。

  • HTTPS(Hypertext Transfer Protocol Secure:超文本传输安全协议)是一种透过计算机网络进行安全通信的传输协议。HTTPS 经由 HTTP 进行通信,但利用 SSL/TLS 来加密数据包。HTTPS 开发的主要目的,是提供对网站服务器的身份认证,保护交换数据的隐私与完整性。

HTTPS 默认工作在 TCP 协议443端口,它的工作流程一般如以下方式:

  1. TCP 三次同步握手
  2. 客户端验证服务器数字证书
  3. DH 算法协商对称加密算法的密钥、hash 算法的密钥
  4. SSL 安全加密隧道协商完成
  5. 网页以加密的方式传输,用协商的对称加密算法和密钥加密,保证数据机密性;用协商的hash算法进行数据完整性保护,保证数据不被篡改。

什么是证书

最顶层的 Global Sign RootCA 是一个根证书,第二个是一个中间证书,最后一个才是用户的颁发证书,这三种证书的效力是:

RootCA >  Intermediates CA > End-User Cert

而且只要信任了 RootCA 由 RootCA 签发的包括其下级签发的证书都会被信任。而 Global Sign RootCA 等是一些默认安装在系统和浏览器中的根证书。这些证书由一些权威机构来维护,可以确保证书的安全和有效性。而内置的这些根证书就允许我们访问一些公共的网站而无需手动信任证书了。

什么是x509证书链

x509证书一般会用到三类文件,key,csr,crt。

Key 是私用密钥,openssl 格式,通常是 rsa 算法。

csr 是证书请求文件,用于申请证书。在制作 csr 文件的时候,必须使用自己的私钥来签署申请,还可以设定一个密钥。

crt 是 CA 认证后的证书文件,签署人用自己的key给你签署的凭证。

如何生成证书

openssl 中有如下后缀名的文件

  • .key格式:私有的密钥
  • .csr格式:证书签名请求(证书请求文件),含有公钥信息,certificate signing request的缩写
  • .crt格式:证书文件,certificate的缩写
  • .crl格式:证书吊销列表,Certificate Revocation List的缩写
  • .pem格式:用于导出,导入证书时候的证书的格式,有证书开头,结尾的格式

使用如下命令生成证书:

1
2
3
openssl genrsa -out ca/rootCA.key 2048
openssl req -new -key ca/rootCA.key -out ca/rootCA.csr
openssl x509 -req -days 3650 -in ca/rootCA.csr -signkey ca/rootCA.key -in rootCA.csr -out ca/rootCA.crt

如何信任证书

只列举了 MacOS 下的情况,其他操作系统自行搜索:

  • 使用命令
1
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ca/rootCA.crt
  • 打开“钥匙串访问”;系统->证书->添加;双击添加的证书->信任->始终信任。

golang 如何代理并解密 https 流量

挑选一个轻量 goproxy 项目进行源码分析:

证书相关:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// Cache 证书缓存接口
type Cache interface {
	Set(host string, c *tls.Certificate)
	Get(host string) *tls.Certificate
}
//证书 struct
type Certificate struct {
	cache Cache
}
// NewCertificate 创建证书
func NewCertificate(cache Cache) *Certificate {
	return &Certificate{
		cache: cache,
	}
}

GenerateTlsConfig 根据根证书生成子证书,并写入 cache 中

 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
// GenerateTlsConfig 生成TLS配置
func (c *Certificate) GenerateTlsConfig(host string) (*tls.Config, error) {
	if h, _, err := net.SplitHostPort(host); err == nil {
		host = h
	}
	if c.cache != nil {
		// 先从缓存中查找证书
		if cert := c.cache.Get(host); cert != nil {
			tlsConf := &tls.Config{
				Certificates: []tls.Certificate{*cert},
			}

			return tlsConf, nil
		}
	}
	pair, err := c.GeneratePem(host, 1, defaultRootCA, defaultRootKey)
	if err != nil {
		return nil, err
	}
	cert, err := tls.X509KeyPair(pair.CertBytes, pair.PrivateKeyBytes)
	if err != nil {
		return nil, err
	}
	tlsConf := &tls.Config{
		Certificates: []tls.Certificate{cert},
	}

	if c.cache != nil {
		// 缓存证书
		c.cache.Set(host, &cert)
	}

	return tlsConf, nil
}

先定位到 server handler 方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func (p *Proxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
	...
	ctx := &Context{
		Req:  req,
		Data: make(map[interface{}]interface{}),
	}
	...
	switch {
	case ctx.Req.Method == http.MethodConnect && p.decryptHTTPS:
		p.forwardHTTPS(ctx, rw)
	case ctx.Req.Method == http.MethodConnect:
		p.forwardTunnel(ctx, rw)
	default:
		p.forwardHTTP(ctx, rw)
	}
}
  • forwardHTTPS: 解密 https 请求
  • forwardTunnel: 隧道直连
  • forwardHTTP: 处理普通 http 请求

forwardHTTPS 方法:

 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
64
65
66
67
68
69
// HTTPS转发
func (p *Proxy) forwardHTTPS(ctx *Context, rw http.ResponseWriter) {
	var err error
	// 获取底层连接
	hijacker, ok := rw.(http.Hijacker)
	if !ok {
		err = fmt.Errorf("web server不支持Hijacker")
	}
	clientConn, _, err := hijacker.Hijack()
	if err != nil {
		err = fmt.Errorf("hijacker错误: %s", err)
	}
	// handler errors
	if err != nil {
		p.delegate.ErrorLog(err)
		rw.WriteHeader(http.StatusBadGateway)
		return
	}
	defer clientConn.Close()
	
	// 隧道连接成功响应行
	_, err = clientConn.Write([]byte("HTTP/1.1 200 Connection established\r\n\r\n"))
	if err != nil {
		p.delegate.ErrorLog(fmt.Errorf("%s - HTTPS解密, 通知客户端隧道已连接失败, %s", ctx.Req.URL.Host, err))
		return
	}
	// 获取子证书
	tlsConfig, err := p.cert.GenerateTlsConfig(ctx.Req.URL.Host)
	if err != nil {
		p.delegate.ErrorLog(fmt.Errorf("%s - HTTPS解密, 生成证书失败: %s", ctx.Req.URL.Host, err))
		rw.WriteHeader(http.StatusBadGateway)
		return
	}
	// 获取一个 TLS Server 新连接
	tlsClientConn := tls.Server(clientConn, tlsConfig)
	tlsClientConn.SetDeadline(time.Now().Add(defaultClientReadWriteTimeout))
	defer tlsClientConn.Close()
	if err := tlsClientConn.Handshake(); err != nil {
		p.delegate.ErrorLog(fmt.Errorf("%s - HTTPS解密, 握手失败: %s", ctx.Req.URL.Host, err))
		return
	}
	// 解密 https reqeust
	buf := bufio.NewReader(tlsClientConn)
	tlsReq, err := http.ReadRequest(buf)
	if err != nil {
		if err != io.EOF {
			p.delegate.ErrorLog(fmt.Errorf("%s - HTTPS解密, 读取客户端请求失败: %s", ctx.Req.URL.Host, err))
		}
		return
	}
	tlsReq.RemoteAddr = ctx.Req.RemoteAddr
	tlsReq.URL.Scheme = "https"
	tlsReq.URL.Host = tlsReq.Host

	ctx.Req = tlsReq
	p.DoRequest(ctx, func(resp *http.Response, err error) {
		if err != nil {
			p.delegate.ErrorLog(fmt.Errorf("%s - HTTPS解密, 请求错误: %s", ctx.Req.URL, err))
			tlsClientConn.Write(badGateway)
			return
		}
		// 写入 response 
		err = resp.Write(tlsClientConn)
		if err != nil {
			p.delegate.ErrorLog(fmt.Errorf("%s - HTTPS解密, response写入客户端失败, %s", ctx.Req.URL, err))
		}
		resp.Body.Close()
	})
}

DoRequest 方法代码如下:

 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
// DoRequest 执行HTTP请求,并调用responseFunc处理response
func (p *Proxy) DoRequest(ctx *Context, responseFunc func(*http.Response, error)) {
	if ctx.Data == nil {
		ctx.Data = make(map[interface{}]interface{})
	}
	if ctx.abort {
		return
	}
	newReq := new(http.Request)
	*newReq = *ctx.Req
	newReq.Header = CloneHeader(newReq.Header)
	removeConnectionHeaders(newReq.Header)
	for _, item := range hopHeaders {
		if newReq.Header.Get(item) != "" {
			newReq.Header.Del(item)
		}
	}
	resp, err := p.transport.RoundTrip(newReq)
	if ctx.abort {
		return
	}
	if err == nil {
		removeConnectionHeaders(resp.Header)
		for _, h := range hopHeaders {
			resp.Header.Del(h)
		}
	}
	responseFunc(resp, err)
}

如上,完成这一 https 解密过程还是很方便的,当然像《HTTP被动扫描代理的那些事》中说的“#离完美的差距“,这次的初探是为了满足对被动扫描器代理好奇🤔。。。

参考文章

  • 《深入理解HTTPS工作原理》https://juejin.cn/post/6844903830916694030
  • 《HTTP被动扫描代理的那些事》https://www.freebuf.com/articles/web/212382.html
  • 《HTTP 与 HTTPS 的区别》https://www.runoob.com/w3cnote/http-vs-https.html
  • 《别闹!自签名证书!》https://zhuanlan.zhihu.com/p/41501360
  • 《Linux专题—Openssl生成证书》https://zhuanlan.zhihu.com/p/104212822
  • https://github.com/ouqiang/goproxy
目录