沙特政府 App 内置银行私钥,漏洞上报竟需沙特身份

@iam_zachi
英语2026年9月08日
235K
1.5K
95
28
1.1K

TL;DR

一位安全研究员发现,沙特官方 Nusuk App 泄露了沙特国家银行(Saudi National Bank)的 RSA 私钥和 OAuth 凭证,且仅由一位数密码保护。由于漏洞上报门户存在地理位置限制,研究员不得不通过推文引发广泛关注才成功提交漏洞。

一个拥有 1000 万以上安装量的政府应用内,一个 2,835 字节的文件中存放着沙特国家银行的实时客户端证书。让任何人注意到它,需要一条病毒式传播的推文。

TL;DR

官方 Nusuk 应用(com.moh.nusukapp,朝觐与副朝部,1000 万+ 安装量,带有 Google Play 的"政府"徽章)附带了一个 PKCS#12 文件,其中包含一个私有 RSA 密钥和由沙特国家银行颁发的客户端证书。该文件的密码被硬编码在应用自身代码中,仅相隔几行。密码只有一个字符:2。

在其旁边,以明文形式存放着该银行 Banking-as-a-Service 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 文件是证书及其私钥的标准捆绑格式,并使用密码加密。

密码就在应用的代码中,距离加载该文件的代码仅几行:

text
1const-string v3, "2"

一个字符,作为字面量存在于反编译的字节码中。执行一次 openssl 命令后:

text
1RSA 私钥,2048 位,2 个质因数
2主题: C=SA, ST=Jeddah, L=Jeddah, O=Nusuk.sa, CN=eshbeata@staq.io
3颁发者: C=SA, ST=Riyadh, L=Riyadh, O=The Saudi National Bank, OU=Finto,
4 CN=Application Issuer
5序列号: 0x24 (36)
6有效期: 2026-04-27 -> 2027-04-27
7扩展密钥用法(关键): TLS Web 客户端身份验证

一个由银行颁发的实时客户端证书,有效期还有一年,用于客户端向服务器进行 TLS 身份验证。

它并非孤立存在。相同的代码路径属于钱包组件,打包为 com.walletstaq,并连接到 Staq Technologies 的 Trustless SDK(该公司运营 SNB 的 Finto BaaS 平台)。还有三个值以明文形式存在:

以及请求的 OAuth 作用域:

text
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 密钥存放在一个可公开下载的工件中,这本身就是发现,无论我个人是否能访问该端点。

试图举报

这是让那条推文病毒式传播的部分,也是更有趣的一半。

我寻找了一种负责任地举报的方式。现有的渠道如下:

渠道

结果

nusuk.sa, haj.gov.sa, hajj.nusuk.sa 上的 security.txt

不存在

沙特 CERT (cert.gov.sa) 漏洞报告页面

重定向到 NCA;自身报告页面已关闭

NCA 漏洞表单 (haseen.gov.sa)

从德国无法访问:超时,被地理封锁

bugbounty.sa

已关闭项目,从外部返回 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),并将其与我分析过的版本进行了对比。三项检查:

  1. 新的 APK 包中没有任何证书容器:没有 .pfx、.p12、.pkcs12、.jks、.bks、.pem 或 .key 扩展名的文件。我还对旧的 nusuk.pfx 进行了哈希处理,并将其与新构建中所有相同大小的文件进行了逐字节比较,以防它只是被重命名了。没有匹配项。
  2. 没有任何已知的凭证。我在所有 DEX 文件、原生库、assets、XML、JSON 和原始资源中搜索了基础 URL、作用域、客户端 ID 和客户端密钥的精确旧值。全部四个均为零命中。
  3. 没有代码路径。版本 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 扩展,因此根本没有引用任何吊销列表。其唯一的吊销端点是:

text
1OCSP - URI: http://finto-ocsp-responder.prod.svc.cluster.local:8080/api/v1/ocsp

.cluster.local 是 Kubernetes 集群的内部 DNS 后缀。它在公共互联网上按定义是不可路由的,并且从该集群外部的任何地方解析都会返回 NXDOMAIN,而且是通过纯 HTTP 在 8080 端口上。

因此,无法从银行基础设施外部检查此证书的吊销状态,因为外部没有任何可查询的东西。对于该集群之外的任何依赖方来说,来自此颁发者的证书实际上是无法撤销的,这本身就值得在某个人的架构审查中单独写一段。

同一个字段还发布了银行 BaaS 平台生产 PKI 的集群名称、命名空间、服务名称和端口,这些信息都包含在一个分发给一千万人的应用中。

只有 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 中。

我的总结

  1. 将东西隐藏在应用中并非安全边界。无论是放在资源文件中、原生 .so 库里、混淆代码后面,还是放在存储在同一个二进制文件中的密码后面,都不是。如果应用能读取它,那么所有安装该应用的人也都能读取。每年都有几个团队在公开场合学到这一点。
  2. 共享的客户端证书不是身份验证。如果一千万台设备出示相同的证书,它只能说明是哪个应用在调用,而无法说明是谁在调用,而且这个应用是任何人都可以下载的文件。第三方 API 的凭证,尤其是银行的凭证,应该放在你控制的服务器上。
  3. 对漏洞披露渠道进行地理围栏本身就是一种漏洞。攻击者不会填写表单。如果向一个全球发布、拥有千万用户的全球应用报告漏洞的唯一方式是身处某个国家境内,那么那些无法访问表单的人恰恰是你最想听到声音的人。一条病毒式传播的推文打开了一个渠道,而一个 security.txt 文件本可以免费打开它。

我在访客模式下分析了公开可用的 Play 商店 APK 包,没有账户,也没有真实的个人数据。我没有尝试绕过银行 API 前的网络级封锁。本文中的每个值要么是结构性的(路径、类名、证书元数据),要么已被编辑;没有发布任何私钥材料或完整密钥。

一键保存

使用 YouMind AI 深度阅读爆款文章

保存原文、追问细节、总结观点,并在一个 AI 工作空间里把爆款文章沉淀成可复用笔记。

了解 YouMind
写给创作者

把你的 Markdown 变成干净的 𝕏 文章

图片上传、表格、代码块,往 𝕏 上手动重排太痛苦。YouMind 把整篇 Markdown 一键转成干净、可直接发布的 𝕏 文章草稿。

试试 Markdown 转 𝕏

更多可拆解样本

近期爆款文章

探索更多爆款文章