转载

WebView与Android java层交互以及可能出现的漏洞

WebView广泛应用于各种App中,通过引入WebUI的方法加速APP的开发。这篇文章主要介绍WebView与JS交互的几种方式以及可能存在的漏洞情况。

Android调用JS代码

主要有两种方式:

  • WebView的loadUrl()
  • WebView的evaluateJavascript()

loadUrl

// 文本名:javascript
<!DOCTYPE html>
<html>

   <head>
      <meta charset="utf-8">
      <title>Carson_Ho</title>
      
// JS代码
     <script>
// Android需要调用的方法
   function callJS(){
      alert("Android调用了JS的callJS方法");
   }
</script>

   </head>

</html>
       button.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               // 通过Handler发送消息
               mWebView.post(new Runnable() {
                   @Override
                   public void run() {

                       // 注意调用的JS方法名要对应上
                       // 调用javascript的callJS()方法
                       mWebView.loadUrl("javascript:callJS()");
                   }
               });
               
           }
       });
// 由于设置了弹窗检验调用结果,所以需要支持js对话框
       // webview只是载体,内容的渲染需要使用webviewChromClient类去实现
       // 通过设置WebChromeClient对象处理JavaScript的对话框
       //设置响应js 的Alert()函数
       mWebView.setWebChromeClient(new WebChromeClient() {
           @Override
           public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
               AlertDialog.Builder b = new AlertDialog.Builder(MainActivity.this);
               b.setTitle("Alert");
               b.setMessage(message);
               b.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
                   @Override
                   public void onClick(DialogInterface dialog, int which) {
                       result.confirm();
                   }
               });
               b.setCancelable(false);
               b.create().show();
               return true;
           }

       });

JS代码必须在 onPageFinished() 之后调用。

evaluateJavascript()

// 只需要将第一种方法的loadUrl()换成下面该方法即可
    mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() {
        @Override
        public void onReceiveValue(String value) {
            //此处为 js 返回的结果
        }
    });
}

对比

WebView与Android java层交互以及可能出现的漏洞

开发中多使用evaluateJavascript()

JS调用Java层提供的方法

这里有三种方法:

  • 通过WebView的addJavascriptInterface()进行对象映射
  • 通过 WebViewClient 的shouldOverrideUrlLoading ()方法回调拦截 url
  • 通过 WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()方法回调拦截JS对话框alert()、confirm()、prompt() 消息
  • WebViewJavascriptBridge

addJavascriptInterface()方法

// 继承自Object类
public class AndroidtoJs extends Object {
// 定义JS需要调用的方法
// 被JS调用的方法必须加入@JavascriptInterface注解
@JavascriptInterface
public void hello(String msg) {
    System.out.println("JS调用了Android的hello方法");
}
}
<html>
   <head>
      <meta charset="utf-8">
      <title>Carson</title>  
      <script>
         
        
         function callAndroid(){
        // 由于对象映射,所以调用test对象等于调用Android映射的对象
            test.hello("js调用了android中的hello方法");
         }
      </script>
   </head>
   <body>
      //点击按钮则调用callAndroid函数
      <button type="button" id="button1" "callAndroid()"></button>
   </body>
</html>
public class MainActivity extends AppCompatActivity {

    WebView mWebView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mWebView = (WebView) findViewById(R.id.webview);
        WebSettings webSettings = mWebView.getSettings();

        // 设置与Js交互的权限
        webSettings.setJavaScriptEnabled(true);

        // 通过addJavascriptInterface()将Java对象映射到JS对象
        //参数1:Javascript对象名
        //参数2:Java对象名
        mWebView.addJavascriptInterface(new AndroidtoJs(), "test");//AndroidtoJS类对象映射到js的test对象

        // 加载JS代码
        // 格式规定为:file:///android_asset/文件名.html
        mWebView.loadUrl("file:///android_asset/javascript.html");

shouldOverrideURlLoading()方法回调拦截url

shouldOverrideUrlLoading() 方法拦截特定协议的url跳转,解析协议并调用相应的方法

<!DOCTYPE html>
<html>

   <head>
      <meta charset="utf-8">
      <title>Carson_Ho</title>
      
     <script>
         function callAndroid(){
            /*约定的url协议为:js://webview?arg1=111&arg2=222*/
            document.location = "js://webview?arg1=111&arg2=222";
         }
      </script>
</head>

<!-- 点击按钮则调用callAndroid()方法  -->
   <body>
     <button type="button" id="button1" "callAndroid()">点击调用Android代码</button>
   </body>
</html>
mWebView.setWebViewClient(new WebViewClient() {
                                      @Override
                                      public boolean shouldOverrideUrlLoading(WebView view, String url) {

                                          // 步骤2:根据协议的参数,判断是否是所需要的url
                                          // 一般根据scheme(协议格式) & authority(协议名)判断(前两个参数)
                                          //假定传入进来的 url = "js://webview?arg1=111&arg2=222"(同时也是约定好的需要拦截的)

                                          Uri uri = Uri.parse(url);                                 
                                          // 如果url的协议 = 预先约定的 js 协议
                                          // 就解析往下解析参数
                                          if ( uri.getScheme().equals("js")) {

                                              // 如果 authority  = 预先约定协议里的 webview,即代表都符合约定的协议
                                              // 所以拦截url,下面JS开始调用Android需要的方法
                                              if (uri.getAuthority().equals("webview")) {

                                                 //  步骤3:
                                                  // 执行JS所需要调用的逻辑
                                                  System.out.println("js调用了Android的方法");
                                                  // 可以在协议上带有参数并传递到Android上
                                                  HashMap<String, String> params = new HashMap<>();
                                                  Set<String> collection = uri.getQueryParameterNames();

                                              }

                                              return true;
                                          }
                                          return super.shouldOverrideUrlLoading(view, url);
                                      }
                                  }
        );
  • 这种方法在获取URL的返回值上比较复杂

onJsAlert() onJsConfirm() onJsPrompt()方法回调拦截JS对话框消息

<!DOCTYPE html>
<html>
   <head>
      <meta charset="utf-8">
      <title>Carson_Ho</title>
      
     <script>
        
	function clickprompt(){
    // 调用prompt()
    var result=prompt("js://demo?arg1=111&arg2=222");
    alert("demo " + result);
}

      </script>
</head>

<!-- 点击按钮则调用clickprompt()  -->
   <body>
     <button type="button" id="button1" "clickprompt()">点击调用Android代码</button>
   </body>
</html>
mWebView.setWebChromeClient(new WebChromeClient() {
                                        // 拦截输入框(原理同方式2)
                                        // 参数message:代表promt()的内容(不是url)
                                        // 参数result:代表输入框的返回值
                                        @Override
                                        public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
                                            // 根据协议的参数,判断是否是所需要的url(原理同方式2)
                                            // 一般根据scheme(协议格式) & authority(协议名)判断(前两个参数)
                                            //假定传入进来的 url = "js://webview?arg1=111&arg2=222"(同时也是约定好的需要拦截的)

                                            Uri uri = Uri.parse(message);
                                            // 如果url的协议 = 预先约定的 js 协议
                                            // 就解析往下解析参数
                                            if ( uri.getScheme().equals("js")) {

                                                // 如果 authority  = 预先约定协议里的 webview,即代表都符合约定的协议
                                                // 所以拦截url,下面JS开始调用Android需要的方法
                                                if (uri.getAuthority().equals("webview")) {

                                                    //
                                                    // 执行JS所需要调用的逻辑
                                                    System.out.println("js调用了Android的方法");
                                                    // 可以在协议上带有参数并传递到Android上
                                                    HashMap<String, String> params = new HashMap<>();
                                                    Set<String> collection = uri.getQueryParameterNames();

                                                    //参数result:代表消息框的返回值(输入值)
                                                    result.confirm("js调用了Android的方法成功啦");
                                                }
                                                return true;
                                            }
                                            return super.onJsPrompt(view, url, message, defaultValue, result);
                                        }

// 通过alert()和confirm()拦截的原理相同,此处不作过多讲述

                                        // 拦截JS的警告框
                                        @Override
                                        public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
                                            return super.onJsAlert(view, url, message, result);
                                        }

                                        // 拦截JS的确认框
                                        @Override
                                        public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
                                            return super.onJsConfirm(view, url, message, result);
                                        }
                                    }
        );

WebViewJavascriptBridge

WebViewJavascriptBridge是WebView和Js交互通信的桥梁,它是实现java和js的互相调用的桥梁。替代了WebView的自带的JavascriptInterface的接口,使得开发者更方便的让js和native灵活交互,使我们的开发更加灵活和安全.

WebViewJavascriptBridge 在Android与IOS下均有对应的实现,一个demo见如下链接: https://www.jianshu.com/p/e37ccf32cb5b

安全隐患

addJavascriptInterface()的安全隐患

我们的js拿到了这个对象,就可以调用这个对象中的许多方法,包括系统类 java.lang.Runtime 类,从而进行任意代码执行。

如以下的payload:

function execute(cmdArgs)  
{  
    // 步骤1:遍历 window 对象
    // 目的是为了找到包含 getClass ()的对象
    // 因为Android映射的JS对象也在window中,所以肯定会遍历到
    for (var obj in window) {  
        if ("getClass" in window[obj]) {  

      // 步骤2:利用反射调用forName()得到Runtime类对象
            alert(obj);          
            return  window[obj].getClass().forName("java.lang.Runtime")  

      // 步骤3:以后,就可以调用静态方法来执行一些命令,比如访问文件的命令
getMethod("getRuntime",null).invoke(null,null).exec(cmdArgs);  

// 从执行命令后返回的输入流中得到字符串,有很严重暴露隐私的危险。
// 如执行完访问文件的命令之后,就可以得到文件名的信息了。
        }  
    }  
}

解决方案

  • Android4.2版本之后被调用的函数必须加入@JavascriptInterface,因此可以避免漏洞攻击;
  • 4.2版本以前可以通过拦截 prompt() 的方式进行修
    • 继承WebView,重写 addJavascriptInterface 方法,维护一个对象映射关系的Map,将需要添加的JS街口放到这个Map中
    • WebView加载页面前加载一段本地的JS代码,远离为:
      • JS调用一个Javascript方法,通过prompt把JS中的信息传递到Android端
      • Android端的onJSPrompt方法中解析信息并且通过反射机制调用Java对象的方法,这样便可以自己设置黑名单或者白名单来实现安全限制,
javascript:(function JsAddJavascriptInterface_(){  
// window.jsInterface 表示在window上声明了一个Js对象

//   jsInterface = 注册的对象名
// 它注册了两个方法,onButtonClick(arg0)和onImageClick(arg0, arg1, arg2)
// 如果有返回值,就添加上return
    if (typeof(window.jsInterface)!='undefined') {      
        console.log('window.jsInterface_js_interface_name is exist!!');}   
    else {  
        window.jsInterface = {     

     // 声明方法形式:方法名: function(参数)
            onButtonClick:function(arg0) {   
// prompt()返回约定的字符串
// 该字符串可自己定义
// 包含特定的标识符MyApp和 JSON 字符串(方法名,参数,对象名等)    
                return prompt('MyApp:'+JSON.stringify({obj:'jsInterface',func:'onButtonClick',args:[arg0]}));  
            },  
              

            onImageClick:function(arg0,arg1,arg2) {   
         return
prompt('MyApp:'+JSON.stringify({obj:'jsInterface',func:'onImageClick',args:[arg0,arg1,arg2]}));  
            },  
        };  
    }  
}  
)()

// 当JS调用 onButtonClick() 或 onImageClick() 时,就会回调到Android中的 onJsPrompt ()
// 我们解析出方法名,参数,对象名
// 再通过反射机制调用Java对象的方法

需要注意的细节问题

加载代码的时机

  • 由于当 WebView 跳转到下一个页面时,之前加载的 JS 可能已经失效
  • 所以,通常需要在以下方法中加载 JS:
onLoadResource();
doUpdateVisitedHistory();
onPageStarted();
onPageFinished();
onReceivedTitle();
onProgressChanged();

过滤掉Object类的方法

  • 由于最终是通过反射得到Android指定对象的方法,所以同时也会得到基类的其他方法(最顶层的基类是 Object类)
  • 为了不把 getClass()等方法注入到 JS 中,我们需要把 Object 的共有方法过滤掉,需要过滤的方法列表如下:
getClass()
hashCode()
notify()
notifyAl()
equals()
toString()
wait()

密码明文存储漏洞

WebView默认开启密码保存功能 :

mWebView.setSavePassword(true)`
  • 开启后,在用户输入密码时,会弹出提示框:询问用户是否保存密码;
  • 如果选择”是”,密码会被明文保到 /data/data/com.package.name/databases/webview.db 中,这样就有被盗取密码的危险

解决方案

关闭密码保存提醒

WebSettings.setSavePassword(false)

域控制不严格漏洞

先看Android里的 WebViewActivity.java

public class WebViewActivity extends Activity {
    private WebView webView;
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_webview);
        webView = (WebView) findViewById(R.id.webView);

        //webView.getSettings().setAllowFileAccess(false);                    (1)
        //webView.getSettings().setAllowFileAccessFromFileURLs(true);         (2)
        //webView.getSettings().setAllowUniversalAccessFromFileURLs(true);    (3)
        Intent i = getIntent();
        String url = i.getData().toString(); //url = file:///data/local/tmp/attack.html 
        webView.loadUrl(url);
    }
 }

/**Mainifest.xml**/
// 将该 WebViewActivity 在Mainifest.xml设置exported属性
// 表示:当前Activity是否可以被另一个Application的组件启动
android:exported="true"

即 A 应用可以通过 B 应用导出的 Activity 让 B 应用加载一个恶意的 file 协议的 url,从而可以获取 B 应用的内部私有文件,从而带来数据泄露威胁

具体:当其他应用启动此 Activity 时, intent 中的 data 直接被当作 url 来加载(假定传进来的 url 为 file:///data/local/tmp/attack.html ),其他 APP 通过使用显式 ComponentName 或者其他类似方式就可以很轻松的启动该 WebViewActivity 并加载恶意url。

下面我们着重分析WebView中getSettings类的方法对 WebView 安全性的影响:

  • setAllowFileAccess()
  • setAllowFileAccessFromFileURLs()
  • setAllowUniversalAccessFromFileURLs()

setAllowFileAccess()

// 设置是否允许 WebView 使用 File 协议
webView.getSettings().setAllowFileAccess(true);     
// 默认设置为true,即允许在 File 域下执行任意 JavaScript 代码

使用 file 域加载的 js代码能够使用进行 同源策略跨域访问 ,从而导致隐私信息泄露

  1. 同源策略跨域访问:对私有目录文件进行访问
  2. 针对 IM 类产品,泄露的是聊天信息、联系人等等
  3. 针对浏览器类软件,泄露的是cookie 信息泄露。

如果不允许使用 file 协议,则不会存在上述的威胁;

webView.getSettings().setAllowFileAccess(true);

但同时也限制了 WebView 的功能,使其不能加载本地的 html 文件

解决方案:

  • 对于不需要使用 file 协议的应用,禁用 file 协议;
setAllowFileAccess(false);
  • 对于需要使用 file 协议的应用,禁止 file 协议加载 JavaScript。
setAllowFileAccess(true); 

// 禁止 file 协议加载 JavaScript
if (url.startsWith("file://") {
    setJavaScriptEnabled(false);
} else {
    setJavaScriptEnabled(true);
}

2. setAllowFileAccessFromFileURLs()

// 设置是否允许通过 file url 加载的 Js代码读取其他的本地文件
webView.getSettings().setAllowFileAccessFromFileURLs(true);
// 在Android 4.1前默认允许
// 在Android 4.1后默认禁止

AllowFileAccessFromFileURLs() 设置为 true 时,攻击者的JS代码为:

<script>
function loadXMLDoc()
{
    var arm = "file:///etc/hosts";
    var xmlhttp;
    if (window.XMLHttpRequest)
    {
        xmlhttp=new XMLHttpRequest();
    }
    xmlhttp.onreadystatechange=function()
    {
        //alert("status is"+xmlhttp.status);
        if (xmlhttp.readyState==4)
        {
              console.log(xmlhttp.responseText);
        }
    }
    xmlhttp.open("GET",arm);
    xmlhttp.send(null);
}
loadXMLDoc();
</script>

// 通过该代码可成功读取 /etc/hosts 的内容数据

解决方案:设置 setAllowFileAccessFromFileURLs(false);

当设置成为 false 时,上述JS的攻击代码执行会导致错误,表示浏览器禁止从 file url 中的 javascript 读取其它本地文件。

3. setAllowUniversalAccessFromFileURLs()

// 设置是否允许通过 file url 加载的 Javascript 可以访问其他的源(包括http、https等源)
webView.getSettings().setAllowUniversalAccessFromFileURLs(true);

// 在Android 4.1前默认允许(setAllowFileAccessFromFileURLs()不起作用)
// 在Android 4.1后默认禁止

AllowFileAccessFromFileURLs() 被设置成true时,攻击者的JS代码是:

// 通过该代码可成功读取 http://www.so.com 的内容
<script>
function loadXMLDoc()
{
    var arm = "http://www.so.com";
    var xmlhttp;
    if (window.XMLHttpRequest)
    {
        xmlhttp=new XMLHttpRequest();
    }
    xmlhttp.onreadystatechange=function()
    {
        //alert("status is"+xmlhttp.status);
        if (xmlhttp.readyState==4)
        {
             console.log(xmlhttp.responseText);
        }
    }
    xmlhttp.open("GET",arm);
    xmlhttp.send(null);
}
loadXMLDoc();
</script>

解决方案:设置 setAllowUniversalAccessFromFileURLs(false);

4. setJavaScriptEnabled()

// 设置是否允许 WebView 使用 JavaScript(默认是不允许)
webView.getSettings().setJavaScriptEnabled(true);  

// 但很多应用(包括移动浏览器)为了让 WebView 执行 http 协议中的 JavaScript,都会主动设置为true,不区别对待是非常危险的。

即使把 setAllowFileAccessFromFileURLs()setAllowUniversalAccessFromFileURLs() 都设置为 false,通过 file URL 加载的 javascript 仍然有方法访问其他的本地文件: 符号链接跨源攻击

前提是允许 file URL 执行 javascript,即 webView.getSettings().setJavaScriptEnabled(true);

这一攻击能奏效的原因是: 通过 javascript 的延时执行和将当前文件替换成指向其它文件的软链接就可以读取到被符号链接所指的文件 。具体攻击步骤:

  1. 把恶意的 js 代码输出到攻击应用的目录下,随机命名为 xx.html,修改该目录的权限;
  2. 修改后休眠 1s,让文件操作完成;
  3. 完成后通过系统的 Chrome 应用去打开该 xx.html 文件
  4. 等待 4s 让 Chrome 加载完成该 html,最后将该 html 删除,并且使用 ln -s 命令为 Chrome 的 Cookie 文件创建软连接

注:在该命令执行前 xx.html 是不存在的;执行完这条命令之后,就生成了这个文件,并且将 Cookie 文件链接到了 xx.html 上。

于是就可通过链接来访问 Chrome 的 Cookie

  1. Google 没有进行修复,只是让Chrome 最新版本默认禁用 file 协议,所以这一漏洞在最新版的 Chrome 中并不存在
  2. 但是,在日常大量使用 WebView 的App和浏览器,都有可能受到此漏洞的影响。通过利用此漏洞,容易出现数据泄露的危险

如果是 file 协议,禁用 javascript 可以很大程度上减小跨源漏洞对 WebView 的威胁。

  1. 但并不能完全杜绝跨源文件泄露。
  2. 例:应用实现了下载功能,对于无法加载的页面,会自动下载到 sd 卡中;由于 sd 卡中的文件所有应用都可以访问,于是可以通过构造一个 file URL 指向被攻击应用的私有文件,然后用此 URL 启动被攻击应用的 WebActivity,这样由于该 WebActivity 无法加载该文件,就会将该文件下载到 sd 卡下面,然后就可以从 sd 卡上读取这个文件了

最终解决方案

  • 对于不需要使用 file 协议的应用,禁用 file 协议;
// 禁用 file 协议;
setAllowFileAccess(false); 
setAllowFileAccessFromFileURLs(false);
setAllowUniversalAccessFromFileURLs(false);
  • 对于需要使用 file 协议的应用,禁止 file 协议加载 JavaScript。
// 需要使用 file 协议
setAllowFileAccess(true); 
setAllowFileAccessFromFileURLs(false);
setAllowUniversalAccessFromFileURLs(false);

// 禁止 file 协议加载 JavaScript
if (url.startsWith("file://") {
    setJavaScriptEnabled(false);
} else {
    setJavaScriptEnabled(true);
}
原文  http://hexo.imagemlt.xyz/post/WebViewSec/index.html
正文到此结束
Loading...