WRITEUP · MOBILE · OAUTH · 2026 · @YNSMROZTAS

Custom scheme, no PKCE

● live clk --:--:-- lab.vulnapp.portal

AndroScope session on an authorized lab build. Vendor identifiers stripped. No exploit kit on this page.

oauth // liveunrooted
radar--:--:--

Summary

On a public Android client I call com.vulnapp.portal (lab build 5.4.4), the OAuth 2.0 authorization-code flow returned to a custom scheme: vulnauth://oauth-callback/app. Android does not prove who owns a custom scheme. A second package on the same device can register the same scheme and receive the returning intent.

The authorization request carried no code_challenge. Without PKCE, an authorization code is not bound to the client that started the login. That combination is the finding. Names, hosts, and tokens from the original program are not published here.

Class: unverified custom-scheme redirect + public client without PKCE. Impact class: account takeover of the app’s identity provider session — if the token endpoint accepts the code alone.

Lab frame

Reconstructed from the live session. Original vendor chrome removed. Prompt is mitsec@com.vulnapp.portal.

← Profile

VulnApp account

SIGN IN
or

Sign in to use portal services on this device.

38  Response tamper (client-trust)
39  OAuth + PKCE observer          ★
40  WebSocket interceptor
D  NATIVE & RUNTIME
48  FULL AUTO (surface-aware)      ★
E  DEFENSE
50  TLS pinning observer           ★

★ start-here for this app
48  surface-aware loadout
39  OAuth + PKCE observer
14  intent / deeplink capture

mitsec@com.vulnapp.portal:

What AndroScope saw

Rootless session. Intent hook on the lab process while the operator tapped Sign in. Two Intent.getData rows mattered:

  1. An HTTPS authorize URL on the IdP, with response_type=code, a state, and redirect_uri=vulnauth://oauth-callback/app. No code_challenge / code_challenge_method.
  2. The custom-scheme return: vulnauth://oauth-callback/app?code=[REDACTED]&state=[REDACTED].

No Digital Asset Links check for that scheme. Tokens did not appear in the app sandbox before the code came back — the code is the portable secret in this flow.

Why the two bugs stack

Custom schemes are an intent filter. They are not App Links. Any package can claim vulnauth. When the browser hands the redirect to Android, the resolver may show a chooser — or deliver to a default handler.

PKCE (RFC 7636) is the control that still saves a public client if that intent is stolen. The client keeps a code_verifier. The server stored a challenge. The token request must prove the verifier. No challenge in the authorize URL means the code is not bound to the app that started login.

This page does not include a second APK, a receiver activity, or a token-endpoint request. Those belong in a private report, not a public writeup.

Impact (class)

If the token endpoint issues an access token (and a refresh token) for that code without a verifier, the holder of the code holds the user’s portal session. Scope depends on what the IdP actually grants — profile, documents, payments. Treat it as full account takeover of that identity, not a cosmetic deeplink bug.

Fix

References

RFC 8252 (OAuth 2.0 for Native Apps), RFC 7636 (PKCE), RFC 6749, Android App Links. Testing notes: Android 13, unrooted, AndroScope gadget-in-target. Original program identifiers removed under disclosure rules.

YUNUS EMRE ÖZTAŞ · MITSEC · LAB BUILD ONLY