移动端产品安全漏洞检测报告中指出了很多Android客户端存在的各类安全隐患。其中,使用WebView控件不当导致的漏洞几乎每份报告中都有提及,为此本文将全面的介绍WebView的安全检测报告中提及的漏洞和其他常见的漏洞,以及漏洞的修复方式。
在使用WebView过程中,主要存在的漏洞有三类:
出现该漏洞的原因有三个:
漏洞产生原因
JS调用Android的其中一个方式是通过addJavascriptInterface接口进行对象映射:
// 参数1:Android的本地对象
// 参数2:JS的对象
// 通过对象映射将Android中的本地对象和JS中的对象进行关联,从而实现JS调用Android的对象和方法
webView.addJavascriptInterface(new JSObject(), "myObj");
所以,漏洞产生原因是:当JS拿到Android这个对象后,就可以调用这个Android对象中所有的方法,包括系统类(java.lang.Runtime类),从而进行任意代码执行。如可以执行命令获取本地设备的SD卡中的文件等信息从而造成信息泄露。
具体获取系统类的描述(结合 Java 反射机制):
以下是攻击的Js核心代码,当APP加载一个外部网页时,攻击者就可以用这段js代码进行漏洞攻击:
function execute(cmdArgs) {
// 步骤1:遍历 window 对象
// 目的是为了找到包含 getClass()的对象
// 因为Android映射的JS对象也在window中,所以肯定会遍历到
for (var obj in window) {
if ("getClass"in window[obj]) {
// 步骤2:利用反射调用forName()得到Runtime类对象
alert(obj);
return window[obj].getClass().forName("java.lang.Runtime")
// 步骤3:以后,就可以调用静态方法来执行一些命令,比如访问文件的命令
getMethod("getRuntime",null).invoke(null,null).exec(cmdArgs);
// 从执行命令后返回的输入流中得到字符串,有很严重暴露隐私的危险。
// 如执行完访问文件的命令之后,就可以得到文件名的信息了。
}
}
}
为了证明这个漏洞,我们加载一个包含恶意JS代码的本地网页,HTML其代码如下:
点击图片把URL传到Java代码
说明:
1、请看execute()这个方法,它遍历所有window的对象,然后找到包含getClass方法的对象,利用这个对象的类,找到java.lang.Runtime对象,然后调用“getRuntime”静态方法方法得到Runtime的实例,再调用exec()方法来执行某段命令。
2、getContents()方法,从流中读取内容,显示在界面上。Java代码实现如下:
mWebView = (WebView) findViewById(R.id.webview);
mWebView.getSettings().setJavaScriptEnabled(true);
mWebView.addJavascriptInterface(new JSInterface(), "jsInterface");
mWebView.loadUrl("file:///android_asset/html/test.html");
加载网页后,运行截图如下:实际运行结果,列出了SDCard中的文件
解决方案
class JsObject {
@JavascriptInterface
public String toString() { return "injectedObject"; }
}
webView.addJavascriptInterface(new JsObject(), "injectedObject");
webView.loadData("", "text/html", null);
webView.loadUrl("javascript:alert(injectedObject.toString())");
安全检测到了这一漏洞,我们采用该修复方式,提高程序编译版本(API 17)后添加该注解代码。
Android4.2版本之前
在Android4.2版本之前这个问题比较难解决,由于可能涉及到被加载网页的修改,5060 Android端没有采用此方案修复。首先,我们肯定不能再调用 addJavascriptInterface 方法了, 如何解决这个问题,最关键的就是要知道JS事件这个动作,JS和Java的交互有prompt、alert等事件动作,这些动作都会对应到WebChromeClient类中的相应方法。对于prompt,它对应的方法是OnJsPrompt方法,通过这个方法,JS就能把信息(文本)传到Java,而Java也能把信息(文本)传到JS中。
具体步骤如下:
(a)让JS调用一个Javascript方法:该方法是通过调用prompt()把JS中的信息(含特定标识,方法名称等)传递到Android端;
(b)在Android的onJsPrompt()中 ,解析传递过来的信息,再通过反射机制调用Java对象的方法,这样实现安全的JS调用Android代码。
关于Android返回给JS的值:可通过prompt()把Java中方法的处理结果返回到Js中。具体需要加载的JS代码如下:
javascript:(function JsAddJavascriptInterface_(){
//window.jsInterface 表示在window上声明了一个Js对象
// jsInterface = 注册的对象名
//它注册了两个方法,onButtonClick(arg0)和onImageClick(arg0, arg1, arg2)
//如果有返回值,就添加上return
if (typeof(window.jsInterface)!='undefined') {
console.log('window.jsInterface_js_interface_nameis exist!!');}
else {
window.jsInterface = {
// 声明方法形式:方法名: function(参数)
onButtonClick:function(arg0) {
// prompt()返回约定的字符串
// 该字符串可自己定义
// 包含特定的标识符MyApp和 JSON 字符串(方法名,参数,对象名等)
return prompt('MyApp:'+JSON.stringify({obj:'jsInterface',func:'onButtonClick',args:[arg0]}));
},
onImageClick:function(arg0,arg1,arg2) {
return prompt('MyApp:'+JSON.stringify({obj:'jsInterface', func:'onImageClick',args:[arg0,arg1,arg2]}));
}
};
}
}
)
// 当JS调用 onButtonClick() 或 onImageClick() 时,就会回调到Android中的 onJsPrompt()
// 我们解析出方法名,参数,对象名
// 再通过反射机制调用Java对象的方法
//Java代码如下:
mWebView.setWebChromeClient(new WebChromeClient() {
// 拦截输入框(原理同方式2)
// 参数message:代表promt()的内容(不是url)
// 参数result:代表输入框的返回值
@Override
public boolean onJsPrompt(WebView view, String url,String message,
String defaultValue, JsPromptResult result) {
// 解析传递过来的信息,再通过反射机制调用Java对象的方法
return super.onJsPrompt(view, url, message, defaultValue, result);
}
// 通过alert()和confirm()拦截的原理相同,此处不作过多讲述
// 拦截JS的警告框
@Override
public boolean onJsAlert(WebView view, String url, Stringmessage, JsResult result) {
return super.onJsAlert(view, url, message, result);
}
// 拦截JS的确认框
@Override
public boolean onJsConfirm(WebView view, String url,String message, JsResult result) {
return super.onJsConfirm(view, url, message, result);
}
});
引入的新问题:
1、加载上述JS代码的时机:由于当 WebView 跳转到下一个页面时,之前加载的 JS 可能已经失效,所以,通常需要在以下方法中加载 JS:
onLoadResource();
doUpdateVisitedHistory();
onPageStarted();
onPageFinished();
onReceivedTitle();
onProgressChanged();
2、需要过滤掉 Object 类的方法:由于最终是通过反射得到Android指定对象的方法,所以同时也会得到基类的其他方法(最顶层的基类是 Object类)。为了不把 getClass()等方法注入到 JS 中,我们需要把 Object 的共有方法过滤掉,需要过滤的方法列表如下:
getClass();
hashCode();
notify();
notifyAl();
equals();
toString();
wait();
漏洞产生原因
解决方案
删除searchBoxJavaBridge_、accessibility、 accessibilityTraversal接口:
// 通过调用该方法删除接口
removeJavascriptInterface();
// 代码如下
mWebView.removeJavascriptInterface(“searchBoxJavaBridge_”);
mWebView.removeJavascriptInterface(“accessibility”);
mWebView.removeJavascriptInterface(“accessibilityTraversal”);
WebView控件默认开启密码保存功能 :
mWebView.setSavePassword(true);
开启后,在用户输入密码时,会弹出提示框:询问用户是否保存密码;如果选择”是”,密码会被明文保到/data/data/com.package.name/databases/webview.db 中,这样就有被盗取密码的危险
关闭密码保存提醒:
WebSettings.setSavePassword(false);
A 应用可以通过 B 应用导出的Activity 让 B 应用加载一个恶意的 file 协议的 url,从而可以获取 B 应用的内部私有文件,从而带来数据泄露威胁:
public class WebViewActivityextendsActivity {
private WebView webView;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_webview);
webView = (WebView) findViewById(R.id.webView);
//webView.getSettings().setAllowFileAccess(false);(1)
//webView.getSettings().setAllowFileAccessFromFileURLs(true);(2)
//webView.getSettings().setAllowUniversalAccessFromFileURLs(true);(3)
Intent i = getIntent();
String url = i.getData().toString();
//url = file:///data/local/tmp/attack.html
webView.loadUrl(url);
}
}
// 将该 WebViewActivity 在Mainifest.xml设置exported属性
// 表示:当前Activity是否可以被另一个Application的组件启动
// android:exported="true"
当其他应用启动此Activity 时, intent 中的 data 直接被当作 url 来加载(假定传进来的 url 为file:///data/local/tmp/attack.html ),其他 APP 通过使用显式ComponentName 或者其他类似方式就可以很轻松的启动该WebViewActivity 并加载恶意url。
// 设置是否允许 WebView 使用 File 协议
webView.getSettings().setAllowFileAccess(true);
// 默认设置为true,即允许在File 域下执行任意 JavaScript 代码
使用 file 域加载的 js代码能够使用进行同源策略跨域访问,从而导致隐私信息泄露。同源策略跨域访问:对私有目录文件进行访问。针对 IM 类产品,泄露的是聊天信息、联系人等等,针对浏览器类软件,泄露的是cookie 信息泄露。
如果不允许使用 file 协议,则不会存在上述的威胁;
webView.getSettings().setAllowFileAccess(false);
但同时也限制了 WebView的功能,使其不能加载本地的 html 文件,移动版的 Chrome 默认禁止加载 file 协议的文件。
// 设置是否允许通过 file url 加载的 Js代码读取其他的本地文件
webView.getSettings().setAllowFileAccessFromFileURLs(true);
// 在Android 4.1前默认允许
// 在Android 4.1后默认禁止
当AllowFileAccessFromFileURLs()设置为 true 时,攻击者的JS代码为:
该代码可成功读取/etc/hosts 的内容数据。当设置成为 false 时,上述JS的攻击代码执行会导致错误,表示浏览器禁止从 fileurl 中的 javascript 读取其它本地文件。
//设置是否允许通过 file url加载的Javascript可以访问其他的源(包括http、https等源)
webView.getSettings().setAllowUniversalAccessFromFileURLs(true);
// 在Android 4.1前默认允许(setAllowFileAccessFromFileURLs()不起作用)
// 在Android 4.1后默认禁止
当AllowFileAccessFromFileURLs()被设置成true时,攻击者的JS代码是:
通过该代码可成功读取http://www.so.com 的内容。
// 设置是否允许 WebView 使用 JavaScript(默认是不允许)
webView.getSettings().setJavaScriptEnabled(true);
// 但很多应用(包括移动浏览器)为了让 WebView 执行http 协议中的 JavaScript,都会主动设置为true,不区别对待是非常危险的。
即使把setAllowFileAccessFromFileURLs()和setAllowUniversalAccessFromFileURLs()都设置为 false,通过 fileURL 加载的 javascript 仍然有方法访问其他的本地文件:符号链接跨源攻击。这一攻击能奏效的原因是:通过javascript 的延时执行和将当前文件替换成指向其它文件的软链接就可以读取到被符号链接所指的文件。如果是 file 协议,禁用javascript 可以很大程度上减小跨源漏洞对 WebView的威胁。
(1)对于不需要使用 file 协议的应用,禁用 file 协议;
// 禁用 file 协议;
setAllowFileAccess(false);
setAllowFileAccessFromFileURLs(false);
setAllowUniversalAccessFromFileURLs(false);
(2)对于需要使用 file 协议的应用,禁止 file 协议加载JavaScript。
// 需要使用 file 协议
setAllowFileAccess(true);
setAllowFileAccessFromFileURLs(false);
setAllowUniversalAccessFromFileURLs(false);
// 禁止 file 协议加载 JavaScript
if (url.startsWith("file://") {
setJavaScriptEnabled(false);
} else {
setJavaScriptEnabled(true);
}