TLS 指纹可以用来作为一些常见语言的网络请求库识别特征。什么是TLS 指纹指纹呢?在TLS 建立链接的时候,会发送一个client hello 的请求,请求会告诉对方,自己支持哪些加密算法、参数等。由于这些参数比较多,并且和顺序无关,有人就想到可以通过这些参数来计算hash 值昨晚指纹。
因为常见的Python,nodejs 等,底层实现中有默认的TLS 参数配置,大版本内几乎很少变更,普通用户也很少回去变更这些配置。所以,可以通过这些指纹,识别出常见的网络库。
一个普通的面向用户的普通,理论上真实用户不会使用nodejs, python 等脚本访问,可以达到识别和反爬的作用。
例如我们实现一个函数,查看下nodejs v18 的TLS 指纹。
function fetchAndPrintData(url) { https.get(url, (response) => { let data = ''; // A chunk of data has been received. response.on('data', (chunk) => { data += chunk; }); // The whole response has been received. response.on('end', () => { console.log(data); }); }).on('error', (err) => { console.error(`Error: ${err.message}`); }); } const url = 'https://tls.browserleaks.com/json'; fetchAndPrintData(url);
// node 18.1 { "user_agent": "", "ja3_hash": "0cce74b0d9b7f8528fb2181588d23793", "ja3_text": "771,4866-4867-4865-49199-49195-49200-49196-158-49191-103-49192-107-163-159-52393-52392-52394-49327-49325-49315-49311-49245-49249-49239-49235-162-49326-49324-49314-49310-49244-49248-49238-49234-49188-106-49187-64-49162-49172-57-56-49161-49171-51-50-157-49313-49309-49233-156-49312-49308-49232-61-60-53-47-255,0-11-10-35-22-23-13-43-45-51,29-23-30-25-24-256-257-258-259-260,0-1-2", "ja3n_hash": "2cdc372ba33ad43cbb1c09aad0566191", "ja3n_text": "771,4866-4867-4865-49199-49195-49200-49196-158-49191-103-49192-107-163-159-52393-52392-52394-49327-49325-49315-49311-49245-49249-49239-49235-162-49326-49324-49314-49310-49244-49248-49238-49234-49188-106-49187-64-49162-49172-57-56-49161-49171-51-50-157-49313-49309-49233-156-49312-49308-49232-61-60-53-47-255,0-10-11-13-22-23-35-43-45-51,29-23-30-25-24-256-257-258-259-260,0-1-2", "akamai_hash": "", "akamai_text": "" }
The field order is as follows:
SSLVersion,Cipher,SSLExtension,EllipticCurve,EllipticCurvePointFormat
把版本,加密套件,扩展等内容按顺序排列然后计算hash值,便可得到一个客户端的TLS FingerPrint,waf防护规则其实就是整理提取一些常见的非浏览器客户端requests,curl的指纹然后在客户端发起https请求时进行识别并拦截。
https://tls.browserleaks.com/json 这个接口可以查看你的指纹信息。
其中的akamai_hash 是HTTP2 的指纹,因为我用的http1 访问,所以没有。在HTTP2 中,
绕过的方法也很简单,调整一下Ciphers顺序或者删除、添加一些不重要的参数就可以。
Node.js 14 中的默认密码集是:
> require('crypto').constants.defaultCoreCipherList.split(':') [ 'TLS_AES_256_GCM_SHA384', 'TLS_CHACHA20_POLY1305_SHA256', 'TLS_AES_128_GCM_SHA256', 'ECDHE-RSA-AES128-GCM-SHA256', 'ECDHE-ECDSA-AES128-GCM-SHA256', 'ECDHE-RSA-AES256-GCM-SHA384', 'ECDHE-ECDSA-AES256-GCM-SHA384', 'DHE-RSA-AES128-GCM-SHA256', 'ECDHE-RSA-AES128-SHA256', 'DHE-RSA-AES128-SHA256', 'ECDHE-RSA-AES256-SHA384', 'DHE-RSA-AES256-SHA384', 'ECDHE-RSA-AES256-SHA256', 'DHE-RSA-AES256-SHA256', 'HIGH', '!aNULL', '!eNULL', '!EXPORT', '!DES', '!RC4', '!MD5', '!PSK', '!SRP', '!CAMELLIA' ]
const tls = require('tls'); const https = require('https'); const defaultCiphers = tls.DEFAULT_CIPHERS.split(':'); const shuffledCiphers = [ defaultCiphers[0], // Swap the 2nd & 3rd ciphers: defaultCiphers[2], defaultCiphers[1], ...defaultCiphers.slice(3) ].join(':'); request = require('https').get('https://en.zalando.de/api/navigation', { ciphers: shuffledCiphers }).on('response', (res) => { console.log(res.statusCode); // Prints 200 });
当然,很多用户还会使用proxy 来访问目标网站,这需要注意。
使用第三方代理更换 IP 地址时,TLS 指纹的变化取决于代理服务器的配置和实现方式。以下是一些可能的情况:
- 透明代理:
- 如果代理服务器只是简单地转发请求而不修改任何 TLS 参数,那么您的客户端 TLS 配置(包括加密套件、TLS 版本等)仍然会被服务器看到,TLS 指纹不会变化。
- HTTPS 代理:
- 如果代理服务器终止了 TLS 连接并与目标服务器重新建立一个新的 TLS 连接(即代理服务器作为中间人重新发起请求),那么代理服务器的 TLS 配置将决定新的 TLS 指纹。在这种情况下,您的客户端 TLS 配置不会被目标服务器看到,TLS 指纹会变化。
- SOCKS 代理:
- 如果您使用的是 SOCKS 代理,TLS 连接通常会直接从您的客户端到达目标服务器,因此您的客户端 TLS 配置仍然会被目标服务器看到,TLS 指纹不会变化。
const axios = require('axios'); const HttpsProxyAgent = require('https-proxy-agent'); const https = require('https'); const cipherSuites = [ 'ECDHE-RSA-AES128-GCM-SHA256', 'ECDHE-RSA-AES256-GCM-SHA384', 'ECDHE-ECDSA-AES128-GCM-SHA256', 'ECDHE-ECDSA-AES256-GCM-SHA384', 'ECDHE-RSA-AES128-SHA', 'ECDHE-RSA-AES256-SHA', 'ECDHE-ECDSA-AES128-SHA', 'ECDHE-ECDSA-AES256-SHA', 'DHE-RSA-AES128-GCM-SHA256', 'DHE-RSA-AES256-GCM-SHA384', 'DHE-RSA-AES128-SHA', 'DHE-RSA-AES256-SHA', 'AES128-GCM-SHA256', 'AES256-GCM-SHA384', 'AES128-SHA', 'AES256-SHA', 'DES-CBC3-SHA', 'RSA+AESGCM', 'RSA+AES', 'RSA+SHA', ]; const tlsVersions = ['TLSv1.2', 'TLSv1.3']; function getRandomElement(arr) { return arr[Math.floor(Math.random() * arr.length)]; } function createHttpsAgent() { const selectedCipher = getRandomElement(cipherSuites); const selectedTlsVersion = getRandomElement(tlsVersions); return new https.Agent({ ciphers: selectedCipher, minVersion: selectedTlsVersion, maxVersion: selectedTlsVersion, }); } async function makeRequest(url, options = {}) { const agent = createHttpsAgent(); // 使用代理地址 const proxy = 'http://proxy.example.com:8080'; const proxyAgent = new HttpsProxyAgent(proxy); proxyAgent.options = { ...proxyAgent.options, ...agent.options, }; const config = { ...options, httpsAgent: proxyAgent, timeout: options.timeout || 5000, // 默认超时时间为5秒 headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', // 默认User-Agent ...options.headers, }, }; try { const response = await axios.get(url, config); return response.data; } catch (error) { if (error.response) { // 服务器返回的错误 console.error(`Error: ${error.response.status} ${error.response.statusText}`); console.error(`Response data: ${error.response.data}`); } else if (error.request) { // 请求发送但没有收到响应 console.error('Error: No response received from the server'); console.error(error.request); } else { // 其他错误 console.error('Error:', error.message); } throw error; // 重新抛出错误,以便调用者可以处理 } } // 示例用法 (async () => { const urls = [ 'https://example.com', 'https://another-example.com', // 添加更多URL进行测试 ]; for (const url of urls) { try { const data = await makeRequest(url, { headers: { 'Custom-Header': 'HeaderValue' }, }); console.log(`Response from ${url}:`, data); } catch (error) { console.error(`Failed to fetch ${url}:`, error.message); } } })();