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);
}
}
})();