一個安裝次數超過 1000 萬的政府應用程式中,一個 2,835 位元組的檔案內含沙烏地國家銀行發行的有效用戶端憑證。要讓任何人正視這個問題,需要一則爆紅的推文。
重點摘要
官方 Nusuk 應用程式(com.moh.nusukapp,由朝覲與副朝部開發,安裝次數超過 1000 萬,並帶有 Google Play 的「政府」徽章)中,包含一個 PKCS#12 檔案,內含一個私密 RSA 金鑰以及由沙烏地國家銀行發行的用戶端憑證。該檔案的密碼就寫死在應用程式程式碼中,距離載入該檔案的位置僅幾行之遙。密碼只有一個字元:2。
在它旁邊,以明文形式存在的是該銀行「銀行即服務」API 的 OAuth2 用戶端 ID 和用戶端密碼,請求的範圍包括 identity accounts cards verification kyc cardpay transfers。
任何從 Google Play 下載該應用程式的人,都能取得所有這些資訊。
我試圖回報這個問題。但對方告訴我,漏洞回報入口僅限沙烏地阿拉伯境內的用戶使用。所以我發了一則推文。這則推文獲得了 150 萬次觀看,然後同一個入口突然要求我提供詳細資訊。一天後,這些憑證就從應用程式中消失了。
https://x.com/iam_zachi/status/2094445016194207745
這篇文章僅涵蓋與銀行相關的發現。以下所有問題都已在正式發布的應用程式中修復,且供應商方面表示憑證已經輪換。
什麼是 Nusuk
Nusuk 是沙烏地阿拉伯政府針對朝覲與副朝的官方平台。它負責處理朝覲許可、電子簽證、預訂以及 Nusuk 卡。該平台由朝覲與副朝部營運,在 Google Play 上被標記為經過驗證的政府應用程式,且安裝次數超過一千萬。它還包含一個錢包功能,即 Nusuk Wallet,該功能與沙烏地國家銀行共同開發,並獲得沙烏地中央銀行 SAMA 的批准。本文討論的重點就是這個錢包。
發現過程
我直接從 Google Play 下載了 APK 套件(版本 17.4.9,版本代碼 131215)並將其解壓縮。這沒什麼特別的:標準的 Kotlin/Compose,沒有加殼,也沒有有意義的混淆。
在應用程式的資源中,位於 res/raw/nusuk.pfx 的是一個 2,835 位元組的 PKCS#12 容器。.pfx 檔案是標準的憑證與私密金鑰捆綁格式,並以密碼加密。
密碼就在應用程式的程式碼中,距離檔案載入處僅幾行:
1const-string v3, "2"
一個字元,以字面形式存在於反編譯的位元組碼中。接著執行一次 openssl 指令:
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
這是一張由銀行發行的有效用戶端憑證,有效期還有一年,用於在 TLS 連線中向伺服器驗證用戶端身份。
這並非孤例。相同的程式碼路徑屬於錢包元件,該元件以 com.walletstaq 打包,並整合了 Staq Technologies 的 Trustless SDK(該公司營運 SNB 的 Finto BaaS 平台)。另外三個值也以明文形式存在:
CLIENT_ID = 379cc899…CLIENT_SECRET = ELrfNe9w…(64 個原始位元組,以 base64 編碼)SERVER_URL = [https://api.baas.alahli.com/api/](https://api.baas.alahli.com/api/)
以及請求的 OAuth 範圍:
1identity accounts cards verification kyc cardpay transfers
這兩個憑證也被串接成一個字串,連同基礎 URL 一起傳遞給除錯記錄器。
(我不會公開金鑰材料或完整的密碼值。重要的是問題的樣貌。)
為什麼這很糟糕(白話解釋)
把銀行的 API 想像成一道有兩道鎖的門。
第一道鎖是雙向 TLS。通常,伺服器會用憑證向您證明其身份。使用 mTLS 時,您也必須用憑證向伺服器證明您的身份。這就是 .pfx 檔案:憑證和證明您擁有該憑證的私密金鑰。它應該是只有合法用戶端系統才擁有的東西。
第二道鎖是 OAuth 用戶端密碼,即應用程式用來向銀行 API 請求存取權杖的密碼。
這兩道鎖都隨著 Google Play 上的一個免費應用程式一起發布,而裝著第一道鎖的盒子的鑰匙就是數字 2。
我確認目標主機確實強制執行了 mTLS:與 api.baas.alahli.com 的 TLS 握手會要求用戶端憑證(它發送了 Acceptable client certificate CA names),而伺服器憑證顯示 CN=*.baas.alahli.com, O=The Saudi National Bank。所以這不是測試環境的裝飾性憑證。它是通往正式銀行 API 大門的憑證,其範圍涵蓋身份、帳戶、卡片、KYC、卡片支付和轉帳。
除了洩漏問題之外,底層還存在一個設計問題。一個用戶端憑證以完全相同的方式發送給一千萬台裝置,無法區分不同的安裝實例。每個副本都呈現相同的憑證,因此該憑證只能告訴銀行是哪個應用程式在呼叫,而無法告知是誰在呼叫。此類 API 的憑證應該放在您自己的後端:應用程式與您的伺服器通訊,您的伺服器再與銀行通訊。
我沒有做的事
我僅對權杖端點(POST /api/tppa/token)執行了一次檢查,以確認這些憑證是否有效。它回傳了 nginx 的 HTTP 403 錯誤。完全不提供憑證的請求也得到相同結果,直接訪問根 URL 也是如此。這是 API 前端的網路層級封鎖,幾乎可以肯定是基於地理位置的,但這並不能說明憑證是否有效。
從沙烏地阿拉伯境外,我無法確定這些憑證是否有效。我到此為止。任何進一步的動作都將是試圖繞過銀行的存取控制,而且這個發現本身並不依賴於此:一個私密金鑰和一個具有轉帳權限的銀行 OAuth 密碼,存在於一個可公開下載的成品中,這就是發現本身,無論我個人是否能連接到該端點。
嘗試回報
這是讓那則推文爆紅的部分,也是更有趣的一半。
我尋找負責任地回報這個問題的方法。現有的管道如下:
管道 | 結果 |
|---|---|
| 不存在 |
沙烏地 CERT( | 重新導向至 NCA;自身回報頁面已關閉 |
NCA 漏洞表單( | 從德國無法連線:逾時、地理封鎖 |
| 已關閉的計畫,從外部回傳 HTTP 403 |
HackerOne / Bugcrowd | Nusuk、該部會或 Elm 沒有相關計畫 |
應用程式商店列表聯絡資訊 | 支援地址,無安全授權 |
這些管道都沒有一個是我能實際聯繫到且具有安全授權的。我還是寄了電子郵件。來自 Haseen 支援團隊的回覆:
「Haseen 入口網站的存取僅限於沙烏地阿拉伯王國境內的用戶。如有任何進一步疑問,您可以透過官方 Haseen 入口網站上的『我們關心』服務與我們聯繫。」
而那個 Haseen 入口網站正是我無法連線的東西。我回信解釋我不是沙烏地人,這是一個政府應用程式中暴露的私人銀行憑證,我只是想把它交出去。得到的回覆再次表示,該表單僅適用於沙烏地公民。
因此,我透過 CERT/CC 的 VINCE 平台提交了一份報告,將其作為協調中介(VRF#26-08-DXMKL),範圍僅限於此發現。當受影響方沒有可聯繫的管道時,這就是你該走的路。
然後,我在推特上發布了這件事,主要是出於沮喪。
這則推文獲得了 150 萬次觀看。幾小時內,Haseen 支援團隊在同一個郵件串中主動寄信給我,而這個郵件串之前曾兩次告訴我該入口網站不適用於我:
「根據我們相關團隊的指示,請您提供關於此安全漏洞的更多詳細資訊。」
我提供了完整的詳細資訊。我寧願問題被修復,也不願在流程上爭對錯。
修復方式
下一個應用程式更新同時發布在 Android 和 iOS 上。我直接從 Play 商店下載了新的 Android 版本(17.5.0,版本代碼 156635),並與我分析的版本進行了比對。進行了三項檢查:
- 新的 APK 套件中沒有任何憑證容器:沒有
.pfx、.p12、.pkcs12、.jks、.bks、.pem或.key副檔名的檔案。我還對舊的nusuk.pfx進行了雜湊處理,並與新版本中所有相同大小的檔案進行了逐位元組比較,以防它只是被重新命名。沒有匹配項。 - 沒有任何已知的憑證。我在所有 DEX 檔案、原生函式庫、資源檔、XML、JSON 和原始資源中,搜尋了舊的基礎 URL、範圍、用戶端 ID 和用戶端密碼的確切值。全部四個項目都沒有找到任何匹配。
- 沒有相關的程式碼路徑。17.4.9 版本在
com.walletstaq和com.trustless套件下包含 4,571 個檔案;17.5.0 版本則為零。在解碼後的版本中,搜尋baas.alahli、tppa/token、nusuk.pfx、憑證主體和發行者名稱,結果都是零匹配。
整個錢包和 BaaS 整合功能都被移除了。步驟 1 中的重新命名檢查排除了這些值只是被移動到套件中其他位置的可能性。
外界無法驗證的部分
從應用程式中移除密碼並不會使密碼失效。舊的 APK 副本會永久存在,而該憑證的有效期到 2027 年 4 月。因此,懸而未決的問題是它是否已被撤銷,以及 OAuth 密碼是否已輪換。
我試圖尋找獨立驗證的方法。但沒有這樣的方法,而原因本身就是一個發現。
該憑證沒有攜帶 crlDistributionPoints 擴展,因此完全沒有引用任何撤銷清單。它唯一的撤銷端點是:
1OCSP - URI: http://finto-ocsp-responder.prod.svc.cluster.local:8080/api/v1/ocsp
.cluster.local 是 Kubernetes 叢集的內部 DNS 尾碼。從定義上講,它在公共網際網路上是不可路由的,並且從該叢集外部的任何地方解析都會得到 NXDOMAIN,而且是透過純 HTTP 連接埠 8080。
因此,無法從銀行基礎設施外部檢查此憑證的撤銷狀態,因為外部根本沒有可以查詢的對象。對於該叢集之外的任何依賴方來說,來自此發行者的憑證實際上是無法撤銷的。這個問題本身就值得在架構審查中佔據一個段落。
同一個欄位還發布了銀行 BaaS 平台的正式環境叢集名稱、命名空間、服務名稱和連接埠,這些資訊存在於一個分發給一千萬人的應用程式中。
只有 SNB、Finto 或 Staq 能夠確認輪換。供應商方面表示憑證已經輪換。我無法獨立驗證這一點。
時間線
日期 (2026) | 事件 |
|---|---|
8 月 29 日 | 分析 APK,在本地確認發現 |
8 月 29 日 | 寄送報告郵件;Haseen 回覆入口網站僅限沙烏地境內用戶 |
8 月 31 日 | 再次嘗試,得到相同回覆。我在推特上發布此事;約 150 萬次觀看 |
9 月 1 日 | Haseen 主動重新開啟郵件串,要求提供詳細資訊。已提供詳細資訊 |
9 月 1 日 | 同時透過 CERT/CC VINCE (VRF#26-08-DXMKL) 作為中介提交報告 |
9 月 1 日 | 版本 17.5.0 在 Google Play 上發布 |
9 月 3 日 | 對未修改的 17.5.0 APK 套件進行重新測試,確認已完全移除 |
9 月 8 日 | 撰寫本文 |
我無法證明這次更新是由我的報告引起的。17.5.0 很可能已經在開發管道中。我能證明的是,這些資料存在於 17.4.9 版本中,而 17.5.0 版本中沒有。
我的總結
- 將東西藏在應用程式中並非安全邊界。無論是藏在資源檔、原生
.so函式庫、混淆程式碼背後,還是藏在儲存在同一個二進位檔中的密碼後面。如果應用程式能讀取它,那麼每個安裝該應用程式的人也都能讀取。每年都有幾個團隊公開學到這一課。 - 共享的用戶端憑證不是身份驗證。如果一千萬台裝置都出示相同的憑證,它只能說明是哪個應用程式在呼叫,而無法說明是誰在呼叫,而且該應用程式只是一個任何人都可以下載的檔案。第三方 API 的憑證,尤其是銀行的 API,應該放在您控制的伺服器上。
- 對漏洞揭露管道進行地理圍欄本身就是一個漏洞。攻擊者不會填寫表單。如果向一個全球發布、擁有千萬用戶的應用程式回報缺陷的唯一方法,是必須實際身在某個國家境內,那麼那些無法使用該表單的人,正是你最想聽取意見的人。一則爆紅的推文打開了一個通道,而一個
security.txt檔案本來可以免費做到這一點。
我在訪客模式下分析了公開可用的 Play 商店 APK 套件,沒有使用任何帳戶,也沒有任何真實的個人資料。我沒有嘗試繞過銀行 API 前的網路層級封鎖。本文中的每個值要不是結構性的(路徑、類別名稱、憑證元資料),就是經過編輯的;沒有發布任何私密金鑰材料或完整的密碼。





