前言
前段时间对SSL指纹的获取实现很感兴趣,从表面到深入再到实现让我更加深刻理解SSL设计。
本篇介绍:
SSL指纹在web容器(Kestrel)下如何获取,并实现一个Middleware来很方便集成到web工程里面(下文附源码地址)。
解析ClientHello的套路以及如何生成SSL指纹
测试不同的客户端的SSL指纹(java curl Fiddler python csharp chrome edge)
本次对SSL指纹的研究就算是完结篇了,
本次系列
从SSL的理解误区走出
SSL证书的作用表现
正文开始
先来说说这个SSL指纹用来干嘛
waf里面一般会有用到
说实话在目前的话也只能干干script Boy
举个例子,之前看过说为什么同样的请求地址一样的参数 一样的httpHeader,在浏览器访问就正常,用python发送的就会被waf拦截
正常用户访问:
[截图出自ParrotSecurity的A佬]
script Boy 用Python 发 Request,直接就被waf拦截了
[截图出自ParrotSecurity的A佬]
类似这样的求问帖有很多
https://stackoverflow.com/questions/60407057/python-requests-being-fingerprinted
https://stackoverflow.com/questions/63343106/how-to-avoid-request-fingerprinted-in-python
结论就是 python的tls握手有特征,被waf识别到独特的指纹了!
SSL指纹识别原理
巨人的肩膀在这里:https://github.com/salesforce/ja3
就是解析TLS握手客户端发送的ClientHello报文并获取
SSLVersion 版本
Cipher 客户端支持的加密套件
SSLExtention SSL的扩展内容集合
EllipticCurve SSL的扩展内容里面的【supported_groups】(CurveP256,CurveP384,CurveP521,X25519)
EllipticCurvePointFormat SSL的扩展参数里面的【sec_point_formats】(uncompressed,ansiX962_compressed_prime,ansiX962_compressed_char2)
把上面解析出来的版本,加密套件,扩展等内容按顺序排列然后计算hash值,便可得到一个客户端的TLS FingerPrint,waf防护规则其实就是整理提取一些常见的非浏览器客户端requests,curl的指纹然后在客户端发起https请求时进行识别并拦截!
同一个客户端的报文对比,除了Ramdom和SessionId有变化其他不变
不同的客户端的报文对比就各式各样了
动手实践
本次技术实现基于aspnet5.0,web容器是微软为aspnetcore打造的高性能Kestrel!
得益于Kestrel的中间件设计,我们可以很容易的
在配置Kestrel的时候指定自己的中间件去拦截ClientHello
(感谢微软大神davidfowl的指点)
webBuilder.UseKestrel(options =>
{var logger = options.ApplicationServices.GetRequiredService<ILogger<Program>>();options.ListenLocalhost(5002, listenOption =>{var httpsOptions = new HttpsConnectionAdapterOptions();//本地测试证书var serverCert = new X509Certificate2("server.pfx", "1234");httpsOptions.ServerCertificate = serverCert;//注册tls拦截中间件listenOption.Use(async (connectionContext, next) =>{await TlsFilterConnectionMiddlewareExtensions.ProcessAsync(connectionContext, next, logger);});listenOption.UseHttps(httpsOptions);});
});
接下来就是在我们自定义的中间件做解析
public static async Task ProcessAsync(ConnectionContext connectionContext, Func<Task> next, ILogger<Program> logger)
{var input = connectionContext.Transport.Input;var minBytesExamined = 0L;while (true){var result = await input.ReadAsync();var buffer = result.Buffer;if (result.IsCompleted){return;}if (buffer.Length == 0){continue;}//开启处理ClientHello报文if (!TryReadHello(buffer, logger, out var abort)){minBytesExamined = buffer.Length;input.AdvanceTo(buffer.Start, buffer.End);continue;}//上面我们读了流这里要归位var examined = buffer.Slice(buffer.Start, minBytesExamined).End;input.AdvanceTo(buffer.Start, examined);if (abort){// Close the connection.return;}break;}await next();
}
解析ClientHello报文
private static bool TryReadHello(ReadOnlySequence<byte> buffer, ILogger logger, out bool abort)
{abort = false;if (!buffer.IsSingleSegment){throw new NotImplementedException("Multiple buffer segments");}var data = buffer.First.Span;TlsFrameHelper.TlsFrameInfo info = default;if (!TlsFrameHelper.TryGetFrameInfo(data, ref info)){return false;}//解析的版本logger.LogInformation("Protocol versions: {versions}", info.SupportedVersions);//解析客户端请求的Host//这里有一个小技巧,waf防御的一个简单的ByPass手段就是绕过域名直接访问Ip进行访问,如果服务端在这里增加一个Host白名单,就能防止绕过。logger.LogInformation("SNI: {host}", info.TargetName);//其他字段省略Console.WriteLine("ClientHello=>" + info);return true;
}
ClientHello报文解析
解析报文没啥特别的,就是根据RCF文档,:
public enum ExtensionType : ushort{server_name = 0,max_fragment_length = 1,client_certificate_url = 2,trusted_ca_keys = 3,truncated_hmac = 4,status_request = 5,user_mapping = 6,client_authz = 7,server_authz = 8,cert_type = 9,supported_groups = 10,// Elliptic curve pointsec_point_formats = 11, // Elliptic curve point formatssrp = 12,signature_algorithms = 13,use_srtp = 14,heartbeat = 15,application_layer_protocol_negotiation = 16,status_request_v2 = 17,signed_certificate_timestamp = 18,client_certificate_type = 19,server_certificate_type = 20,padding = 21,encrypt_then_mac = 22,extended_master_secret = 23,token_binding = 24,cached_info = 25,tls_lts = 26,compress_certificate = 27,record_size_limit = 28,pwd_protect = 29,pwd_clear = 30,password_salt = 31,session_ticket = 35,pre_shared_key = 41,early_data = 42,supported_versions = 43,cookie = 44,psk_key_exchange_modes = 45,certificate_authorities = 47,oid_filters = 48,post_handshake_auth = 49,signature_algorithms_cert = 50,key_share = 51,renegotiation_info = 65281}
tips:解析Extention的套路:
byte数组 前2个byte为长度 后面的是内容 然后根据RFC的struct进行解析
struct枚举 我从Go SDK的tls直接复制过来用的
GoSDK里面的注释很详细 RFC的相关也在注释里面,点赞!
这部分代码有点多,我都放在了我的github,想研究的可以
点击查看
最后就是按照原理将数据进行拼接在md5生成SSL指纹
public string getSig()
{StringBuilder sb = new StringBuilder();//版本sb.Append((int)Header.Version);sb.Append(",");//加密套件if (_ciphers != null){sb.Append(string.Join("-", _ciphers.Select(r => (int)r)));}sb.Append(",");//SSL扩展字段if (_extensions != null){sb.Append(string.Join("-", _extensions.Select(r => (int)r)));}sb.Append(",");//Elliptic curve pointsif (_supportedgroups != null){sb.Append(string.Join("-", _supportedgroups.Select(r => (int)r)));}sb.Append(",");// Elliptic curve point formatsif (_ecPointFormats != null){sb.Append(string.Join("-", _ecPointFormats.Select(r => (int)r)));}String str = sb.ToString();using var md5 = MD5.Create();var result = md5.ComputeHash(Encoding.ASCII.GetBytes(str));var strResult = BitConverter.ToString(result);//和其他语言的实现保持一致var sig = strResult.Replace("-", "").ToLower();return sig;
}
精彩时刻 来试试效果
用不同的客户端来测试下看看收集的指纹
1. chrome
指纹:
192,
0-4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,
0-23-65281-10-11-35-16-5-13-18-51-45-43-27-21,
29-23-24,
0
2. edge
指纹:
192,
0-4865-4866-4867-49195-49199-49196-49200-52393-52392-49171-49172-156-157-47-53,
0-23-65281-10-11-35-16-5-13-18-51-45-43-27-17513-21,
29-23-24,
0
新版edge也是用的chromium的内核,Extention扩展多了一个17513
3. csharp的HttpClient
指纹:
3072,
49196-49195-49200-49199-159-158-49188-49187-49192-49191-49162-49161-49172-49171-157-156-61-60-53-47-10,
0-10-11-13-35-23-65281,
29-23-24,
0
4. Fiddler
指纹:
3072,
49196-49195-49200-49199-159-158-49188-49187-49192-49191-49162-49161-49172-49171-157-156-61-60-53-47-10,
0-10-11-13-35-23-65281,
29-23-24,
0
因为Fiddler是csharp写的,应该用的都是微软的封装的ssl实现吧。
所以和csharp的HttpClient是一样的指纹。
5. java JDK自带的HttpsURLConnection
指纹:
3072,
49187-49191-60-49189-49193-103-64-49161-49171-47-49156-49166-51-50-49195-49199-156-49197-49201-158-162-49160-49170-10-49155-49165-22-19-255,
10-11-13,
23-1-3-19-21-6-7-9-10-24-11-12-25-13-14-15-16-17-2-18-4-5-20-8-22,
0
明显可以看出来 EllipticCurve 多了很多!
6. Apache HttpClient
指纹:
3072,
49188-49192-61-49190-49194-107-106-49162-49172-53-49157-49167-57-56-49187-49191-60-49189-49193-103-64-49161-49171-47-49156-49166-51-50-49196-49195-49200-157-49198-49202-159-163-49199-156-49197-49201-158-162-255,
10-11-13-23,
23-24-25,
0
相比上面几个 在 EllipticCurve 上面有明显不一样!
7. curl
指纹:
192,
4866-4867-4865-49196-49200-159-52393-52392-52394-49195-49199-158-49188-49192-107-49187-49191-103-49162-49172-57-49161-49171-51-157-156-61-60-53-47-255,
0-11-10-13172-16-22-23-49-13-43-45-51-21,
29-23-30-25-24,
0-1-2
8. python3的Request
指纹:
192,
4866-4867-4865-49196-49200-49195-49199-52393-52392-163-159-162-158-52394-49327-49325-49188-49192-49162-49172-49315-49311-107-106-57-56-49326-49324-49187-49191-49161-49171-49314-49310-103-64-51-50-157-156-49313-49309-49312-49308-61-60-53-47-255,
0-11-10-35-22-23-13-43-45-51-21,
29-23-30-25-24,
0-1-2
哈哈,实践是检验真理的唯一标准,
不难看出来为什么阿里的waf这么容易就能干掉curl 和 python脚本
ByPass 有办法??当然可以
可以私信交流
我是正东,学的越多不知道也越多。如果决定去深究一个东西, 一定要完全搞懂, 并认真总结一篇博客让以后能在短时间拾起来 ( 因为不搞懂你很难写一篇半年后还能理解的博客 )
欢迎白嫖点赞!