前言

逆向模拟的时候都会遇见这种情况:浏览器发送请求直接通过,转成py发送请求就会被拦截,无论header,data,cookie怎么对照浏览器写,或者是用curl_request和别的发包工具都会被精准拦截。问题就出现在指纹+waf检测
测试指纹拦截网址:aHR0cHM6Ly93d3cuY2VhaXIuY29tL3poLw==

TLS指纹

1.什么是TLS指纹:

TLS指纹是一种用于识别和验证TLS(传输层安全)通信的技术。

TLS指纹可以通过检查TLS握手过程中使用的密码套件、协议版本和加密算法等信息来确定TLS通信的特征。由于每个TLS实现使用的密码套件、协议版本和加密算法不同,因此可以通过比较TLS指纹来判断通信是否来自预期的源或目标。

TLS指纹可以用于检测网络欺骗、中间人攻击、间谍活动等安全威胁,也可以用于识别和管理设备和应用程序。

2.常见TLS指纹对比:

测试网站:https://tls.browserleaks.com/json

requests:

curl_cffi:

Edg:

Chrome:

不同客户端的指纹不同。拿curl_cffi来解释一下含义

第一个值771:表示 JA3 版本,即用于生成指纹的 JA3 脚本的版本。

第二个值:4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53客户端按顺序请求的密码套件

第三个值:16-43-11-65281-45-27-23-51-10-0-18-35-5-17513-13-21 TLS扩展字段

第四个值:29-23-24 Elliptic Curver椭圆曲线

第五个值:0  EC Point Formats   这个0代表非压缩格式

3.常见的TLS指纹绕过方法

修改客户端Hello报文中的字段(如TLS版本、加密套件、扩展列表),使其与合法客户端一致。使用工具或库(如curl、Python的urllib3)定制TLS握手行为。
利用中间层代理(如mitmproxy、Gost)重写TLS指纹,或通过云函数(如AWS Lambda)转发流量以隐藏原始指纹。
模拟浏览器指纹(如Chrome/Firefox的TLS特征),通过开源库(如tls-client)生成合法指纹。

WAF检测(Akamai的子类)

1.什么是WAF检测:

WAF(Web Application Firewall,Web 应用防火墙)是部署在网站入口的一道安全屏障,它会检查每一个 HTTP 请求是否符合正常浏览器的行为特征:从网络层的 TLS 指纹(JA3/JA4,记录客户端的密码套件、扩展、椭圆曲线等参数)、HTTP/2 帧参数、TCP 连接特征,到应用层的请求头、Cookie、请求频率和访问路径,综合判断请求是来自真实用户还是爬虫/攻击工具,可疑请求会被拦截或触发验证码,正常请求则放行到后端服务器。

2.Akamai指纹

拿谷歌浏览器讲

指纹是:1:65536;2:0;4:6291456;6:262144|15663105|0|m,a,s,p

SETTINGS 帧:1:65536;2:0;4:6291456;6:262144

1. 1:65536:HEADER_TABLE_SIZE即头部表大小为64KB,指的是用于存储请求头和响应头的大小,它是可以调整的。这个字段指明了使用64KB的头部表大小

2.  2.0:HTTP2_VERSION,指示此请求使用的HTTP/2版本。0表示H2,表示启用了HTTP/2协议。

3. 6291456:INITIAL_WINDOW_SIZE流级别初始窗口,约 6MB

4.262144:MAX_HEADER_LIST_SIZE请求头最大字节数,256KB

WINDOW_UPDATE 帧:15663105 计算方式是INITIAL_WINDOW_SIZE+ 10485760 - 1 

 PRIORITY 帧的流 ID:0 表示客户端对整个链接设置优先级

 伪头顺序:m,a,s,p

m:method 请求方法

a:authority 主机名如www.xxxx.com

s:scheme 协议,如https

p:path路径 如/xxx/xxx

可以根据这个顺序来区分浏览器类型

HTTP反向代理

了解了tls指纹和akamami指纹和waf检测,就可以去规避了,基本上的tls指纹用curl_cffi就可以通过,但是waf检测不仅仅检测了tls指纹,它还检测了ja4,http/2指纹(H2SETTINGS帧),TCP层,请求频率,cookie,还有header。这里采用了go语言的原生tls指纹来请求,go的tls指纹模仿的会比市面上常见的requests,curl_cffi, httpclient等要更全面,更像浏览器

封装GO的反向代理

1.导包,声明基础信息

import (
	"bufio"
	"crypto/tls"
	"fmt"
	"io"
	"log"
	"net"
	"net/http"
	"net/http/cookiejar"
	"net/url"
	"strings"
	"time"
)

const (
	targetHost = "www.ceair.com"
	proxyAddr  = ":18080"
	userAgent  = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
)

2.初始化,创建COOKIE, Transport,创建http.Client

var client *http.Client

func init() {
	jar, _ := cookiejar.New(nil)
	tr := &http.Transport{
		TLSClientConfig: &tls.Config{
			InsecureSkipVerify: true,
			MinVersion:         tls.VersionTLS12,
		},
		MaxIdleConns:       10,
		IdleConnTimeout:    90 * time.Second,
		ForceAttemptHTTP2:  true,
		DisableCompression: false,
	}
	client = &http.Client{
		Transport: tr,
		Jar:       jar,
		Timeout:   30 * time.Second,
	}
}

3.warmup()模拟正常用户,携带响应cookie

func warmup() error {
	req, _ := http.NewRequest("GET", "https://"+targetHost+"/", nil)
	req.Header.Set("User-Agent", userAgent)
	req.Header.Set("Accept", "text/html,application/xhtml+xml;q=0.9,*/*;q=0.8")
	req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
	resp, err := client.Do(req)
	if err != nil {
		return fmt.Errorf("homepage: %v", err)
	}
	io.Copy(io.Discard, resp.Body)
	resp.Body.Close()
	log.Printf("[warmup] GET / -> %d", resp.StatusCode)

	searchURL := fmt.Sprintf("https://%s/zh/cny/shopping/oneway/PEK-SHA/%s",
		targetHost, time.Now().AddDate(0, 0, 7).Format("2006-01-02"))
	req2, _ := http.NewRequest("GET", searchURL, nil)
	req2.Header.Set("User-Agent", userAgent)
	req2.Header.Set("Accept", "text/html,application/xhtml+xml;q=0.9,*/*;q=0.8")
	req2.Header.Set("Accept-Language", "zh-CN,zh;q=0.9")
	req2.Header.Set("Referer", "https://"+targetHost+"/")
	resp2, err := client.Do(req2)
	if err != nil {
		return fmt.Errorf("search: %v", err)
	}
	io.Copy(io.Discard, resp2.Body)
	resp2.Body.Close()
	log.Printf("[warmup] GET search -> %d", resp2.StatusCode)

	cookies := client.Jar.Cookies(req.URL)
	names := make([]string, len(cookies))
	for i, c := range cookies {
		names[i] = c.Name
	}
	log.Printf("[warmup] cookies: %d %v", len(cookies), names)
	return nil
}

4.handle()读取请求,构造参数

func handle(conn net.Conn) {
	defer conn.Close()
	conn.SetDeadline(time.Now().Add(120 * time.Second))

	reader := bufio.NewReader(conn)
	req, err := http.ReadRequest(reader)
	if err != nil {
		return
	}

	t0 := time.Now()
	log.Printf("[req] %s %s", req.Method, req.URL.Path)

	reqBody, _ := io.ReadAll(req.Body)
	req.Body.Close()

	targetURL := &url.URL{
		Scheme:   "https",
		Host:     targetHost,
		Path:     req.URL.Path,
		RawQuery: req.URL.RawQuery,
	}

	resp, bodyBytes, err := doRequest(targetURL, req, reqBody)
	if err != nil {
		log.Printf("[req] error: %v", err)
		writeError(conn, 502, "Bad Gateway")
		return
	}

	if isCDNBlocked(bodyBytes) {
		log.Printf("[req] CDN BLOCKED, rewarmup + retry...")
		warmup()
		resp2, body2, err2 := doRequest(targetURL, req, reqBody)
		if err2 == nil {
			resp = resp2
			bodyBytes = body2
		}
	}

	elapsed := time.Since(t0)
	if isCDNBlocked(bodyBytes) {
		log.Printf("[res] CDN_BLOCKED (%dms, %d bytes)", elapsed.Milliseconds(), len(bodyBytes))
	} else {
		log.Printf("[res] %d OK (%dms, %d bytes)", resp.StatusCode, elapsed.Milliseconds(), len(bodyBytes))
	}

	writeResponse(conn, resp, bodyBytes)
}

5.doRequest()发包转发到目标网站,最多两次重试

func doRequest(targetURL *url.URL, orig *http.Request, body []byte) (*http.Response, []byte, error) {
	for i := 0; i < 2; i++ {
		var bodyReader io.Reader
		if len(body) > 0 {
			bodyReader = strings.NewReader(string(body))
		}

		proxyReq, err := http.NewRequest(orig.Method, targetURL.String(), bodyReader)
		if err != nil {
			continue
		}
		buildHeaders(proxyReq, orig)

		resp, err := client.Do(proxyReq)
		if err != nil {
			log.Printf("[req] attempt %d failed: %v", i+1, err)
			continue
		}

		bodyBytes, _ := io.ReadAll(resp.Body)
		resp.Body.Close()
		return resp, bodyBytes, nil
	}
	return nil, nil, fmt.Errorf("all retries failed")
}

6.返回响应,并重新计算content-length等值

func writeResponse(conn net.Conn, resp *http.Response, bodyBytes []byte) {
	fmt.Fprintf(conn, "HTTP/1.1 %d %s\r\n", resp.StatusCode, http.StatusText(resp.StatusCode))
	for k, vs := range resp.Header {
		if k == "Transfer-Encoding" || k == "Content-Length" || k == "Content-Encoding" {
			continue
		}
		for _, v := range vs {
			fmt.Fprintf(conn, "%s: %s\r\n", k, v)
		}
	}
	fmt.Fprintf(conn, "Content-Length: %d\r\n", len(bodyBytes))
	fmt.Fprintf(conn, "Connection: close\r\n\r\n")
	conn.Write(bodyBytes)
}

融合到代码

将url替换成代理接口,然后转发给代理,代理去请求目标网站。

TLS_PROXY = "http://localhost:18080"
brief_url = f"{TLS_PROXY}/portal/v3/shopping/briefInfo"

结果对比

正常使用curl_cffi请求

使用代理请求

法律与道德边界

强调技术研究的合规性,避免用于非法渗透测试或绕过安全防护。建议在授权测试环境中验证技术可行性。

个人观点,仅供参考学习。如有侵权,联系下架

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐