Một tệp tin 2.835 byte bên trong ứng dụng chính phủ với hơn 10 triệu lượt cài đặt chứa chứng chỉ client còn hiệu lực của Ngân hàng Quốc gia Ả Rập Xê Út. Phải mất một dòng tweet lan truyền mới có người chịu xem xét nó.
Tóm tắt
Ứng dụng Nusuk chính thức (com.moh.nusukapp, Bộ Hajj và Umrah, hơn 10 triệu lượt cài đặt, mang huy hiệu "Chính phủ" của Google Play) đã phát hành một tệp PKCS#12 chứa khóa RSA riêng tư và chứng chỉ client do Ngân hàng Quốc gia Ả Rập Xê Út cấp. Mật khẩu cho tệp đó được mã hóa cứng cách đó vài dòng trong chính mã nguồn của ứng dụng. Nó chỉ là một ký tự duy nhất: 2.
Bên cạnh đó, ở dạng văn bản thuần túy, là ID client OAuth2 và client secret cho API Ngân hàng như một Dịch vụ (Banking-as-a-Service) của ngân hàng, yêu cầu các phạm vi identity accounts cards verification kyc cardpay transfers.
Bất kỳ ai tải ứng dụng từ Google Play đều có tất cả những thông tin này.
Tôi đã cố gắng báo cáo. Tôi được thông báo rằng cổng thông tin lỗ hổng chỉ khả dụng cho người dùng ở Ả Rập Xê Út. Vì vậy, tôi đã đăng tweet về nó. Tweet đã đạt 1,5 triệu lượt xem, và đột nhiên cùng một cổng thông tin đó yêu cầu chi tiết. Một ngày sau, thông tin xác thực đã biến mất khỏi ứng dụng.
https://x.com/iam_zachi/status/2094445016194207745
Bài viết này chỉ đề cập đến phát hiện về ngân hàng. Mọi thứ bên dưới đã được sửa trong ứng dụng đang phát hành, và phía nhà cung cấp xác nhận thông tin xác thực đã được thay đổi.
Nusuk là gì
Nusuk là nền tảng chính thức của chính phủ Ả Rập Xê Út dành cho Hajj và Umrah. Nó xử lý giấy phép hành hương, thị thực điện tử, đặt chỗ và Thẻ Nusuk. Nó được vận hành bởi Bộ Hajj và Umrah, được gắn cờ là ứng dụng chính phủ đã xác minh trên Google Play và có hơn mười triệu lượt cài đặt. Nó cũng chứa một tính năng ví, Ví Nusuk, được xây dựng cùng với Ngân hàng Quốc gia Ả Rập Xê Út và được SAMA, ngân hàng trung ương Ả Rập Xê Út, phê duyệt. Ví là phần mà bài viết này đề cập đến.
Phát hiện
Tôi đã tải bộ APK trực tiếp từ Google Play (phiên bản 17.4.9, versionCode 131215) và giải nén nó. Không có gì đặc biệt: Kotlin/Compose tiêu chuẩn, không có packer, không có sự che giấu mã đáng kể.
Bên trong tài nguyên của ứng dụng, tại res/raw/nusuk.pfx, có một vùng chứa PKCS#12 có kích thước 2.835 byte. Tệp .pfx là định dạng gói tiêu chuẩn cho chứng chỉ cùng với khóa riêng tư của nó, được mã hóa bằng mật khẩu.
Mật khẩu nằm trong mã nguồn của ứng dụng, cách vài dòng so với nơi tệp được tải:
1const-string v3, "2"
Một ký tự, nằm trong mã bytecode đã được dịch ngược dưới dạng một giá trị chữ. Một lệnh gọi openssl sau đó:
1RSA private key, 2048 bit, 2 prime factors2Subject: C=SA, ST=Jeddah, L=Jeddah, O=Nusuk.sa, CN=eshbeata@staq.io3Issuer: C=SA, ST=Riyadh, L=Riyadh, O=The Saudi National Bank, OU=Finto,4 CN=Application Issuer5Serial: 0x24 (36)6Valid: 2026-04-27 -> 2027-04-277Extended Key Usage (critical): TLS Web Client Authentication
Một chứng chỉ client còn hiệu lực do ngân hàng cấp, còn giá trị thêm một năm nữa, dùng để xác thực client với máy chủ qua TLS.
Nó không đơn độc. Cùng một đường dẫn mã là thành phần ví, được đóng gói dưới dạng com.walletstaq và kết nối với Trustless SDK từ Staq Technologies, công ty vận hành nền tảng Finto BaaS của SNB. Ba giá trị nữa nằm ở đó ở dạng văn bản rõ:
- CLIENT_ID = 379cc899…
- CLIENT_SECRET = ELrfNe9w… (64 byte thô, được mã hóa base64)
- SERVER_URL = https://api.baas.alahli.com/api/
với phạm vi OAuth được yêu cầu:
1identity accounts cards verification kyc cardpay transfers
Cả hai thông tin xác thực cũng được nối thành một chuỗi duy nhất và được chuyển cho một trình ghi log gỡ lỗi, cùng với URL cơ sở.
(Tôi không công bố tài liệu khóa hoặc các giá trị bí mật đầy đủ. Điều quan trọng ở đây là hình dạng của vấn đề.)
Tại sao điều này tồi tệ, theo cách nói đơn giản
Hãy nghĩ về API của ngân hàng như một cánh cửa với hai ổ khóa.
Ổ khóa đầu tiên là mutual TLS. Thông thường, máy chủ chứng minh danh tính của nó với bạn bằng chứng chỉ. Với mTLS, bạn cũng phải chứng minh danh tính của mình với máy chủ bằng chứng chỉ. Đó là tệp .pfx: chứng chỉ và khóa riêng tư chứng minh bạn sở hữu nó. Nó được cho là thứ chỉ có hệ thống client hợp pháp mới có.
Ổ khóa thứ hai là client secret OAuth, mật khẩu ứng dụng sử dụng để yêu cầu mã thông báo truy cập từ API của ngân hàng.
Cả hai ổ khóa đều được phát hành bên trong một ứng dụng miễn phí trên Google Play, và chìa khóa cho hộp chứa ổ khóa đầu tiên là chữ số 2.
Tôi đã xác nhận rằng máy chủ đích thực sự thực thi mTLS: bắt tay TLS với api.baas.alahli.com yêu cầu chứng chỉ client (nó gửi Acceptable client certificate CA names), và chứng chỉ máy chủ có nội dung CN=*.baas.alahli.com, O=The Saudi National Bank. Vì vậy, đây không phải là chứng chỉ trang trí cho môi trường thử nghiệm. Đó là thông tin xác thực cho cửa trước của một API ngân hàng sản xuất, với các phạm vi bao gồm danh tính, tài khoản, thẻ, KYC, thanh toán thẻ và chuyển tiền.
Tách biệt với rò rỉ, có một vấn đề thiết kế bên dưới nó. Một chứng chỉ client được phát hành giống hệt nhau cho mười triệu thiết bị không thể phân biệt lần cài đặt này với lần cài đặt khác. Mọi bản sao đều trình bày cùng một thông tin xác thực, vì vậy chứng chỉ cho ngân hàng biết ứng dụng nào đang gọi và không có thông tin gì về ai đang gọi. Thông tin xác thực cho một API như thế này thuộc về backend của riêng bạn: ứng dụng nói chuyện với máy chủ của bạn, máy chủ của bạn nói chuyện với ngân hàng.
Những gì tôi đã không làm
Tôi đã chạy chính xác một lần kiểm tra đối với điểm cuối mã thông báo (POST /api/tppa/token) để xem liệu thông tin xác thực có còn hoạt động không. Nó trả về HTTP 403 từ nginx. Một yêu cầu không có chứng chỉ nào cũng vậy, và URL gốc trần trụi cũng vậy. Đó là một khối cấp mạng nằm trước API, gần như chắc chắn là theo địa lý, và nó không nói gì về việc liệu thông tin xác thực có hoạt động hay không.
Từ bên ngoài Ả Rập Xê Út, tôi không thể xác định liệu những thông tin xác thực này có còn hoạt động hay không. Tôi đã dừng lại ở đó. Bất cứ điều gì xa hơn sẽ là một nỗ lực để vượt qua kiểm soát truy cập của ngân hàng, và phát hiện này không phụ thuộc vào điều đó: một khóa riêng tư và một bí mật OAuth ngân hàng với phạm vi chuyển tiền nằm trong một tạo tác có thể tải xuống công khai chính là phát hiện, cho dù cá nhân tôi có thể truy cập điểm cuối hay không.
Cố gắng báo cáo
Đây là phần đã làm cho tweet lan truyền và là nửa thú vị hơn.
Tôi đã tìm cách để báo cáo điều này một cách có trách nhiệm. Những gì tồn tại:
Kênh
Kết quả
security.txt trên nusuk.sa, haj.gov.sa, hajj.nusuk.sa
Không tồn tại
Trang báo cáo lỗ hổng của Saudi CERT (cert.gov.sa)
Chuyển hướng đến NCA; trang báo cáo riêng đã đóng cửa
Biểu mẫu lỗ hổng NCA (haseen.gov.sa)
Không thể truy cập từ Đức: hết thời gian chờ, bị chặn địa lý
bugbounty.sa
Chương trình đã đóng, HTTP 403 từ bên ngoài
HackerOne / Bugcrowd
Không có chương trình cho Nusuk, Bộ hoặc Elm
Danh sách liên hệ trên cửa hàng ứng dụng
Địa chỉ hỗ trợ, không có nhiệm vụ bảo mật
Không có kênh nào trong số đó để lại cho tôi một kênh có nhiệm vụ bảo mật mà tôi thực sự có thể tiếp cận. Tôi vẫn gửi email. Phản hồi từ Hỗ trợ Haseen:
"Quyền truy cập vào cổng thông tin Haseen bị giới hạn cho người dùng trong Vương quốc Ả Rập Xê Út. Đối với bất kỳ thắc mắc nào khác, bạn có thể liên hệ với chúng tôi qua dịch vụ 'Chúng tôi quan tâm' có sẵn trên Cổng thông tin Haseen chính thức."
Cổng thông tin Haseen, thứ mà tôi không thể truy cập. Tôi đã trả lời giải thích rằng tôi không phải là người Ả Rập Xê Út, rằng đây là chứng chỉ ngân hàng tư nhân bị lộ trong một ứng dụng chính phủ và tôi chỉ muốn giao nó. Câu trả lời, một lần nữa, là biểu mẫu chỉ hoạt động cho công dân của Vương quốc Ả Rập Xê Út.
Vì vậy, tôi đã gửi báo cáo tới CERT/CC thông qua nền tảng VINCE của họ với tư cách là trung gian điều phối (VRF#26-08-DXMKL), chỉ giới hạn trong phát hiện này. Đó là con đường bạn đi khi bên bị ảnh hưởng không có kênh liên lạc riêng nào có thể tiếp cận.
Và sau đó tôi đã tweet về nó, chủ yếu là vì thất vọng.
Tweet đã đạt 1,5 triệu lượt xem. Trong vòng vài giờ, Hỗ trợ Haseen đã gửi email cho tôi, không được yêu cầu, trên cùng một chuỗi thư đã hai lần nói với tôi rằng cổng thông tin không dành cho tôi:
"Theo nhóm liên quan của chúng tôi, vui lòng cung cấp cho chúng tôi thêm chi tiết về lỗ hổng bảo mật."
Tôi đã gửi chi tiết đầy đủ. Tôi thà để mọi thứ được sửa còn hơn là đúng về quy trình.
Bản sửa lỗi
Bản cập nhật ứng dụng tiếp theo đã được phát hành trên cả Android và iOS. Tôi đã tải bản dựng Android mới (17.5.0, versionCode 156635) trực tiếp từ Play và so sánh nó với những gì tôi đã phân tích. Ba lần kiểm tra:
- Không có vùng chứa chứng chỉ nào trong bộ APK mới: không có tệp có phần mở rộng .pfx, .p12, .pkcs12, .jks, .bks, .pem hoặc .key. Tôi cũng đã băm tệp nusuk.pfx cũ và so sánh từng byte với mọi tệp có cùng kích thước trong bản dựng mới, đề phòng trường hợp nó chỉ được đổi tên. Không có kết quả khớp.
- Không có thông tin xác thực nào đã biết. Tôi đã tìm kiếm các giá trị cũ chính xác cho URL cơ sở, phạm vi, ID client và client secret trên tất cả các tệp DEX, thư viện gốc, nội dung, XML, JSON và tài nguyên thô. Không có kết quả nào cho cả bốn giá trị.
- Không có đường dẫn mã. Phiên bản 17.4.9 chứa 4.571 tệp trong các gói com.walletstaq và com.trustless; 17.5.0 chứa 0 tệp. Các điểm đánh dấu baas.alahli, tppa/token, nusuk.pfx, chủ thể chứng chỉ và tên tổ chức phát hành đều trả về 0 kết quả trong bản dựng đã được giải mã.
Toàn bộ ví và tích hợp BaaS đã bị loại bỏ. Việc kiểm tra đổi tên trong bước 1 là điều loại trừ khả năng các giá trị chỉ đơn giản được chuyển đến nơi khác trong gói.
Phần mà không ai bên ngoài có thể xác minh
Việc xóa bí mật khỏi ứng dụng không làm mất hiệu lực của bí mật đó. Các bản sao APK cũ vẫn tồn tại vĩnh viễn và chứng chỉ có giá trị đến tháng 4 năm 2027. Vì vậy, câu hỏi còn bỏ ngỏ là liệu nó đã bị thu hồi và liệu bí mật OAuth đã được thay đổi hay chưa.
Tôi đã tìm cách để kiểm tra điều đó một cách độc lập. Không có cách nào, và lý do tự nó là một phát hiện.
Chứng chỉ không mang phần mở rộng crlDistributionPoints, vì vậy không có danh sách thu hồi nào được tham chiếu. Điểm cuối thu hồi duy nhất của nó là:
1OCSP - URI: http://finto-ocsp-responder.prod.svc.cluster.local:8080/api/v1/ocsp
.cluster.local là hậu tố DNS nội bộ của một cụm Kubernetes. Theo định nghĩa, nó không thể định tuyến trên internet công cộng và nó phân giải thành NXDOMAIN từ bất kỳ đâu bên ngoài cụm đó, qua HTTP thông thường trên cổng 8080.
Vì vậy, trạng thái thu hồi của chứng chỉ này không thể được kiểm tra từ bên ngoài cơ sở hạ tầng của ngân hàng, bởi vì không có gì ở ngoài này để truy vấn. Đối với bất kỳ bên phụ thuộc nào bên ngoài cụm đó, các chứng chỉ từ tổ chức phát hành này thực sự không thể thu hồi được, điều này xứng đáng có một đoạn văn trong bài đánh giá kiến trúc của ai đó.
Cùng một trường đó cũng đã công bố tên cụm, không gian tên, tên dịch vụ và cổng của PKI sản xuất của nền tảng BaaS của một ngân hàng, trong một ứng dụng được phân phối cho mười triệu người.
Chỉ SNB, Finto hoặc Staq mới có thể xác nhận việc thay đổi. Phía nhà cung cấp xác nhận thông tin xác thực đã được thay đổi. Tôi không có cách nào để xác minh điều đó một cách độc lập.
Dòng thời gian
Ngày (2026)
Sự kiện
29 tháng 8
APK được phân tích, phát hiện được xác nhận cục bộ
29 tháng 8
Báo cáo được gửi qua email; Haseen trả lời rằng cổng thông tin bị giới hạn cho người dùng ở Ả Rập Xê Út
31 tháng 8
Nhiều lần thử, cùng một câu trả lời. Tôi tweet về nó; ~1,5 triệu lượt xem
1 tháng 9
Haseen mở lại chuỗi thư không được yêu cầu, yêu cầu chi tiết. Chi tiết đã được gửi
1 tháng 9
Báo cáo cũng được gửi tới CERT/CC VINCE (VRF#26-08-DXMKL) với tư cách trung gian
1 tháng 9
Phiên bản 17.5.0 được phát hành trên Google Play
3 tháng 9
Kiểm tra lại trên bộ APK 17.5.0 chưa sửa đổi xác nhận việc xóa bỏ hoàn toàn
8 tháng 9
Bài viết này
Tôi không thể chứng minh rằng bản cập nhật là do báo cáo của tôi gây ra. 17.5.0 có thể đã có sẵn trong quy trình phát triển. Điều tôi có thể chỉ ra là tài liệu đã có trong 17.4.9 và không có trong 17.5.0.
Những gì tôi rút ra từ điều này
- Việc giấu thứ gì đó trong ứng dụng không phải là ranh giới bảo mật. Không phải trong tài nguyên, không phải trong tệp .so gốc, không phải đằng sau sự che giấu mã hoặc mật khẩu được lưu trữ trong cùng một tệp nhị phân. Nếu ứng dụng có thể đọc được nó, thì tất cả những người cài đặt ứng dụng cũng có thể. Một số nhóm học được điều này một cách công khai mỗi năm.
- Chứng chỉ client dùng chung không phải là xác thực. Nếu mười triệu thiết bị trình bày cùng một chứng chỉ, nó cho biết ứng dụng nào đang gọi và không có thông tin gì về ai đang gọi, và ứng dụng là một tệp mà bất kỳ ai cũng có thể tải xuống. Thông tin xác thực cho API của bên thứ ba, đặc biệt là của ngân hàng, thuộc về máy chủ mà bạn kiểm soát.
- Việc giới hạn địa lý kênh tiết lộ lỗ hổng của bạn tự nó là một lỗ hổng. Những kẻ tấn công không điền vào biểu mẫu. Nếu cách duy nhất để báo cáo lỗi trong một ứng dụng được phát hành toàn cầu cho mười triệu người là phải ở trong một quốc gia, thì những người không thể truy cập biểu mẫu chính xác là những người bạn muốn nghe ý kiến nhất. Phải mất một tweet lan truyền để mở một kênh mà một tệp security.txt lẽ ra đã mở miễn phí.
Tôi đã phân tích bộ APK Play Store có sẵn công khai ở chế độ khách, không có tài khoản và không có dữ liệu cá nhân thực tế. Tôi đã không thực hiện bất kỳ nỗ lực nào để vượt qua khối cấp mạng trước API ngân hàng. Mọi giá trị trong bài viết này đều mang tính cấu trúc (đường dẫn, tên lớp, siêu dữ liệu chứng chỉ) hoặc đã được biên tập lại; không có tài liệu khóa riêng tư và không có bí mật đầy đủ nào được công bố.





