Файл розміром 2 835 байтів у державному додатку з понад 10 мільйонами завантажень містив живий клієнтський сертифікат для Саудівського національного банку. Щоб хтось звернув на це увагу, знадобився вірусний твіт.
TL;DR
Офіційний додаток Nusuk (com.moh.nusukapp, Міністерство хаджу та умри, понад 10 млн завантажень, з позначкою "Уряд" у Google Play) містив файл PKCS#12 із закритим ключем RSA та клієнтським сертифікатом, виданим Саудівським національним банком. Пароль до цього файлу був жорстко закодований у коді самого додатка, всього за кілька рядків від місця його завантаження. Це був один символ: 2.
Поруч, у відкритому вигляді, знаходилися ідентифікатор клієнта OAuth2 та секретний ключ для Banking-as-a-Service API банку, які запитували області доступу identity accounts cards verification kyc cardpay transfers.
Кожен, хто завантажив додаток із Google Play, мав усе це.
Я спробував повідомити про це. Мені сказали, що портал для звітування про вразливості доступний лише користувачам у Саудівській Аравії. Тому я написав твіт. Твіт набрав 1,5 млн переглядів, і раптом той самий портал захотів отримати деталі. Через день облікові дані зникли з додатка.
https://x.com/iam_zachi/status/2094445016194207745
Ця стаття охоплює лише знахідку, пов'язану з банком. Усе, описане нижче, було виправлено в поточній версії додатка, а сторона постачальника стверджує, що облікові дані були змінені.
Що таке Nusuk
Nusuk — це офіційна платформа уряду Саудівської Аравії для хаджу та умри. Вона займається дозволами на паломництво, електронними візами, бронюваннями та карткою Nusuk. Її експлуатує Міністерство хаджу та умри, вона позначена як перевірений державний додаток у Google Play і має понад десять мільйонів завантажень. Вона також містить функцію гаманця, Nusuk Wallet, створену спільно з Саудівським національним банком і схвалену SAMA, центральним банком Саудівської Аравії. Саме про цей гаманець і йдеться в цій статті.
Знахідка
Я завантажив набір APK безпосередньо з Google Play (версія 17.4.9, versionCode 131215) і розпакував його. Нічого екзотичного: стандартний Kotlin/Compose, без пакувальника, без значущої обфускації.
У ресурсах додатка, за адресою res/raw/nusuk.pfx, знаходився контейнер PKCS#12 розміром 2 835 байтів. Файл .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 і підключений до Trustless SDK від Staq Technologies, компанії, яка керує платформою Finto BaaS від SNB. Там же, у відкритому вигляді, знаходилися ще три значення:
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: рукостискання TLS з api.baas.alahli.com запитує клієнтські сертифікати (він надсилає Acceptable client certificate CA names), а сертифікат сервера містить CN=*.baas.alahli.com, O=The Saudi National Bank. Отже, це не був декоративний сертифікат для тестового середовища. Це були облікові дані для вхідних дверей продакшн-API банку з областями доступу, що охоплюють ідентифікацію, рахунки, картки, KYC, карткові платежі та перекази.
Окремо від витоку, під цим лежить проблема дизайну. Клієнтський сертифікат, який однаково надсилається на десять мільйонів пристроїв, не може відрізнити одну інсталяцію від іншої. Кожна копія пред'являє ті самі облікові дані, тому сертифікат повідомляє банку, який додаток викликає, і нічого не повідомляє про те, хто викликає. Облікові дані для такого API повинні знаходитися за вашим власним сервером: додаток спілкується з вашим сервером, а ваш сервер спілкується з банком.
Чого я не робив
Я виконав лише одну перевірку кінцевої точки токена (POST /api/tppa/token), щоб побачити, чи є облікові дані активними. Вона повернула HTTP 403 від nginx. Те саме сталося із запитом без сертифіката, а також із голою кореневою URL-адресою. Це мережевий блок, розташований перед API, майже напевно географічний, і він нічого не говорить про те, чи працюють облікові дані.
З-за меж Саудівської Аравії я не міг визначити, чи були ці облікові дані активними. Я зупинився на цьому. Будь-що подальше було б спробою обійти контроль доступу банку, а знахідка від цього не залежить: закритий ключ і банківський секрет OAuth з областю переказів, що знаходяться в загальнодоступному артефакті, — це і є знахідка, незалежно від того, чи можу я особисто досягти кінцевої точки.
Спроба повідомити про це
Це та частина, яка зробила твіт вірусним, і це цікавіша половина.
Я шукав спосіб відповідально повідомити про це. Ось що існує:
Канал
Результат
security.txt на nusuk.sa, haj.gov.sa, hajj.nusuk.sa
Не існує
Сторінка звітування про вразливості Saudi CERT (cert.gov.sa)
Перенаправляє на NCA; власна сторінка звітування закрита
Форма звітування про вразливості NCA (haseen.gov.sa)
Недоступна з Німеччини: тайм-аут, геоблокування
bugbounty.sa
Закрита програма, HTTP 403 ззовні
HackerOne / Bugcrowd
Немає програми для Nusuk, Міністерства або Elm
Контакти в магазині додатків
Адреси підтримки, без мандату безпеки
Жоден із них не залишив мені каналу з мандатом безпеки, до якого я міг би фактично звернутися. Я все одно надіслав електронного листа. Відповідь від Haseen Support:
"Доступ до порталу Haseen обмежений для користувачів у межах Королівства Саудівська Аравія. З будь-яких додаткових питань ви можете зв'язатися з нами через службу 'Ми піклуємося', доступну на офіційному порталі Haseen."
Портал Haseen, до якого я не можу отримати доступ. Я відповів, пояснивши, що я не саудівець, що це приватний банківський сертифікат, виставлений у державному додатку, і що я просто хочу його передати. Відповідь знову була такою: форма працює лише для громадян KSA.
Тому я подав звіт до CERT/CC через їхню платформу VINCE як координуючий посередник (VRF#26-08-DXMKL), обмежившись лише цією знахідкою. Це шлях, яким ви йдете, коли постраждала сторона не має власного доступного каналу.
А потім я написав про це твіт, переважно через розчарування.
Твіт набрав 1,5 млн переглядів. За кілька годин Haseen Support надіслав мені електронного листа без попереднього запиту, у тому самому ланцюжку, де мені двічі сказали, що портал не для мене:
"Згідно з нашою відповідальною командою, будь ласка, надайте нам більше деталей про вразливість безпеки."
Я надіслав повні деталі. Я краще хочу, щоб проблему виправили, ніж бути правим щодо процесу.
Виправлення
Наступне оновлення додатка вийшло як на Android, так і на iOS. Я завантажив нову збірку Android (17.5.0, versionCode 156635) безпосередньо з Play і порівняв її з тією, яку аналізував. Три перевірки:
- Жодного контейнера сертифіката в новому наборі APK: немає файлів із розширеннями .pfx, .p12, .pkcs12, .jks, .bks, .pem або .key. Я також обчислив хеш старого
nusuk.pfxі порівняв його побайтово з кожним файлом такого ж розміру в новій збірці, на випадок, якщо його просто перейменували. Збігів не знайдено. - Жодних відомих облікових даних. Я шукав точні старі значення базової URL-адреси, області доступу, ідентифікатора клієнта та секрету клієнта в усіх DEX-файлах, нативних бібліотеках, ресурсах, XML, JSON та необроблених ресурсах. Нуль збігів для всіх чотирьох.
- Жодного шляху коду. Версія 17.4.9 містила 4 571 файл у пакетах
com.walletstaqтаcom.trustless; версія 17.5.0 містить нуль. Маркериbaas.alahli,tppa/token,nusuk.pfx, тема сертифіката та ім'я емітента повертають нуль збігів у декодованій збірці.
Увесь гаманець та інтеграція BaaS були видалені. Перевірка перейменування на кроці 1 виключає можливість того, що значення просто перемістилися в інше місце пакета.
Частина, яку ніхто ззовні не може перевірити
Видалення секрету з додатка не анулює сам секрет. Старі копії APK залишаються доступними назавжди, а сертифікат був дійсний до квітня 2027 року. Тому відкрите питання: чи був він відкликаний і чи був змінений секрет OAuth.
Я шукав спосіб перевірити це незалежно. Його немає, і сама причина цього є знахідкою.
Сертифікат не містить розширення crlDistributionPoints, тому список відкликання взагалі не згадується. Його єдина кінцева точка відкликання:
1OCSP - URI: http://finto-ocsp-responder.prod.svc.cluster.local:8080/api/v1/ocsp
.cluster.local — це внутрішній суфікс DNS кластера Kubernetes. За визначенням, він не маршрутизується в публічному інтернеті і розпізнається як NXDOMAIN з будь-якого місця за межами цього кластера, через звичайний HTTP на порту 8080.
Отже, статус відкликання цього сертифіката неможливо перевірити ззовні інфраструктури банку, оскільки тут немає чого запитувати. Для будь-якої залежної сторони за межами цього одного кластера сертифікати від цього емітента фактично не підлягають відкликанню, що само по собі заслуговує на абзац у чиємусь огляді архітектури.
Те саме поле також опублікувало назву кластера, простір імен, назву служби та порт продакшн-PKI платформи BaaS банку в додатку, поширеному серед десяти мільйонів людей.
Лише SNB, Finto або Staq можуть підтвердити зміну. Сторона постачальника стверджує, що облікові дані були змінені. Я не маю можливості перевірити це незалежно.
Хронологія
Дата (2026)
Подія
29 серпня
APK проаналізовано, знахідку підтверджено локально
29 серпня
Звіт надіслано електронною поштою; Haseen відповідає, що портал обмежений для користувачів у KSA
31 серпня
Повторні спроби, та сама відповідь. Я пишу твіт; ~1,5 млн переглядів
1 вересня
Haseen без попереднього запиту відновлює ланцюжок, просить деталі. Деталі надіслано
1 вересня
Звіт також подано до CERT/CC VINCE (VRF#26-08-DXMKL) як посередник
1 вересня
Версія 17.5.0 опублікована в Google Play
3 вересня
Повторне тестування на незміненому наборі APK 17.5.0 підтверджує повне видалення
8 вересня
Ця стаття
Я не можу довести, що оновлення було спричинене моїм звітом. 17.5.0, можливо, вже була в розробці. Що я можу показати, так це те, що матеріал був у версії 17.4.9 і відсутній у версії 17.5.0.
Що я виніс із цього
- Приховування чогось у додатку не є межею безпеки. Не в ресурсах, не в нативному .so, не за обфускацією або паролем, що зберігається в тому самому бінарному файлі. Якщо додаток може це прочитати, то може і кожен, хто встановлює додаток. Кілька команд дізнаються про це публічно щороку.
- Спільний клієнтський сертифікат не є автентифікацією. Якщо десять мільйонів пристроїв пред'являють один і той самий сертифікат, це говорить про те, який додаток викликає, і нічого не говорить про те, хто викликає, а додаток — це файл, який може завантажити будь-хто. Облікові дані для стороннього API, особливо банківського, повинні знаходитися на сервері, який ви контролюєте.
- Геоблокування вашого каналу розкриття вразливостей саме по собі є вразливістю. Зловмисники не заповнюють форми. Якщо єдиний спосіб повідомити про недолік у додатку, опублікованому глобально для десяти мільйонів людей, — це фізично перебувати в одній країні, то люди, які не можуть отримати доступ до форми, — це саме ті, кого ви найбільше хочете почути. Знадобився вірусний твіт, щоб відкрити канал, який файл
security.txtвідкрив би безкоштовно.
Я проаналізував загальнодоступний набір APK Play Store у гостьовому режимі, без облікового запису та без реальних особистих даних. Я не робив жодних спроб обійти мережевий блок перед банківським API. Кожне значення в цій статті є або структурним (шляхи, назви класів, метадані сертифіката), або скороченим; жоден матеріал закритого ключа та жодні повні секрети не опубліковано.





