web容器獲取SSL指紋實現和ByPass

前言

前段時間對SSL指紋的獲取實現很感興趣,從表面到深入再到實現讓我更加深刻理解SSL設計。 本篇介紹:

  • SSL指紋在web容器(Kestrel)下如何獲取,並實現一個Middleware來很方便集成到web工程裏面(下文附源碼地址)。
  • 解析ClientHello的套路以及如何生成SSL指紋
  • 測試不同的客戶端的SSL指紋(java curl Fiddler python csharp chrome edge)

本次對SSL指紋的研究就算是完結篇了, 本次系列

  1. 從SSL的理解誤區走出
  2. SSL證書的作用表現
  3. 正文開始

先來說說這個SSL指紋用來幹嘛

  • waf裏面一般會有用到
  • 說實話在目前的話也只能幹干script Boy

舉個例子,之前看過說為什麼同樣的請求地址一樣的參數 一樣的httpHeader,在瀏覽器訪問就正常,用python發送的就會被waf攔截

正常用戶訪問: image [截圖出自ParrotSecurity的A佬]

script Boy 用Python 發 Request,直接就被waf攔截了

image

[截圖出自ParrotSecurity的A佬]

類似這樣的求問帖有很多

結論就是 python的tls握手有特徵,被waf識別到獨特的指紋了!

image

SSL指紋識別原理

巨人的肩膀在這裡://github.com/salesforce/ja3

image

就是解析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請求時進行識別並攔截!

動手實踐

本次技術實現基於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 points
        ec_point_formats = 11, // Elliptic curve point formats
        srp = 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進行解析

image

struct枚舉 我從Go SDK的tls直接複製過來用的

image

image

GoSDK裏面的注釋很詳細 RCF的相關也在注釋裏面,點贊!

這部分代碼有點多,我都放在了我的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 points
    if (_supportedgroups != null)
    {
        sb.Append(string.Join("-", _supportedgroups.Select(r => (int)r)));
    }
    sb.Append(",");
    // Elliptic curve point formats
    if (_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 有辦法??當然可以

可以私信交流

 

 

我是正東,學的越多不知道也越多。這個公眾號是我的實驗小天地,我會分享一些我開源的工具(歡迎你來提意見),好玩好用的新技術。如果你也和我一樣喜歡折騰技術請關注 !

Tags: