ไฟล์ขนาด 2,835 ไบต์ภายในแอปพลิเคชันรัฐบาลที่มีการติดตั้งมากกว่า 10 ล้านครั้ง บรรจุใบรับรองไคลเอนต์ที่ใช้งานจริงของธนาคารแห่งชาติซาอุดีอาระเบีย การจะให้ใครสักคนมาสนใจมันต้องอาศัยทวีตที่กลายเป็นไวรัล
สรุปโดยย่อ
แอปพลิเคชัน Nusuk อย่างเป็นทางการ (com.moh.nusukapp, กระทรวงฮัจญ์และอุมเราะห์, ติดตั้งมากกว่า 10 ล้านครั้ง, มีป้าย "Government" จาก Google Play) ได้บรรจุไฟล์ PKCS#12 ที่มีคีย์ส่วนตัว RSA และใบรับรองไคลเอนต์ที่ออกโดยธนาคารแห่งชาติซาอุดีอาระเบีย รหัสผ่านของไฟล์ดังกล่าวถูกฝังไว้ในโค้ดของแอปพลิเคชันเอง ห่างออกไปไม่กี่บรรทัด มันเป็นตัวอักษรเพียงตัวเดียว: 2
ถัดจากนั้น ในรูปแบบข้อความธรรมดา ยังมี OAuth2 client ID และ client secret สำหรับ API Banking-as-a-Service ของธนาคาร ซึ่งขอขอบเขตการเข้าถึง 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 มาตรฐาน ไม่มี packer ไม่มีการทำให้สับสนที่มีนัยสำคัญ
ภายในทรัพยากรของแอปพลิเคชัน ที่ res/raw/nusuk.pfx มีคอนเทนเนอร์ PKCS#12 ขนาด 2,835 ไบต์ ไฟล์ .pfx เป็นรูปแบบมาตรฐานสำหรับการรวมใบรับรองพร้อมกับคีย์ส่วนตัว ซึ่งเข้ารหัสด้วยรหัสผ่าน
รหัสผ่านอยู่ในโค้ดของแอปพลิเคชัน ห่างจากตำแหน่งที่โหลดไฟล์ไม่กี่บรรทัด:
1const-string v3, "2"
อักขระหนึ่งตัว ปรากฏเป็นค่าคงที่ใน bytecode ที่ถูก decompile หลังจากเรียก 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/
พร้อมกับขอบเขต OAuth ที่ร้องขอ:
1identity accounts cards verification kyc cardpay transfers
ข้อมูลรับรองทั้งสองรายการยังถูกต่อเข้าด้วยกันเป็นสตริงเดียวและส่งให้กับ debug logger พร้อมกับ base URL
(ฉันจะไม่เผยแพร่เนื้อหาคีย์หรือค่าลับทั้งหมด สิ่งที่สำคัญคือลักษณะของปัญหา)
เหตุใดสิ่งนี้จึงแย่ อธิบายแบบเข้าใจง่าย
ลองนึกภาพ API ของธนาคารเป็นประตูที่มีแม่กุญแจสองตัว
แม่กุญแจดอกแรกคือ mutual TLS โดยปกติแล้วเซิร์ฟเวอร์จะพิสูจน์ตัวตนให้คุณเห็นด้วยใบรับรอง ด้วย mTLS คุณก็ต้องพิสูจน์ตัวตนต่อเซิร์ฟเวอร์ด้วยใบรับรองเช่นกัน นั่นคือไฟล์ .pfx: ใบรับรองและคีย์ส่วนตัวที่พิสูจน์ว่าคุณเป็นเจ้าของมัน มันควรจะเป็นสิ่งที่เฉพาะระบบไคลเอนต์ที่ถูกต้องตามกฎหมายเท่านั้นที่มี
แม่กุญแจดอกที่สองคือ OAuth client secret ซึ่งเป็นรหัสผ่านที่แอปพลิเคชันใช้เพื่อขอโทเค็นการเข้าถึงจาก 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 ธนาคารที่ใช้งานจริง โดยมีขอบเขตครอบคลุม identity, accounts, cards, KYC, card payments และ transfers
นอกเหนือจากการรั่วไหลแล้ว ยังมีปัญหาด้านการออกแบบที่ซ่อนอยู่ ใบรับรองไคลเอนต์ที่จัดส่งเหมือนกันไปยังอุปกรณ์สิบล้านเครื่องไม่สามารถแยกแยะการติดตั้งหนึ่งจากอีกเครื่องหนึ่งได้ ทุกสำเนานำเสนอข้อมูลรับรองเดียวกัน ดังนั้นใบรับรองจึงบอกธนาคารได้เพียงว่าแอปพลิเคชันใดกำลังเรียก แต่ไม่บอกว่าใครเป็นผู้เรียก ข้อมูลรับรองสำหรับ API ประเภทนี้ควรอยู่เบื้องหลังแบ็คเอนด์ของคุณเอง: แอปพลิเคชันคุยกับเซิร์ฟเวอร์ของคุณ เซิร์ฟเวอร์ของคุณคุยกับธนาคาร
สิ่งที่ฉันไม่ได้ทำ
ฉันทำการตรวจสอบเพียงครั้งเดียวกับ token endpoint (POST /api/tppa/token) เพื่อดูว่าข้อมูลรับรองยังใช้งานได้อยู่หรือไม่ ซึ่งส่งคืน HTTP 403 จาก nginx เช่นเดียวกับคำขอที่ไม่มีใบรับรองเลย และเช่นเดียวกับ URL รากเปล่า นั่นคือการบล็อกระดับเครือข่ายที่อยู่ด้านหน้า API ซึ่งเกือบแน่นอนว่าเป็นการจำกัดตามภูมิศาสตร์ และไม่ได้บอกอะไรเลยว่าข้อมูลรับรองนั้นใช้งานได้หรือไม่
จากนอกประเทศซาอุดีอาระเบีย ฉันไม่สามารถระบุได้ว่าข้อมูลรับรองเหล่านี้ยังใช้งานได้หรือไม่ ฉันหยุดเพียงเท่านี้ การดำเนินการใดๆ เพิ่มเติมจะเป็นการพยายามเลี่ยงการควบคุมการเข้าถึงของธนาคาร และการค้นพบนี้ไม่ได้ขึ้นอยู่กับสิ่งนั้น: คีย์ส่วนตัวและความลับ OAuth ของธนาคารที่มีขอบเขตการโอนเงิน ซึ่งอยู่ในสิ่งประดิษฐ์ที่สามารถดาวน์โหลดได้ต่อสาธารณะ คือการค้นพบ ไม่ว่าฉันจะสามารถเข้าถึง endpoint ได้ด้วยตนเองหรือไม่ก็ตาม
ความพยายามในการรายงาน
นี่คือส่วนที่ทำให้ทวีตกลายเป็นไวรัล และเป็นครึ่งหนึ่งที่น่าสนใจกว่า
ฉันมองหาวิธีที่จะรายงานเรื่องนี้อย่างมีความรับผิดชอบ สิ่งที่มีอยู่:
ช่องทาง
ผลลัพธ์
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
ข้อมูลติดต่อในรายการ App Store
ที่อยู่อีเมลสนับสนุน, ไม่มีอำนาจหน้าที่ด้านความปลอดภัย
ไม่มีช่องทางใดที่ให้ฉันมีช่องทางที่มีอำนาจหน้าที่ด้านความปลอดภัยที่ฉันสามารถเข้าถึงได้จริง ฉันยังคงส่งอีเมล คำตอบจากฝ่ายสนับสนุน Haseen:
"การเข้าถึงพอร์ทัล Haseen ถูกจำกัดเฉพาะผู้ใช้ภายในราชอาณาจักรซาอุดีอาระเบีย สำหรับข้อสงสัยเพิ่มเติม คุณสามารถติดต่อเราผ่านบริการ 'We Care' ที่มีอยู่ในพอร์ทัล Haseen อย่างเป็นทางการ"
พอร์ทัล Haseen ซึ่งเป็นสิ่งที่ฉันไม่สามารถเข้าถึงได้ ฉันตอบกลับไปโดยอธิบายว่าฉันไม่ใช่ชาวซาอุดีอาระเบีย ว่านี่คือใบรับรองธนาคารเอกชนที่เปิดเผยในแอปพลิเคชันรัฐบาล และฉันแค่อยากจะส่งมอบมันให้ คำตอบอีกครั้งคือแบบฟอร์มนี้ใช้ได้เฉพาะพลเมืองของ KSA เท่านั้น
ดังนั้นฉันจึงยื่นรายงานกับ CERT/CC ผ่านแพลตฟอร์ม VINCE ของพวกเขาในฐานะตัวกลางประสานงาน (VRF#26-08-DXMKL) โดยจำกัดขอบเขตเฉพาะการค้นพบนี้เท่านั้น นั่นคือเส้นทางที่คุณใช้เมื่อฝ่ายที่ได้รับผลกระทบไม่มีช่องทางที่สามารถเข้าถึงได้ของตนเอง
จากนั้นฉันก็ทวีตเกี่ยวกับเรื่องนี้ ส่วนใหญ่แล้วด้วยความหงุดหงิด
ทวีตดังกล่าวมีผู้เข้าชม 1.5 ล้านครั้ง ภายในไม่กี่ชั่วโมง ฝ่ายสนับสนุน Haseen ส่งอีเมลถึงฉัน โดยไม่ได้รับการร้องขอ ในเธรดเดียวกันที่เคยบอกฉันสองครั้งว่าพอร์ทัลนี้ไม่ใช่สำหรับฉัน:
"ตามที่ทีมงานที่เกี่ยวข้องแจ้ง กรุณาให้รายละเอียดเพิ่มเติมเกี่ยวกับช่องโหว่ด้านความปลอดภัยแก่เรา"
ฉันส่งรายละเอียดทั้งหมด ฉันอยากให้สิ่งนี้ได้รับการแก้ไขมากกว่าที่จะถูกต้องเกี่ยวกับกระบวนการ
การแก้ไข
อัปเดตแอปพลิเคชันครั้งถัดไปวางจำหน่ายบนทั้ง Android และ iOS ฉันดึงบิลด์ Android ใหม่ (17.5.0, versionCode 156635) โดยตรงจาก Play และเปรียบเทียบกับสิ่งที่ฉันวิเคราะห์ไว้ การตรวจสอบสามประการ:
- ไม่มีคอนเทนเนอร์ใบรับรองใดๆ ในชุด APK ใหม่: ไม่มีไฟล์ที่มีนามสกุล .pfx, .p12, .pkcs12, .jks, .bks, .pem หรือ .key ฉันยังแฮชไฟล์ nusuk.pfx เก่าและเปรียบเทียบแบบไบต์ต่อไบต์กับไฟล์ทุกไฟล์ที่มีขนาดเท่ากันในบิลด์ใหม่ ในกรณีที่มันถูกเปลี่ยนชื่อเท่านั้น ไม่พบการจับคู่
- ไม่มีข้อมูลรับรองที่ทราบค่าใดๆ ฉันค้นหาค่าเก่าที่แน่นอนสำหรับ base URL, scope, client ID และ client secret ในไฟล์ 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 secret ถูกเปลี่ยนหรือไม่
ฉันพยายามหาวิธีตรวจสอบสิ่งนั้นอย่างอิสระ ไม่มีวิธีดังกล่าว และเหตุผลก็คือการค้นพบในตัวมันเอง
ใบรับรองไม่มีส่วนขยาย crlDistributionPoints ดังนั้นจึงไม่มีการอ้างอิงถึงรายการเพิกถอนเลย จุดสิ้นสุดการเพิกถอนเพียงจุดเดียวคือ:
1OCSP - URI: http://finto-ocsp-responder.prod.svc.cluster.local:8080/api/v1/ocsp
.cluster.local คือส่วนต่อท้าย DNS ภายในของคลัสเตอร์ Kubernetes โดยนิยามแล้วมันไม่สามารถกำหนดเส้นทางได้บนอินเทอร์เน็ตสาธารณะ และแก้ไขเป็น NXDOMAIN จากทุกที่ภายนอกคลัสเตอร์นั้น ผ่าน HTTP ธรรมดาบนพอร์ต 8080
ดังนั้นสถานะการเพิกถอนของใบรับรองนี้จึงไม่สามารถตรวจสอบได้จากภายนอกโครงสร้างพื้นฐานของธนาคาร เนื่องจากไม่มีอะไรให้สอบถามที่นี่ สำหรับฝ่ายที่ต้องพึ่งพาใดๆ นอกคลัสเตอร์นั้น ใบรับรองจากผู้ออกนี้จะไม่สามารถเพิกถอนได้อย่างมีประสิทธิภาพ ซึ่งสมควรได้รับย่อหน้าในการทบทวนสถาปัตยกรรมของใครบางคน
ฟิลด์เดียวกันนั้นยังเผยแพร่ชื่อคลัสเตอร์, namespace, ชื่อบริการ และพอร์ตของ 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 ของธนาคาร ทุกค่าในบทความนี้เป็นโครงสร้าง (พาธ, ชื่อคลาส, ข้อมูลเมตาของใบรับรอง) หรือถูกแก้ไข; ไม่มีเนื้อหาคีย์ส่วนตัวและไม่มีความลับทั้งหมดที่ถูกเผยแพร่





