Um arquivo de 2.835 bytes dentro de um aplicativo governamental com mais de 10 milhões de instalações continha um certificado de cliente ativo do Banco Nacional Saudita. Foi preciso um tweet viral para que alguém desse atenção ao caso.
Resumo
O aplicativo oficial Nusuk (com.moh.nusukapp, Ministério do Hajj e Umrah, mais de 10 milhões de instalações, com o selo "Governamental" da Google Play) distribuía um arquivo PKCS#12 contendo uma chave privada RSA e um certificado de cliente emitido pelo Banco Nacional Saudita. A senha do arquivo estava codificada diretamente no código do aplicativo, a poucas linhas de distância. Era um único caractere: 2.
Ao lado, em texto simples, estavam o ID e o segredo do cliente OAuth2 para a API de Banking-as-a-Service do banco, solicitando os escopos: identity accounts cards verification kyc cardpay transfers.
Qualquer pessoa que baixasse o aplicativo da Google Play tinha acesso a tudo isso.
Tentei denunciar. Disseram-me que o portal de vulnerabilidades está disponível apenas para usuários na Arábia Saudita. Então, twittei sobre o caso. O tweet teve 1,5 milhão de visualizações e, de repente, o mesmo portal pediu os detalhes. Um dia depois, as credenciais foram removidas do aplicativo.
https://x.com/iam_zachi/status/2094445016194207745
Este artigo cobre apenas a descoberta relacionada ao banco. Tudo o que está descrito abaixo já foi corrigido no aplicativo em produção, e o fornecedor afirma que as credenciais foram rotacionadas.
O que é o Nusuk
O Nusuk é a plataforma oficial do governo saudita para o Hajj e a Umrah. Ele gerencia autorizações de peregrinação, e-visas, reservas e o Cartão Nusuk. É operado pelo Ministério do Hajj e Umrah, é identificado como um aplicativo governamental verificado na Google Play e tem mais de dez milhões de instalações. Ele também contém um recurso de carteira, a Carteira Nusuk, desenvolvida em conjunto com o Banco Nacional Saudita e aprovada pelo SAMA, o banco central saudita. A carteira é a parte sobre a qual este artigo trata.
A descoberta
Baixei o conjunto de APKs diretamente da Google Play (versão 17.4.9, versionCode 131215) e o descompactei. Nada de exótico: Kotlin/Compose padrão, sem empacotador, sem ofuscação significativa.
Dentro dos recursos do aplicativo, em res/raw/nusuk.pfx, havia um contêiner PKCS#12 de 2.835 bytes. Um arquivo .pfx é o formato padrão para agrupar um certificado e sua chave privada, criptografados com uma senha.
A senha estava no código do aplicativo, a poucas linhas de onde o arquivo é carregado:
1const-string v3, "2"
Um caractere, presente como um literal no bytecode descompilado. Um comando openssl depois:
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
Um certificado de cliente ativo emitido por um banco, válido por mais um ano, destinado a autenticar um cliente junto a um servidor via TLS.
Não estava sozinho. O mesmo caminho de código é o componente da carteira, empacotado como com.walletstaq e integrado ao Trustless SDK da Staq Technologies, a empresa que opera a plataforma Finto BaaS do SNB. Mais três valores estavam lá em texto claro:
- CLIENT_ID = 379cc899…
- CLIENT_SECRET = ELrfNe9w… (64 bytes brutos, codificados em base64)
- SERVER_URL = https://api.baas.alahli.com/api/
com o escopo OAuth solicitado:
1identity accounts cards verification kyc cardpay transfers
Ambas as credenciais também foram concatenadas em uma única string e passadas para um logger de depuração, junto com a URL base.
(Não estou publicando o material da chave ou os valores completos do segredo. O que importa aqui é a forma do problema.)
Por que isso é grave, em termos simples
Pense na API do banco como uma porta com duas fechaduras.
A primeira fechadura é o TLS mútuo. Normalmente, um servidor prova sua identidade para você com um certificado. Com o mTLS, você também precisa provar sua identidade para o servidor com um certificado. Esse é o arquivo .pfx: o certificado e a chave privada que provam que você é o dono dele. É suposto ser algo que apenas o sistema cliente legítimo possui.
A segunda fechadura é o segredo do cliente OAuth, a senha que o aplicativo usa para solicitar um token de acesso à API do banco.
Ambas as fechaduras estavam dentro de um aplicativo gratuito na Google Play, e a chave para a caixa que guardava a primeira era o dígito 2.
Confirmei que o host de destino realmente impõe mTLS: o handshake TLS com api.baas.alahli.com solicita certificados de cliente (ele envia Acceptable client certificate CA names), e o certificado do servidor lê CN=*.baas.alahli.com, O=The Saudi National Bank. Portanto, não era um certificado decorativo para um ambiente de teste. Era a credencial para a porta da frente de uma API bancária de produção, com escopos que cobrem identidade, contas, cartões, KYC, pagamentos com cartão e transferências.
Separadamente do vazamento, há um problema de design subjacente. Um certificado de cliente que é distribuído de forma idêntica para dez milhões de dispositivos não pode distinguir uma instalação de outra. Cada cópia apresenta a mesma credencial, então o certificado diz ao banco qual aplicativo está chamando e nada sobre quem está chamando. Credenciais para uma API como essa pertencem ao seu próprio backend: o aplicativo fala com seu servidor, e seu servidor fala com o banco.
O que eu não fiz
Executei exatamente uma verificação no endpoint de token (POST /api/tppa/token) para ver se as credenciais estavam ativas. Ele retornou HTTP 403 do nginx. O mesmo aconteceu com uma solicitação sem nenhum certificado e com a URL raiz nua. Isso é um bloqueio em nível de rede na frente da API, quase certamente geográfico, e não diz nada sobre se as credenciais funcionam.
De fora da Arábia Saudita, não pude determinar se essas credenciais estavam ativas. Parei por aí. Qualquer coisa além disso seria uma tentativa de contornar o controle de acesso de um banco, e a descoberta não depende disso: uma chave privada e um segredo OAuth bancário com escopo de transferência em um artefato publicamente baixável é a descoberta, independentemente de eu conseguir ou não acessar o endpoint pessoalmente.
Tentando denunciar
Esta é a parte que fez o tweet viralizar, e é a metade mais interessante.
Procurei uma maneira de denunciar isso de forma responsável. O que existe:
Canal
Resultado
security.txt em nusuk.sa, haj.gov.sa, hajj.nusuk.sa
Não existe
Página de denúncia de vulnerabilidades do Saudi CERT (cert.gov.sa)
Redireciona para o NCA; página própria desativada
Formulário de vulnerabilidades do NCA (haseen.gov.sa)
Inacessível da Alemanha: timeout, bloqueio geográfico
bugbounty.sa
Programa fechado, HTTP 403 de fora
HackerOne / Bugcrowd
Nenhum programa para Nusuk, Ministério ou Elm
Contatos na listagem da loja de aplicativos
Endereços de suporte, sem mandato de segurança
Nada disso me deixou um canal com mandato de segurança que eu pudesse realmente acessar. Enviei e-mail mesmo assim. A resposta do Suporte Haseen:
"O acesso ao portal Haseen é restrito a usuários dentro do Reino da Arábia Saudita. Para quaisquer outras dúvidas, você pode entrar em contato conosco através do serviço 'Nós nos Importamos' disponível no Portal Oficial Haseen."
O Portal Haseen, que é exatamente o que não consigo acessar. Respondi explicando que não sou saudita, que se trata de um certificado bancário privado exposto em um aplicativo governamental e que só queria entregá-lo. A resposta, novamente, foi que o formulário só funciona para cidadãos da KSA.
Então, registrei um relatório no CERT/CC através da plataforma VINCE deles como intermediário coordenador (VRF#26-08-DXMKL), com escopo apenas para esta descoberta. Essa é a rota que você segue quando a parte afetada não tem um canal acessível próprio.
E então twittei sobre o assunto, principalmente por frustração.
O tweet teve 1,5 milhão de visualizações. Em poucas horas, o Suporte Haseen me enviou um e-mail, sem ser solicitado, na mesma thread que duas vezes me disse que o portal não era para mim:
"De acordo com nossa equipe relevante, por favor, forneça mais detalhes sobre a vulnerabilidade de segurança."
Enviei todos os detalhes. Prefiro que a coisa seja corrigida do que estar certo sobre o processo.
A correção
A próxima atualização do aplicativo foi lançada no Android e no iOS. Baixei a nova versão do Android (17.5.0, versionCode 156635) diretamente da Play e a comparei com o que havia analisado. Três verificações:
- Nenhum contêiner de certificado em nenhum lugar do novo conjunto de APKs: nenhum arquivo com extensão .pfx, .p12, .pkcs12, .jks, .bks, .pem ou .key. Também fiz o hash do antigo nusuk.pfx e o comparei byte a byte com todos os arquivos do mesmo tamanho na nova versão, caso ele tivesse sido simplesmente renomeado. Nenhuma correspondência.
- Nenhuma das credenciais conhecidas. Pesquisei os valores antigos exatos para URL base, escopo, ID do cliente e segredo do cliente em todos os arquivos DEX, bibliotecas nativas, assets, XML, JSON e recursos brutos. Zero resultados para todos os quatro.
- Nenhum caminho de código. A versão 17.4.9 continha 4.571 arquivos nos pacotes
com.walletstaqecom.trustless; a 17.5.0 contém zero. Os marcadoresbaas.alahli,tppa/token,nusuk.pfx, o assunto do certificado e o nome do emissor retornam zero resultados na versão decodificada.
Toda a integração da carteira e do BaaS foi removida. A verificação de renomeação na etapa 1 é o que descarta a possibilidade de os valores terem sido simplesmente movidos para outro lugar no pacote.
A parte que ninguém de fora pode verificar
Remover um segredo de um aplicativo não invalida o segredo. Cópias antigas do APK permanecem disponíveis para sempre, e o certificado era válido até abril de 2027. Portanto, a questão em aberto é se ele foi revogado e se o segredo OAuth foi rotacionado.
Procurei uma maneira de verificar isso de forma independente. Não existe, e a razão é, por si só, uma descoberta.
O certificado não possui extensão crlDistributionPoints, portanto nenhuma lista de revogação é referenciada. Seu único endpoint de revogação é:
1OCSP - URI: http://finto-ocsp-responder.prod.svc.cluster.local:8080/api/v1/ocsp
.cluster.local é o sufixo DNS interno de um cluster Kubernetes. Por definição, não é roteável na internet pública e resolve para NXDOMAIN de qualquer lugar fora desse cluster, via HTTP simples na porta 8080.
Portanto, o status de revogação deste certificado não pode ser verificado de fora da infraestrutura do banco, porque não há nada aqui para consultar. Para qualquer parte confiável fora desse cluster, os certificados deste emissor são efetivamente irrevogáveis, o que merece um parágrafo na revisão de arquitetura de alguém.
Esse mesmo campo também publicou o nome do cluster, namespace, nome do serviço e porta da PKI de produção da plataforma BaaS de um banco, em um aplicativo distribuído para dez milhões de pessoas.
Apenas o SNB, a Finto ou a Staq podem confirmar a rotação. O fornecedor afirma que as credenciais foram rotacionadas. Não tenho como verificar isso de forma independente.
Linha do tempo
Data (2026)
Evento
29 de agosto
APK analisado, descoberta confirmada localmente
29 de agosto
Relatório enviado por e-mail; Haseen responde que o portal é restrito a usuários na KSA
31 de agosto
Tentativas repetidas, mesma resposta. Eu twitto sobre o caso; ~1,5 milhão de visualizações
1º de setembro
Haseen reabre a thread sem ser solicitado, pede detalhes. Detalhes enviados
1º de setembro
Relatório também registrado no CERT/CC VINCE (VRF#26-08-DXMKL) como intermediário
1º de setembro
Versão 17.5.0 publicada na Google Play
3 de setembro
Reteste no conjunto de APKs não modificado da versão 17.5.0 confirma a remoção completa
8 de setembro
Este artigo
Não posso provar que a atualização foi causada pelo meu relatório. A versão 17.5.0 pode muito bem já estar em desenvolvimento. O que posso mostrar é que o material estava na versão 17.4.9 e não está na versão 17.5.0.
O que eu aprendi com isso
- Esconder algo no aplicativo não é uma fronteira de segurança. Não em recursos, não em um .so nativo, não atrás de ofuscação ou de uma senha armazenada no mesmo binário. Se o aplicativo pode ler, todos que instalarem o aplicativo também podem. Várias equipes aprendem isso publicamente todos os anos.
- Um certificado de cliente compartilhado não é autenticação. Se dez milhões de dispositivos apresentam o mesmo certificado, ele diz qual aplicativo está chamando e nada sobre quem está chamando, e o aplicativo é um arquivo que qualquer um pode baixar. Credenciais para uma API de terceiros, especialmente de um banco, pertencem a um servidor que você controla.
- Bloquear geograficamente seu canal de divulgação de vulnerabilidades é, por si só, uma vulnerabilidade. Atacantes não preenchem formulários. Se a única maneira de relatar uma falha em um aplicativo publicado globalmente para dez milhões de pessoas é estar fisicamente dentro de um país, então as pessoas que não conseguem acessar o formulário são exatamente aquelas de quem você mais precisa ouvir. Foi preciso um tweet viral para abrir um canal que um arquivo security.txt teria aberto de graça.
Analisei o conjunto de APKs publicamente disponível da Play Store no modo convidado, sem conta e sem dados pessoais reais. Não fiz nenhuma tentativa de contornar o bloqueio em nível de rede na frente da API bancária. Cada valor neste artigo é estrutural (caminhos, nomes de classes, metadados de certificados) ou foi editado; nenhum material de chave privada e nenhum segredo completo são publicados.





