查询 25 次,耗时 0.2112 秒

Android WebView 攻击面实战:在本地靶场复现 JavaScript Bridge 与不安全 URL 加载问题

ghhjxf

New member

ghhjxf Rep
0
0
0
Rep
0
ghhjxf Vouches
Vouches
0
2 MONTHS
2 2 MONTHS OF SERVICE
ghhjxf 获得
0
0
0
获得
0
ghhjxf 凭证
0
0
0
凭证
0
主题
8
点赞
0
HakB
2 月
在社区成长 2 月
LEVEL 1 1 XP
 
一、背景

WebView 被广泛用于混合应用、活动页面、帮助中心和第三方登录流程。它既具有浏览器解析 HTML、JavaScript 的能力,又可以通过 JavaScript Bridge 调用 Android 原生方法,因此其安全边界比普通网页更复杂。

实际测试中,风险通常不是由某一个配置单独造成的,而是多个问题组合形成:应用允许外部输入决定 WebView 加载地址,同时给所有页面注入高权限 JavaScript 接口;或者应用仅通过字符串前缀判断域名,导致攻击者控制的页面也能进入受信任 WebView。本文在本地 Android 模拟器和自建 HTTP 服务中复现这类问题,并给出检测与修复方法。全部操作仅用于自有应用、授权测试环境或本地靶场。

二、漏洞原理与攻击面

典型危险链路如下:

1. Activity 通过 Deep Link、Intent Extra、二维码或服务端下发参数接收 URL。
2. 应用未严格校验 URL 的协议、主机、端口和重定向结果,直接调用 WebView.loadUrl。
3. WebView 对所有页面启用了 JavaScript,并通过 addJavascriptInterface 注入原生对象。
4. 外部页面中的 JavaScript 调用带有 @JavascriptInterface 的方法,从而访问本不应对外暴露的应用能力。

还需要关注以下配置:

- setAllowFileAccess(true):允许访问 file:// 资源,若与路径可控、旧版本兼容逻辑组合,可能扩大本地文件攻击面。
- setAllowUniversalAccessFromFileURLs(true):允许 file 页面跨域访问其他来源,通常不应开启。
- onReceivedSslError 中调用 handler.proceed():忽略证书错误,使 WebView 失去 TLS 身份校验能力。
- shouldOverrideUrlLoading 无条件返回 false:所有跳转继续在当前 WebView 内加载,包括非预期域名。
- setMixedContentMode(MIXED_CONTENT_ALWAYS_ALLOW):HTTPS 页面可加载 HTTP 子资源,增加内容被替换的风险。
- 使用 startsWith、contains 或正则片段判断可信域名:例如将 https://trusted.example.evil.test 错判为可信地址。

三、前置条件

建议准备以下环境:

1. Android Studio 与 Android SDK。
2. Android 模拟器,建议选择仍受支持的系统版本。
3. 一台本地测试计算机,用于运行 HTTP 服务。
4. adb 命令可正常识别模拟器。
5. 仅包含虚构数据的测试应用,不要使用生产密钥、真实账号或真实令牌。

确认设备连接:

adb devices

启动本地页面服务:

mkdir webview-lab
cd webview-lab
python3 -m http.server 8000 --bind 0.0.0.0

Android 官方模拟器访问宿主机时通常使用 10.0.2.2,因此测试页面地址可设为 http://10.0.2.2:8000/。其他模拟器或真机的网络地址可能不同,应根据实际网络环境调整。

四、构建最小化授权靶场

创建一个测试 Activity,并在 AndroidManifest.xml 中注册一个仅用于实验的 Deep Link:

<activity
android:name=".LabWebViewActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="labapp" android:host="webview" />
</intent-filter>
</activity>

下面是故意保留缺陷的 Kotlin 示例。该示例只返回固定实验字符串,不应连接任何真实业务能力:

class LabBridge {
@android.webkit.JavascriptInterface
fun getLabValue(): String {
return "LOCAL_DEMO_ONLY"
}
}

class LabWebViewActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val webView = WebView(this)
setContentView(webView)

webView.settings.javaScriptEnabled = true
webView.addJavascriptInterface(LabBridge(), "NativeBridge")

val target = intent.data?.getQueryParameter("url")
if (target != null) {
webView.loadUrl(target)
}
}
}

这里的核心问题不是 JavaScript Bridge 一定有漏洞,而是外部输入可以决定页面来源,同时原生接口被无条件注入。即便接口当前只返回版本号,一旦后续开发人员加入读取用户信息、调用支付、文件操作或身份凭据相关方法,风险就会显著上升。

五、创建本地验证页面

在 webview-lab 目录创建 index.html:

<!doctype html>
<html>
<head><meta charset="utf-8"><title>WebView Lab</title></head>
<body>
<h3>本地授权测试页</h3>
<pre id="result">waiting</pre>
<script>
try {
const value = window.NativeBridge.getLabValue();
document.getElementById('result').textContent = value;
} catch (e) {
document.getElementById('result').textContent = 'Bridge unavailable: ' + e;
}
</script>
</body>
</html>

该页面只检查接口是否向非受信任来源暴露,不读取文件、不访问账号数据,也不向外部服务器发送信息。

六、分步骤复现与检测

步骤 1:安装并启动测试应用

在 Android Studio 中安装应用,或者使用:

adb install -r app-debug.apk

步骤 2:通过 Deep Link 传入本地页面地址

adb shell am start -a android.intent.action.VIEW -d "labapp://webview?url=http%3A%2F%2F10.0.2.2%3A8000%2Findex.html"

若页面显示 LOCAL_DEMO_ONLY,说明外部可控页面成功进入 WebView,并能够调用 NativeBridge。此结果足以证明信任边界设计不当,不需要继续尝试敏感操作。

步骤 3:静态检查配置

在获得授权的源码中搜索以下关键词:

rg "addJavascriptInterface|javaScriptEnabled|allowFileAccess|allowUniversalAccessFromFileURLs|MIXED_CONTENT|onReceivedSslError|loadUrl" app/src

如果只能审计自有 APK,可使用 jadx 进行代码阅读:

jadx -d jadx-out app-debug.apk
rg "addJavascriptInterface|setJavaScriptEnabled|handler.proceed|loadUrl" jadx-out

静态检查时记录以下信息:

- Bridge 对象名称及所有带 @JavascriptInterface 的公开方法。
- WebView 加载地址来自常量、服务端配置、Intent 还是用户输入。
- 页面跳转及 HTTP 30x 重定向后是否重新校验来源。
- Bridge 是否在加载不受信任内容前被 removeJavascriptInterface 移除。
- Activity、Service、Receiver 是否导出,是否存在权限保护。

步骤 4:检查错误的域名白名单

以下写法不安全:

if (url.startsWith("https://trusted.example")) {
webView.loadUrl(url)
}

它可能错误接受 trusted.example.evil.test,或忽略端口、用户信息、编码和大小写带来的边界问题。contains("trusted.example") 同样不可靠。

正确方式是使用 Uri 解析后分别比较字段:

val uri = Uri.parse(input)
val allowed = uri.scheme == "https" &&
uri.host?.lowercase() == "trusted.example" &&
uri.port == -1

if (allowed) {
webView.loadUrl(uri.toString())
} else {
finish()
}

如果业务允许子域名,应明确判断主域边界:

val host = uri.host?.lowercase() ?: ""
val allowedHost = host == "trusted.example" || host.endsWith(".trusted.example")

不能仅使用 endsWith("trusted.example"),因为 eviltrusted.example 也会满足该条件。

步骤 5:检查页面内跳转

初始 URL 合法并不代表后续跳转始终合法。可在 WebViewClient 中统一校验:

override fun shouldOverrideUrlLoading(
view: WebView,
request: WebResourceRequest
): Boolean {
val uri = request.url
val allowed = uri.scheme == "https" && uri.host == "trusted.example"
if (!allowed) {
return true
}
return false
}

对于 tel、mailto 或自定义 Scheme,不应直接交给隐式 Intent。需要建立明确白名单,并在启动外部组件前确认目标和参数符合预期。

七、修复方案

1. 最小化 JavaScript Bridge

如果页面不需要调用原生能力,就不要使用 addJavascriptInterface。必须使用时,只暴露最少方法,参数要进行长度、格式、状态和权限校验。不要暴露任意命令执行、任意文件读写、任意 Intent 启动、原始身份令牌读取等通用能力。

2. 将可信内容与不可信内容分离

不要在同一个带 Bridge 的 WebView 中打开广告、客服链接、用户输入 URL 或任意外部站点。可为外部内容使用系统浏览器或独立 WebView,并确保该 WebView 不注入原生接口。

3. 严格限制 URL

只允许 HTTPS;使用 Uri 解析并精确比较 scheme、host 和 port;每次主框架跳转都重新校验;对服务端重定向后的最终地址同样执行检查。若页面集合固定,可进一步使用精确路径白名单。

4. 收紧 WebView 设置

val settings = webView.settings
settings.javaScriptEnabled = false // 无业务需求时关闭
settings.allowFileAccess = false
settings.allowContentAccess = false
settings.domStorageEnabled = false // 无需求时关闭
settings.mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW

启用 JavaScript 前,应确认页面确实需要。配置不应机械照搬,必须结合最低 Android 版本和业务功能进行回归测试。

5. 正确处理 TLS 错误

不要在 onReceivedSslError 中调用 handler.proceed()。安全做法是取消加载:

override fun onReceivedSslError(
view: WebView?,
handler: SslErrorHandler,
error: SslError?
) {
handler.cancel()
}

开发环境需要调试代理时,应通过仅在 debug 构建启用的 Network Security Configuration 信任测试证书,不能把忽略证书错误的逻辑带入发布版本。

6. 控制组件导出

若 WebView Activity 不需要由其他应用启动,应设置 android:exported="false"。如果确实需要 Deep Link,应采用已验证的 HTTPS App Link、严格参数校验和必要的登录态确认。敏感操作不能只因为请求来自某个 Deep Link 就自动执行。

7. 建立发布前自动检查

可在代码审查或 CI 中重点检测 handler.proceed()、setAllowUniversalAccessFromFileURLs(true)、任意 loadUrl 输入和新增 @JavascriptInterface 方法。自动扫描只能发现线索,最终仍需人工确认数据来源、页面信任级别和业务授权逻辑。

八、修复后的验证

修复后重新执行前述 adb Deep Link 命令,预期结果应满足至少一项:

- Activity 不再对外导出,外部调用失败。
- URL 因 host 不在白名单而被拒绝。
- 外部页面改由系统浏览器打开。
- 独立 WebView 未注入 NativeBridge,页面显示 Bridge unavailable。
- HTTP 地址因只允许 HTTPS 而被拦截。

同时还应测试合法业务页面、登录跳转、返回键、文件上传和网络异常场景,避免安全修改破坏正常功能。

九、风险与注意事项

1. 不要在生产应用中加入演示 Bridge,也不要使用真实令牌验证风险。
2. 测试页面应运行在本机或隔离实验网,不应部署到公共互联网。
3. WebView 调试开关 WebView.setWebContentsDebuggingEnabled(true) 只应在调试构建启用。
4. 截图、日志和测试报告中可能包含 URL 参数、Cookie 或用户标识,提交前应脱敏。
5. 漏洞评级要结合 Bridge 实际能力、可控页面来源、是否需要用户交互、登录状态和数据敏感度,不能仅看到 addJavascriptInterface 就直接判定为高危。

十、常见问题

问题 1:只加载 HTTPS 页面就一定安全吗?

不一定。HTTPS 只保护传输和服务器身份,不能证明页面本身可信。如果 URL 可控、域名校验错误,或受信任站点存在开放重定向和可控脚本内容,Bridge 仍可能暴露。

问题 2:Bridge 方法只返回应用版本号,需要修复吗?

风险可能较低,但仍应修复信任边界。低敏感接口容易在后续迭代中被扩展,最终形成高权限通道。建议从架构上限制可加载页面,而不是依赖当前方法“暂时不敏感”。

问题 3:设置 android:exported="false" 是否足够?

不够。应用内部页面、推送、二维码或服务端配置仍可能把不可信地址传给 WebView。组件导出控制和 URL 校验必须同时实施。

问题 4:为什么不建议忽略 SSL 错误方便抓包?

因为该逻辑一旦进入生产版本,会让所有用户都失去证书校验保护。授权测试应在 debug 版本中配置测试 CA,并确保 release 构建不信任用户证书或测试证书。

十一、总结

Android WebView 渗透测试的重点不是寻找单个危险 API,而是梳理完整数据流:谁能够控制 URL、页面最终从哪里加载、是否发生重定向、页面具有什么原生能力、组件是否对外暴露,以及 TLS 和文件访问策略是否合理。最有效的修复原则是缩小信任边界:不需要的能力关闭,外部内容与原生 Bridge 隔离,URL 使用结构化白名单校验,TLS 错误直接终止,并通过回归测试确认修复有效。按照本文的本地靶场流程,可以在不接触真实目标和真实数据的前提下完成复现、证据收集、修复和复测闭环。
 
顶部