Bypass SSL/TLS Client Authentication

Deep-dive Analysis

Trong quá trình pentest dự án X thì nhóm mình có gặp một case study khá hay về SSL/TLS Client Authentication. Do ứng dụng này thuộc lĩnh vực healthcare nên phía dev ngoài sử dụng phương pháp authentication truyền thống thì họ còn sử dụng Client-Certificate based authentication. Điều này tức là để đăng nhập vào ứng dụng, ngoài việc biết được username và mật khẩu thì máy tính sử dụng cũng phải được “tin tưởng”. Họ yêu cầu users phải cài Client Certificate lên máy tính cá nhân thông qua một ứng dụng khác là applicationY.exe

Client Certificate này được dùng để xác minh danh tính với phía Server, xác định xem máy tính đó có “tin tưởng” hay không.

Theo như mô tả về nghiệp vụ của ứng dụng thì Certificate này sẽ chỉ được cài lên những máy tính nội bộ. Việc này khiến cho attacker dù biết được username và mật khẩu cũng không thể đăng nhập được nếu như không sử dụng các máy tính kể trên. Tuy nhiên thì cách này cũng không đảm bảo an toàn tuyệt đối, attacker có thể bằng một cách nào đó export Certificate trên ra và cài vào máy tính của mình. Đây cũng chính là vấn đề mà nhóm mình gặp phải.

Mô tả vấn đề

Client Certificate giống như Server Certificate, được trao đổi trong quá trình TLS handshake. Server sau khi gửi Server Certificate thì sẽ yêu cầu phía client gửi Client Certificate để xác minh danh tính. Quá trình này còn có tên gọi khác như mutual authentication hay 2-way-SSL, khi mà cả Server và Client đều phải xác minh danh tính của đối phương.

Tuy nhiên vấn đề sẽ phát sinh khi setup đi qua Burp Proxy để pentest. Do Burp Proxy đóng vai trò trung gian giữa Client và Server nên kết nối TLS client-server liền mạch sẽ bị tách thành 2 nửa. Nửa đầu từ Client đến Burp Proxy và nửa sau từ Burp Proxy đến Server.

Kết nối TLS bị gián đoạn nên quá trình Client Authentication cũng bị gián đoạn theo. Server lúc này sẽ yêu cầu Burp Proxy chứ không phải Client gửi Client Certificate. Để xử lý những trường hợp thế này thì Burp Suite hỗ trợ add Client TLS Certificates trong User options.

Burp hỗ trợ import Certificate dạng PKCS#12, hiểu nôm na PKCS#12 gộp Certificate với Private Key tương ứng rồi nén lại (thường là nén sử dụng password để bảo vệ Private Key). PKCS#12 thường có đuôi là .pfx hoặc .p2.

Khi import/export Client Certificate thì Private Key có vai trò vô cùng quan trọng. Nó dùng để chứng minh chủ thể là chủ sở hữu của Certificate tương ứng, nếu không, bất cứ ai có được Certificate đều có thể clone lại nó.

This proof is done by the client creating a signature over previous handshake messages using its private key and sending this signature inside the CertificateVerify message. This signature can be verified by the server using the public key from the clients certificate the client has send before in the Certificate message.

Nhiệm vụ lúc này sẽ là export Certificate kèm Private Key tương ứng để import vào Burp Suite. certmgr.msc trên Windows hỗ trợ việc này.

Tuy nhiên thì không thể export Private Key được 🥲🥲🥲

Như có đề cập ở trên, Private Key dùng để chứng minh chủ thể là chủ sở hữu của Certificate tương ứng. Nếu như không có Private Key thì Certificate này gần như vô dụng, vì nó không thể giúp xác minh danh tính của Client → Không thể truy cập ứng dụng.

Dưới dây là request đăng nhập khi đi qua Burp Proxy mà không có Certificate hợp lệ.

Không export được Certificate kèm Private Key tương ứng → Không sử dụng Burp được.

Phân tích vấn đề

Do không thể export được Certificate kèm Private Key theo cách truyền thống nên chúng ta sẽ phải debug applicationY.exe (ứng dụng mà cài Client Certificate lên máy tính người dùng) để xem nó thực hiện nhứng tác vụ gì.

Có thể tóm tắt luồng hoạt động của applicationY.exe gồm 2 giai đoạn chính là:

  1. Tạo Certificate Signing Request (CSR) và gửi lên Server.

  2. Sau khi Server validate các thông tin thì sẽ trả về Client Certificate, ứng dụng sẽ cài Client Certificate này lên máy tính người dùng.

Tạo CSR

Tại giai đoạn này thì method CertificateServices.ProcessCSRRequest()sẽ được gọi:

Method này sẽ gọi tới CsrCreator.CreateCSR()để tạo một Certificate Signing Request mới:

Code đoạn này khá dài tuy nhiên chúng ta chỉ cần quan tâm là method này trả về privateKeycert.

privateKey ở đây chỉ là cách dev đặt tên biến, không hề liên quan gì đến Private Key được đề cập ở các phần trên

Set breakpoint ở đây, chúng ta sẽ lấy được giá trị của certprivateKey như sau:

cert = "MIICH...[Omitted]..."
privateKey = "RUNTM...[Omitted]..."

Quay trở lại với CertificateServices.ProcessCSRRequest():

Lúc này cert được format theo CertreqConst.CertreqContent tạo thành message với CertreqContent có dạng:

public static readonly string CertreqContent = "-----BEGIN NEW CERTIFICATE REQUEST-----\r\n{0}-----END NEW CERTIFICATE REQUEST-----\r\n";
message = "-----BEGIN NEW CERTIFICATE REQUEST-----\r\nMIICH...[Omitted]...-----END NEW CERTIFICATE REQUEST-----\r\n"

message sau đó sẽ được gửi lên server. Ngoài ra privateKey ở trên cũng được gán cho client.CipherInfo.CSRPrivateKey.

Cài Client Certificate lên máy

Sau khi Server hoàn thành việc validate CSR thì sẽ gửi về Client Certificate để cài lên máy tính của người dùng. Đoạn này method CertificateServices.InstallCertification()sẽ được gọi:

Method này sẽ cài 3 Certificate hay còn gọi là Chain of Trust lên máy người dùng. Tuy nhiên Certificate cũng như dòng code mà chúng ta quan tâm chỉ là:

bool flag7 = !CertificateManager.InstallCertification(text, client.CipherInfo.CSRPrivateKey, client.HospitalName, StoreName.My);

Đoạn này gọi đến method CertificateManager.InstallCertification():

Input của method này bao gồm certBase64, privateKey, friendlyNamestorePlace.

Ở dòng 130, CreatePFX() sẽ tạo pfx message với các thông tin từ certBase64friendlyName. privateKey sẽ đóng vai trò là password của pfx message này (Xem thêm tài liệu của Microsoft về method CreatePFX tại đây).

Sau đó, một instance mới của class X509Certificate2 sẽ được khởi tạo thông qua constructor new X509Certificate2(). Input của constructor này bao gồm:

  • rawDatapfx message đã được base64 decode

  • privateKey là password để access pfx message

([Xem thêm tài liệu của Microsoft về X509Certificate2 Constructors tại đây](docs.microsoft.com/en-us/dotnet/api/system...)

Instance này thực chất là một X.509 certificate và sau đó sẽ được cài lên máy tính người dùng (dòng 134).

Quay trở lại dòng code tại CertificateServices.InstallCertification():

bool flag7 = !CertificateManager.InstallCertification(text, client.CipherInfo.CSRPrivateKey, client.HospitalName, StoreName.My);

Lúc này:

certBase64    ←   text
privateKey    ←   client.CipherInfo.CSRPrivateKey
friendlyName  ←   client.HospitalName
storePlace    ←   StoreName.My

Vậy, method CertificateServices.InstallCertification() sẽ nhận dữ liệu từ Server, kết hợp với dữ liệu từ Client để tạo thành pfx messange, password của pfx messange đó là client.CipherInfo.CSRPrivateKey, pfx message này sau đó được import vào máy tính người dùng. Có 2 điểm cần chú ý là:

  • pfx message này chính là file pfx mà chúng ta cần. Thay vì để import vào máy tính người dùng thì để import vào Burp.

  • password của pfx messange là client.CipherInfo.CSRPrivateKeyhay chính là privateKey mà app gen ra ở phần trên.

Giải quyết vấn đề

Sau khi phân tích ở trên thì có 2 thứ mà chúng ta cần phải lấy được là pfx messageprivateKey (a.k.a password của pfx message). Tất cả các thông tin này đều gói gọn trong 1 dòng code tại method CertificateManager.InstallCertification():

string s = cx509Enrollment.CreatePFX(privateKey, PFXExportOptions.PFXExportChainWithRoot, EncodingType.XCN_CRYPT_STRING_BASE64);
s = "MIILH...[Omitted]..."
privateKey = "RUNTM...[Omitted]..."

Đầu tiên cần xử lý biến s để tạo thành pfx message:

Xử lý các kí tự CRLF (\r\n):

MIILHwIBAzCCCtsGCSqGSIb3DQEHAaCCCswEggrIMIIKxDCCAc0GCSqGSIb3DQEH
AaCCAb4EggG6MIIBtjCCAbIGCyqGSIb3DQEMCgECoIHMMIHJMBwGCiqGSIb3DQEM
AQMwDgQIu2o7Ku+CEMECAgfQBIGo6qiseo5M6BjdoiqhZqJlUQKaltxQ+z8CWf75
..........................[Omitted].............................
1OyepwejMDswHzAHBgUrDgMCGgQUKDHTB7W/vNwhUWaaAKDHUwqVeFwEFO7y8mo/
JYEinRglSqg8I43Uc703AgIH0A==

Sau đó base64 decode tạo thành file sas.pfx. Lúc này có thể dùng privateKey ở trên làm password để import/export cert:

from OpenSSL import crypto

pfxPassword = "RUNTMSAAAAAa2fvbKt8JurFRsLY.....\r\n.....\r\n"

# Load file pfx
p12 = crypto.load_pkcs12(open('./sas.pfx', 'rb').read(), pfxPassword.encode())

# Dump private key
print(crypto.dump_privatekey(crypto.FILETYPE_PEM, p12.get_privatekey()).decode('utf-8'))
PS C:\Users\c1nd3rell4\Desktop\Final> python dumb_privatekey.py
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgCMpoyOKoo1Ak52A4
...[Omitted]...
-----END PRIVATE KEY-----

Tuy nhiên khi import file sas.pfx vào Burp và sử dụng mật khẩu là privateKey, ta sẽ nhận được thông báo lỗi “keystore password was incorrect”:

Lý giải cho điều này là do password chứa các kí tự CRLF (\r\n), code sẽ xử lý các kí tự này như các kí tự điều khiển, còn Burp xử lý chúng như các kí tự bình thường → sai password.

Để giải quyết vấn đề này thì cần đổi password của file pfx thành một chuỗi string bình thường. Một trong những cách đổi password là tạo lại file pfx mới cùng với password mới.

Nhóm mình sử dụng module chilkat của python để thực hiện việc này:

import sys
import chilkat

cert = chilkat.CkCert()

pfxFilename = "./sas.pfx"
pfxPassword = "RUNTMSAAAAAa2fvbKt8JurFRsLY.....\r\n.....\r\n"

# Load file pfx
success = cert.LoadPfxFile(pfxFilename,pfxPassword)
if (success != True):
    print(cert.lastErrorText())
    sys.exit()

# Create new file pfx and new password
cert.ExportToPfxFile('sas_new_cert.pfx','god_sonqh2',False)

Đoạn code trên sẽ tạo file pfx mới có tên là sas_new_cert.pfx với password là god_sonqh2. Import file cert mới vào Burp:

Request đăng nhập trước khi import cert:

Request đăng nhập sau khi import cert:

Step-by-Step Solution

Bước 1: Cài đặt các công cụ cần thiết

  • Cài đặt ứng dụng ClientAuthentication.exe. Chỉ thực hiện việc cài app, chưa thực hiện việc cài Client Certification.

  • Cài đặt ứng dụng dnSpy bản 32 bit.

  • Cài đặt module Chilkat cho Python.

Bước 2: Setup dnSpy

Khởi động dnSpy, chọn FileOpen… và duyệt tới thư mục ứng dụng ClientAuthentication.exe

Chọn file Domain.dll và ấn Open

Tìm đến class CertificateManager và method InstallCertification() giống như sau:

public static bool InstallCertification(string certBase64, string privateKey, string friendlyName, StoreName storePlace)

Tìm đến dòng code sử dụng method CreatePFX() và đặt breakpoint tại dòng này:

string text = cx509Enrollment.CreatePFX(privateKey, PFXExportOptions.PFXExportChainWithRoot, EncodingType.XCN_CRYPT_STRING_BASE64);

Bước 3: Debug và lấy các thông tin cần thiết

Khởi động ClientAuthentication.exe, sau đó vào dnSpyDebugAttach to Process… chọn process ClientAuthentication.exe và ấn Attach.

Truy cập trang web cài Client Certificate và tiến hành cài như bình thường

Quay trở lại dnSpy, khi breakpoint được toggle thì ấn F11 (Step into)

2 giá trị cần lấy để tạo Client Certificate mới là textprivateKey.

privateKey = "RUNTM...[Omitted]..."
text = "MIILNw...[Omitted]..."

Bước 4: Tạo Client Certificate mới

Sử dụng đoạn script sau để tạo Client Certificate mới với textprivateKey lấy ở trên:

import argparse, os, base64, sys, chilkat

# Parser
argParser = argparse.ArgumentParser()
argParser.add_argument("-m", "--pfxmsg", type=str, required=True,
                       help="Personal Information Exchange (PFX) message gathered during debugging.")
argParser.add_argument("-p", "--password", type=str, required=True,
                       help="Password for the PFX message gathered during debugging.")
argParser.add_argument("-np", "--newPassword", type=str, required=False, default="Sas@2023",
                       help="New password for the new PFX message (Default is 'Sas@2023').")
args = argParser.parse_args()

# Handle pfx message and write to file
message = args.pfxmsg.encode('utf8').decode('unicode_escape')
password = args.password.encode('utf8').decode('unicode_escape')
data = base64.b64decode(message)

f = open('./temp.pfx','wb')
f.write(data)
f.close()

# Load pfx file
cert = chilkat.CkCert()
success = cert.LoadPfxFile("./temp.pfx", password)
if (success != True):
    print(cert.lastErrorText())
    sys.exit()

# Create new file pfx with new password
cert.ExportToPfxFile('SAS.pfx', args.newPassword, False)
os.unlink("./temp.pfx")
PS C:\Users\c1nd3rell4\Desktop> python genNewCert.py -h
usage: genNewCert.py [-h] -m PFXMSG -p PASSWORD [-np NEWPASSWORD]

options:
  -h, --help            show this help message and exit
  -m PFXMSG, --pfxmsg PFXMSG
                        Personal Information Exchange (PFX) message gathered during debugging.
  -p PASSWORD, --password PASSWORD
                        Password for the PFX message gathered during debugging.
  -np NEWPASSWORD, --newPassword NEWPASSWORD
                        New password for the new PFX message (Default is 'Sas@2023').
PS C:\Users\c1nd3rell4\Desktop> python genNewCert.py -m "MIIL..." -p "RUNT.."

Bước 5: Import Cert vào Burp Suite

Truy cập SettingsNetworkTLS và kéo xuống phần Client TLS certificates

Chọn Add → Nhập Destination host → Chọn file pfx vừa tạo và nhập password (mặc định là Sas@2023)