Go 绕过 Cloudflare 防护
书接上文,在用了简单的界面模拟后chatgpt用是能用了,但受限制条件太多,终究不是长久之法,还是比不上逆向接口来的高效实用。经过一番折腾最后成功实现接口的调用,这期就来先探究下为何网页操作可以,而在代码里模拟接口却被403拒绝的问题。
TLS Fingerprinting
上文说到逆向受阻主要是受到 Cloudflare 的反爬虫防护,经过查找发现 tls-client 这个库竟然可以绕过 Cloudflare,在一番上手使用后效果是有的,但有几点让我极其难以忍受
- 对
net/http
侵入性过强,丧失对标准库net/http
使用 - 构建出的程序体积上大了好几兆,触发了我的强迫症
于是继续探究这个库究竟用了什么魔法绕过 Cloudflare。在其 Readme.md 有这么一段内容引起了我的注意
Some people think it is enough to change the user-agent header of a request to let the server think that the client requesting a resource is a specific browser. Nowadays this is not enough, because the server might use a technique to detect the client browser which is called TLS Fingerprinting.
这段内容大概意思是:仅仅用 User-Agent
来伪装浏览器是不够的,服务器可能会通过 TLS 指纹识别
来判断请求。随后引用了 TLS 指纹识别如何工作这篇文章来介绍 TLS 指纹识别
。
根据文章的说法在 TLS 握手期间 Client Hello
会发送大量的客户端信息,这些的信息会被防护厂家做甄别过滤,防止恶意访问,信息的详细解读可以参考 tls13.xargs.org ,目前常用的客户端标记方法是 ja3,它通过计算下列信息的 MD5 来标识
1 | SSLVersion,Cipher,SSLExtension,EllipticCurve,EllipticCurvePointFormat |
知道大概的原理了,那么 tls-client 是如何解决的呢?查看 tls-client的依赖 会发现了一个关于 tls 的依赖 github.com/bogdanfinn/utls
,去查看 bogdanfinn/utls
介绍里写着 Fork 的 https://gitlab.com/yawning/utls
,再去看 yawning/utls
好家伙这介绍里写的 Fork 的 https://github.com/refraction-networking/utls
,去到 refraction-networking/utls
这回对了,star 数和介绍都相当到位,这个才是真正处理 TLS 指纹识别
。
关于客户端请求的代码在官方示例有详细介绍,这里仿照写一段测试代码
1 | func TestUtls(t *testing.T) { |
就在我以为十拿九稳的时候,请求之后得到结果却给我了一盆冷水 403
1 | HTTP/2.0 403 Forbidden |
这让我意识到一定有什么东西在 tls-client 库里我没注意到,于是我经过一系列的查找终于让我找了关键信息 HTTP/2 Fingerprinting
HTTP/2 Fingerprinting
说起来找到这个关键信息在于我某次调整 http.Transport
意外强制使用了 HTTP/1.1
然后得到了 200 的响应码,让我意识到也许问题不在 TLS 指纹识别
而在于 HTTP/2
也存在某种类似的指纹机制,谷歌一下 http2 Fingerprinting
第一篇 http2-fingerprinting 就是我们想要的答案。
在这篇文章里介绍到 http2 Fingerprinting
它可以识别浏览器类型和版本,或者是否使用脚本。该方法依赖于 HTTP/2
协议的内部机制,与简单的前身 HTTP/1.1 相比,该协议不太为人所知。
先来介绍下 HTTP/2
协议,HTTP/2 是一种二进制协议,与文本协议 HTTP/1.1
不同。 HTTP/2
中的消息由帧组成,有十种类型的帧服务于不同的目的。在请求的交互中 通常会有几种帧可能会被用来做浏览器与脚本的区分
- SETTINGS
- WINDOW_UPDATE
- HEADERS
- PRIORITY
这里着重介绍其中最关键的 SETTINGS
帧,通过 SETTINGS
帧,客户端向服务器通知其 HTTP/2
配置项。客户端可以通过六种不同的设置来控制参数,例如并发流的最大数量、HTTP 标头的最大数量、默认窗口大小以及是否支持服务器推送功能。我们可以通过 https://browserleaks.com/http2 ,来观察所用的浏览器用的相关 HTTP/2
帧信息,我用的 Chrome SETTINGS
帧参数大概如下
1 | SETTINGS_HEADER_TABLE_SIZE: 65536 |
由于每个 HTTP/2
客户端都有自己独特的 SETTINGS
帧设置。且不受到实际的 HTTP 请求影响,最主要的是 SETTINGS
帧设置通常被认为是繁琐的,一般会被软件厂商封装,用户通常难以设置这些值,这便给安全厂商提供了一个绝佳的机制来验证请求。
很明显 Go 的 http.Client
也一定是有一套不同于浏览器的设置,那么该如何改变这些值伪装成浏览器呢?在上面的例子中 tr.NewClientConn(conn)
点进去会发现源码中有这么一段
1 | func (t *Transport) newClientConn(c net.Conn, singleUse bool) (*ClientConn, error) { |
if 中的方法 t.maxFrameReadSize()
t.maxHeaderListSize()
对应了 http2.Transport
中的配置,结果测试微调上面的代码 http2.Transport{}
改成如下内容
1 | tr := http2.Transport{MaxHeaderListSize: 262144} |
即可获取到非 403 的响应头,表明通过鉴权。
原生 net/http 适配
最难的一环通过检测是完成了,但是如何优雅的集成到原生的 net/http
库里呢?要知道折腾这么老半天可就是想要替换掉 tls-client 这个臃肿的三方库,为了集成只能接着啃源码。根据观察发现在 http
源码中有个 (t *Transport) onceSetNextProtoDefaults()
函数,这个函数调用了http2configureTransports
配置 http2.Transport
,最关键的一点来了我直接贴上源码
1 | t2, err := http2configureTransports(t) |
看到了么,源码中也有设置 MaxHeaderListSize
这一流程,可以通过 MaxResponseHeaderBytes
来关联设置 MaxHeaderListSize
,最后总结一下关于原生 绕过 Cloudflare 的设置
1 | h1 := (http.DefaultTransport).(*http.Transport).Clone() |
可能有的人会问 TLS 指纹识别
呢,这里经过测试 Cloudflare 没有用这个验证,只用了 HTTP/2
验证,个人猜测是担心浏览器的更新升级之后变更了 TLS 指纹
影响访问。
写在最后
最初只是对第三方库侵入 net/http
而感到不爽,当抽丝剥茧 找到最后的答案时竟然让我感到了一丝滑稽,原来我们找一圈来绕过 Cloudflare 的方式竟然如此简单。当然非常感谢 tls-client 这个库为我带来这一次宝贵的学习经验,也希望这篇文章可以帮到你。