以上文章由来自OPPO子午互联网安全实验室【Zery】有赏投稿,也欢迎广大朋友继续投稿,详情可点击OSRC重金征集文稿!!!了解~~
温馨提示:建议广大作者朋友们用markdown格式投稿,特别是包含大量代码的文章
前言
Hybrid混合开发模式正在被越来越多的APP所使用,这种模式中一个绕不开的话题就是Js如何与Java进行交互,而早在Android 4.2之前的版本中,由webview提供Js与java交互的addjavascriptInterface接口就出现了严重的安全漏洞,虽然此后Android限制了只有添加@JavascriptInterface的方法才可以被js调用,但这种交互方式导致了很多兼容性问题,这也让很多开发人员脑洞大开另辟蹊径开发出了其它交互方式,其中就包括本文的主角:
RainbowBridge
RainbowBridge
什么是RainbowBridge?RainbowBridge其实是JsBridge中的一种。我们将从一个demo开始,向大家展示RainbowBridge的整个流程。
首先这个APP会在webview中加载一个html,其中有如下代码:
<button onclick = "RainbowBridge.callMethod('JsInvokeJavaScope', 'getToken',{},function(msg){alert(JSON.stringify(msg.data.token))});">获取已登录的Tokenbutton>
我们看到这段Js调用了RainbowBridge.callMethod,并在回调中取得了返回的token,而这个Token是由Java层维护的,那么就说明这里实际上是Js调用Java方法并取得调用结果。
我们继续跟进RainbowBridge.js中,看一下callMethod是怎么实现的:
![3aa8868352b5a5238b6dfadecc4aaa19.png](https://i-blog.csdnimg.cn/blog_migrate/7907cb4a83d125a847e5f12301dfb0ba.png)
在callMethod中调用PrivateMethod.callNativeMethod(clazz, port, method, param),而callNativeMethod如下:
![d780b63f7f70e4c3a836e2a94ab9ab0b.png](https://i-blog.csdnimg.cn/blog_migrate/2f7747d4567e48741fba4f7e664a52d5.png)
它将传入callNativeMethod的参数clazz、port、method、jsonStr拼接成一个uri,其中JS_BRIDGE_PROTOCOL_SCHEMA为"rainbow",所以按照传入的参数,这个uri应该为: "rainbow:// JsInvokeJavaScope:/getToken?"
紧接着将这个uri作为message调用win.prompt()
到这里,熟悉Android WebView的同学可能已经猜到接下来的流程了,在win.prompt()执行后,Android中会调用对应的WebChromeClient类的onJsPrompt()方法,这时流程就从Js层走到了Java层。
那么onJsPrompt()中又进行了怎样的操作呢,首先我们在onCreate()中找到当前对应的WebChromeClient类为JSBridgeWebChromeClient():
![1ff98e8a58a67d8ca3360816bec27063.png](https://i-blog.csdnimg.cn/blog_migrate/5c1cd17630a25493284da90f70fccb6f.png)
JSBridgeWebChromeClient类中重写了onJsPrompt()方法,其中调用了JsCallJava.newInstance().call()方法,并传入了message arg3:
![a01f48bdd0cd19ff1eab0d9e2fc3481d.png](https://i-blog.csdnimg.cn/blog_migrate/d41e3f417df7e1c5832651396cf81bd9.png)
在JsCallJava.newInstance().call()中依次调用JsCallJava类的this. parseMessage(String)和this. invokeNativeMethod(WebView)
![abb441d6d910537f763db02d7d5eea64.png](https://i-blog.csdnimg.cn/blog_migrate/273577c503be79630b16bc568a3aa3f4.png)
在this.parseMessage(String)中,从Js传入的message作为uri被解析,并对this. mClassName、this. mMethodName、this. mPort、this. mParams进行赋值:
![9191f356d131b5b89c11e6fa4ddd0954.png](https://i-blog.csdnimg.cn/blog_migrate/0b1b562fe76af4f2f9adf03dac71ba22.png)
this.parseMessage(String)执行结束后继续执行this.invokeNativeMethod(WebView):
![ed1cb696d8eeb4ff3f797e0dbdb13962.png](https://i-blog.csdnimg.cn/blog_migrate/167cb6a52cf1498d4f535ab04bc924f3.png)
其中首先调用NativeMethodInjectHelper.findMethod()
获取了类名为this.mClassName中方法名为this.mMethodName的方法,接下来判断方法是否存在,若存在就将this.mParams和一个JsCallback对象一起构造一个Object,JsCallback对象这里暂且不表,最后使用Method.invoke调用这个方法:
![86c1b71719bc919d36788ec36480cf65.png](https://i-blog.csdnimg.cn/blog_migrate/79229f39670b31a9765be8e014df7936.png)
至此,我们已经分析出了一次Js调用Java的流程,那么被调用的Java状态、返回值该怎么回传到Js呢?怎么触发Js回调呢?我们接着往下看,既然这个例子s是Js调用Java的JsInvokeJavaScope类的getToken()方法,那么我们找到这个方法看下:
![923c3af6a6cb690ecc4e5cae061e3f54.png](https://i-blog.csdnimg.cn/blog_migrate/4b9607a7e057a5faed3943d0fa5ef1b6.png)
可以看到其中将获取的token和imsi构造成一个json对象,接着调用JsCallback.invokeJsCallback(),在JsCallback.invokeJsCallback ()中又调用了call():
![a86b1752c0ab13729df06184dafc52e3.png](https://i-blog.csdnimg.cn/blog_migrate/849b1f532ff606164fb1e9628829983d.png)
终于来到最后处理的call()方法里:
![5169fee66fcab9eb4f752ff25e86df58.png](https://i-blog.csdnimg.cn/blog_migrate/228b3c6f9c9f49cfd270ea183b9b63cd.png)
可以看出来这里call()方法就是将Js所调用的Java方法(本例中为getToken)的返回值、状态等信息拼接称为一个Js语句并用loadUrl执行,这个完整的Js语句为:
"javascript:RainbowBridge.onComplete(,{status: {code: 0,msg: ''},data: {}});"
其中arg10为getToken()中构造的json对象,作为getToken的返回值。
ok,Java所有部分也执行完了,当loadUrl执行Js中的RainbowBridge.onComplete时,流程回到Js中:
![0d2b9f5a33b3ef7920449b9001593f8b.png](https://i-blog.csdnimg.cn/blog_migrate/0ca768b8b15d7859561c51c7dc29b89e.png)
调用了:
![aceea8815b30ff15e251ee48dd5b8dac.png](https://i-blog.csdnimg.cn/blog_migrate/b7afba06d840c920ea6bef2a71a87237.png)
最终将返回结果传入一开始RainbowBridge.callMethod注册的回调中执行。
整个Js调用Java的流程到此结束。
我们画一张图来展示一下从callMethod()到callBack()的完整流程:
![039370c674cb557390fb489c54b75825.png](https://i-blog.csdnimg.cn/blog_migrate/f26c7fde639fdb257129ec4414f62c6d.png)
看完上面是不是有点晕?没关系我们用一句话总结一下:
RainbowBridge重写了WebChromeClient类中的回调函数onJsPrompt(),当Js调用Java方法时,把目标Java类名,方法名,参数传入window.prompt(),Java层的回调函数onJsPrompt()被触发,它将调用invoke去执行接收到的目标方法,执行完成后使用loadUrl()将结果返回给Js层。
简化一下就是这样的:
![c3679c52589294e50308caaee175d539.png](https://i-blog.csdnimg.cn/blog_migrate/c5751c911f00f0f6c2cc81ac025a4786.png)
安全性
从上面可以看出,Js层向Java层传入类名、方法名和参数,就可以调用这个方法。那么Js是不是可以调用Java层的任意方法呢?要回答这个问题,首先要搞清楚Java是在哪里拿到需要调用的方法的,也就分析是上述findMethod()的实现。
我们找到findMethod()的代码,看起来是在this.mArrayMap这个Map中查找类名和方法名:
![18d13e9d300dd20158335e9e9c6da50e.png](https://i-blog.csdnimg.cn/blog_migrate/28406f340ce6d1904ff6e016ec614580.png)
而this.mArrayMap在putMethod()方法中被赋值:
![c0e209b8fe9a5f41cd58785f235db91b.png](https://i-blog.csdnimg.cn/blog_migrate/7fc5ef87aaa8e4fa38e16bf4d42e9ffb.png)
我们看下这个putMethod()方法,它其实是将之前parseMessage()解析的类名和类中的方法名保存入this.mArrayMap,当然,我们看到在对this.mArrayMap进行写入之前,会先对该Method进行两个判断:
![a11f9b32cd2cf33fcbfe313a63d04923.png](https://i-blog.csdnimg.cn/blog_migrate/f53202855360ff5ef352897391e93382.png)
这两个判断就是:
a.该方法必须被Public Static修饰
b.该方法的参数必须依次为WebView, JSONObject, JsCallBack这三个类型。
只有通过这两个判断的方法,才能被放入Map中供Js调用调用。
找到方法的来源后,我们还得继续往回看putMethod()的交叉引用,找到这个类的来源。我们可以看到putMethod()在inject()方法中被调用,传入putMethod()的类来自于this.mInjectClasses:
![e19bddbcd800ded91124b8e90a097f75.png](https://i-blog.csdnimg.cn/blog_migrate/0911379cbd25fc58b3da5c114c0b1681.png)
而这个this.mInjectClasses在clazz()中被赋值:
![69d0259d039102fd5681544f342da712.png](https://i-blog.csdnimg.cn/blog_migrate/deb51d780d4d8142f3233ec00fdb53a5.png)
找到clazz()的交叉引用,就在onCreate()初始化webview的地方:
![886c7728cae1a00947c8cbc010b154fe.png](https://i-blog.csdnimg.cn/blog_migrate/2178865072dfd2ca802db3288333ad28.png)
那么这个问题就清楚了,总结一下,Js只能调用传入JsBridge.clazz的类中的方法,而不能调用任意类的任意方法。而且被调用的方法必须是Public Static,同时有且仅有WebView, JSONObject, JsCallBack这三个类型的参数。
虽然RainbowBridge对Js可调用的Java类和方法进行了限制,Js无法调用任意类和方法,但在一些情况下由于业务需要,Java还是会给Js开放功能很强大的方法,这种情况下,一旦webview加载恶意Js就可以调用到这些很危险的方法,因此为了限制能调用Java方法的Js,RainbowBridge可以在Js调用Java方法时对调用的Js进行白名单检查:
![eb5b55737fd95595fc596ceda46bd3a4.png](https://i-blog.csdnimg.cn/blog_migrate/a239e9e0e64c551e798028b02ff5555b.png)
这里的demo在onJsPrompt()中添加了一个checkWhiteList()方法,对调用的url进行检查,检查通过之后才能调用Java方法。
漏洞
由上一节可知,RainbowBridge对Js调用的Java方法做了限制,同时也可以增加对调用的Js进行白名单检查。但是,由于RainbowBridge框架未提供统一的白名单检查,留给程序员自由发挥的空间很大,这里面就有出现安全漏洞的可能。我们仍以上面这个demo为例对这类漏洞进行完整的展示。
首先要能让webview加载一个恶意的html,这样我们才能用这个html中的恶意Js去调用应用Java层的重要方法。在本例中,MainActivity存在一个scheme入口:
![282ec8ecd2cdaa69d73962b8b70842df.png](https://i-blog.csdnimg.cn/blog_migrate/6686d030cf94f085364ee82d3e389926.png)
同时在MainActivity中会获取这个scheme的参数”url”作为url在webview中打开:
![ad411b866412be84a57832953e627ff1.png](https://i-blog.csdnimg.cn/blog_migrate/2484593f32614829e3155378a885367d.png)
因此我们只需要在一个html里构造foo://main.zery.com/?url=这样的scheme,当受害者在浏览器中打开这个html触发scheme,就可以跳转到demo应用并在webview中打开指定的url。
接下来需要让指定url中的恶意Js去调用Java方法,由之前对RainbowBridge的分析,我们可以很容易地写出调用Java方法的Js语句,但onJsPrompt()中还有checkWhiteList()对调用者进行检查:
![f0f48cf573bd3cafb7faaaba5f8a250f.png](https://i-blog.csdnimg.cn/blog_migrate/06ead6244587fb18abf2104df1dad36a.png)
注意到这里的白名单校验使用了endsWith("zery.com")这样的方式,只需要注册evlizery.com这样的域名,就可以很容易地绕过该白名单校验。
同时,这里使用了java.net.URL.getHost()来获取域名,这个getHost方法低版本是存在被绕过的漏洞的,即https://www.evil.com\\zery.com 这个url的getHost获取的结果是www.evil.com\zery.com ,可以通过endsWith的校验,而打开的结果是跳转到www.evil.com
通过这个白名单校验的漏洞,我们就可以让恶意Js去调用Java方法,最终的poc为
Web Entry test
在evil.html中的恶意Js:
function jsgetToken() {RainbowBridge.callMethod('JSCommondMethod','getToken',{},function(msg){ if(msg.status.code == 0){ var token = msg.data.token; alert(token) } }); }
最终效果:
![f4b8a4b177764b3958f4c05b7130290e.png](https://i-blog.csdnimg.cn/blog_migrate/6fd8f5588e3db77cab67aa9df9e84cfe.png)
结语
从本质上来说,这个例子中的漏洞还是属于白名单校验不严的问题,RainbowBridge只是提供了一个攻击的途径。
就像攻击addjavascriptInterface接口要寄希望于开发人员忘了删掉@JavascriptInterface注解一样。
但与RainbowBridge类似还有多种JsBridge的方法可以实现Js调用Java,对于这个攻击面来说,本文也旨在抛砖引玉,希望上文对RainbowBridge做的一些粗浅的分析,能引出更多JsBridge的漏洞利用姿势。
![9af0858c1dece699eae646209163c76a.png](https://i-blog.csdnimg.cn/blog_migrate/0a915f105abaa88d6825ab0b7810de52.jpeg)