Table of contents
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ó.
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à:
Tạo Certificate Signing Request (CSR) và gửi lên Server.
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ề privateKey
và cert
.
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 cert
và privateKey
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
, friendlyName
và storePlace
.
Ở dòng 130, CreatePFX()
sẽ tạo pfx message với các thông tin từ certBase64
và friendlyName
. 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:
rawData
là pfx message đã được base64 decodeprivateKey
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.CSRPrivateKey
hay 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 message và privateKey (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 File → Open… 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 dnSpy → Debug → Attach 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à text
và privateKey
.
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 text
và privateKey
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 Settings → Network → TLS 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
)