-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Port MASTG-TEST-0026: Testing Implicit Intents (android) (by @appknox) #3271
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
ea2f924
3ab2c2d
6a6b447
bf6baa4
afc7e8c
c4092ce
f9191bd
fafeb66
2c7ee5b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||
| package="com.example.datainteceptor"> | ||
|
|
||
| <application android:label="AttackerApp"> | ||
|
|
||
| <!-- Default launcher activity --> | ||
| <activity android:name=".MainActivity" | ||
| android:exported="true"> | ||
| <intent-filter> | ||
| <action android:name="android.intent.action.MAIN" /> | ||
| <category android:name="android.intent.category.LAUNCHER" /> | ||
| </intent-filter> | ||
| </activity> | ||
|
|
||
| <!-- Interceptor activity to catch implicit intent --> | ||
| <activity android:name=".InterceptorActivity" | ||
| android:exported="true"> | ||
| <intent-filter> | ||
| <action android:name="org.owasp.mastestapp.PROCESS_SENSITIVE_DATA" /> | ||
| <category android:name="android.intent.category.DEFAULT" /> | ||
| </intent-filter> | ||
| </activity> | ||
|
|
||
| </application> | ||
| </manifest> | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know there are other demos with The reason is that the reversed XML has additional entries while the entries from the original remain the same. Or are there exceptions to that? @cpholguera is there some reason for that I missed in https://docs.google.com/document/d/1EMsVdfrDBAu0gmjWAUEs60q-fWaOmDB5oecY9d9pOlg/edit?pli=1&tab=t.0 ? |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||
| android:versionCode="1" | ||
| android:versionName="1.0" | ||
| android:compileSdkVersion="35" | ||
| android:compileSdkVersionCodename="15" | ||
| package="org.owasp.mastestapp" | ||
| platformBuildVersionCode="35" | ||
| platformBuildVersionName="15"> | ||
| <uses-sdk | ||
| android:minSdkVersion="29" | ||
| android:targetSdkVersion="35"/> | ||
| <uses-permission android:name="android.permission.INTERNET"/> | ||
| <permission | ||
| android:name="org.owasp.mastestapp.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION" | ||
| android:protectionLevel="signature"/> | ||
| <uses-permission android:name="org.owasp.mastestapp.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION"/> | ||
| <application | ||
| android:theme="@style/Theme.MASTestApp" | ||
| android:label="@string/app_name" | ||
| android:icon="@mipmap/ic_launcher" | ||
| android:debuggable="true" | ||
| android:testOnly="true" | ||
| android:allowBackup="true" | ||
| android:supportsRtl="true" | ||
| android:extractNativeLibs="false" | ||
| android:fullBackupContent="@xml/backup_rules" | ||
| android:roundIcon="@mipmap/ic_launcher_round" | ||
| android:appComponentFactory="androidx.core.app.CoreComponentFactory" | ||
| android:dataExtractionRules="@xml/data_extraction_rules"> | ||
| <activity | ||
| android:theme="@style/Theme.MASTestApp" | ||
| android:name="org.owasp.mastestapp.MainActivity" | ||
| android:exported="true" | ||
| android:windowSoftInputMode="adjustResize"> | ||
| <intent-filter> | ||
| <action android:name="android.intent.action.MAIN"/> | ||
| <category android:name="android.intent.category.LAUNCHER"/> | ||
| </intent-filter> | ||
| </activity> | ||
| <activity | ||
| android:name="org.owasp.mastestapp.VulnerableActivity" | ||
| android:exported="true"> | ||
| <intent-filter> | ||
| <action android:name="org.owasp.mastestapp.PROCESS_SENSITIVE_DATA"/> | ||
| <category android:name="android.intent.category.DEFAULT"/> | ||
| </intent-filter> | ||
| </activity> | ||
| <activity | ||
| android:name="androidx.compose.ui.tooling.PreviewActivity" | ||
| android:exported="true"/> | ||
| <activity | ||
| android:name="androidx.activity.ComponentActivity" | ||
| android:exported="true"/> | ||
| <provider | ||
| android:name="androidx.startup.InitializationProvider" | ||
| android:exported="false" | ||
| android:authorities="org.owasp.mastestapp.androidx-startup"> | ||
| <meta-data | ||
| android:name="androidx.emoji2.text.EmojiCompatInitializer" | ||
| android:value="androidx.startup"/> | ||
| <meta-data | ||
| android:name="androidx.lifecycle.ProcessLifecycleInitializer" | ||
| android:value="androidx.startup"/> | ||
| <meta-data | ||
| android:name="androidx.profileinstaller.ProfileInstallerInitializer" | ||
| android:value="androidx.startup"/> | ||
| </provider> | ||
| <receiver | ||
| android:name="androidx.profileinstaller.ProfileInstallReceiver" | ||
| android:permission="android.permission.DUMP" | ||
| android:enabled="true" | ||
| android:exported="true" | ||
| android:directBootAware="false"> | ||
| <intent-filter> | ||
| <action android:name="androidx.profileinstaller.action.INSTALL_PROFILE"/> | ||
| </intent-filter> | ||
| <intent-filter> | ||
| <action android:name="androidx.profileinstaller.action.SKIP_FILE"/> | ||
| </intent-filter> | ||
| <intent-filter> | ||
| <action android:name="androidx.profileinstaller.action.SAVE_PROFILE"/> | ||
| </intent-filter> | ||
| <intent-filter> | ||
| <action android:name="androidx.profileinstaller.action.BENCHMARK_OPERATION"/> | ||
| </intent-filter> | ||
| </receiver> | ||
| </application> | ||
| </manifest> |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This demo implements a special case where the developer mistakenly uses an implicit intent and an exported Activity for supposedly internal ICP. This is generally not the intended use case of implicit intents, as they should be used when the developers do not know which app should react to it. I think this should be more clearly described in the demo. Further, the issue of implicit intent hijacking is more general, as actions can be anything from built-in to the same package name or completely custom. And depending on the configuration, the likelihood of an attack varies. This demo declares two intent filters for the custom action. This means the user must, in any case, choose between the valid and the malicious app:
I think this should be mentioned in the demo because it is relevant to the risk. To cover the different situations, we should make different demos. Let me suggest the following:
We take your proposal for the first two but implement the two examples from the MASTG-V1-Test as new demos so we don't lose them. These demos should also demonstrate situations where no app chooser is shown to the user. For example, if an implicit intent is used for which there is only the attacker app available. I will propose this in the discussion #2997. I made a fork of the PR with a suggestion for updates to the first two demos: https://github.com/bernhste/owasp-mastg/tree/TEST-0026-draft-updated-DEMO Please take a look and let me know what you think. I'm also happy to implement additional demos. |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,42 @@ | ||||||
| --- | ||||||
| platform: android | ||||||
| title: Implicit Intent Hijacking with same custom intent filter | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Title capitalization. |
||||||
| id: MASTG-DEMO-0058 | ||||||
| code: [kotlin] | ||||||
| test: MASTG-TEST-0286 | ||||||
| profiles: [L1, L2] | ||||||
| --- | ||||||
|
|
||||||
| ### Sample | ||||||
|
|
||||||
| The code snippet below demonstrates the use of an implicit intent with sensitive data and a custom action, without specifying a target component. | ||||||
|
|
||||||
| {{ MastgTest.kt # MastgTest_reversed.java }} | ||||||
|
|
||||||
| ### MastgTestAttacker.kt | ||||||
|
|
||||||
| The attacker app has an exported activity that includes a corresponding `<intent-filter>` which registers the identical custom action, enabling it to capture the implicit intent sent out by the victim app. | ||||||
|
|
||||||
| {{ MastgTestAttacker.kt }} | ||||||
|
|
||||||
| ### MastgTestInternalData.kt | ||||||
|
|
||||||
| A component within the vulnerable app originally intended to process sensitive data, but is exposed via an implicit intent mechanism. Although not explicitly targeted, it can be hijacked by any app that claims to handle the same action. | ||||||
|
|
||||||
| {{ MastgTestInternalData.kt }} | ||||||
|
|
||||||
| ### Steps | ||||||
|
|
||||||
| 1. Install the attacker app on a device using @MASTG-TECH-0004. | ||||||
| 2. On the vulnerable app, click on start to start the test. | ||||||
|
|
||||||
| {{ MastgTestAttacker.kt }} | ||||||
| {{ AndroidManifestAttacker_app.xml }} | ||||||
|
|
||||||
| ### Observation | ||||||
|
|
||||||
| The attacker app successfully intercepted the intent containing sensitive extras such as tokens, API keys, and credentials. This confirms that any app declaring a matching `<intent-filter>` can receive these values without restriction. | ||||||
|
|
||||||
| ### Evaluation | ||||||
|
|
||||||
| The test fails due to the use of an exported activity (VulnerableActivity) that includes an intent filter with a custom action. Combined with the implicit intent in `MastgTest.kt`, this creates a vulnerable pattern where sensitive data is transmitted to an untrusted receiver. | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package org.owasp.mastestapp | ||
|
|
||
| import android.util.Log | ||
| import android.content.Context | ||
| import android.content.Intent | ||
|
|
||
| class MastgTest (private val context: Context){ | ||
|
|
||
| fun mastgTest(): String { | ||
| val sensitiveString = "Hello from the OWASP MASTG Test app." | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the placeholder string from the template app. I would suggest using the GUI to inform the user about the status of the test. Something like that: // Launch implicit intent - any app can intercept this
try {
context.startActivity(vulnerableIntent)
r.add(Status.FAIL, "Hijackable implicit intent launched")
} catch (e: Exception) {
r.add(Status.ERROR, e.toString())
} |
||
|
|
||
| // Vulnerable: Using implicit intent with sensitive data | ||
| val vulnerableIntent = Intent().apply { | ||
| action = "org.owasp.mastestapp.PROCESS_SENSITIVE_DATA" | ||
| putExtra("sensitive_token", "auth_token_12345") | ||
| putExtra("user_credentials", "admin:password123") | ||
| putExtra("api_key", "sk-1234567890abcdef") | ||
| putExtra("message", sensitiveString) | ||
| } | ||
|
|
||
| // Launch implicit intent - any app can intercept this | ||
| try { | ||
| context.startActivity(vulnerableIntent) | ||
| Log.d("MASTG-TEST", "Launched vulnerable implicit intent with sensitive data") | ||
| } catch (e: Exception) { | ||
| Log.e("MASTG-TEST", "Failed to launch intent: ${e.message}") | ||
| } | ||
|
|
||
| Log.d("MASTG-TEST", sensitiveString) | ||
| return sensitiveString | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,24 @@ | ||||||
| package com.example.datainteceptor | ||||||
|
||||||
| package com.example.datainteceptor | |
| package com.example.datainterceptor |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,15 @@ | ||||||
| package com.example.datainteceptor | ||||||
|
||||||
| package com.example.datainteceptor | |
| package com.example.datainterceptor |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| package org.owasp.mastestapp | ||
|
|
||
| import android.app.Activity | ||
| import android.os.Bundle | ||
| import android.util.Log | ||
| import android.widget.TextView | ||
|
|
||
| class VulnerableActivity : Activity() { | ||
|
|
||
| override fun onCreate(savedInstanceState: Bundle?) { | ||
| super.onCreate(savedInstanceState) | ||
|
|
||
| // Simple layout to show the received data | ||
| val textView = TextView(this) | ||
| textView.text = "Processing sensitive data..." | ||
| setContentView(textView) | ||
|
|
||
| // Process the received intent data | ||
| val receivedData = StringBuilder("Received sensitive data:\n") | ||
|
|
||
| intent?.let { intent -> | ||
| intent.getStringExtra("sensitive_token")?.let { | ||
| receivedData.append("Token: $it\n") | ||
| Log.d("VULNERABLE-APP", "Received token: $it") | ||
| } | ||
|
|
||
| intent.getStringExtra("user_credentials")?.let { | ||
| receivedData.append("Credentials: $it\n") | ||
| Log.d("VULNERABLE-APP", "Received credentials: $it") | ||
| } | ||
|
|
||
| intent.getStringExtra("api_key")?.let { | ||
| receivedData.append("API Key: $it\n") | ||
| Log.d("VULNERABLE-APP", "Received API key: $it") | ||
| } | ||
|
|
||
| intent.getStringExtra("message")?.let { | ||
| receivedData.append("Message: $it\n") | ||
| Log.d("VULNERABLE-APP", "Received message: $it") | ||
| } | ||
| } | ||
|
|
||
| textView.text = receivedData.toString() | ||
| Log.d("VULNERABLE-APP", "VulnerableActivity processed data") | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| package org.owasp.mastestapp; | ||
|
|
||
| import android.content.Context; | ||
| import android.content.Intent; | ||
| import android.util.Log; | ||
| import kotlin.Metadata; | ||
| import kotlin.jvm.internal.Intrinsics; | ||
|
|
||
| /* compiled from: MastgTest.kt */ | ||
| @Metadata(d1 = {"\u0000\u0018\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0003\n\u0002\u0010\u000e\n\u0000\b\u0007\u0018\u00002\u00020\u0001B\u000f\u0012\u0006\u0010\u0002\u001a\u00020\u0003¢\u0006\u0004\b\u0004\u0010\u0005J\u0006\u0010\u0006\u001a\u00020\u0007R\u000e\u0010\u0002\u001a\u00020\u0003X\u0082\u0004¢\u0006\u0002\n\u0000¨\u0006\b"}, d2 = {"Lorg/owasp/mastestapp/MastgTest;", "", "context", "Landroid/content/Context;", "<init>", "(Landroid/content/Context;)V", "mastgTest", "", "app_debug"}, k = 1, mv = {2, 0, 0}, xi = 48) | ||
| /* loaded from: classes3.dex */ | ||
| public final class MastgTest { | ||
| public static final int $stable = 8; | ||
| private final Context context; | ||
|
|
||
| public MastgTest(Context context) { | ||
| Intrinsics.checkNotNullParameter(context, "context"); | ||
| this.context = context; | ||
| } | ||
|
|
||
| public final String mastgTest() { | ||
| Intent vulnerableIntent = new Intent(); | ||
| vulnerableIntent.setAction("org.owasp.mastestapp.PROCESS_SENSITIVE_DATA"); | ||
| vulnerableIntent.putExtra("sensitive_token", "auth_token_12345"); | ||
| vulnerableIntent.putExtra("user_credentials", "admin:password123"); | ||
| vulnerableIntent.putExtra("api_key", "sk-1234567890abcdef"); | ||
| vulnerableIntent.putExtra("message", "Hello from the OWASP MASTG Test app."); | ||
| try { | ||
| this.context.startActivity(vulnerableIntent); | ||
| Log.d("MASTG-TEST", "Launched vulnerable implicit intent with sensitive data"); | ||
| } catch (Exception e) { | ||
| Log.e("MASTG-TEST", "Failed to launch intent: " + e.getMessage()); | ||
| } | ||
| Log.d("MASTG-TEST", "Hello from the OWASP MASTG Test app."); | ||
| return "Hello from the OWASP MASTG Test app."; | ||
| } | ||
| } |
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. According to the Demo Guideline a demo should be fully self-contained. Exceptions are tools or rules. There is a discussion about this topic: #3263 It is basically about separating the demo sample code and the actual test using different tools. I would be happy to hear your opinion. Also, please see the comment in MASTG-DEMO-0058 about a suggested change here. |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,36 @@ | ||||||
| --- | ||||||
| platform: android | ||||||
| title: Implicit intent to trigger internal app components | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Title capitalization. |
||||||
| id: MASTG-DEMO-0059 | ||||||
| code: [kotlin] | ||||||
| test: MASTG-TEST-0287 | ||||||
| profiles: [L1, L2] | ||||||
| --- | ||||||
|
|
||||||
| ### Sample | ||||||
|
|
||||||
| The manifest snippet outlines an exported activity featuring an `<intent-filter>` with a unique action. This allows the component to be reachable by any application on the device that registers the identical intent action, which could allow a malicious app to capture such intents. | ||||||
|
|
||||||
| {{ ../MASTG-DEMO-0058/AndroidManifest_reversed.xml }} | ||||||
|
|
||||||
| ### Steps | ||||||
|
|
||||||
| Let's run our @MASTG-TOOL-0110 rule against the manifest file and code. | ||||||
|
|
||||||
| {{ ../../../../rules/mastg-android-custom-intent-filter-intercept.yml }} | ||||||
|
||||||
| {{ ../../../../rules/mastg-android-custom-intent-filter-intercept.yml }} | |
| {{ ../../../../rules/mastg-android-custom-intent-intecept.yml }} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
|
|
||
|
|
||
| ┌────────────────┐ | ||
| │ 1 Code Finding │ | ||
| └────────────────┘ | ||
|
|
||
| AndroidManifest_reversed.xml | ||
| ❯❱ exported-implicit-custom-action | ||
| Exported activity with custom implicit intent action. May enable intent hijacking. | ||
|
|
||
| 41┆ <activity | ||
| 42┆ android:name="org.owasp.mastestapp.VulnerableActivity" | ||
| 43┆ android:exported="true"> | ||
| 44┆ <intent-filter> | ||
| 45┆ <action android:name="org.owasp.mastestapp.PROCESS_SENSITIVE_DATA"/> | ||
| 46┆ <category android:name="android.intent.category.DEFAULT"/> | ||
| 47┆ </intent-filter> | ||
| 48┆ </activity> |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,2 @@ | ||||||
| # shellcheck disable=SC2148 | ||||||
| NO_COLOR=true semgrep -c ../../../../rules/mastg-android-custom-intent-filter-intercept.yml ../MASTG-DEMO-0058/AndroidManifest_reversed.xml --text -o output.txt | ||||||
|
||||||
| NO_COLOR=true semgrep -c ../../../../rules/mastg-android-custom-intent-filter-intercept.yml ../MASTG-DEMO-0058/AndroidManifest_reversed.xml --text -o output.txt | |
| NO_COLOR=true semgrep -c ../../../../rules/mastg-android-custom-intent-intecept.yml ../MASTG-DEMO-0058/AndroidManifest_reversed.xml --text -o output.txt |

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a typo in the package name: 'datainteceptor' should be 'datainterceptor' to match the correct spelling of 'interceptor'.