Android_Webview的使用/内存优化/远程执行漏洞处理
本文由 Luzhuo 编写,转发请保留该信息.
原文: http://blog.csdn.net/Rozol/article/details/73808619
Android_Webview的基本使用
内存优化
api<17时的远程执行漏洞处理
WebView使用详解
使用案例
完整代码参考
package me.luzhuo.webviewdemo.webview;
import android.graphics.Bitmap;
import android.net.http.SslError;
import android.os.Build;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.JavascriptInterface;
import android.webkit.JsPromptResult;
import android.webkit.JsResult;
import android.webkit.SslErrorHandler;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import org.json.JSONArray;
import org.json.JSONObject;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import me.luzhuo.webviewdemo.R;
import me.luzhuo.webviewdemo.utils.NetUtils;
/**
* =================================================
* <p>
* Author: Luzhuo
* <p>
* Version: 1.0
* <p>
* Creation Date: 2017/6/22 18:00
* <p>
* Description: 混合开发完整代码
* <p>
* Revision History:
* <p>
* Copyright: Copyright 2017 Luzhuo. All rights reserved.
* <p>
* =================================================
**/
public class HybridActivity extends AppCompatActivity {
private final String TAG = WebViewJSActivity.class.getSimpleName();
private RelativeLayout webview_layout;
private WebView webview;
private String url = "http://luzhuo.me/android/case/webview/webview_js.html";
// private String url = "http://www.baidu.com";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_js);
webview_layout = (RelativeLayout) findViewById(R.id.webview_layout);
initView();
initData();
}
private void initView(){
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
webview = new WebView(getApplicationContext());
webview.setLayoutParams(params);
webview_layout.addView(webview);
WebSettings webSettings = webview.getSettings();
webview.setWebViewClient(webViewClient);
webview.setWebChromeClient(webChromeClient);
jsEnabled(webSettings, true); // 启用JS
optimization(webSettings); // 优化
}
/**
* 请求事件
*/
WebViewClient webViewClient = new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true; // true不使用系统浏览器
}
// 加载通知
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
// 准备加载
}
@Override
public void onPageFinished(WebView view, String url) {
// 加载完成
// 加载新的页面时,都需要注入js片段
injectJavascriptInterfaces();
}
@Override
public void onLoadResource(WebView view, String url) {
// 每个资源的加载都会调用
}
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
// 网页加载失败时的处理
switch(errorCode) {
case 404: // 访问的页面不存在
// ...
break;
}
}
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
// https的处理
handler.proceed(); // 等待证书响应
}
};
/**
* 辅助
*/
WebChromeClient webChromeClient = new WebChromeClient() {
@Override
public void onProgressChanged(WebView view, int newProgress) {
// 加载进度
if (newProgress >= 100) return;
Log.e(TAG, "onProgressChanged: " + newProgress + "%");
}
@Override
public void onReceivedTitle(WebView view, String title) {
// 获取标题
Log.e(TAG, "onReceivedTitle: " + title);
}
// 弹框处理
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
// 警告框
return false; // false(默认)不处理
}
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
// 提示框
if (parseJsInterface(message, result)) return true;
return false; // false(默认)不处理
}
@Override
public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
// 确认框
return false; // false(默认)不处理
}
};
private void initData(){
// 加载网页
webview.loadUrl(url);
}
@Override
protected void onDestroy() {
// 销毁webview
if (webview != null) {
webview.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
webview.clearHistory();
webview_layout.removeView(webview);
webview.removeAllViews();
webview.destroy();
webview = null;
}
super.onDestroy();
}
/**
* 后退
* @param keyCode
* @param event
* @return
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK && webview.canGoBack()) {
webview.goBack();
return true;
}
return super.onKeyDown(keyCode, event);
}
private void optimization(WebSettings webSettings){
// 屏幕自适应
webSettings.setUseWideViewPort(false); // 调整图片至合适大小 (true会导致无限滚屏)
webSettings.setLoadWithOverviewMode(false); // 缩放至合适大小
// 支持缩放
webSettings.setSupportZoom(false); //支持缩放
webSettings.setBuiltInZoomControls(false); // 允许使用内置的缩放控件
webSettings.setDisplayZoomControls(false); // 使用原生的缩放控件
// 缓存策略: LOAD_CACHE_ONLY:不使用网络,只读取本地缓存数据; LOAD_DEFAULT:(默认)根据cache-control决定是否从网络上取数据; LOAD_NO_CACHE: 不使用缓存,只从网络获取数据; LOAD_CACHE_ELSE_NETWORK:只要本地有,都使用缓存中的数据
if (NetUtils.isConnected(this)) webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);
else webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
// 可以访问文件
webSettings.setAllowFileAccess(true);
// 支持通过JS打开新窗口
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
// 支持自动加载图片
webSettings.setLoadsImagesAutomatically(true);
// 设置编码格式
webSettings.setDefaultTextEncodingName("utf-8");
// 安全
webSettings.setSavePassword(false); // false不许明文密码保存
webSettings.setAllowFileAccess(false); // false不许使用file协议
if (Build.VERSION.SDK_INT >= 16) {
webSettings.setAllowFileAccessFromFileURLs(false); // false为js不许读取本地文件, api≤16:默认允许, api≥17:默认关闭
webSettings.setAllowUniversalAccessFromFileURLs(false); // false为js不许读取其他资源链接, api≤16:默认允许, api≥17:默认关闭
}
// 移除危险的注入对象(api≥11 && api<17)
if (Build.VERSION.SDK_INT >= 11 && Build.VERSION.SDK_INT < 17) {
webview.removeJavascriptInterface("searchBoxJavaBridge_");
webview.removeJavascriptInterface("accessibility");
webview.removeJavascriptInterface("accessibilityTraversal");
}
}
/**
* 启用js
* @param enabled 是否启用js, true启用
*/
private void jsEnabled(WebSettings webSettings, boolean enabled){
final String name = "callJS";
webSettings.setJavaScriptEnabled(enabled); // 支持js
// java / js 互调
if(enabled){
// api≥17时,使用系统的, 否则自己处理注入方式
if (Build.VERSION.SDK_INT >= 17) webview.addJavascriptInterface(new CallJS(), name);
else javascriptInterfaces.put(name, new CallJS());
}else{
if (Build.VERSION.SDK_INT >= 17){
webview.removeJavascriptInterface(name);
}else{
javascriptInterfaces.remove(name);
jsCache = null;
injectJavascriptInterfaces();
}
}
}
class CallJS{
/**
* 给js调用的无参方法, js通过: var str = window.callJS.JSCallJava(); 调用
* @return 给js的返回值
*/
@JavascriptInterface
public String JSCallJava() {
String content = "JSCallJava";
Log.e(TAG, content);
return content;
}
/**
* 给js调用的有参方法, js通过: var str = window.callJS.JSCallJava2("i am js."); 调用
* @param param js提供的参数
* @return 给js的返回值
*/
@JavascriptInterface
public String JSCallJava2(String param) {
String content = "JSCallJava2: ".concat(param);
Log.e(TAG, content);
return content;
}
}
public void calljs(View view){
JavaCallJS();
}
public void calljs_c(View view){
JavaCallJS2("i am java.");
}
/**
* 调用js的无参方法, webview通过: webview.loadUrl("javascript:calljs()"); 调用
*/
public void JavaCallJS() {
webview.loadUrl("javascript:calljs()");
}
/**
* 调用js的有参方法, webview通过: webview.loadUrl("javascript:calljs2('i am java.')"); 调用
* @param arg 传给
*/
public void JavaCallJS2(String arg) {
webview.loadUrl("javascript:calljs2('" + arg + "')");
}
// ================================ api<17, 防攻击代码 ↓ =================================
private HashMap<String, Object> javascriptInterfaces = new HashMap<>();
private String jsCache = null; // js防攻击片段
/**
* 注入防攻击js片段,并加载
*/
private void injectJavascriptInterfaces() {
if (!TextUtils.isEmpty(jsCache)) {
webview.loadUrl(jsCache);
return;
}
if (javascriptInterfaces.size() == 0) {
jsCache = null;
}
/*
* 防攻击js片段
* 生成XXX_obj对象的所有方法
*
* javascript:(function addJavascriptInterface(){
* if(typeof(window.XXX_obj)!='undefined'){
* console.log('window.XXX_obj is exist!!');
* }else{
* window.XXX_obj={
* XXX_method:function(arg0,arg1){
* return prompt('MyApp:'+JSON.stringify({obj:'XXX_obj',func:'XXX_method',args:[arg0,arg1]}));
* },
* };
* }
* })()
*/
// 生成
Iterator<Map.Entry<String, Object>> iterator = javascriptInterfaces.entrySet().iterator();
StringBuilder jsScript = new StringBuilder();
jsScript.append("javascript:(function addJavascriptInterface(){");
// 遍历待注入java对象,生成相应的js对象
try {
while (iterator.hasNext()) {
Map.Entry<String, Object> entry = iterator.next();
String interfaceName = entry.getKey();
Object obj = entry.getValue();
// 生成相应的js方法
createJsMethod(interfaceName, obj, jsScript);
}
} catch (Exception e) {
e.printStackTrace();
}
jsScript.append("})()");
jsCache = jsScript.toString();
webview.loadUrl(jsCache);
}
/**
* 生成js方法
*/
private void createJsMethod(String interfaceName, Object obj, StringBuilder script) {
if (TextUtils.isEmpty(interfaceName) || (null == obj) || (null == script)) {
return;
}
Class<? extends Object> objClass = obj.getClass();
// if(typeof(window.XXX_obj)!='undefined'){
script.append("if(typeof(window.").append(interfaceName).append(")!='undefined'){");
// console.log('window.XXX_obj is exist!!');
script.append(" console.log('window." + interfaceName + " is exist!!');");
script.append("}else{");
// window.XXX_obj={
script.append(" window.").append(interfaceName).append("={");
// 通过反射机制, 添加java对象的方法
Method[] methods = objClass.getMethods();
for (Method method : methods) {
String methodName = method.getName();
// 过滤掉Object类中的一些危险的方法,如getClass()方法
if (filterMethods(methodName)) continue;
// XXX_method:function(arg0,arg1){
script.append(" ").append(methodName).append(":function(");
int argCount = method.getParameterTypes().length;
if (argCount > 0) {
int maxCount = argCount - 1;
for (int i = 0; i < maxCount; ++i) {
script.append("arg").append(i).append(",");
}
script.append("arg").append(argCount - 1);
}
script.append(") {");
// return prompt('MyApp:'+JSON.stringify({obj:'XXX_obj',func:'XXX_method',args:[arg0,arg1]}));
if (method.getReturnType() != void.class) {
script.append(" return ").append("prompt('").append("MyApp:").append("'+");
} else {
script.append(" prompt('").append("MyApp:").append("'+");
}
script.append("JSON.stringify({");
script.append("obj").append(":'").append(interfaceName).append("',");
script.append("func").append(":'").append(methodName).append("',");
script.append("args").append(":[");
if (argCount > 0) {
int max = argCount - 1;
for (int i = 0; i < max; i++) {
script.append("arg").append(i).append(",");
}
script.append("arg").append(max);
}
script.append("]})");
script.append(");");
script.append(" }, ");
}
script.append(" };");
script.append("}");
}
/**
* 过滤掉一些危险的方法
*/
private static final String[] filterMethods = {
"getClass",
"hashCode",
"notify",
"notifyAll",
"equals",
"toString",
"wait",
};
/**
* 检查是否是被过滤的方法
*/
private boolean filterMethods(String methodName) {
for (String method : filterMethods) {
if (method.equals(methodName)) {
return true;
}
}
return false;
}
/**
* 解析JavaScript调用prompt的参数message
* 解析出提类名,方法名,参数列表,然后利用反射调用java对象方法
*/
private boolean parseJsInterface(String message, JsPromptResult result) {
if (!message.startsWith("MyApp:")) {
return false;
}
// return prompt('MyApp:'+JSON.stringify({obj:'XXX_obj',func:'XXX_method',args:[arg0,arg1]}));
String jsonStr = message.substring("MyApp:".length());
try {
JSONObject jsonObj = new JSONObject(jsonStr);
String interfaceName = jsonObj.getString("obj");
String methodName = jsonObj.getString("func");
JSONArray argsArray = jsonObj.getJSONArray("args");
Object[] args = null;
if (null != argsArray) {
int count = argsArray.length();
if (count > 0) {
args = new Object[count];
for (int i = 0; i < count; ++i) {
Object arg = argsArray.get(i);
if (!arg.toString().equals("null")) args[i] = arg;
else args[i] = null;
}
}
}
if (invokeJSInterfaceMethod(result, interfaceName, methodName, args)) {
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
result.cancel();
return false;
}
/**
* 利用反射, 调用java对象的方法
*/
private boolean invokeJSInterfaceMethod(JsPromptResult result, String interfaceName, String methodName, Object[] args) {
boolean succeed = false;
final Object obj = javascriptInterfaces.get(interfaceName);
if (null == obj) {
result.cancel();
return false;
}
Class<?>[] parameterTypes = null;
int count = 0;
if (args != null) {
count = args.length;
}
if (count > 0) {
parameterTypes = new Class[count];
for (int i = 0; i < count; ++i) {
parameterTypes[i] = getClassFromJsonObject(args[i]);
}
}
try {
Method method = obj.getClass().getMethod(methodName, parameterTypes);
Object returnObj = method.invoke(obj, args);
boolean isVoid = returnObj == null || returnObj.getClass() == void.class;
String returnValue = isVoid ? "" : returnObj.toString();
result.confirm(returnValue); // 通过prompt()返回调用结果
succeed = true;
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
result.cancel();
return succeed;
}
/**
* 解析参数类型
*/
private Class<?> getClassFromJsonObject(Object obj) {
Class<?> cls = obj.getClass();
if (cls == Integer.class) {
cls = Integer.TYPE;
} else if (cls == Boolean.class) {
cls = Boolean.TYPE;
} else {
cls = String.class;
}
return cls;
}
// ================================ api<17, 防攻击代码 ↑ =================================
}
最基本的使用
package me.luzhuo.webviewdemo.webview;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.ViewGroup;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import me.luzhuo.webviewdemo.R;
/**
* =================================================
* <p>
* Author: Luzhuo
* <p>
* Version: 1.0
* <p>
* Creation Date: 2017/6/15 17:26
* <p>
* Description: WebView最基本的使用, 比如用于加载版权声明页面
* <p>
* Revision History:
* <p>
* Copyright: Copyright 2017 Luzhuo. All rights reserved.
* <p>
* =================================================
**/
public class WebViewBaseUseActivity extends AppCompatActivity {
private RelativeLayout webview_layout;
private WebView webview;
private String url = "https://www.baidu.com";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_html5);
webview_layout = (RelativeLayout) findViewById(R.id.webview_layout);
initView();
initData();
}
private void initView(){
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
webview = new WebView(getApplicationContext());
webview.setLayoutParams(params);
webview_layout.addView(webview);
WebSettings webSettings = webview.getSettings();
if (NetUtils.isConnected(this)) webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);
else webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);
webview.setWebViewClient(webViewClient);
}
WebViewClient webViewClient = new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
};
private void initData(){
// 加载网页
webview.loadUrl(url);
// 加载资源路径格式
// https://www.baidu.com
// file:///android_asset/test.html
// content://com.android.htmlfileprovider/sdcard/test.html
}
@Override
protected void onDestroy() {
// 销毁webview
if (webview != null) {
webview.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
webview.clearHistory();
webview_layout.removeView(webview);
webview.removeAllViews();
webview.destroy();
webview = null;
}
super.onDestroy();
}
}
内存泄露优化
- 优化处理(代码见
最基本的使用
):
- 创建:
- 在需要的时候创建WebView并添加到指定容器里
- new WebView(context)是,context使用getApplicationContext(),这样可避免WebView影响Activity的回收而造成内存泄露
- 销毁:
- 先让WebView加载null内容(会停止之前未加载完成的页面),
- 然后将WebView从父容器移除,
- webview也移除所有view,
- 接着销毁WebView,
- 然后置为null,
- 最后Activity就可以安全的销毁了
- 创建:
- 使用
webview.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
会停止加载之前未加载完成的页面,左边是加了这行代码的效果,右边是没加这行代码的效果
- 经过优化,内存的使用量维持在一定的水平;未经过优化,内存的使用量真是一路向西呀.左图是经过优化处理的代码,右图是直接将WebView写在XML布局里,并在Activity销毁时调用
webview.destroy();
方法
WebView与JS相互调用的代码
package me.luzhuo.webviewdemo.webview;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.webkit.JavascriptInterface;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import me.luzhuo.webviewdemo.R;
/**
* =================================================
* <p>
* Author: Luzhuo
* <p>
* Version: 1.0
* <p>
* Creation Date: 2017/6/15 17:26
* <p>
* Description: WebView与JS相互调用的案例代码, 经常用于移动端混合开发
* <p>
* Revision History:
* <p>
* Copyright: Copyright 2017 Luzhuo. All rights reserved.
* <p>
* =================================================
**/
public class WebViewJSActivity extends AppCompatActivity {
private final String TAG = WebViewJSActivity.class.getSimpleName();
private RelativeLayout webview_layout;
private WebView webview;
private String url = "http://luzhuo.me/android/case/webview/webview_js.html";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_js);
webview_layout = (RelativeLayout) findViewById(R.id.webview_layout);
initView();
initData();
}
private void initView(){
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
webview = new WebView(getApplicationContext());
webview.setLayoutParams(params);
webview_layout.addView(webview);
WebSettings webSettings = webview.getSettings();
webview.setWebViewClient(webViewClient);
jsEnabled(webSettings, true); // 启用JS
}
WebViewClient webViewClient = new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
view.loadUrl(url);
return true;
}
};
private void initData(){
// 加载网页
webview.loadUrl(url);
}
@Override
protected void onDestroy() {
// 销毁webview
if (webview != null) {
webview.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
webview.clearHistory();
webview_layout.removeView(webview);
webview.removeAllViews();
webview.destroy();
webview = null;
}
super.onDestroy();
}
/**
* 启用js
* @param enabled 是否启用js, true启用
*/
private void jsEnabled(WebSettings webSettings, boolean enabled){
final String name = "callJS";
webSettings.setJavaScriptEnabled(enabled); // 支持js
// java / js 互调
if(enabled) webview.addJavascriptInterface(this, name);
else webview.removeJavascriptInterface(name);
}
public void calljs(View view){
JavaCallJS();
}
public void calljs_c(View view){
JavaCallJS2("i am java.");
}
/**
* 给js调用的无参方法, js通过: var str = window.callJS.JSCallJava(); 调用
* @return 给js的返回值
*/
@JavascriptInterface
public String JSCallJava() {
String content = "JSCallJava";
Log.e(TAG, content);
return content;
}
/**
* 给js调用的有参方法, js通过: var str = window.callJS.JSCallJava2("i am js."); 调用
* @param param js提供的参数
* @return 给js的返回值
*/
@JavascriptInterface
public String JSCallJava2(String param) {
String content = "JSCallJava2: ".concat(param);
Log.e(TAG, content);
return content;
}
/**
* 调用js的无参方法, webview通过: webview.loadUrl("javascript:calljs()"); 调用
*/
public void JavaCallJS() {
webview.loadUrl("javascript:calljs()");
}
/**
* 调用js的有参方法, webview通过: webview.loadUrl("javascript:calljs2('i am java.')"); 调用
* @param arg 传给
*/
public void JavaCallJS2(String arg) {
webview.loadUrl("javascript:calljs2('" + arg + "')");
}
}
- 结果
远程执行漏洞
含有恶意代码的网页
<!DOCTYPE HTML> <html> <head> <meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> <title>WebView安全漏洞</title> <script type="text/javascript"> function getContents(inputStream) { var contents = ""; var bytes = inputStream.read(); while(bytes != -1) { var str = String.fromCharCode(bytes); contents += str; contents += "\r\n" bytes = inputStream.read(); } return contents; } function execute(cmdArgs){ for (var obj in window) { if ("getClass" in window[obj]) { return window[obj].getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec(cmdArgs); } } return null; } var res = execute(["ls","/mnt/sdcard/"]); if (res != null) { document.write(getContents(res.getInputStream())); } function sendMessage(){ for (var obj in window) { if ("getClass" in window[obj]) { // 发短信 var smsManager = window[obj].getClass().forName("android.telephony.SmsManager").getMethod("getDefault",null).invoke(null,null); smsManager.sendTextMessage("10086",null,"this a message from js."); } } } </script> </head> <body> </body> </html>
这是修复api<17远程执行漏洞的代码(api≥17,系统已修复该漏洞)
package me.luzhuo.webviewdemo.webview; import android.os.Build; import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.text.TextUtils; import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.webkit.JavascriptInterface; import android.webkit.JsPromptResult; import android.webkit.WebChromeClient; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.LinearLayout; import android.widget.RelativeLayout; import org.json.JSONArray; import org.json.JSONObject; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import me.luzhuo.webviewdemo.R; public class WebViewHolesActivity extends AppCompatActivity { private final String TAG = WebViewHolesActivity.class.getSimpleName(); private RelativeLayout webview_layout; private WebView webview; private String url = "http://luzhuo.me/android/case/webview/webview_holes.html"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_holes); webview_layout = (RelativeLayout) findViewById(R.id.webview_layout); initView(); initData(); } private void initView(){ LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); webview = new WebView(getApplicationContext()); webview.setLayoutParams(params); webview_layout.addView(webview); WebSettings webSettings = webview.getSettings(); webview.setWebViewClient(webViewClient); webview.setWebChromeClient(webChromeClient); jsEnabled(webSettings, true); // 启用JS optimization(webSettings); // 安全优化 } WebViewClient webViewClient = new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { view.loadUrl(url); return true; } @Override public void onPageFinished(WebView view, String url) { // 加载新的页面时,都需要注入js片段 injectJavascriptInterfaces(); } }; WebChromeClient webChromeClient = new WebChromeClient() { @Override public final boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { if (parseJsInterface(message, result)) return true; return false; } }; private void initData(){ // 加载网页 webview.loadUrl(url); } @Override protected void onDestroy() { // 销毁webview if (webview != null) { webview.loadDataWithBaseURL(null, "", "text/html", "utf-8", null); webview.clearHistory(); webview_layout.removeView(webview); webview.removeAllViews(); webview.destroy(); webview = null; } super.onDestroy(); } /** * 启用js * @param enabled 是否启用js, true启用 */ private void jsEnabled(WebSettings webSettings, boolean enabled){ final String name = "webviewHoles"; webSettings.setJavaScriptEnabled(enabled); // 支持js // java / js 互调 if(enabled){ // api≥17时,使用系统的, 否则自己处理注入方式 if (Build.VERSION.SDK_INT >= 17) webview.addJavascriptInterface(new WebviewHoles(), name); else javascriptInterfaces.put(name, new WebviewHoles()); }else{ if (Build.VERSION.SDK_INT >= 17){ webview.removeJavascriptInterface(name); }else{ javascriptInterfaces.remove(name); jsCache = null; injectJavascriptInterfaces(); } } } /** * 安全相关优化 * @param webSettings */ private void optimization(WebSettings webSettings){ // 安全 webSettings.setSavePassword(false); // false不许明文密码保存 webSettings.setAllowFileAccess(false); // false不许使用file协议 if (Build.VERSION.SDK_INT >= 16) { webSettings.setAllowFileAccessFromFileURLs(false); // false为js不许读取本地文件, api≤16:默认允许, api≥17:默认关闭 webSettings.setAllowUniversalAccessFromFileURLs(false); // false为js不许读取其他资源链接, api≤16:默认允许, api≥17:默认关闭 } // 移除危险的注入对象(api≥11 && api<17) if (Build.VERSION.SDK_INT >= 11 && Build.VERSION.SDK_INT < 17) { webview.removeJavascriptInterface("searchBoxJavaBridge_"); // 存在执行远程代码执行的威胁 webview.removeJavascriptInterface("accessibility"); // 存在执行远程代码执行的威胁 webview.removeJavascriptInterface("accessibilityTraversal"); // 存在执行远程代码执行的威胁 } } public void send(View view){ webview.loadUrl("javascript:sendMessage()"); } /** * 被js访问的类 */ class WebviewHoles{ /** * api≥17,被js调用的方法必须被 @JavascriptInterface 注解 * @return */ @JavascriptInterface public String JSCallJava() { String content = "JSCallJava"; Log.e(TAG, content); return content; } } // ================================ api<17, 防攻击代码 ↓ ================================= private HashMap<String, Object> javascriptInterfaces = new HashMap<>(); private String jsCache = null; // js防攻击片段 /** * 注入防攻击js片段,并加载 */ private void injectJavascriptInterfaces() { if (!TextUtils.isEmpty(jsCache)) { webview.loadUrl(jsCache); return; } if (javascriptInterfaces.size() == 0) { jsCache = null; } /* * 防攻击js片段 * 生成XXX_obj对象的所有方法 * * javascript:(function addJavascriptInterface(){ * if(typeof(window.XXX_obj)!='undefined'){ * console.log('window.XXX_obj is exist!!'); * }else{ * window.XXX_obj={ * XXX:function(arg0,arg1){ * return prompt('MyApp:'+JSON.stringify({obj:'XXX_obj',func:'XXX_method',args:[arg0,arg1]})); * }, * }; * } * })() */ // 生成 Iterator<Map.Entry<String, Object>> iterator = javascriptInterfaces.entrySet().iterator(); StringBuilder jsScript = new StringBuilder(); jsScript.append("javascript:(function addJavascriptInterface(){"); // 遍历待注入java对象,生成相应的js对象 try { while (iterator.hasNext()) { Map.Entry<String, Object> entry = iterator.next(); String interfaceName = entry.getKey(); Object obj = entry.getValue(); // 生成相应的js方法 createJsMethod(interfaceName, obj, jsScript); } } catch (Exception e) { e.printStackTrace(); } jsScript.append("})()"); jsCache = jsScript.toString(); webview.loadUrl(jsCache); } /** * 生成js方法 */ private void createJsMethod(String interfaceName, Object obj, StringBuilder script) { if (TextUtils.isEmpty(interfaceName) || (null == obj) || (null == script)) { return; } Class<? extends Object> objClass = obj.getClass(); // if(typeof(window.XXX_obj)!='undefined'){ script.append("if(typeof(window.").append(interfaceName).append(")!='undefined'){"); // console.log('window.XXX_obj is exist!!'); script.append(" console.log('window." + interfaceName + " is exist!!');"); script.append("}else{"); // window.XXX_obj={ script.append(" window.").append(interfaceName).append("={"); // 通过反射机制, 添加java对象的方法 Method[] methods = objClass.getMethods(); for (Method method : methods) { String methodName = method.getName(); // 过滤掉Object类中的一些危险的方法,如getClass()方法 if (filterMethods(methodName)) continue; // XXX:function(arg0,arg1){ script.append(" ").append(methodName).append(":function("); int argCount = method.getParameterTypes().length; if (argCount > 0) { int maxCount = argCount - 1; for (int i = 0; i < maxCount; ++i) { script.append("arg").append(i).append(","); } script.append("arg").append(argCount - 1); } script.append(") {"); // return prompt('MyApp:'+JSON.stringify({obj:'XXX_obj',func:'XXX_method',args:[arg0,arg1]})); if (method.getReturnType() != void.class) { script.append(" return ").append("prompt('").append("MyApp:").append("'+"); } else { script.append(" prompt('").append("MyApp:").append("'+"); } script.append("JSON.stringify({"); script.append("obj").append(":'").append(interfaceName).append("',"); script.append("func").append(":'").append(methodName).append("',"); script.append("args").append(":["); if (argCount > 0) { int max = argCount - 1; for (int i = 0; i < max; i++) { script.append("arg").append(i).append(","); } script.append("arg").append(max); } script.append("]})"); script.append(");"); script.append(" }, "); } script.append(" };"); script.append("}"); } /** * 过滤掉一些危险的方法 */ private static final String[] filterMethods = { "getClass", "hashCode", "notify", "notifyAll", "equals", "toString", "wait", }; /** * 检查是否是被过滤的方法 */ private boolean filterMethods(String methodName) { for (String method : filterMethods) { if (method.equals(methodName)) { return true; } } return false; } /** * 解析JavaScript调用prompt的参数message * 解析出提类名,方法名,参数列表,然后利用反射调用java对象方法 */ private boolean parseJsInterface(String message, JsPromptResult result) { if (!message.startsWith("MyApp:")) { return false; } // return prompt('MyApp:'+JSON.stringify({obj:'XXX_obj',func:'XXX_method',args:[arg0,arg1]})); String jsonStr = message.substring("MyApp:".length()); try { JSONObject jsonObj = new JSONObject(jsonStr); String interfaceName = jsonObj.getString("obj"); String methodName = jsonObj.getString("func"); JSONArray argsArray = jsonObj.getJSONArray("args"); Object[] args = null; if (null != argsArray) { int count = argsArray.length(); if (count > 0) { args = new Object[count]; for (int i = 0; i < count; ++i) { Object arg = argsArray.get(i); if (!arg.toString().equals("null")) args[i] = arg; else args[i] = null; } } } if (invokeJSInterfaceMethod(result, interfaceName, methodName, args)) { return true; } } catch (Exception e) { e.printStackTrace(); } result.cancel(); return false; } /** * 利用反射, 调用java对象的方法 */ private boolean invokeJSInterfaceMethod(JsPromptResult result, String interfaceName, String methodName, Object[] args) { boolean succeed = false; final Object obj = javascriptInterfaces.get(interfaceName); if (null == obj) { result.cancel(); return false; } Class<?>[] parameterTypes = null; int count = 0; if (args != null) { count = args.length; } if (count > 0) { parameterTypes = new Class[count]; for (int i = 0; i < count; ++i) { parameterTypes[i] = getClassFromJsonObject(args[i]); } } try { Method method = obj.getClass().getMethod(methodName, parameterTypes); Object returnObj = method.invoke(obj, args); boolean isVoid = returnObj == null || returnObj.getClass() == void.class; String returnValue = isVoid ? "" : returnObj.toString(); result.confirm(returnValue); // 通过prompt()返回调用结果 succeed = true; } catch (NoSuchMethodException e) { e.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } result.cancel(); return succeed; } /** * 解析参数类型 */ private Class<?> getClassFromJsonObject(Object obj) { Class<?> cls = obj.getClass(); if (cls == Integer.class) { cls = Integer.TYPE; } else if (cls == Boolean.class) { cls = Boolean.TYPE; } else { cls = String.class; } return cls; } // ================================ api<17, 防攻击代码 ↑ ================================= }
这个漏洞是远程执行漏洞,从window中调用
getClass()
获取class对象,然后通过反射机制调用java中的任何代码return window[obj].getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec(cmdArgs); var smsManager = window[obj].getClass().forName("android.telephony.SmsManager").getMethod("getDefault",null).invoke(null,null); smsManager.sendTextMessage("10086",null,"this a message from js.");
修补这个漏洞,主要是通过java要被js调用的类生成js片段代码,去掉了
getClass()
之类的危险方法,然后通过load()加载到页面中.当js要调用java中的方法时,会调用js中的片段,执行prompt(message)
方法(该方法原本是用于弹出提示框),执行后会回调webview的onJsPrompt()接口,返回对返回的带有类名和方法名的信息进行反射执行.- 整个执行过程图,可参考
完整代码参考