Государственное приложение Саудовской Аравии допустило утечку закрытого ключа банка. Чтобы сообщить об этом, пришлось стать «саудовцем».

@iam_zachi
АНГЛИЙСКИЙ08 сент. 2026 г.
235K
1.5K
95
28
1.1K

Суть

Исследователь безопасности обнаружил, что официальное саудовское приложение Nusuk допустило утечку закрытого ключа RSA и учетных данных OAuth для Saudi National Bank, защищенных паролем из одной цифры. Сообщение об уязвимости потребовало вирусного твита из-за географических ограничений на порталах для отчетов об ошибках.

Файл размером 2 835 байта внутри государственного приложения с более чем 10 миллионами установок содержал действующий клиентский сертификат Саудовского национального банка. Чтобы кто-то обратил на это внимание, потребовался вирусный твит.

Краткое содержание

Официальное приложение 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 — это стандартный формат для связки сертификата и его закрытого ключа, зашифрованный паролем.

Пароль был в коде приложения, в нескольких строках от места загрузки файла:

text
1const-string v3, "2"

Один символ, находящийся в декомпилированном байт-коде в виде литерала. Один вызов openssl спустя:

text
1RSA private key, 2048 bit, 2 prime factors
2Subject: C=SA, ST=Jeddah, L=Jeddah, O=Nusuk.sa, CN=eshbeata@staq.io
3Issuer: C=SA, ST=Riyadh, L=Riyadh, O=The Saudi National Bank, OU=Finto,
4 CN=Application Issuer
5Serial: 0x24 (36)
6Valid: 2026-04-27 -> 2027-04-27
7Extended 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/

с запрашиваемой областью OAuth:

text
1identity accounts cards verification kyc cardpay transfers

Оба учётных данных также были объединены в одну строку и переданы отладочному логгеру вместе с базовым URL.

(Я не публикую ключевой материал или полные значения секретов. Важна суть проблемы.)

Почему это плохо, простыми словами

Представьте себе API банка как дверь с двумя замками.

Первый замок — это взаимный TLS (mTLS). Обычно сервер доказывает вам свою личность с помощью сертификата. При 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, но ничего не говорит о том, кто вызывает. Учётные данные для такого 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:

«Доступ к порталу Haseen ограничен для пользователей, находящихся на территории Королевства Саудовская Аравия. По любым дополнительным вопросам вы можете связаться с нами через службу «Мы заботимся», доступную на официальном портале Haseen.»

Портал Haseen, до которого я не могу добраться. Я ответил, объяснив, что я не саудовец, что это сертификат частного банка, обнаруженный в государственном приложении, и что я просто хочу передать его. Ответ снова был таким: форма работает только для граждан КСА.

Поэтому я подал отчёт в CERT/CC через их платформу VINCE в качестве координирующего посредника (VRF#26-08-DXMKL), ограничившись только этой находкой. Это путь, который вы выбираете, когда у пострадавшей стороны нет доступного собственного канала.

А затем я написал об этом в твиттере, в основном из чувства разочарования.

Твит набрал 1,5 миллиона просмотров. В течение нескольких часов служба поддержки Haseen написала мне по электронной почте без запроса, в той же переписке, где мне дважды говорили, что портал не для меня:

«По информации от нашей соответствующей команды, пожалуйста, предоставьте нам более подробную информацию об уязвимости безопасности.»

Я отправил полные детали. Я предпочитаю, чтобы проблема была исправлена, чем быть правым в отношении процесса.

Исправление

Следующее обновление приложения вышло как на Android, так и на iOS. Я скачал новую сборку для Android (17.5.0, versionCode 156635) напрямую из Play и сравнил её с той, что анализировал. Три проверки:

  1. Никакого контейнера сертификата нигде в новом наборе APK: ни одного файла с расширениями .pfx, .p12, .pkcs12, .jks, .bks, .pem или .key. Я также вычислил хеш старого nusuk.pfx и сравнил его побайтово с каждым файлом того же размера в новой сборке, на случай, если его просто переименовали. Совпадений нет.
  2. Никаких известных учётных данных. Я искал точные старые значения базового URL, области, идентификатора клиента и секрета клиента во всех DEX-файлах, нативных библиотеках, ресурсах, XML, JSON и сырых ресурсах. Ноль совпадений по всем четырём.
  3. Никакого пути в коде. Версия 17.4.9 содержала 4 571 файл в пакетах com.walletstaq и com.trustless; версия 17.5.0 содержит ноль. Маркеры baas.alahli, tppa/token, nusuk.pfx, субъект сертификата и имя эмитента дают ноль совпадений в декодированной сборке.

Весь кошелёк и интеграция BaaS были удалены. Проверка переименования на шаге 1 исключает возможность того, что значения просто переместились куда-то ещё в пакете.

Часть, которую никто извне не может проверить

Удаление секрета из приложения не делает его недействительным. Старые копии APK остаются доступными навсегда, а сертификат был действителен до апреля 2027 года. Поэтому открытым остаётся вопрос, был ли он отозван и был ли заменён OAuth-секрет.

Я попытался найти способ проверить это независимо. Его нет, и причина сама по себе является находкой.

Сертификат не содержит расширения crlDistributionPoints, поэтому список отзыва вообще не указан. Его единственная конечная точка отзыва:

text
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 отвечает, что портал ограничен для пользователей внутри КСА

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.

Что я бы вынес из этого

  1. Сокрытие чего-либо в приложении не является границей безопасности. Не в ресурсах, не в нативном .so, не за обфускацией или паролем, хранящимся в том же бинарном файле. Если приложение может это прочитать, то и все, кто установил приложение, тоже могут. Несколько команд узнают об этом публично каждый год.
  2. Общий клиентский сертификат — это не аутентификация. Если десять миллионов устройств предъявляют один и тот же сертификат, он говорит, какое приложение вызывает API, но ничего не говорит о том, кто вызывает, а приложение — это файл, который любой может скачать. Учётные данные для стороннего API, особенно банковского, должны находиться на сервере, который вы контролируете.
  3. Геоблокировка вашего канала раскрытия уязвимостей сама по себе является уязвимостью. Злоумышленники не заполняют формы. Если единственный способ сообщить об ошибке в приложении, опубликованном глобально для десяти миллионов человек, — это физически находиться внутри одной страны, то люди, которые не могут получить доступ к форме, — это именно те, кого вы больше всего хотите услышать. Потребовался вирусный твит, чтобы открыть канал, который файл security.txt открыл бы бесплатно.

Я анализировал общедоступный набор APK из Play Store в гостевом режиме, без учётной записи и без реальных личных данных. Я не предпринимал попыток обойти сетевую блокировку перед банковским API. Каждое значение в этой статье является либо структурным (пути, имена классов, метаданные сертификата), либо отредактированным; никакой ключевой материал и никакие полные секреты не публикуются.

Сохранение в один клик

Используйте YouMind для глубокого чтения вирусных статей с помощью ИИ

Сохраняйте источники, задавайте точные вопросы, обобщайте аргументы и превращайте вирусные статьи в полезные заметки в одном рабочем пространстве ИИ.

Исследовать YouMind
Для авторов

Превратите ваш Markdown в аккуратную статью для 𝕏

Когда вы публикуете длинные тексты, изображения, таблицы и блоки кода, форматирование в 𝕏 становится мучением. YouMind превращает полный черновик в Markdown в чистую статью, готовую к публикации в 𝕏.

Попробовать Markdown для 𝕏

Другие паттерны для анализа

Недавние виральные статьи

Смотреть другие виральные статьи