정부 앱(1,000만+ 설치) 내부의 2,835바이트 파일에 사우디 국립은행의 라이브 클라이언트 인증서가 포함되어 있었습니다. 누군가 이를 살펴보게 하는 데는 바이럴 트윗 하나면 충분했습니다.
요약
공식 Nusuk 앱(com.moh.nusukapp, 하지 및 음라부, 1,000만+ 설치, Google Play '정부' 배지 보유)에는 개인 RSA 키와 사우디 국립은행이 발급한 클라이언트 인증서가 포함된 PKCS#12 파일이 포함되어 있었습니다. 해당 파일의 비밀번호는 앱 자체 코드에서 몇 줄 떨어진 곳에 하드코딩되어 있었습니다. 단 한 글자였습니다: 2.
그 옆에는 일반 텍스트로 은행의 BaaS(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에서 공식 정부 앱으로 표시되어 있으며, 1,000만 회 이상 설치되었습니다. 또한 사우디 국립은행과 함께 구축되고 사우디 중앙은행인 SAMA의 승인을 받은 지갑 기능인 Nusuk Wallet도 포함되어 있습니다. 이 지갑이 이 글에서 다루는 부분입니다.
발견 사항
Google Play에서 직접 APK 세트(버전 17.4.9, versionCode 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
은행이 발급한 라이브 클라이언트 인증서로, 1년 더 유효하며 TLS를 통해 클라이언트를 서버에 인증하는 용도로 사용됩니다.
이것만 있는 것이 아니었습니다. 동일한 코드 경로는 com.walletstaq로 패키징되고 SNB의 Finto BaaS 플랫폼을 운영하는 회사인 Staq Technologies의 Trustless SDK에 연결된 지갑 구성 요소입니다. 일반 텍스트로 세 가지 값이 더 있었습니다:
- CLIENT_ID = 379cc899…
- CLIENT_SECRET = ELrfNe9w… (64 raw bytes, base64-encoded)
- SERVER_URL = https://api.baas.alahli.com/api/
요청된 OAuth 범위는 다음과 같습니다:
1identity accounts cards verification kyc cardpay transfers
두 자격 증명 모두 단일 문자열로 연결되어 기본 URL과 함께 디버그 로거에 전달되었습니다.
(저는 키 자료나 전체 시크릿 값을 공개하지 않습니다. 여기서 중요한 것은 문제의 형태입니다.)
이것이 왜 심각한 문제인지, 쉽게 설명하자면
은행의 API를 두 개의 자물쇠가 있는 문이라고 생각해 보십시오.
첫 번째 자물쇠는 상호 TLS(mTLS)입니다. 일반적으로 서버는 인증서로 자신의 신원을 증명합니다. 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로 표시됩니다. 따라서 이것은 테스트 환경을 위한 장식용 인증서가 아니었습니다. 이는 신원, 계정, 카드, KYC, 카드 결제 및 송금 범위를 포함하는 프로덕션 뱅킹 API의 정문 자격 증명이었습니다.
유출과는 별개로, 그 밑에는 설계 문제가 있습니다. 천만 대의 기기에 동일하게 제공되는 클라이언트 인증서는 한 설치를 다른 설치와 구분할 수 없습니다. 모든 복사본이 동일한 자격 증명을 제시하므로, 인증서는 은행에 어떤 앱이 호출하는지만 알려줄 뿐 누가 호출하는지는 전혀 알려주지 않습니다. 이와 같은 API의 자격 증명은 자체 백엔드 뒤에 있어야 합니다: 앱은 사용자 서버와 통신하고, 사용자 서버는 은행과 통신해야 합니다.
내가 하지 않은 일
자격 증명이 활성화되어 있는지 확인하기 위해 토큰 엔드포인트(POST /api/tppa/token)에 대해 정확히 한 번의 확인을 실행했습니다. nginx에서 HTTP 403을 반환했습니다. 인증서 없이 요청한 경우와 기본 루트 URL도 마찬가지였습니다. 이는 API 앞에 있는 네트워크 수준의 차단으로, 거의 확실히 지리적 차단이며 자격 증명이 작동하는지 여부에 대해서는 아무것도 알려주지 않습니다.
사우디아라비아 외부에서는 이 자격 증명이 활성화되어 있는지 확인할 수 없었습니다. 그 이상은 진행하지 않았습니다. 그 이상의 행동은 은행의 액세스 제어를 우회하려는 시도였을 것이며, 발견 사실은 이에 의존하지 않습니다: 개인 키와 송금 범위가 있는 은행 OAuth 시크릿이 공개적으로 다운로드 가능한 아티팩트에 존재한다는 것이 발견 사실이며, 제가 개인적으로 엔드포인트에 도달할 수 있는지 여부와는 무관합니다.
신고 시도
이 부분이 트윗을 바이럴하게 만든 부분이며, 더 흥미로운 절반입니다.
책임감 있게 신고할 방법을 찾았습니다. 존재하는 것은 다음과 같습니다:
채널
결과
nusuk.sa, haj.gov.sa, hajj.nusuk.sa의 security.txt
존재하지 않음
Saudi CERT (cert.gov.sa) 취약점 신고 페이지
NCA로 리디렉션; 자체 신고 페이지 폐쇄
NCA 취약점 양식 (haseen.gov.sa)
독일에서 접근 불가: 시간 초과, 지리적 차단
bugbounty.sa
폐쇄된 프로그램, 외부에서 HTTP 403
HackerOne / Bugcrowd
Nusuk, Ministry 또는 Elm에 대한 프로그램 없음
앱 스토어 등록 연락처
지원 주소, 보안 권한 없음
이 중 제가 실제로 도달할 수 있는 보안 권한이 있는 채널은 없었습니다. 그래도 이메일을 보냈습니다. Haseen Support의 답변:
"Haseen 포털에 대한 액세스는 사우디아라비아 왕국 내 사용자로 제한됩니다. 추가 문의 사항이 있으시면 공식 Haseen 포털에서 제공되는 'We Care' 서비스를 통해 문의하시기 바랍니다."
제가 도달할 수 없는 바로 그 Haseen 포털입니다. 제가 사우디인이 아니며, 정부 앱에 노출된 민간 은행 인증서이며, 단지 이를 전달하고 싶다고 설명하는 답장을 보냈습니다. 다시 한 번, 양식은 KSA 시민에게만 작동한다는 답변이 돌아왔습니다.
그래서 저는 CERT/CC에 VINCE 플랫폼을 통해 조정 중개자(VRF#26-08-DXMKL)로 신고서를 제출했으며, 이번 발견 건으로만 범위를 한정했습니다. 이는 영향을 받은 당사자가 자체적으로 도달 가능한 채널이 없을 때 취하는 경로입니다.
그리고 나서 대부분 좌절감에 트윗을 올렸습니다.
트윗은 150만 회 조회수를 기록했습니다. 몇 시간 만에 Haseen Support가 저에게 이메일을 보냈습니다. 이전에 두 번이나 포털이 저를 위한 것이 아니라고 말했던 동일한 스레드에서 말입니다:
"관련 팀에 따르면, 보안 취약점에 대한 자세한 정보를 제공해 주시기 바랍니다."
전체 세부 정보를 보냈습니다. 절차에 대해 옳다는 것보다 문제가 해결되는 것이 더 중요했습니다.
수정 사항
다음 앱 업데이트가 Android와 iOS 모두에 출시되었습니다. Play에서 직접 새 Android 빌드(17.5.0, versionCode 156635)를 가져와 분석했던 것과 비교했습니다. 세 가지 확인 사항:
- 새 APK 세트 어디에도 인증서 컨테이너 없음:
.pfx,.p12,.pkcs12,.jks,.bks,.pem또는.key확장자를 가진 파일 없음. 또한 이전nusuk.pfx의 해시를 계산하여 새 빌드의 모든 동일한 크기 파일과 바이트 단위로 비교했습니다. 단순히 이름이 변경되었을 경우를 대비해서입니다. 일치하는 항목 없음. - 알려진 자격 증명 없음. 모든 DEX 파일, 네이티브 라이브러리, assets, XML, JSON 및 raw 리소스에서 기본 URL, 범위, 클라이언트 ID 및 클라이언트 시크릿의 정확한 이전 값을 검색했습니다. 네 가지 모두 0건.
- 코드 경로 없음. 버전 17.4.9에는
com.walletstaq및com.trustless패키지 아래 4,571개의 파일이 포함되어 있었습니다. 17.5.0에는 0개입니다.baas.alahli,tppa/token,nusuk.pfx, 인증서 주체 및 발급자 이름 마커는 디코딩된 빌드에서 모두 0건입니다.
전체 지갑 및 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으로 확인되며, 포트 8080의 일반 HTTP를 통해서도 마찬가지입니다.
따라서 이 인증서의 폐기 상태는 은행 인프라 외부에서는 확인할 수 없습니다. 왜냐하면 여기에는 쿼리할 대상이 없기 때문입니다. 해당 단일 클러스터 외부의 모든 신뢰 당사자에게 이 발급자의 인증서는 사실상 폐기가 불가능하며, 이는 누군가의 아키텍처 검토에서 별도의 문단을 할애할 가치가 있습니다.
동일한 필드는 또한 천만 명에게 배포된 앱에서 은행 BaaS 플랫폼의 프로덕션 PKI의 클러스터 이름, 네임스페이스, 서비스 이름 및 포트를 공개했습니다.
SNB, Finto 또는 Staq만이 교체를 확인할 수 있습니다. 공급업체 측에서는 자격 증명이 교체되었다고 밝혔습니다. 이를 독립적으로 확인할 방법은 없습니다.
타임라인
날짜 (2026)
이벤트
8월 29일
APK 분석, 발견 사항 로컬 확인
8월 29일
이메일로 신고; Haseen, 포털이 KSA 내 사용자로 제한된다고 답변
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 Store APK 세트를 분석했으며, 계정이나 실제 개인 데이터는 사용하지 않았습니다. 뱅킹 API 앞의 네트워크 수준 차단을 우회하려는 시도는 전혀 하지 않았습니다. 이 글의 모든 값은 구조적(경로, 클래스 이름, 인증서 메타데이터)이거나 편집되었습니다. 개인 키 자료나 전체 시크릿은 공개되지 않습니다.





